← Back · NextBSD Research · companion to the NVIDIA userland / display plan and the NVIDIA kext porting plan
LoginWindowIssue #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.
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.
card0 problem: VMs, hybrids, headless, failsafeTwo separate problems were tangled together in one ticket, and the interesting one is a kernel bug that nobody upstream has actually root-caused.
| Issue | What we thought | What 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.
card0 appears” — falseI 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.
Lost display notification proves async init” — falseIt 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.
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.
Every step below was read in the shipping source and independently re-verified by hand.
BUG_ON proves itThe 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.
vm_pagepage = 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_lock ≠ VPB_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_acquire ← lkpi_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.
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.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.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.
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 mapping — linux_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 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.
register_fictitious_range() → vm_phys_fictitious_reg_range() lives in drm_os_freebsd.c:82-107 and is called from amdgpu_device.c:4504, radeon_fbdev.c:291, and i915/display/intel_fbdev.c:277.grep -rn vm_phys_fictitious over the entire NVIDIA 595.84 tree: zero hits.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.
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.
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:
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.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.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.nextbsd, nextbsd-userland/overlay or gershwin-on-nextbsd. A wrong verdict is unrecoverable without a rescue USB.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.
| Mechanism | What 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 / IOServiceGetMatchingService | Clients 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. |
LaunchEvents → com.apple.iokit.matching | The 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. |
| WindowServer | Waits on IOFramebuffer — IOServiceGetMatchingService → IOServiceGetBusyState → IOServiceWaitQuiet. Never a /dev entry. |
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.
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.
| Component | Status | Reality check |
|---|---|---|
| In-kernel IOKit matcher | Shipped | sys/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 IORegistry | Shipped | sys/dev/iokit/iokit_registry.c, /dev/ioregistry, frozen ioctl ABI, Mach-port watch channel. Real, not synthesized. |
| libIOKit APIs | Shipped | IOServiceGetMatchingService(s), IOServiceAddMatchingNotification, IORegistryEntryGetBusyState, IOServiceWaitQuiet, IOKitWaitQuiet — all real implementations, none are stubs. |
| busyState / waitQuiet (#176) | Merged, but | Kernel 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. |
| kextd | Fire-and-forget | mach_msg RCV → OSKextLoad → kldload(2) → loop. No reply to the kernel, no attach callback, no way to say “I have pushed everything.” |
| launchd ordering | Absent | No 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. |
| notifyd | Unusable | Both 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 / kextstat | Shipped | The sleeper asset — see §10. /usr/sbin/kextload <bundle> is a genuine synchronous barrier. |
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.
card0 problem: VMs, hybrids, headless, failsafeYou 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.
| Environment | PCI id | DRM node? | Gate must |
|---|---|---|---|
| Bare-metal NVIDIA | 10de:* | yes (nvidia-drm) | Wait for the load, pin Driver "nvidia". |
| Bare-metal Intel / AMD | 8086:* / 1002:* | yes (i915 / amdgpu) | Wait, pin modesetting. |
| Hybrid laptop | both | card0=iGPU, card1=dGPU | Wait 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. |
| VirtualBox | 80ee:beef / 15ad:0405 | no | Terminal verdict, sub-second. drm-kmod 6.12-lts ships no vboxvideo (it has exactly amd, display, i915, radeon, scheduler, ttm). |
| VMware | 15ad:0405/0406 | no | Same — vmwgfx was removed from drm-kmod. |
QEMU -vga std CI | 1234:1111 | no | Same. This is what CI boots, so CI can only ever exercise the negative branch of the gate. |
| Parallels / UTM / qxl / virtio | 1ab8:4005 etc. | no | Same. |
| Hyper-V Gen2, ramfb, arm64 SoC, serial-only | — | no PCI display at all | sysctl dev.vgapci is empty → start immediately. Never an error, never a wait. |
| BMC / ASPEED server | 1a03:2000 | no | No claimant → start on scfb. |
| NVIDIA card, no kext installed | 10de:* | no | 0 s, not 10 s. Today this stalls for the full timeout on every boot. |
Kext present, kldload fails | any | no | Log 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.
| Option | Verdict | Why |
|---|---|---|
| Raise the 10 s cap | Rejected | The 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 card0 | Rejected | Waits 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) | Rejected | The 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 DRM | Rejected | The 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) | Rejected | A 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 glue | Rejected | Every 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 waits | Viable, later | Apple-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.matching | Right, long-term | The 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 patches | Rejected | Does 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 LinuxKPI | Ship first | The 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 verdict | Ship second | A 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 failsafes | Ship with it | Prerequisite for anything that can decline to start X. |
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).
kldload lines. On NextBSD they are already dead — build.sh removes /sbin/kldload entirely (#193), so they are command-not-found noise. But on the Gershwin-on-FreeBSD branch line 33 is live and wrong: it greps 0x1022, which is AMD’s host-bridge vendor, not 0x1002 (the GPU). And the NVIDIA line has always been a silent no-op: /boot/modulesn/nvidia.ko.xf86-video-vesa out of the fallback chain — but see the open question below first; this one is not safe to do blind.drmModeGetResources probe. All three are vacuous — they test state that is already true when card0 appears.IOKitWaitQuiet. It cannot see nvidia-drm, and it goes quiet before card0 exists. It would be a regression wearing an Apple badge.pfn += page_offset is compiled out on FreeBSD. (This page said otherwise for a day. It was wrong.)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 0x1022→0x1002 on the Gershwin-on-FreeBSD branch). Everything else waits on the two readings.
| Repo | Change | Notes |
|---|---|---|
nextbsd-kernel-modulesFirst |
#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-systemSecond |
#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-kernelFirst |
#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-userlandLong 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. |
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:
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.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.
# 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"
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.)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.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.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./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.