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

From: <bugzilla-noreply_at_freebsd.org>
Date: Wed, 05 Aug 2026 07:21:59 UTC
https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=297293

            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: bugs@FreeBSD.org
          Reporter: lwhsu@FreeBSD.org
 Attachment #273472 text/plain
         mime type:

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

Commit e18844223d1e ("fget_procdesc(): change error for non-procdesc type from
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. 
Since this commit, Qt loses the exit status of its child process and reports 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 returning
EBADF for non-procdesc fd") partially reverted it.  e18844223d1e brings EINVAL
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 waits
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) != 0)
                        err(1, "pipe");
                if (pdgetpid(p[0], &pid) != -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, struct
rusage *rusage)
        {
            ...
            if (system_has_forkfd()) {
                /* if this is one of our pipes, not a procdesc/pidfd, we'll get
an EBADF */
                ret = system_forkfd_wait(ffd, info, options, rusage);
                if (disable_fork_fallback() || ret != -1 || errno != EBADF)
                    return ret;
            }
            ret = read(ffd, &payload, sizeof(payload));

forkfd does not remember which kind of fd it has, so it calls pdgetpid() first
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 process
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 = {};
        QT_EINTR_LOOP(ret, forkfd_wait(forkfd, &info, nullptr));
        exitCode = info.status;
        exitStatus = info.code == CLD_EXITED ? QProcess::NormalExit :
QProcess::CrashExit;

info.code is 0, which is not CLD_EXITED, so Qt reports a crash.  ktrace shows
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=0  OK
        same as konsole does:          Qt says: CrashExit  exitCode=0  WRONG
(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 after: 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

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