mach_port MIG client kernel + libmachHalf of libmach’s mach_port subsystem returns KERN_SUCCESS without doing anything. This is the plan to replace it with generated MIG stubs, in the layering Apple actually uses — written after two runtime probes overturned the previous draft.
mach_port sub-plan. The wider programme — all seven kernel
subsystems, .defs ownership, the syscall-band arithmetic, sequencing and the ticket map —
lives in Restoring MIG as NextBSD’s kernel RPC
substrate. Read that first; this page is the detail for the one subsystem that is reconstructed and
gate-verified.
Kernel MIG works on NextBSD today. Two hand-built messages sent to mach_task_self() on gershwin-on-nextbsd both returned correct data — including a complex reply carrying out-of-line memory. The receive path is not broken, and no kernel change is required to make these calls function.
The defect is entirely in userland. src/libmach/mach_traps.c has 14 real syscall traps and ~18 entry points that fabricate success — including both calls mportset_callback() needs — mach_port_get_set_status returns an empty member list and mach_port_get_attributes returns KERN_RESOURCE_SHORTAGE — which is why on-demand Mach launch in launchd is dead, and mach_port_mod_refs, which silently no-ops reference counting. These were never a retreat from MIG; no kernel .defs has ever existed in the tree. They are link-stage scaffolding from 2026-05-14 that was never revisited.
The fix is to reconstruct mach_port.defs and regenerate both sides. Client and server then agree by construction — the entire purpose of MIG — and the tree gains a source of truth it currently lacks: mach_port_server.c is 5,720 lines of generated C that nobody can regenerate.
The traps stay. 9 of NextBSD’s 12 traps correspond to real entries in Apple’s mach_trap_table. Apple’s shape is not “MIG or traps” — it is MIG as the universal floor, with traps as an accelerator that falls back to MIG. NextBSD has this inverted.
An earlier version of this page recommended wiring a syscall trap and stated that a MIG client “cannot be generated.” That recommendation is withdrawn. The distinction matters, because one half of the claim was true and the other was not:
| Claim | Status |
|---|---|
Our in-tree migcom cannot emit a client matching the existing frozen server | Still true. Verified by building migcom. IS_KERN_PROC_DATA (routine.h:482) makes mach_port_name_t a non-port type, so rtSimpleRequest = TRUE and the request carries no msgh_body — which the frozen server rejects. |
| …therefore MIG is unusable here and a trap is required | Wrong. A non-sequitur. We are not obliged to match the frozen server — we can regenerate it too, at which point both sides use migcom’s form and agree. |
| The kernel MIG receive path may panic on a cold round-trip | Wrong. Inherited from a stale comment in mach_test.c:12-26 (“panics with strange destination rights… becomes a Phase C concern”) and deferred rather than tested. It was tested. It works. |
Recorded deliberately: the deferral was the actual error. A one-afternoon runtime probe would have settled the architecture before any of the design work around it, and the comment that caused the deferral describes a different code path.
A fair challenge before committing: did we once use MIG for these calls and retreat to traps because
making MIG work was too hard? If so, this plan would be walking back into a known dead end. Searched
across all 578 commits of nextbsd-userland:
| Question | Answer |
|---|---|
Has a .defs for any kernel subsystem ever existed — including deleted? | No. git log --all --diff-filter=AD over mach_port.defs, task.defs, host_priv.defs, mach_host.defs returns nothing. |
| Has a generated kernel MIG client ever existed? | No. Same search over *User.c forms returns nothing. |
| Where did the stubs come from, then? | The day launchd-842 was vendored verbatim (2283518), followed hours later by a4af1d5 — “Phase I1c: link-stage round 2”. Sibling commits are labelled “compile-clear stubs for HAVE_MACH”. |
The traps were never a retreat from MIG — MIG was never tried here. The traps came
first (9917b83, “libmach userland shim for the wired Mach trap family”), and
the stubs were link-stage scaffolding to get the vendored launchd to build. They were never revisited.
The one commit that superficially looks like a retreat — 176c543,
“drop the mach.ko trap multiplexor for dedicated syscalls” — is about syscall
slot allocation, folding five ops out from behind one multiplexed slot. It does not mention MIG.
9fe3d79
(2026-05-23) shows MIG out-of-line transfer being debugged end to end for launchctl list,
with lldb, a serial console and targeted kernel printfs. The resolution was a deliberate trade:
mig_deallocate and vm_deallocate became no-ops with a bounded leak,
because the OOL pointer handed back is an interior offset into a kernel-mmap’d message body rather
than a page base, so free() faults and munmap() would fail. The commit message
names the proper fix — “mmap-based mig_allocate + an OOL-receive path that
hands back a real base pointer and true mapped size” — and defers it
“for when the leak budget shows up as a real problem.” Phase 3 is that
moment. See the risks below.
Not for this layer, and the distinction is worth stating precisely because the headline impression is the opposite.
Where the impression is right: XPC has largely displaced MIG for new userland IPC — app↔daemon and daemon↔daemon. That is what Apple documents for third parties, and where their own newer services go.
Where it does not apply: XPC is layered on Mach messaging, not a replacement for
it, and there is no XPC path to the kernel. Kernel RPC — mach_port_*,
task_*, host_*, vm_* — is MIG and remains MIG. So is exception
handling (exc.defs, mach_exc.defs). Measured against Apple’s current sources:
bootstrap_cmds-138, tagged 2025-04-30..defs files, and mach_port.defs is listed in MIG_DEFS in osfmk/mach/Makefile — so Apple generates mach_port_server.c from it at build time. Path A restores precisely Apple’s arrangement rather than inventing one.mach_eventlink.defs, arcade_register.defs, memory_error_notification.defs, mach_voucher*.defs, iocompressionstats_notification.defs are all modern additions.So this is not investment in a deprecated technology. It is the one layer XPC does not reach.
MIG and XPC are not alternatives, and getting MIG working does not make libxpc
droppable. They sit at different layers and answer different questions. MIG is compile-time-typed
RPC whose interface is fixed in a .defs; XPC is dynamically typed, self-describing dictionaries
with no interface definition at all. xpc_dictionary_get_string(msg, key) for an arbitrary
runtime key is not expressible in MIG, by construction.
The tree already carries 24 .defs files — configd, ipconfigd, notifyd,
ASL, WLAN, kext_tools, libdispatch’s firehose, and seven for launchd alone
(job.defs, job_forward.defs, job_reply.defs,
protocol_jobmgr.defs, helper.defs, internal.defs,
notify.defs). That same launchd holds 178 XPC references
(xpc_domain, xpc_service, xpc_bootstrapper,
xpc_singleton_domain). The XPC layer rides on the MIG control protocol, exactly as it does at
Apple. src/libxpc is the iXsystems 2014–15 implementation, ~13k LOC, serialising
nvlists over raw mach_msg — a real implementation with real consumers
(syslog 84 references, Libnotify 58), not a stub layer.
aslmanager — today the
only consumer of on-demand Mach launch, and therefore the acceptance test for this whole plan —
registers itself with xpc_connection_create_mach_service("com.apple.aslmanager", …).
libxpc is what will exercise the MIG fix. Dropping it would remove the thing that proves
the work.
mach_port.defs needs is already in the tree at
src/libmach/include/mach/: mach_port_name_t and
mach_port_name_array_t in std_types.defs; mach_port_flavor_t,
ipc_space_t and mach_port_info_t in mach_types.defs. The last of
these reads array[*:17] of integer_t, which matches the frozen server’s
port_info_out[17] exactly — an independent check that the reconstruction is on the
right track before a line of it is written.
Two C programs, compiled natively on the VM with /usr/local/bin/clang19 against /usr/lib/system/libsystem_kernel.so.0. No kernel build, no reboot, nothing that could damage the host — worst case the test process dies.
mach_port_get_attributes, msgh_id 3217, MACH_PORT_RECEIVE_STATUS. The message is hand-built to the shape the frozen server demands, which is the inverse of modern MIG output: MACH_MSGH_BITS_COMPLEX set with a descriptor count of zero.
sizeof(Request) = 48 (server expects 48 from userland)
allocated probe port: 0x11
sending msgh_id=3217 COMPLEX, desc_count=0, size=48 ...
mach_msg returned 0x0 (0)
reply msgh_id = 3317 (expect 3317)
RetCode = 0
outCnt = 10
mps_pset=0 seqno=0 mscount=0 qlimit=5 MSGCOUNT=0
RESULT: kernel MIG WORKS
qlimit=5 is MACH_PORT_QLIMIT_DEFAULT — real kernel state, not a zeroed buffer. That distinction is what makes this a result rather than a coincidence.
mach_port_get_set_status, msgh_id 3214 — the exact call libmach fakes. A port set is populated with a known member via the real mach_port_move_member trap, then the kernel is asked for its membership back. This exercises vm_map_copyin in the kernel and the OOL copyout into our address space, which probe 1 did not.
pset=0x11 member=0x12
move_member -> 0x0 (ok)
sending msgid 3214 for pset 0x11 ...
mach_msg -> 0x0
reply id=3314 size=56 complex=yes desc_count=1
membersCnt=1 ool.size=4 ool.address=0x1000
member[0] = 0x12 <-- the port we inserted
RESULT: OOL round-trip WORKS -- kernel returned the real member
mach_port_gst_helper, allocates, and copies out. The kernel has been able to answer this question all along. Userland simply never asked.
The guiding requirement is to use MIG where Apple does. Apple’s own sources answer this precisely, and the answer is not “everywhere”.
Every mach_port routine in libsyscall has a generated MIG client named _kernelrpc_mach_port_*. Some routines additionally have a trap, which is tried first and falls back to MIG on failure:
/* xnu/libsyscall/mach/mach_port.c */
mach_port_allocate(ipc_space_t task, mach_port_right_t right, mach_port_name_t *name)
{
kern_return_t rv;
rv = _kernelrpc_mach_port_allocate_trap(task, right, name);
if (rv == MACH_SEND_INVALID_DEST) {
rv = _kernelrpc_mach_port_allocate(task, right, name); /* MIG */
}
return rv;
}
mach_port_names and mach_port_get_set_status have no trap at all at Apple. You cannot return a variable-length array through a register-based trap interface. This is precisely why our stub had to invent an empty list rather than do something useful:
mach_port_get_set_status(ipc_space_t task, mach_port_name_t name,
mach_port_name_array_t *members, mach_msg_type_number_t *membersCnt)
{
kern_return_t rv;
rv = _kernelrpc_mach_port_get_set_status(task, name, members, membersCnt);
return rv; /* MIG only. No trap exists. */
}
In mach_port_get_attributes, Apple handles a kernel whose trap does not exist yet by mapping the failure onto MACH_SEND_INVALID_DEST so control falls through to MIG:
rv = _kernelrpc_mach_port_get_attributes_trap(task, name, flavor,
port_info_out, port_info_outCnt);
#ifdef __x86_64__
/* REMOVE once XBS kernel has new trap */
if (rv == ((1 << 24) | 40)) { /* see mach/i386/syscall_sw.h */
rv = MACH_SEND_INVALID_DEST;
}
#endif
if (rv == MACH_SEND_INVALID_DEST) {
rv = _kernelrpc_mach_port_get_attributes(task, name, flavor,
port_info_out, port_info_outCnt);
}
MIG is the always-correct path. The trap is an optimisation that is permitted to be absent. A NextBSD that has traps but no MIG has built the fast lane without the road.
Compared against the 130 entries in xnu/osfmk/kern/syscall_sw.c. Bare-name matching gives false negatives here — Apple names the mach_port entries _kernelrpc_…_trap — so the comparison below matches both forms.
| NextBSD trap (mach_traps.c) | Apple | Action |
|---|---|---|
mach_msg_trap | trap | Keep as-is. Genuine Mach traps at Apple too. |
task_self_trap | trap | |
host_self_trap | trap | |
thread_self_trap | trap | |
mach_port_allocate | _kernelrpc_mach_port_allocate_trap | Keep, but reshape into Apple’s trap-first / MIG-fallback form. |
mach_port_deallocate | _kernelrpc_mach_port_deallocate_trap | |
mach_port_insert_right | _kernelrpc_mach_port_insert_right_trap | |
mach_port_move_member | _kernelrpc_mach_port_move_member_trap | |
mach_port_request_notification | _kernelrpc_mach_port_request_notification_trap | |
host_set_special_port | MIG only | Convert to MIG. These are NextBSD inventions with no counterpart in Apple’s trap table. |
task_get_special_port | MIG only | |
task_set_special_port | MIG only |
Roughly 18 entry points in mach_traps.c return KERN_SUCCESS having done nothing. The mach_port ones:
mach_port_get_set_status <-- kills on-demand Mach launch in launchd
mach_port_type <-- Apple traps this, with MIG fallback
mach_port_mod_refs <-- silently no-ops reference counting
mach_port_set_mscount
mach_port_get_context
mach_port_set_context
mach_port_construct
mach_port_destruct
The current get_set_status, in full — this is the whole reason on-demand launch is dead:
mach_port_get_set_status(mach_port_name_t task, mach_port_name_t name,
mach_port_name_array_t *members, mach_msg_type_number_t *membersCnt)
{
(void)task; (void)name;
if (members != NULL)
*members = NULL;
if (membersCnt != NULL)
*membersCnt = 0;
return KERN_SUCCESS;
}
A code search for MACH_MSGH_BITS_COMPLEX across nextbsd-userland returns zero hits. No userland code hand-builds messages to the mach_port subsystem, so regenerating the server cannot break an existing caller. This is what makes a kernel-side regeneration tractable rather than reckless.
Two runtime probes, above. Outcome: kernel MIG works for both simple and complex-with-OOL replies; no kernel change is needed for correctness. Probes retained at migprobe.c / migprobe2.c and built on the VM at /tmp/migprobe{,2} so any later regression is one command away from being caught.
mach_port.defs no build requiredRecover the IDL for all 36 routines from the frozen mach_port_server.c. Signatures are directly readable from the dispatch bodies, e.g. :3453 gives mach_port_get_attributes(task, name, flavor, port_info_out, port_info_outCnt).
Apple’s mach_port.defs cannot be used as-is. NextBSD’s msgh_id map diverges from XNU mid-subsystem — 3217 is get_attributes here, and Apple’s ordering differs — so the reconstruction must preserve NextBSD’s order, using skip; for gaps.
Verification gate: run the in-tree migcom on the reconstruction and diff the generated server against the frozen one, routine by routine. The dispatch table (3200–3236, routine[36]) must come out identical. The only systematic difference should be the msgh_body in requests. Anything else is a transcription error and must be resolved before proceeding.
Add mach_port.defs to the tree as the source of truth and replace mach_port_server.c with generated output. Per the standing requirement, kernel changes land and merge before userland depends on them.
Belongs in src-overlay/, not patches/ — this is NextBSD’s own code, not a FreeBSD delta.
Generate mach_portUser.c into libmach and replace the fabricating mach_port stubs with real generated clients.
On-demand launch is broken twice over, and both breaks sit in the same eleven lines.
mportset_callback() (src/launchd/src/runtime.c:541) is the
per-event dispatcher for the demand port set:
mach_port_get_set_status(mach_task_self(), demand_port_set, &members, &membersCnt)
→ stub returns membersCnt = 0 ⇒ the loop body never executes
for (i = 0; i < membersCnt; i++) {
mach_port_get_attributes(..., MACH_PORT_RECEIVE_STATUS, &status, &statusCnt)
→ stub returns KERN_RESOURCE_SHORTAGE ⇒ continue, every member skipped
if (status.mps_msgcount) { ...dispatch the job... }
}
vm_deallocate(mach_task_self(), (vm_address_t)members, ...)
→ no-op ⇒ leaks once get_set_status is real
Fixing get_set_status alone would not revive on-demand launch —
get_attributes returns KERN_RESOURCE_SHORTAGE and every member is skipped
regardless. Both must land together. Conveniently, those are exactly the two routines Phase 0 probed,
and both are proven working against the kernel.
This phase therefore also depends on the OOL deallocation fix — see risks.
Reshape the 9 surviving traps into trap-first / MIG-fallback. Today resolve_syscall() failure returns KERN_RESOURCE_SHORTAGE; under Apple’s shape that is exactly where control should fall through to the MIG client. Convert the 3 non-Apple traps (host_set_special_port, task_get_special_port, task_set_special_port) to MIG.
Re-run both probes; confirm aslmanager — today the only consumer of on-demand Mach launch — actually spawns on demand; multiple reboot cycles; launchctl list diffed against a pre-change baseline for regressions.
mach_port_mod_refs real is a behavioural change, not a bug fix. It currently no-ops. Once it actually drops references, any latent over-release or missing-retain in the tree stops being harmless and starts destroying ports. This is the single most likely source of new instability and should land in its own commit, separable from the rest.
vm_deallocate in libmach
is a no-op. Once mach_port_get_set_status returns real out-of-line memory, every demand-port
event maps a fresh page that is never released — and mportset_callback is the hot path,
firing on every on-demand Mach message. It cannot simply be un-no-op’d: there are
12+ call sites in libsystem_asl relying on current semantics, and the 2026-05-23 commit
documents why naïve munmap fails on an interior pointer. The correct fix is the one that
commit specifies — make the OOL receive path hand back a true base and mapped size, then make
vm_deallocate real. Probe 2 is mildly encouraging here (ool.address=0x1000
is page-aligned), but that is one observation on one routine and settles nothing on its own.
msgh_id drift. IDs are subsystem base plus .defs index. A single misplaced or omitted skip; silently renumbers every routine after it, and the failure mode is calling the wrong kernel function rather than an error. The dispatch-table diff catches this; nothing else will.
mach_port routines at once, or only those with live callers? Whole-subsystem is cleaner and matches “measure ten times, cut once”, but widens the Phase 1 transcription surface.
task and host have the same frozen-server / stubbed-client split, and three of the traps above are task/host routines. Same treatment, or a separate effort once mach_port proves the pattern?
mach_port_type, mod_refs, construct, destruct, insert_member, extract_member, guard, unguard and get_attributes; we do not. Once MIG works these are pure optimisation. Deferred unless something measures slow.
Measured on gershwin-on-nextbsd. Apple sources: apple-oss-distributions/xnu, libsyscall/mach/mach_port.c and osfmk/kern/syscall_sw.c. Claims from measurement are marked apart from claims from reasoning throughout; where this page contradicts an earlier draft, the earlier draft is wrong and §“What changed” says why.