← Back · NextBSD Research · companion to the NVIDIA userland / display plan and the NVIDIA kext porting plan

GPU readiness & LoginWindow

Issue #390 asked: how does LoginWindow know the GPU is ready, and how would Apple do it? A 68-agent research workflow — six parallel passes, 60 adversarial verifiers, a synthesis and a completeness critic — came back with an answer nobody wanted: the premise is wrong, and the panic is not a readiness problem at all. It is a kernel defect: nvidia-drm hands LinuxKPI a raw VRAM BAR physical address as a PFN, and on amd64 PHYS_TO_VM_PAGE() returns a pointer to a vm_page_array entry that the kernel never initializes — so the only guard (a NULL check) passes and vm_page_busy_acquire() dereferences boot-time DRAM garbage. This page documents the mechanism, why it is intermittent, why the upstream “fix” does not fix it, how Apple genuinely solves readiness, and what to ship.

2026-07-13. Sources are file:line against nextbsd-userland, nextbsd-kernel, nextbsd-kernel-modules, gershwin-system, gershwin-on-nextbsd, freebsd-src@releng/15.1, freebsd/drm-kmod@6.12-lts, freebsd/freebsd-ports@main and NVIDIA 595.84. Every load-bearing claim below was handed to an adversarial verifier instructed to refute it; the three that decided the recommendation were then re-verified by hand.

Retraction ×2 This page has been corrected once already, and the correction was itself wrong. (1) My original #390 analysis — “NVKMS keeps initializing after card0” — is false (§2), and that still stands. (2) The first version of this page then blamed an integer underflow in nvidia-drm and told you to apply the upstream ports patch. That is also false. The underflow is real but inert on the faulting path, and the upstream patch cannot fix this panic — it is still wrong on FreeBSD as merged. The verified mechanism is in §3, and it puts the register_fictitious_range() theory back.

Contents

  1. The short version
  2. Three things #390 got wrong
  3. What actually panics — the verified mechanism
  4. Why the upstream patch does not fix it
  5. So what is #390 about? The four ordering bugs that survive
  6. How Apple actually does it
  7. What NextBSD already has — an inventory
  8. The no-card0 problem: VMs, hybrids, headless, failsafe
  9. Options, ranked
  10. The recommended gate
  11. What not to do
  12. Ticket plan
  13. Open questions — answer before shipping

1. The short version

Two separate problems were tangled together in one ticket, and the interesting one is a kernel bug that nobody upstream has actually root-caused.

IssueWhat we thoughtWhat it actually is
#391
the panic
A LinuxKPI↔nvidia-drm mapping defect exposed by a boot race. Exactly that — and it is only that. For a vidmem-backed NVKMS surface, nvidia-drm hands LinuxKPI the raw VRAM BAR physical address as a PFN. If that BAR sits below top-of-RAM, PHYS_TO_VM_PAGE() returns an in-range pointer into vm_page_array — to an entry amd64 never initializes — so the NULL guard passes and vm_page_busy_acquire() dereferences boot-time DRAM garbage. No gate prevents it. No upstream fix exists.
#390
the gate
ls /dev/dri/card* fires too early because NVKMS is still initializing; we need a real readiness signal, maybe IOKit-shaped. There is nothing to wait for after card0. nvidia-drm’s entire init runs synchronously inside kldload, and drm_dev_register() is the last step. The gate is still broken — for four different reasons than the one we filed (§5).

Ship order — genuinely unresolved The completeness critic warned that a correct gate guarantees nvidia-drm binds before X starts, which could turn an intermittent panic into a deterministic one. But §3 turned up a fact that may invert that: the fault is only reachable through the DRM OBJT_MGTDEVICE populate path. X on the nvidia DDX talks to /dev/nvidia0, whose older OBJT_DEVICE path uses vm_page_getfake() and handles arbitrary BAR addresses safely. That is exactly why upstream’s reporter saw “X11 fine, Plasma-Wayland panics every time” — and why our own hardware test recorded 0 panics once Driver "nvidia" was active.

So the gate + pinning Driver "nvidia" might remove our exposure rather than harden it — if our panic is really modesetting hotplug-attaching to nvidia-drm (the wrong-DDX bug in §5.2, which our own display plan already recorded as fatal). The discriminating evidence is one line of /var/log/Xorg.0.log from the panicking boot: which DDX was loaded? Do not ship either way until that is read.

2. Three things #390 got wrong

2.1 “NVKMS keeps initializing after card0 appears” — false

I asserted this from dmesg ordering. It does not survive contact with the source. In NVIDIA 595.84, nv_drm_register_drm_device() calls nv_drm_dev_load(), which performs all of the bring-up — nvKms->allocateDevice(), mode-config init, CRTC/plane enumeration, encoder/connector enumeration — and only then calls drm_dev_register(), the call that creates /dev/dri/card0. The source even says so:

/* Load DRM device before registering it */
nv_drm_dev_load(dev);          ← allocateDevice, mode-config, CRTCs, connectors
...
drm_dev_register(dev, 0);      ← /dev/dri/card0 appears HERE, last
nvidia-drm-drv.c:2019-2026 (595.84)

