efi_enter()/efi_leave() are not re-entrancy-safe despite LK_CANRECURSE: nested entry clobbers the global efi_savevm and the outer efi_leave() then installs a NULL vmspace β pmap panic (latent landmine)
Summary
The recursive lock invites nesting but efi_enter unconditionally overwrites the global efi_savevm: a nested entry would save efi_vmspace over the real user vmspace and the outer leave installs NULL -> vmspace_pmap(NULL) panic. No nested path exists today (all six call sites verified). Drop-CANRECURSE-or-depth fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5482 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.6 KB | β raw | |
| VERDICT.md | β | 1.9 KB | β raw | |
| verdict.json | β | 2.2 KB | view raw | |
| manifest.json | β | 506 B | view raw |
DF-5482 β efi_enter()/efi_leave() are not re-entrancy-safe despite LK_CANRECURSE: a nested efi_enter() clobbers the global efi_savevm and the outer efi_leave() then restores a NULL vmspace β pmap_setlwpvm(NULL) panic. Latent landmine (no nested path exists today).
FILE: sys/platform/pc64/x86_64/efirt.c (pass 2)
ROOT CAUSE (path:line):
* efirt.c:396: efi_lock is created LK_CANRECURSE β recursion is
explicitly permitted by the lock, advertising nested EFI calls as
legal.
* efirt.c:352: efi_enter() unconditionally does
efi_savevm = td->td_lwp->lwp_vmspace; β on a nested entry (same
thread, already inside the window) lwp_vmspace is ALREADY
efi_vmspace, so the outer context is lost.
* efirt.c:367-370: efi_leave() restores pmap_setlwpvm(td->td_lwp,
efi_savevm) and then sets efi_savevm = NULL β the inner leave
leaves the thread on efi_vmspace with efi_savevm == NULL; the outer
leave then calls pmap_setlwpvm(lp, NULL) β lp->lwp_vmspace = NULL β
vmspace_pmap(NULL) deref (pmap.c:6408-6410) β NULL-pointer panic.
REACHABILITY: none today β every efi_ entry point calls efi_enter() exactly once and firmware does not re-enter the kernel. The hazard fires the day any nested path appears (e.g. an eventhandler or var-service callback issuing another efi_ from inside the window, which is precisely what LK_CANRECURSE invites). Defensive/hardening finding: make the lock non-recursive (turn silent recursion into a deadlock/LKpanic at dev time) or track nesting depth per-thread and save/restore lwp_vmspace on a stack.
PACK: analysis-only; untested by design (Info hardening class).
DF-5482 VERDICT β efi_enter/efi_leave reentrancy landmine (latent)
Status: untested (Info hardening class; no reachable nested path exists today β nothing to reproduce by construction).
Trace
lockinit(&efi_lock, "efi", 0, LK_CANRECURSE)(efirt.c:396) β nested acquisition by the same thread is permitted.- Nested
efi_enter()(same thread):efi_savevm = td->td_lwp->lwp_vmspace(efirt.c:352) now stores efi_vmspace (set by the outer entry via pmap_setlwpvm), destroying the saved user context. - Inner
efi_leave():pmap_setlwpvm(td->td_lwp, efi_savevm)restores efi_vmspace onto itself;efi_savevm = NULL(efirt.c:367-370). - Outer
efi_leave():pmap_setlwpvm(td->td_lwp, NULL)βlp->lwp_vmspace = NULL(pmap.c:6408) βpmap = vmspace_pmap(newvm)(pmap.c:6410) dereferences NULL β panic.
No current caller nests (all six entry points wrap one efi_enter; the firmware cannot call back into the kernel), so this is latent. The mismatch between the lock advertising recursion and the save/restore machinery being single-flight is the defect; simplest fix: drop LK_CANRECURSE from efi_lock so any future nesting fails loudly at development time (or add a per-thread depth counter).
Verification
None possible/needed: the trigger path does not exist in the current call graph (verified by reading all callers of efi_enter: efirt.c:496, 525, 540, 569, 584, 600 β each wrapped once, no nesting possible from firmware side). Filed as Info so the landmine is on record.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
(No fix.diff for this item: documented recommendation only - see Fix above.)
Fix verification
not_testableNot applicable β latent defect, nothing to validate.
Confirmed kernel references
Detail
Evidence (decisive lines)
VERDICT.md four-step trace: efirt.c:396 (LK_CANRECURSE), efirt.c:352 (unconditional global save), efirt.c:367-370 (restore+NULL), pmap.c:6408-6410 (NULL vmspace deref).
PoC changes
None β trigger path does not exist in the current call graph (verified against all callers).
Verified recommended fix
Remove LK_CANRECURSE from efi_lock (or track nesting depth per-thread and stack the saved vmspace).
Verdict
efi_lock is created LK_CANRECURSE (efirt.c:396), inviting nested acquisition, but efi_enter()/efi_leave() are single-flight by construction: efi_enter() unconditionally overwrites the global efi_savevm (efirt.c:352) and efi_leave() clears it (efirt.c:370). A nested entry (same thread) would save efi_vmspace over the real user vmspace; the inner leave then restores efi_vmspace onto itself and NULLs efi_savevm; the outer leave calls pmap_setlwpvm(td->td_lwp, NULL) β lp->lwp_vmspace = NULL (pmap.c:6408) β vmspace_pmap(NULL) at pmap.c:6410 β NULL-pointer panic. No nested path exists today (all six efi_enter call sites β efirt.c:496/525/540/569/584/600 β wrap exactly one call; firmware cannot re-enter the kernel), so this is a latent landmine, not a reachable bug: Info hardening. Untested by construction; recommend dropping LK_CANRECURSE so any future nesting fails loudly, or adding a per-thread depth counter.
No comments yet.