libdispatch fork← NextBSD Research · Home · July 9, 2026
NextBSD carries a fork of libdispatch (the “wake me when this happens” library) with Apple’s Mach messaging turned on, because six base components genuinely need it. Gershwin ships the stock Apple libdispatch with Mach off, because that is the only build that makes the global menu work. The bug is that both are named libdispatch.so, so on FreeBSD they can be loaded in place of each other — which is what crashed launchctl/dbus/sshd.
The fix is one rename: give the base NextBSD fork a unique soname — libsystem_dispatch.so — and relink the base components onto it. Gershwin’s stock libdispatch.so is never touched. Two different names can never be confused, so the collision is gone. NextBSD owns this fork permanently (it is never synced with Apple again), so renaming it outright is clean — nothing upstream can undo it.
Verdict: Rename the base fork → libsystem_dispatch.so; leave Gershwin’s stock alone
event.c/event_kevent.c kqueue patches. So both builds have to exist. The only real problem is that they share a name; the rename fixes exactly that, and nothing else has to change.
The Gershwin global menu bar is supposed to switch to whatever app you bring to the front. On NextBSD it did not: it latched onto the first app and stayed there. The menu learns about app switches by watching the X11 display for a "which window is active now" event. It does that watch through a libdispatch file-descriptor read source — a small piece of plumbing that says "wake me when this socket has data."
The root cause was one compile-time number. NextBSD's libdispatch is a fork of the same code Gershwin uses, byte-for-byte identical except for a single flag, EV_UDATA_SPECIFIC, which the fork set to 0x0100 so that Apple's Mach messaging support would compile. The problem: on FreeBSD (which NextBSD is built on) the value 0x0100 already means something else to the kernel — "fire this once, then delete it." So the menu's watch fired exactly one time and then the kernel threw it away and never re-armed it. The menu got its first app-switch notification and then went deaf. That same flag also switched libdispatch into a Darwin-only bookkeeping model that mis-identifies file-descriptor watches on FreeBSD, compounding the failure. Timers and Mach-port plumbing were unaffected, which is why the rest of the system still booted — only file-descriptor watches like the menu's broke.
People tried several fixes. Stripping just the "fire once" bit made a single watch re-arm in tests, but did not fix the deeper wrong-bookkeeping problem. Setting the flag all the way back to zero fixed file-descriptor watches but would have broken Apple's Mach messaging (libxpc), which genuinely needs the Darwin model. The clean answer everyone converged on: keep the Darwin model ONLY for the one Mach-messaging case and route everything else through the portable path.
The famous "log out and back in fixes it" clue was a red herring. The login screen starts one X display at boot and never shuts it down on logout, and the old session's "active window" value is left behind on that still-running display. A second login inherits that stale value, so the menu latches once and looks fixed — but app-switching is still dead. A truly fresh display has no leftover value, so the menu shows nothing. It was a mask, the same way manually restarting the menu process temporarily "fixed" it.
How did we end up with TWO libdispatches? Rather than wait for the surgical base fix, the Gershwin side shipped its own libdispatch built with Mach turned OFF (which sidesteps the poisoned flag) and installed it so Gershwin apps load it ahead of the system copy, while system daemons keep the Mach-on copy. Two builds of the same library, kept apart by load order. That is the split. The base per-path fix — restricting the Darwin model to Mach-only and stripping the bad bits from everything else — was written three days later and is still sitting uncommitted in the working tree. Once it is committed and proven, the reason for the second libdispatch goes away.
Keep both builds; give the base fork its own name so the two can never collide. Gershwin is left entirely alone — it runs plain upstream libdispatch, exactly as on FreeBSD/Linux.
libdispatch to the unique soname libsystem_dispatch.so (a build-harness change — a soname flag in build-userland.sh or patchelf --set-soname). It stays a Mach (HAVE_MACH) build; only its name changes. It is a permanent fork — never synced with Apple again — so this is a clean, owned rename.-ldispatch consumers (libxpc, launchd, notifyd, SystemConfiguration, IOKit, notify, asl, plus CoreFoundation and the daemons) onto libsystem_dispatch.so.libdispatch.so — no rename, no NextBSD special-casing, no patches. It becomes the only libdispatch.so on the system.DT_NEEDED on libsystem_dispatch.so or a Mach-cluster lib — the one rule that keeps the two builds out of the same process.launchctl/dbus/sshd “Undefined symbol” poisoning can’t happen, and Gershwin runs pure upstream libdispatch.Note: no libdispatch source is patched on either side. Gershwin’s is stock Apple; the base fork is used as-is (Mach) and merely renamed. The earlier event.c/event_kevent.c kqueue experiments are irrelevant — they never made the menu track, and none of this depends on them.
A "disposition" is simply the decision we have reached about one specific library: given how that library works and how (or whether) Gershwin uses it, which build of libdispatch should it link against, and what — if anything — do we need to change. Think of it as a one-line verdict per library so a maintainer can scan the whole system at a glance instead of re-deriving the reasoning each time. Each row answers four questions. "Needs Mach?" asks whether the library genuinely requires Apple's Mach messaging support inside libdispatch — that is true only for libraries that create a Mach-receive dispatch source (the plumbing behind libxpc and friends); everything else only needs portable timers and file-descriptor watches. "Gershwin uses" asks whether a Gershwin desktop/GUI process actually loads this library — this matters because the two-runtime hazard only bites when two different libdispatches could end up in the SAME process, so a library no Gershwin app ever loads is low-risk no matter what it needs. "Recommended dispatch" is the payoff: which single libdispatch this library should bind to — under Option A the answer is almost always "the one unified base libdispatch," because that build serves both Mach daemons and portable GUI apps from a single file. "Action" is the concrete to-do: usually "no change" for daemon-only libraries, and "delete the Gershwin-specific workaround" for the handful of pieces that currently exist only to prop up the second libdispatch.
libdispatch? Only libraries that create a Mach-receive source (the plumbing behind libxpc) do.libsystem_dispatch.so, or Gershwin’s stock libdispatch.so (or, for libdns_sd, none at all).libdispatch (the runtime itself) Mixedlibsystem_dispatch.so. Gershwin keeps stock libdispatch.so.libsystem_dispatch.so (permanent NextBSD fork — never synced with Apple again). Gershwin’s stock Apple build is untouched.In plain terms: This is the one library the whole debate is about; the goal is exactly one copy that works for both daemons and GUI apps.
libBlocksRuntime PortablelibBlocksRuntime.soIn plain terms: A small support library that must never be duplicated in one process, so ship one clean canonical copy.
libxpc Machlibsystem_dispatch.so (the renamed base fork)-ldispatch to the renamed base soname libsystem_dispatch.so.In plain terms: Apple's IPC library that genuinely needs Mach, but it only lives in daemons so it never risks a Gershwin GUI process.
libCoreFoundation Portablelibsystem_dispatch.so (the renamed base fork)-ldispatch to the renamed base soname libsystem_dispatch.so.In plain terms: Harmless portable consumer that Gershwin does not touch anyway.
libSystemConfiguration Machlibsystem_dispatch.so (the renamed base fork)-ldispatch to the renamed base soname libsystem_dispatch.so.In plain terms: Daemon-side config library needing Mach; irrelevant to the GUI side.
libIOKit Machlibsystem_dispatch.so (the renamed base fork)-ldispatch to the renamed base soname libsystem_dispatch.so.In plain terms: Hardware/IO library needing Mach; daemon-only, no Gershwin exposure.
libnotify Machlibsystem_dispatch.so (the renamed base fork)-ldispatch to the renamed base soname libsystem_dispatch.so.In plain terms: Notification plumbing needing Mach; stays entirely on the daemon side.
libsystem_asl Machlibsystem_dispatch.so (the renamed base fork)-ldispatch to the renamed base soname libsystem_dispatch.so.In plain terms: System-logging client needing Mach; not loaded by Gershwin apps.
libdns_sd (mDNS client) PortableIn plain terms: The single base library Gershwin GUI apps load, and it is safe precisely because it carries no dispatch at all.
launchd / launchctl / notifyd / syslogd / aslmanager / ipconfigd / mDNSResponder daemon / diskarbitrationd / hostnamed Mixedlibsystem_dispatch.so (the renamed base fork)-ldispatch to the renamed base soname libsystem_dispatch.so.In plain terms: System daemons that keep the Mach-capable libdispatch; they live in their own processes so they are outside the two-libdispatch hazard.
There is no real contradiction here; the documents are describing different layers. The audit asked: "does the mDNS (Bonjour-style network discovery) client drag libdispatch into a Gershwin app?" The answer, confirmed against the current source, is No. The client library, libdns_sd, is a plain Unix-domain-socket stub — it talks to the mDNSResponder daemon over a local socket at /var/run/mDNSResponder using ordinary sendmsg calls. It contains zero Mach code and zero dispatch code, because its dispatch paths are compiled out unless the target is Apple (_DNS_SD_LIBDISPATCH is 1 only on __APPLE__, and this build targets FreeBSD). GNUstep drives it the old-fashioned way: it takes the socket file descriptor and polls it with select() on the NSRunLoop. So dispatch never enters a Gershwin GUI process through mDNS. Only the out-of-process daemon links Mach dispatch, and a daemon in its own process cannot cause the two-runtime hazard.
The mdns-network-browse-mach-plan.html document is not a counter-example. It agrees libdns_sd is today an AF_UNIX, non-Mach, dispatch-free stub — the Mach MIG transport it describes is a PROPOSAL, not something that exists. There is no .defs (MIG interface) file anywhere in the tree, and mach_bridge.c is a bare skeleton that only registers a name and returns; it never serves a Mach message loop. The network-crash doc is a third, separate layer: a libs-base build-config bug where mDNS support was disabled (HAVE_MDNS=0), leaving a nil backend that crashed; its fix (adding -L/usr/lib/system so the link probe passes) is already present in Install-System-Domain.sh.
Bottom line for Option A: mDNS as-built is fully compatible — no Mach, no dispatch in-process. The one forward-looking flag is that IF the mach-plan's client side were ever implemented, it WOULD inject a Mach surface directly into Gershwin GUI processes, which is exactly the hazard Option A exists to prevent. So Option A should explicitly mark the Mach-MIG mDNS client proposal as "re-review before adopting," not fold it in.