nextbsd-userland/src/libdispatch, consumers across src/*, and the kernel Mach overlay · 2026‑07‑06Don't swap to stock/Gershwin libdispatch — it would break boot and all XPC IPC. Six production
components depend on libdispatch's Mach sources (DISPATCH_SOURCE_TYPE_MACH_RECV / dispatch_mach_t), and a stock
(HAVE_MACH=0) build can't compile or link them. But your instinct is half-right: the fork's original rationale
— self-contain Mach without touching the kernel — is now obsolete (the kernel was modified), and the fd-source bug is a
design mismatch introduced by turning Mach on, fixable cleanly without dropping the fork. The right move is fix the flag
poisoning properly and keep the fork (Option A), with a principled reshape available later (Option C).
We already proved the patched libdispatch delivers X PropertyNotify 5/5 in the exact WindowMonitor pattern,
yet the running Menu still doesn't track. So neither fixing nor replacing libdispatch fixes the menu — that failure is
downstream. This review is the libdispatch architecture decision on its own merits, not the menu fix.
It's upstream apple/swift-corelibs-libdispatch (vendored at bc13504) built HAVE_MACH=ON on FreeBSD, plus a set of
"task #39" glue shims to make Apple's Mach path compile and link against libmach. Not a rewrite — a build-config choice plus
glue. The Mach machinery (src/mach.c 107 KB, voucher.c, the event_kevent.c EVFILT_MACHPORT
source types) is upstream Apple code that generic-BSD builds simply never compile.
This is the crux, and it reframes the whole question. Upstream, on a generic BSD with no system EV_UDATA_SPECIFIC, zeroes both
EV_UDATA_SPECIFIC and EV_VANISHED, so fd sources go out as the portable EV_ADD|EV_ENABLE|EV_DISPATCH — no bug.
But Mach has a hard gate:
// upstream: mach support requires the Darwin direct-knote flag
#if !EV_UDATA_SPECIFIC
# error mach support requires EV_UDATA_SPECIFIC
#endif
To turn Mach on, the fork bypassed that #error (5e9a460) and gave the flag a truthy value — event_config.h:213,
commit ca62703: "Drop to Apple's canonical 0x0100, which fits uint16 and doesn't collide with FreeBSD's EV_* bits." The author checked
the userspace‑visible flag list and missed EV_FORCEONESHOT 0x0100 and EV_KEEPUDATA 0x0200. Because
EV_UDATA_SPECIFIC is a global flag on all direct-knote sources — not just Mach — forcing it to 0x0100
re-poisoned every fd source:
fd source flags = EV_UDATA_SPECIFIC(0x100) | EV_DISPATCH(0x80) | EV_VANISHED(0x200) | EV_ADD | EV_ENABLE FreeBSD kernel reads 0x100 = EV_FORCEONESHOT, 0x200 = EV_KEEPUDATA -> EINVAL -> every READ/WRITE source silently dead
Corollary (worth an audit): the same poisoned flags ride the other direct-knote sources — proc, signal,
vnode, memorystatus — so those may also be silently EINVAL-dead. The impact is potentially broader
than fd read/write.
This is what blocks a stock swap. Six production consumers use Mach dispatch sources / the channel API:
| Consumer | Uses | Load-bearing? |
|---|---|---|
libxpc/xpc_connection.c:241 | MACH_RECV recv source — the XPC transport | Yes — all XPC IPC |
launchd/runtime.c:232 | MACH_RECV on the demand-port-set | Yes (but thin — just a wakeup; job dispatch already runs off synthetic kevents) |
notifyd.c:1435/1442 | full dispatch_mach_t channel protocol | Yes — hardest to replace |
IOKitNotify.c:337, notify_client.c:943, SCNotify.c:351 | MACH_RECV notification delivery | Yes for those subsystems |
| CoreFoundation / desktop runloop | pipe+kqueue __CFPort, plain kevent() | No dependency — not a blocker |
MACH_SEND / vouchers / firehose | tests + internal only | No production use |
A stock HAVE_MACH=0 libdispatch would fail to compile/link libxpc, launchd, notifyd, IOKit, Libnotify, SystemConfiguration →
broken boot + all XPC IPC. Notably, Gershwin/CoreFoundation itself does not depend on it (it uses pipe+kqueue),
so the desktop is not the blocker — the system daemons are.
Your recollection is confirmed in the kernel Mach overlay and history:
compat/mach/ipc/ipc_pset.c implements filt_machport at kqueue slot
-16 (EVFILT_MACHPORT, reserved by patches/0003). It delivers Mach port-set receive-readiness (optionally the
message inline) as a native kevent — XNU-style. A plain kqueue()+native knote delivers Mach messages with no libdispatch at all
(proven by mach_kmod CI tests).mach_msg, the _kernelrpc_mach_port_* / mach_vm_* families,
*_self_trap, timers, semaphores — dynamically registered, numbers published via sysctl mach.syscall.<name>. The kernel is
not pristine (edits to syscalls.master, event.h, a whole compat/mach/ subsystem).mach_trap_mux folded 5 ops behind one syscall. Commit 779cdc8 (#175) "drop the mach.ko trap multiplexor for dedicated
syscalls" deleted it after patches/0001 widened the lkmnosys band to 58 slots. The self-pipe "event bell" bridge was
likewise retired (#168 Stage 5) once the native filter landed.event_mach_freebsd.c (per-source pthread on
mach_msg) was deleted; the build flipped to stock Apple event_kevent.c over the native filter (gated by a
LIBDISPATCH-MACH-OK boot test). So libdispatch no longer bridges Mach itself — the kernel does.What still legitimately needs the fork: the HAVE_MACH build (for the Mach source types the six consumers use) and the
libmach shim that issues the Mach syscalls. The original "don't touch the kernel" reason is gone; the Mach feature set is what remains
load-bearing.
Restore upstream's per-path zeroing so EV_UDATA_SPECIFIC/EV_VANISHED reach only
EVFILT_MACHPORT (Mach) knotes, and fd/timer/portable sources emit plain EV_ADD|EV_ENABLE|EV_DISPATCH. Cleaner than the one-line
strip and it also un-breaks the other direct-knote sources (proc/signal/vnode). Then audit those for the same
EINVAL.
Cost: low. Keeps the large Apple surface (mach.c/voucher.c) that upstream doesn't test on BSD.
Now that the kernel has a native filt_machport, carry Mach source types on the portable kqueue path (no
EV_UDATA_SPECIFIC, no kevent_id/QoS, no direct-knotes). Removes the exact machinery that caused the bug while keeping Mach
features.
Cost: medium — must prove dispatch_mach_t re-arm survives without direct-knote (event_kevent.c:444‑473). Best
long-term shape.
Eliminates the bug class by construction, but a HAVE_MACH=0 libdispatch strands six consumers. Viable only after porting
each Mach-recv consumer onto the native path (libxpc/IOKit/SC moderate; notifyd's channel state machine hardest) — re-introducing bespoke
mach_msg loops that #168 just removed.
Cost: high, bounded. Large regression risk. Only if eliminating Mach from libdispatch is an explicit goal.
Fix the flag poisoning properly and keep the fork (Option A) now. The Mach machinery is load-bearing today (XPC/launchd/notifyd/
IOKit/SC), so it can't be dropped for stock libdispatch without a substantial, boot-critical rewrite of those consumers. The clean fix — restore per-path
zeroing so the Darwin flags only ever reach Mach knotes — is low-cost, structurally correct, and likely fixes a broader class than just fd sources
(audit proc/signal/vnode next). Option C is the principled follow-up if reducing the Apple-specific surface is a goal, and it's newly
feasible because the kernel now ships a native filt_machport. Option B (swap to Gershwin's libdispatch) is not worth it
— it breaks Mach IPC, and it wouldn't fix the menu anyway.