[Bug 297293] fget_procdesc(): EBADF -> EINVAL change breaks Qt's forkfd

[email protected] Wed, 05 Aug 2026 07:21:59 +0000
Newsgroups gmane.os.freebsd.bugs
Message-ID <[email protected]/bugzilla/>
https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=3D297293

            Bug ID: 297293
           Summary: fget_procdesc(): EBADF -> EINVAL change breaks Qt's
                    forkfd
           Product: Base System
           Version: 16.0-CURRENT
          Hardware: Any
                OS: Any
            Status: New
          Severity: Affects Only Me
          Priority: ---
         Component: kern
          Assignee: [email protected]
          Reporter: [email protected]
 Attachment #273472 text/plain
         mime type:

Created attachment 273472
  --> https://bugs.freebsd.org/bugzilla/attachment.cgi?id=3D273472&action=
=3Dedit
qrepro.cpp

Commit e18844223d1e ("fget_procdesc(): change error for non-procdesc type f=
rom
EBADF to EINVAL", 2026-07-16) changes the error value of pdgetpid(2),
pdkill(2), pddupfd(2) and pdwait(2) when the fd is open but is not a process
descriptor.

Qt's bundled forkfd uses that EBADF to find out that the fd is its own pipe=
.=20
Since this commit, Qt loses the exit status of its child process and report=
s a
normal exit as a crash.  konsole(1) now prints when a shell exits:

        Warning: Program '/usr/local/bin/zsh' crashed.

zsh exits with status 0, there is no signal and no core file.  /bin/sh and =
bash
give the same message.

This is the same problem as in September 2025: fd9e09cb2ab0 made the same
change for pdgetpid(2), and a85525a5c8b2 ("pdgetpid(2): switch back returni=
ng
EBADF for non-procdesc fd") partially reverted it.  e18844223d1e brings EIN=
VAL
back through the new fget_procdesc() helper.

## The changed error value

        /*
         * cc -o repro repro.c
         *
         * When Qt starts a child with fork() instead of pdfork(), forkfd w=
aits
on its
         * own pipe.  Before forkfd reads that pipe, it calls pdgetpid() on=
 the
fd and
         * looks at errno.  This is that call.
         */
        #include <sys/procdesc.h>
        #include <err.h>
        #include <errno.h>
        #include <unistd.h>

        int
        main(void)
        {
                pid_t pid;
                int p[2];

                if (pipe(p) !=3D 0)
                        err(1, "pipe");
                if (pdgetpid(p[0], &pid) !=3D -1)
                        errx(1, "pdgetpid() on a pipe should not succeed");
                warn("pdgetpid(pipe fd) failed with errno %d", errno);
                return (0);
        }

On 16.0-CURRENT (kernel built from 5c533d39e75c):

        repro: pdgetpid(pipe fd) failed with errno 22: Invalid argument

Before e18844223d1e this was errno 9 (EBADF).

## What Qt does with it

qtbase/src/3rdparty/forkfd/forkfd.c:

        int forkfd_wait4(int ffd, struct forkfd_info *info, int options, st=
ruct
rusage *rusage)
        {
            ...
            if (system_has_forkfd()) {
                /* if this is one of our pipes, not a procdesc/pidfd, we'll=
 get
an EBADF */
                ret =3D system_forkfd_wait(ffd, info, options, rusage);
                if (disable_fork_fallback() || ret !=3D -1 || errno !=3D EB=
ADF)
                    return ret;
            }
            ret =3D read(ffd, &payload, sizeof(payload));

forkfd does not remember which kind of fd it has, so it calls pdgetpid() fi=
rst
and uses errno to find out.  Without a child process modifier Qt uses vfork=
()
and forkfd uses pdfork(), so ffd is a process descriptor.  With a child pro=
cess
modifier forkfd uses fork(), and ffd is the pipe above.  konsole always hits
the second case, because KPtyProcess always sets a child process modifier.

With EINVAL, forkfd_wait() returns -1 and the pipe is never read.

qtbase/src/corelib/io/qprocess_unix.cpp then keeps its zeroed struct:

        forkfd_info info =3D {};
        QT_EINTR_LOOP(ret, forkfd_wait(forkfd, &info, nullptr));
        exitCode =3D info.status;
        exitStatus =3D info.code =3D=3D CLD_EXITED ? QProcess::NormalExit :
QProcess::CrashExit;

info.code is 0, which is not CLD_EXITED, so Qt reports a crash.  ktrace sho=
ws
the correct payload (CLD_EXITED, status 0) is written to the pipe, and only
never read.

qrepro.cpp (attached) shows it with Qt alone, no pty involved.  The child is
"/bin/sh -c 'exit 0'" in both runs, the only difference is the child process
modifier:

        plain QProcess:                Qt says: NormalExit exitCode=3D0  OK
        same as konsole does:          Qt says: CrashExit  exitCode=3D0  WR=
ONG
(the child exited 0)

## Notes

- lib/libsys/pdfork.2 still documents EBADF for pddupfd(), which now goes
through fget_procdesc().
- On Linux, pidfd_send_signal(2) and waitid(P_PIDFD) return EBADF when the =
fd
is not a pidfd, which is probably why forkfd checks for EBADF.
- stable/15 still returns EBADF (07debe52b30a).  e18844223d1e has "MFC afte=
r: 1
week", and after the MFC Qt programs there will have the same problem.

If the new error value is intended, I am happy to report this to Qt and to
other projects that use these interfaces.  In that case, please do not MFC =
it
for now

--=20
You are receiving this mail because:
You are the assignee for the bug.=