And LKPI_DRIVER_MODULE runs the whole thing from the MOD_LOAD event handler — i.e. inside kldload, under kld_sx, synchronously. The instant card0 exists, there is nothing left to settle. Raising the 10 s cap, adding a settle delay, or probing drmModeGetResources() are all vacuous: they test state that is already true before the node appears.

2.2 “Lost display notification proves async init” — false

It is an EVO core-channel notifier timeout that NVKMS papers over (nvkms-dma.c:257-268), emitted from modeset work that X itself triggers when it becomes DRM master. It arrives because X started. It cannot be waited out before X starts, and it is not a readiness signal.

2.3 “A readiness gate would prevent the panic” — false, and this is the load-bearing one

The panic reproduces with no X at all: a GTX 750 Ti under Hyprland and under niri (FreeBSD forums 102022), and the same RTX 3060 Ti under kwin_wayland (drm-kmod#308). The NVKMS GEM ioctls are DRM_RENDER_ALLOW (nvidia-drm-drv.c:1807-1815), so the faulting path is reachable through the render node, with no DRM master, by any unprivileged GL client. No gate, of any shape, in any process, prevents it.

3. What actually panics — the verified mechanism

Every step below was read in the shipping source and independently re-verified by hand.

3.1 The faulting object is vidmem-backed, and BUG_ON proves it

The fault handler has two branches:

page_offset = vmf->pgoff - drm_vma_node_start(&gem->vma_node);   /* 0 - 0x100000 => underflows */

if (nv_nvkms_memory->pages_count == 0) {              /* VIDMEM */
    pfn = (unsigned long)(uintptr_t)nv_nvkms_memory->pPhysicalAddress;
    pfn >>= PAGE_SHIFT;                                /* <- a raw VRAM BAR1 address */
#if defined(NV_LINUX)
    pfn += page_offset;                               /* <- COMPILED OUT ON FreeBSD */
#endif
} else {                                              /* SYSMEM */
    BUG_ON(page_offset >= nv_nvkms_memory->pages_count);
    pfn = page_to_pfn(nv_nvkms_memory->pages[page_offset]);
}
nvidia-drm-gem-nvkms-memory.c:119-138 (595.84, FreeBSD tarball)

LinuxKPI’s BUG_ON is an unconditional panic(), not an INVARIANTS-gated no-op:

#define	BUG_ON(cond)		do {				\
	if (cond) {						\
		panic("BUG ON %s failed at %s:%d",		\
		    __stringify(cond), __FILE__, __LINE__);	\
	}							\
} while (0)
freebsd-src releng/15.1, sys/compat/linuxkpi/common/include/linux/kernel.h:90-95

So if the sysmem branch were taken with an underflowed page_offset, the box would print BUG ON page_offset >= ...not a general protection fault. Both reporters photographed “Fatal trap 9: general protection fault”. Therefore the fault took the vidmem branch (pages_count == 0, which is exactly what nvKms->isVidmem() short-circuits to on a discrete GPU).

The underflow is a red herring On the vidmem branch, pfn += page_offset sits inside #if defined(NV_LINUX) and is compiled out on FreeBSD. The underflowed value is computed and then never used. It cannot be the cause — and the first version of this page was wrong to say it was. pfn is simply the raw BAR1 physical address.

3.2 The kernel then dereferences an uninitialized vm_page

