⬢ DragonFlyBSD Kernel Audit
DF-5832 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-5832",
  "status": "not_reproduced",
  "reproduced": false,
  "impact": "none",
  "confidence": "likely",
  "verdict": "Structurally certain by inspection, not reproduced on the guest within ~80,000 race rounds. naccess() writes the cached world-r+x traversal flag NCF_WXOK from a lockless VOP_GETATTR_LITE snapshot while holding the ncp only SHARED (vfs_nlookup.c:1720-1762), and the fast path at vfs_nlookup.c:1616-1621 returns success for the VEXEC directory-search check purely on that flag. chmod(2) clears the flag via cache_inval_wxok() (vfs_cache.c:2054-2070) which takes NO ncp lock (only vp->v_spin) and is not ordered against the setters read-modify-write, so an naccess() whose GETATTR predates the mode change can commit atomic_set_short(NCF_WXOK) AFTER the clear - resurrecting a revoked search permission indefinitely (the fast path never revalidates while the flag is set; only another chmod of the directory re-clears it). The manifestation requires the setter to be stalled (interrupt/preemption) inside its ~150ns GETATTR->atomic_set span for longer than the chmods mode-commit-to-cache_inval_wxok gap (~0.3-2us), with the commit landing inside that span. Rig: 60-100 one-shot unprivileged workers (unlink of a nonexistent name: a modifying lookup forces the full-path naccess on D, and DELETE-on-nonexistent runs no later full-path naccess on D, so a straddling workers write would survive its own syscall) released via a pipe-burst barrier against a racing root chmod 0777->0700, probed after each round by an unprivileged stat(D/probe); four rig designs (hammer2 and tmpfs targets, 6-vCPU guest, including a cross-CPU IPI-storm amplifier via mmap/munmap churn and a ping flood) ran ~80,000 rounds total without a win. Controls verified the flag lifecycles observable halves: an unprivileged stat inside a 0700 root-owned formerly-0777 directory correctly returns EACCES after a quiet chmod (clear works), and the pre-chmod stat succeeds. Root cause remains real by code proof; win probability on this virtualized guest is simply below what ~80k rounds sampled. Recommend fixing (cheap, protocol-level) rather than waiting for a field manifestation: a permission revocation that silently fails to take effect is exactly the class of bug that surfaces in the wild years later.",
  "exploit_chain": "Unproven on guest; theoretical chain: (1) attacker keeps modifying lookups (unlink of a nonexistent name) streaming through a world-r+x directory D; (2) the owner/root revokes traversal with chmod D 0700; (3) if any in-flight naccess on D read the pre-chmod attributes and is stalled (interrupt) across the chmods SETATTR-commit-to-cache_inval_wxok gap, its atomic_set_short resurrects NCF_WXOK after the clear; (4) the attackers subsequent stats/opens of entries inside D take the naccess fast path (vfs_nlookup.c:1616-1621) and are allowed without any mode check, indefinitely (until the next chmod of D) - unauthorized traversal/read access to files inside the restricted directory whose own modes permit access. Only the directory VEXEC check is bypassed; create/delete still require the real directory write permission.",
  "evidence": "wxokrace.c (rig with burst barrier + one-shot workers + unprivileged probes); run.log (~68k rounds, IPI-storm assisted, no win), run.2.log (~8k rounds ping-flood assisted, no win), run.3.log (~40k rounds earlier design, no win); README.md control results (quiet chmod: pre-chmod stat OK / post-chmod EACCES); fix.diff (proposed, not build-validated)",
  "kernel_refs": [
    "sys/kern/vfs_nlookup.c:1616-1621",
    "sys/kern/vfs_nlookup.c:1720-1723",
    "sys/kern/vfs_nlookup.c:1758-1762",
    "sys/kern/vfs_nlookup.c:1630-1658",
    "sys/kern/vfs_cache.c:2054-2070",
    "sys/kern/vfs_syscalls.c:3411-3424",
    "sys/vfs/tmpfs/tmpfs_vnops.c:404-431",
    "sys/vfs/hammer2/hammer2_vnops.c:370-424"
  ],
  "poc_changes": "Rig authored fresh (no seed); iterated through four designs: (1) per-round fork burst - smeared by fork cost, post-inval workers cleared resurrections; (2) persistent worker pool + pipe-broadcast burst - chmod syscall latency (~5-15us) pushed the mode commit past the entire burst, so no worker was ever in-flight at the commit; (3) chmod re-timed into the middle of the burst window (W=25us, commit at ~12-19us) - straddle candidates present but no stall landed in the ~150ns span; (4) same plus cross-CPU IPI-storm (mmap/munmap churn, 6 threads) and ping flood to amplify in-span preemption - still no win in ~68k rounds.",
  "attempts": 4,
  "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Tue Oct 13 10:58:07 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (DF-5831-fix kernel; WXOK code paths untouched by that fix)",
  "runtime_sec": 3000,
  "guest_dirty": 0,
  "build_cmd": "cc -O2 -o /tmp/wxokrace /root/poc/DF-5832/wxokrace.c",
  "run_cmd": "/tmp/wxokrace /tmp/wxok 999999 24 25   (as root; workers and probes run as nobody; expected on win: WIN at round N: nobody stat(...) succeeds while dir mode is 0700 / BYPASS DEMONSTRATED)",
  "code_hash": "1db47abf041934f09266cda0a14778c546fc6674f10a8e730613456afe4181be",
  "notes": "Not reproduced within ~80,000 rounds across four rig designs on the 6-vCPU guest (including interrupt-storm amplification). Root cause is code-proven: cache_inval_wxok() holds no ncp lock while the naccess() setter holds only a shared ncp lock across its GETATTR->atomic_set span, so clear-then-stale-set ordering is possible. The missing ingredient is a sub-microsecond stall inside a ~150ns window coinciding with the mode commit; a bare-metal machine with a busy interrupt load, or an attacker with more patience/parallelism than this audit window, plausibly wins it. fix.diff is a PROPOSED fix (not build-validated since the bug did not reproduce); it serializes the clear with the setter by locking each ncp exclusively in cache_inval_wxok() and calling it from setfmode() without the vnode lock held.",
  "recommended_fix": "Serialize the clear with the setter: make cache_inval_wxok() lock each ncp exclusively before clearing NCF_WXOK/NCF_NOTX (restart-safe scan, holding only an ncp reference across the lock), and reorder setfmode() to drop the vnode lock (keeping a reference) before calling it, since taking the ncp exclusive lock under a held vnode lock would invert the ncp-first lock order against naccess()s shared-ncp->vget(shared-vp) sequence (see fix.diff).",
  "fix_status": "not_testable",
  "fix_baseline_reproduced": 0,
  "fix_patched_reproduced": 0,
  "fix_verdict": "Fix not build-validated: the race itself did not reproduce on this guest within ~80k rounds, so there was no baseline manifestation to verify a patched kernel against. fix.diff is included as the proposed change; its ordering argument is mechanical (any setter whose GETATTR predates the mode change holds the ncp shared across GETATTR->atomic_set, so an exclusive-locked clear is always ordered after that write).",
  "fix_evidence": "fix.diff (proposed); README.md control results documenting the working halves of the flag lifecycle"
}