page = vm_page_grab_iter(vm_obj, pindex, VM_ALLOC_NOCREAT, &pages);
if (page == NULL) {
    page = PHYS_TO_VM_PAGE(IDX_TO_OFF(pfn));
    if (page == NULL)
        return (VM_FAULT_SIGBUS);                       /* <- the ONLY guard */
    if (!vm_page_busy_acquire(page, VM_ALLOC_WAITFAIL)) /* <- the crash */
sys/compat/linuxkpi/common/src/linux_page.c:526-531

On amd64 (VM_PHYSSEG_DENSE), PHYS_TO_VM_PAGE() is bounds-checked — but the bound is the array span, not validity:

pi = atop(pa);
if (pi >= first_page && (pi - first_page) < vm_page_array_size) {
    m = &vm_page_array[pi - first_page];    /* <- NO validity check */
    return (m);
}
return (vm_phys_fictitious_to_vm_page(pa)); /* NULL unless a driver registered it */
sys/vm/vm_page.c:1300-1305

And here is the load-bearing fact — amd64 never initializes the vm_page_array entries that correspond to PCI/MMIO holes:

#if defined(__i386__) && defined(VM_PHYSSEG_DENSE)     /* <- amd64 EXCLUDED */
	for (ii = 0; ii < vm_page_array_size; ii++) {
		m = &vm_page_array[ii];
		vm_page_init_page(m, (first_page + ii) << PAGE_SHIFT, 0, VM_FREEPOOL_DEFAULT);
		m->flags = PG_FICTITIOUS;
	}
#endif
sys/vm/vm_page.c:783-790 — only pages inside vm_phys_segs are initialized on amd64

…and pmap_page_array_startup() maps that array from vm_phys_early_alloc() and never zeroes it (sys/amd64/amd64/pmap.c:4960-4999). So the returned struct vm_page is boot-time DRAM garbage. vm_page_busy_acquire() loads m->object, fails the tryacquire (garbage busy_lockVPB_UNBUSIED), and calls VM_OBJECT_WOWNED(obj)rw_wowned(&obj->lock) on a garbage pointer — trap 9, non-canonical address.

Corroboration drm-kmod#308’s backtrace literally shows the faulting IP inside _rw_wowned(), called from vm_page_busy_acquirelkpi_vmf_insert_pfn_prot_locked__nv_drm_gem_nvkms_handle_vma_fault. That is this mechanism, frame for frame. Trap 9 (not 12) is the signature of a non-canonical pointer — exactly what a garbage m->object produces.

3.3 Why it is intermittent — and why some machines never see it

  1. The garbage varies boot to boot. Whatever DRAM vm_phys_early_alloc() handed out lands in that vm_page. A non-canonical m->object → GPF. A NULL or canonical one → no crash that boot. This is the “reboot and retry often succeeds” symptom, and it is the piece a deterministic mechanism could never explain.
  2. Whether the session CPU-faults a vidmem NVKMS mapping at all. X on the nvidia DDX goes through /dev/nvidia0, whose older OBJT_DEVICE path uses vm_page_getfake() and handles arbitrary BAR addresses safely (linux_compat.c:452-492). Only the DRM OBJT_MGTDEVICE populate path panics. Upstream’s reporter: X11 fine, Plasma-Wayland panics every time.
  3. Whether the box is even vulnerable. The BAR must sit below top-of-RAM to land inside vm_page_array’s span. Above it, the fictitious lookup returns NULL and you get a clean SIGBUS, not a panic.

Cheap falsifiable test On the RTX box, compare the NVIDIA BAR1 base (pciconf -lb) against top-of-RAM (sysctl hw.physmem, vm.phys_segs). This mechanism predicts BAR1 lands below top-of-RAM on machines that panic, and above it on machines that only ever SIGBUS. If BAR1 is above top-of-RAM on our box, this diagnosis is wrong and I want to know.

4. Why the upstream patch does not fix it

The first version of this page told you to apply freebsd-ports commit 1959aaa9de6a (“Fix GPF in some configs”, 2026-07-01), which carries NVIDIA’s c81cb2d8759. Retracted. Here is what that patch actually does and why it cannot help.

Its only effect on the vidmem path is to enable pfn += page_offset (by deleting the #if defined(NV_LINUX)). The PFN remains a BAR-derived PFN handed to PHYS_TO_VM_PAGE(). Nothing in the patch registers a fictitious range and nothing changes the pager path — so every step of §3.2 still executes.

Worse: as merged, the helper is still wrong on FreeBSD. Compare it with TTM, which is correct (and is why amdgpu/i915 do not do this):

/* TTM — correct on FreeBSD.  ttm_bo_vm.c:210-211 */
page_offset = ((address - vma->vm_start) >> PAGE_SHIFT) + vma->vm_pgoff
              - drm_vma_node_start(&bo->base.vma_node);      /* = pidx + N - N = pidx  OK */

/* NVIDIA c81cb2d8759, as merged into ports.  nvidia-drm-helper.h */
unsigned long offset = OFF_TO_IDX((uintptr_t)vmf->virtual_address);  /* = pidx, 0-based */
return offset - drm_vma_node_start(&gem->vma_node);                  /* = pidx - N  => UNDERFLOW */

The helper drops the + vma->vm_pgoff term that cancels the subtraction. On FreeBSD pidx is 0-based within the mappinglinux_file_mmap_single() sets *offset = 0 (linux_compat.c:1420) after handing the DRM fake offset to cdev_pager_allocate(), and the VMA is built with vm_start = 0, vm_pgoff = node_start (:1302-1304). So the correct FreeBSD answer is just OFF_TO_IDX(vmf->virtual_address), and the merged code computes pidx - 0x100000.

It was tested, and it “worked” The reporter on FreeBSD PR 296195 did verify on hardware (“kernel panic no longer occurs and plasma6 loads”), and NVIDIA’s own author hedged: “I’m not 100% it could fix this issue.” The most likely reconciliation — inference, not established — is that subtracting ~4 GiB moved the bogus PFN out of the uninitialized hole and into real, initialized RAM, so the fault silently succeeded while mapping the wrong physical memory. His own follow-ups (console-switch corruption, stale content from a dead Wayland session) are consistent with that. “The panic stopped on one machine” is not evidence the offset math is right — and the source says it is not.

The real fix, and it is ours to write

The gap the first version of this page dismissed as “incidental” is the actual defect: nvidia-drm never registers its VRAM aperture, while every in-tree FreeBSD DRM driver does.

Registering the aperture initializes exactly those vm_page_array hole entries in place, so PHYS_TO_VM_PAGE() stops returning garbage. Independently, lkpi_vmf_insert_pfn_prot_locked() should validate the PFN instead of trusting a NULL check that garbage passes — that is the “userland must never panic the kernel” principle #391 was opened on, and it is now the primary fix, not defence-in-depth.

What survives from the first version

One finding stands, and it is still worth a ticket — just not as the panic fix. build.yml:800 applies a hand-picked allowlist of four extra-patch-* files, while the ports framework auto-applies every files/patch-*. So we do silently drop upstream nvidia-drm patches. That is a real defect in the build; it simply is not what is panicking the box.

5. So what is #390 about? The four ordering bugs that survive

The gate does not fix the panic. It never did. But it is not worthless — it is fixing four real, currently-live bugs that were hiding behind the panic, and every one of them is about correctness and boot behaviour, not safety:

  1. X can start before the GPU kext load has even been requested. kextd’s boot personality push races LoginWindow’s launchd start, and launchd has no ordering primitive at all (its own com.apple.kextd.plist says so). A poll-then-give-up loop structurally cannot wait for a load nobody has asked for yet. This is the actual race, and it is not the one we filed.
  2. Wrong-DDX selection. XLibre picks nvidia_drv.so only via its OutputClass MatchDriver "nvidia-drm" rule, which requires the DRM platform device to exist at server start. The FreeBSD PCI fallback list for 0x10de is [modesetting, nvidia, nv]modesetting first. Start X early and you get the wrong DDX, or xf86platformAddDevice() hotplug-attaches modesetting to nvidia-drm later and takes the CREATE_DUMB path already proven fatal on this box.
  3. Hybrid-GPU misattribution. DRM primary minors come first-come-first-served from one shared xarray, so on any Intel-iGPU + NVIDIA-dGPU laptop i915kms takes card0 and nvidia-drm takes card1. ls /dev/dri/card* is then satisfied with certainty, on iteration 1, by the wrong GPU — made worse by LoginWindow.sh:33, which kldloads i915kms itself immediately before the loop.
  4. No failsafe, and a 10 s stall on every box with no DRM driver. There is no safe-graphics mode anywhere in nextbsd, nextbsd-userland/overlay or gershwin-on-nextbsd. A wrong verdict is unrecoverable without a rescue USB.

6. How Apple actually does it

You asked whether we should add “some kind of Mach support to LoginWindow so it cannot start until IOKit says we are good — however Apple does it.” Here is what Apple actually does, and the honest verdict on whether it would have helped.

Readiness is driver-asserted, never inferred

MechanismWhat it really is
registerService()The driver calls it when it is ready to be used, publishing an IOService into the registry. It is a promise by the driver (IOService.h:650-655), not a kernel check. Nothing verifies it.
IOServiceAddMatchingNotification / IOServiceGetMatchingServiceClients block until that service is published, rather than spinning on the filesystem.
IOKitWaitQuiet() / IOServiceWaitQuiet()Blocks until the registry is quiet — all matching/probing settled. XNU holds the registry root busy from StartIOKitMatching() until iokitDaemonLaunched(), so it cannot succeed vacuously at boot. Returns kIOReturnTimeout on deadline.
LaunchEventscom.apple.iokit.matchingThe launchd-native shape: a job is launched by a service matching. Real example: com.apple.iomfb_fdr_loader.plist uses IOPropertyMatch { IOMFBLaunchFDR: true } — the job starts when the driver sets that property.
WindowServerWaits on IOFramebufferIOServiceGetMatchingServiceIOServiceGetBusyStateIOServiceWaitQuiet. Never a /dev entry.

Why Apple has no race at all — and why we can’t copy it

The structural detail that matters: IOFramebuffer::start() only publishes. enableController() runs inside IOFramebuffer::open()the WindowServer’s open of the framebuffer user client is what drives modeset. The display server is not racing the driver; it is the trigger. There is no window to lose.

nvidia-drm.ko inverts that completely. It is a Linux-shaped module with no IOService, no registerService(), and nothing to publish. And per §2, it has no readiness edge left to publish anyway — by the time anything could observe it, init is done.

The honest answer Apple’s model requires the driver to sign a contract. An opaque vendor blob will never sign it. We can build the whole Apple-shaped apparatus — and we arguably should, because launch-on-hardware is the right architecture for NextBSD — but IOKit matching fires on publication, which is the same too-early edge as /dev/dri/card0. The event plumbing is the easy half; the readiness predicate is the whole problem, and it does not exist. Build it because it is right, not because it fixes this.

7. What NextBSD already has — an inventory

The good news from the research: we have far more of this machinery than LoginWindow.sh uses. The bad news: none of it can see nvidia-drm.

ComponentStatusReality check
In-kernel IOKit matcherShippedsys/dev/iokit/iokit_catalogue.c. hwregd is retired (#218); the kernel matches and asks kextd to load via Mach message to HOST_KEXTD_PORT. Three load triggers, no settle timer (#67 is closed).
In-kernel IORegistryShippedsys/dev/iokit/iokit_registry.c, /dev/ioregistry, frozen ioctl ABI, Mach-port watch channel. Real, not synthesized.
libIOKit APIsShippedIOServiceGetMatchingService(s), IOServiceAddMatchingNotification, IORegistryEntryGetBusyState, IOServiceWaitQuiet, IOKitWaitQuietall real implementations, none are stubs.
busyState / waitQuiet (#176)Merged, butKernel patch 0002 brackets device_probe_and_attach() with device_match_start/end; mach.ko keeps mach.bus.busy + a mach_wait_quiet syscall. It cannot see nvidia-drm at all — that registers via LKPI_DRIVER_MODULE (a MOD_LOAD evhand), not newbus. It goes quiet before card0 exists. Gating on it is strictly weaker than today’s loop.
kextdFire-and-forgetmach_msg RCV → OSKextLoadkldload(2) → loop. No reply to the kernel, no attach callback, no way to say “I have pushed everything.”
launchd orderingAbsentNo ordering key. KeepAlive→PathState, WatchPaths, QueueDirectories are parsed nowhere in core.c. LaunchEvents parses (core.c:3038) but no event-monitor job exists, so the stream has no publisher and any LaunchEvents plist silently never fires. xpc_set_event_stream_handler is declared (xpc.h:2525) and defined nowhere.
notifydUnusableBoth in-tree clients had to comment out notify_post/notify_register_dispatch because libnotify aborts the caller (#160, still open). Do not build on it.
kextload / kextstatShippedThe sleeper asset — see §10. /usr/sbin/kextload <bundle> is a genuine synchronous barrier.

The barrier that already exists

This one deserves its own note, because the critic tried to kill it and the attempt proved it instead.

kern_kldload() takes kld_sx exclusive and holds it unbroken across linker_file_sysinit() — i.e. across MOD_LOAD, which is where nv_drm_init() and drm_dev_register() run. kextd’s autoload goes through that same syscall. The critic objected that OSKextLoad short-circuits in userland (if (OSKextIsLoaded(thisKext)) continue;, OSKext.c:8654) and would skip the syscall, leaving no barrier. But:

Boolean OSKextIsLoaded(OSKextRef aKext) {
    ...
    return (kldfind(execName) >= 0) ? true : false;   ← a live syscall, not a cache
}
nextbsd-userland/src/kext_tools/kext.subproj/OSKext.c:10050-10068

kldfind(2) takes kld_sx exclusive. So the “short-circuit” is a lock acquisition: it blocks until the concurrent loader’s MOD_LOAD has returned, then reports loaded. The barrier holds in every interleaving — including the one that matters, where kextd has not yet been asked to load anything, because then kextload simply performs the load itself.

Trap Never use kextstat -m <module> as the barrier. That is modfind(2), which takes only MOD_SLOCK and sees a module registered before MOD_LOAD runs. It will lie to you in precisely the racing window. Use kldstat -n (kldfind) or the kextload return.

8. The no-card0 problem: VMs, hybrids, headless, failsafe

You flagged this and you were right to: a gate that hangs — or that starts X on the wrong assumption — on these is unacceptable. The decisive insight is that the gate must be driver-keyed, not vendor-keyed. Today’s grep -qE 'vendor=0x(8086|1002|10de)' allowlist is an accident waiting to happen: it goes stale the day a virtual-GPU kext ships.

EnvironmentPCI idDRM node?Gate must
Bare-metal NVIDIA10de:*yes (nvidia-drm)Wait for the load, pin Driver "nvidia".
Bare-metal Intel / AMD8086:* / 1002:*yes (i915 / amdgpu)Wait, pin modesetting.
Hybrid laptopbothcard0=iGPU, card1=dGPUWait for both, keyed per PCI device. Never glob. Do not use hw.pci.default_vgapci_unit to skip the NVIDIA arm — on Optimus that names the iGPU, and the dGPU is the thing that panics.
VirtualBox80ee:beef / 15ad:0405noTerminal verdict, sub-second. drm-kmod 6.12-lts ships no vboxvideo (it has exactly amd, display, i915, radeon, scheduler, ttm).
VMware15ad:0405/0406noSame — vmwgfx was removed from drm-kmod.
QEMU -vga std CI1234:1111noSame. This is what CI boots, so CI can only ever exercise the negative branch of the gate.
Parallels / UTM / qxl / virtio1ab8:4005 etc.noSame.
Hyper-V Gen2, ramfb, arm64 SoC, serial-onlyno PCI display at allsysctl dev.vgapci is empty → start immediately. Never an error, never a wait.
BMC / ASPEED server1a03:2000noNo claimant → start on scfb.
NVIDIA card, no kext installed10de:*no0 s, not 10 s. Today this stalls for the full timeout on every boot.
Kext present, kldload failsanynoLog loudly, fall back to scfb, do not stall.

The design consequence: resolve which kext claims each GPU by grepping IOPCIPrimaryMatch in the on-disk /System/Library/Extensions/*/Contents/Info.plist. That is race-free by construction — it reads the on-disk repo, not the kernel catalogue (which only exists after kextd’s push, and kextd’s push is the thing we are racing). “No claimant” then becomes a sub-second fact rather than a 10-second timeout, and when BochsGraphics.kext or VBoxGraphics.kext eventually ship, the same code starts waiting for them correctly with no edit.

9. Options, ranked

OptionVerdictWhy
Raise the 10 s capRejectedThe loop already exits early and successfully. The cap only governs the case where no driver will ever load — where it is pure wasted boot time.
Settle delay after card0RejectedWaits out something that does not exist. If it appears to help, that is only because it accidentally waits for kextd’s kldload — and the barrier does that deterministically, for free.
DRM readiness probe (drmModeGetResources)RejectedThe most tempting wrong answer. Connector/CRTC enumeration all happen inside nv_drm_dev_load(), before drm_dev_register(). The probe passes on the first attempt, at the same instant ls card0 does. It looks rigorous and is a no-op.
Stop X touching DRMRejectedThe hoped-for sleeper fix. Killed: the NVKMS GEM ioctls are DRM_RENDER_ALLOW, and the validated Driver "nvidia" Xorg holds the render node open for DRI3/GLX anyway.
Gate on IOKit waitQuiet (#176)RejectedA regression, and the most dangerous wrong answer because it looks authoritative. It cannot see nvidia-drm (not a newbus attach), it goes quiet before card0 exists, it returns success on timeout, and it is host-global.
Readiness sysctl in NVIDIA’s glueRejectedEvery signal it could publish fires at-or-earlier-than card0. The state we want lives in a closed blob and is not exported.
kextd publishes readiness; LoginWindow waitsViable, laterApple-shaped, and KeepAlive→OtherJobEnabled genuinely works. But the best predicate kextd can compute is “my loads returned” — which the barrier already observes directly, synchronously, with no IPC. And it fails closed on every VM.
Apple-native com.apple.iokit.matchingRight, long-termThe correct architecture; NextBSD is unusually well-placed for it. But ~700-1000 new lines, fail-closed on every VM by default, and it still does not fix this bug.
Apply the upstream nvidia-drm patchesRejectedDoes not fix this panic (§4). It only enables pfn += page_offset; the PFN stays a BAR address handed to PHYS_TO_VM_PAGE(). And as merged the helper is still wrong on FreeBSD (it drops TTM’s + vma->vm_pgoff term and underflows). Apply it for correctness of the offset math if you like — but not as the fix.
Register the VRAM aperture as a fictitious range + harden LinuxKPIShip firstThe actual fix. vm_phys_fictitious_reg_range() initializes the vm_page_array hole entries so PHYS_TO_VM_PAGE() stops returning garbage — exactly what amdgpu/i915/radeon already do and nvidia-drm alone omits. Plus: make lkpi_vmf_insert_pfn_prot_locked() validate the PFN and SIGBUS rather than trust a NULL check garbage passes.
Synchronous kext-load barrier + terminal verdictShip secondA real barrier (kld_sx), not a poll. No timeout, no sleep, zero new C. Closes the load-not-yet-requested race that no poll design can.
kenv failsafesShip with itPrerequisite for anything that can decline to start X.

10. The recommended gate

After #391 lands. Zero new C; every primitive below already ships.

# LoginWindow.sh — replaces the poll loop. No sleep, no seq, no timeout.

# ---------- 0. ESCAPE HATCHES (nothing like this exists today) ----------
[ "$(kenv -q nextbsd.nox)" = "1" ] && exec /bin/sh -c 'while :; do sleep 3600; done'
if [ "$(kenv -q nextbsd.safe_graphics)" = "1" ]; then
    force_scfb; exec LoginWindow          # <- ALWAYS boots. The recovery path.
fi

# ---------- 1. ENUMERATE EVERY PCI DISPLAY DEVICE ----------
# Not `ls /dev/dri/card*` (globs the wrong GPU on a hybrid) and not
# hw.pci.default_vgapci_unit (names the CONSOLE owner, which on Optimus is
# the iGPU — while the dGPU is the thing that matters).
for oid in $(sysctl -Nq dev.vgapci | grep '\.%pnpinfo$'); do ... done
[ -z "$GPUS" ] && { force_scfb; exec LoginWindow; }   # Hyper-V, ramfb, serial-only

# ---------- 2. WHICH INSTALLED KEXT CLAIMS EACH GPU? ----------
# Race-free BY CONSTRUCTION: reads the ON-DISK kext repo, not the kernel
# catalogue (which only exists after kextd's push — the thing we are racing).
kext_for_gpu() {                              # $1=vendor $2=device
    mw=$(printf '0x%s%s' "$2" "$1")           # IOPCIPrimaryMatch word = 0xDDDDVVVV
    for p in /System/Library/Extensions/*/Contents/Info.plist; do
        grep -qi "$mw" "$p" && { echo "${p%/Contents/Info.plist}"; return 0; }
    done
    return 1
}

# No claimant => TERMINAL, sub-second verdict. This is KNOWLEDGE, not a timeout.
# Covers: VirtualBox, VMware, QEMU (incl. CI), Parallels, UTM, BMC, and an
# NVIDIA card with no kext installed. Driver-keyed, so it does not go stale
# the day BochsGraphics.kext ships.
[ -z "$CLAIMED" ] && { force_scfb; exec LoginWindow; }

# ---------- 3. THE BARRIER ----------
# kextload -> OSKextLoad -> kldfind(2)/kldload(2). kern_kldload() holds kld_sx
# EXCLUSIVE across MOD_LOAD, and kextd's autoload goes through the same syscall.
# So this BLOCKS until nv_drm_init() — including drm_dev_register() — has
# returned, then succeeds. It also works when kextd has NOT YET REQUESTED the
# load, which is exactly what the old poll loop could not do.
for k in $CLAIMED; do kextload "$k" || log "kextload $k FAILED"; done

# ---------- 4. DID IT ACTUALLY BIND? ----------
# dev.drm.<minor>.PCI_ID is created for every drm_dev_register, nvidia-drm
# included. NOT `kextstat -m` — that is modfind(2), which lies pre-MOD_LOAD.
[ -z "$BOUND" ] && { force_scfb; exec LoginWindow; }   # loaded but never bound

# ---------- 5. PIN THE DDX ----------
# XLibre picks nvidia_drv.so ONLY via OutputClass MatchDriver "nvidia-drm",
# i.e. only if the DRM platform device exists AT SERVER START — which step 3
# now guarantees. The FreeBSD PCI fallback for 0x10de is
# [modesetting, nvidia, nv] — modesetting FIRST — and a hotplugged DRM device
# defaults to modesetting, i.e. the CREATE_DUMB path proven fatal on this box.
# NOTE: if nvidia-drm is bound but nvidia_drv.so is NOT activated, refuse the
# NVIDIA DDX and fall back to scfb — do NOT pin modesetting onto nvidia-drm.

exec LoginWindow

Refusal must never exit io.github.gershwin-desktop.loginwindow.plist has RunAtLoad and KeepAlive=<true/> (verified). job_keepalive() returns true on !ondemand without ever looking at exit status — so any exit yields an infinite respawn loop throttled to 10 s. A refusal path must terminate in a non-exiting state (exec sleep infinity).

Also in this PR

11. What not to do

12. Ticket plan

Unblocked Three of these need no hardware input at all and can be done before the readings in §13: the build.yml patch-allowlist fix; the nextbsd.safe_graphics/nextbsd.nox failsafes (plus the “refusal must never exit” bug — KeepAlive=<true/> makes any exit an infinite 10 s respawn loop); and deleting the three dead kldload lines (fixing 0x10220x1002 on the Gershwin-on-FreeBSD branch). Everything else waits on the two readings.

RepoChangeNotes
nextbsd-kernel-modules
First
#391: register the NVIDIA VRAM aperture as a fictitious range Call drm-kmod’s register_fictitious_range() (→ vm_phys_fictitious_reg_range()) over the NVIDIA BAR1/VRAM aperture at nvidia-drm device registration — exactly what amdgpu_device.c:4504, radeon_fbdev.c:291 and intel_fbdev.c:277 already do, and what nvidia-drm alone omits (zero hits for vm_phys_fictitious in the whole 595.84 tree). Goes in the nvidia-portpatch.sh we already own. First, get pciconf -lb from the box and confirm BAR1 sits below top-of-RAM.
gershwin-system
Second
#390: replace the poll loop with the kext-load barrier Per §10. State explicitly in the PR that this fixes ordering and does not fix the panic.
gershwin-system nextbsd.safe_graphics / nextbsd.nox kenv failsafes Refusal must never exit. Use absolute paths (/usr/sbin/kextload, /bin/kenv) — launchd’s PATH is not to be trusted.
nextbsd Loader: a Safe Graphics menu entry beastie_disable="YES" today, so the menu ships but is unreachable (the OK prompt still is). Without this, a wrong verdict needs a rescue USB.
nextbsd-kernel
First
#391 backstop: harden lkpi_vmf_insert_pfn_prot_locked() Its only guard is if (page == NULL) return VM_FAULT_SIGBUS — and on amd64/DENSE, PHYS_TO_VM_PAGE() returns a non-NULL pointer to an uninitialized vm_page for any address inside the array span, including a PCI hole. Require the pa to be in a physical segment or fictitious-registered; SIGBUS otherwise. This closes the unprivileged local kernel-panic DoS through the DRM render node regardless of what NVIDIA does, and it is the literal principle #391 was opened on. Note NextBSD’s kernel carries no LinuxKPI overlay today, so this is a new patch in nextbsd-kernel/patches/.
nextbsd-kernel-modules Apply all of the port’s files/patch-*, not an allowlist build.yml:800 hand-picks four extra-patch-* files; the ports framework auto-applies every files/patch-*. We therefore silently drop upstream nvidia-drm patches (including 1959aaa9de6a). A real build defect worth fixing on its own merits — but it is not the panic fix, and the offset patch it would pull in is itself wrong on FreeBSD (§4).
nextbsd-userland #176 follow-up: IOKitWaitQuiet must return kIOReturnTimeout; add an anti-vacuous busy floor Two genuine defects, independent of #390 — and the reason waitQuiet must not be the GPU gate. NextBSD’s bus_busy starts at zero, so it succeeds vacuously at t=0; XNU holds the registry root busy until the kext daemon checks in.
nextbsd-kernel-modules Land BochsGraphics.kext (1234:1111) CI boots QEMU -vga std and lands on scfb with no DRM node, so CI can only ever prove the gate’s negative branch. A bochs kext would give it a real match→autoload→bind path.
nextbsd-userland
Long term
UserEventAgent-equivalent + com.apple.iokit.matching stream launchd already parses LaunchEvents and has the whole XPC event-provider surface — but no publisher exists, so the stream silently never fires. Build it because it is the right architecture, not as a fix for this.

13. Open questions — answer before shipping

The completeness critic’s list, minus what I settled by hand (the kextload barrier is real; NextBSD’s kernel carries no LinuxKPI overlay; the conftest CFLAGS delta is innocent; the box has no Intel iGPU, so hybrid card0 misattribution is latent-only and not our intermittency). The first two now gate everything:

  1. Is BAR1 below top-of-RAM? The §3 mechanism predicts it is. If it is above top-of-RAM, PHYS_TO_VM_PAGE() returns NULL, the guard fires, and you would get a SIGBUS rather than a panic — which would falsify this whole diagnosis. Gates writing the #391 patch; nothing else.
  2. Which DDX is X actually loading? If modesetting, our panic is the wrong-DDX bug (§5.2) taking the DRM dumb-buffer path, and the gate + pinning Driver "nvidia" removes our exposure rather than making it deterministic — inverting the ship-order warning in §1. If nvidia, the critic’s warning stands and #391 must land first. This decides the sequencing of both tickets.

    Caveat The panicking boot’s Xorg.0.log probably did not survive the panic (unflushed). Do not plan on reading it. Ask the equivalent question of any boot instead — and note that if nvidia_drv.so was never activated (it is a separate pkg step), X cannot select the nvidia DDX and must be on modesetting/scfb, which would settle it on its own.

The 30-second read, when you are next at the box

# which DDX is X actually loading?
grep -iE "Loading.*drivers/|Using .* driver|\(II\) (NVIDIA|modesetting|scfb)" /var/log/Xorg.0.log
ls -l /usr/local/lib/xorg/modules/drivers/      # is nvidia_drv.so even present / activated?
sysctl hw.nvidiadrm.modeset hw.nvidiadrm.fbdev  # fbdev must be 0 (the portpatch sed is `opt` and no-ops silently)

# is the NVIDIA BAR1 below top-of-RAM?  (the §3 mechanism predicts YES on a panicking box)
pciconf -lb | grep -i -A6 nvidia
sysctl hw.physmem vm.phys_segs

# and the basics nobody has captured yet
pciconf -lv | grep -A3 vgapci
dmesg | grep -iE "nvidia|drm|nvkms"
  1. BIOS-mode VirtualBox will break if we do the scfb/vesa change blind. Both boot tests use OVMF, so CI always has an EFI GOP framebuffer and scfb always works. VirtualBox defaults to BIOS, where there is no GOP framebuffer for scfb and no DRM driver — making vesa the only DDX that can work. Do not write Driver "scfb" and do not remove xf86-video-vesa until someone boots BIOS-mode VirtualBox on today’s image and reports which DDX X actually selects. (Also: xlibre-minimal RUN_DEPENDS on vesa, so it cannot simply be deleted from pkglist.txt.)
  2. Define the nvidia-drm-bound-but-nvidia_drv.so-not-activated case. Activation is a separate pkg step, so this is a real state. Pinning modesetting there would select the CREATE_DUMB path this document elsewhere calls fatal. The safe action is to refuse the NVIDIA DDX and fall back — not to pin modesetting.
  3. Does nvidia_drv.so still work with hw.nvidiadrm.modeset=0? This decides whether modeset=0 is a valid loader-level safe-graphics knob for NVIDIA, or a black screen.
  4. Is hw.nvidiadrm.fbdev actually off in our kexts? nvidia-portpatch.sh:124’s fbdev sed is marked opt and no-ops silently if the string drifts. Grep CI for opt no-op: drm fbdev and check the sysctl on the box — if fbdev is on, that is an independent bug fighting X for modeset ownership.
  5. Does the single getty on /dev/console survive Xorg’s VT takeover? (#272.) Until answered, the kenv knobs — not the console — are the only proven rescue path.

Produced by a 68-agent workflow (six parallel research passes, 60 adversarial verifiers, a synthesis, and a completeness critic), then corrected twice by hand. Worth recording how: the initial research blamed an integer underflow and declared the panic fixed upstream. The critic objected that the mechanism predicted a deterministic panic while the ticket reports an intermittent one — and that objection, chased down, broke the story open: the underflow is compiled out on FreeBSD, the sysmem branch would have tripped a live BUG_ON rather than a GPF, and the real cause is a raw BAR PFN reaching an uninitialized vm_page_array entry that amd64 never initializes. The lesson is the critic’s, not the researchers’: a mechanism that cannot explain the defining symptom is not the mechanism. Corrections to #390 and #391 follow from this page.