DF-2685 / verdict.json
{ "finding_id": "DF-2685", "status": "reproduced", "reproduced": 1, "impact": "dos", "confidence": "certain", "verdict": "REPRODUCED on stock DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC): mlock() on swapped-out anonymous memory returns success but presents ZERO-FILLED pages (16,253,937 of 16,777,216 words destroyed in the decisive run; 16,340,977 in the first run). Root cause: TRYPAGER() (sys/vm/vm_fault.c:383-385) disables the pager for ALL VM_FAULT_WIRE_MASK faults, so vm_fault_object() never calls vm_pager_get_page() for wiring faults and instead zero-fills a fresh page at vm_fault.c:2327 and pmap_enter()s it wired. POSIX mlock semantics (pages become resident WITH their contents) are violated with fully silent data destruction. On stock x86-64 the mlock syscall is SYSCAP_RESTRICTEDROOT-gated (vm_mmap.c:1029-1035), so the direct victim population is privileged software that wires memory (key-holding daemons, mlockall services, NVMM guest RAM via vm_map_kernel_wiring, brk under MAP_WIREFUTURE) precisely when a local user has forced the box into swap pressure; no error is reported anywhere. FIX VALIDATED: with TRYPAGER allowing VM_FAULT_USER_WIRE, rebuilt kernel (#1 Aug 30 19:03:41) returns 'OK: data intact after mlock' (bad=0, rc=0) under equivalent swap pressure (1121MB swapped vs 1026MB baseline).", "exploit_chain": "Local unprivileged user allocates ~4.6GB on a 4GB box driving a privileged mlock-using process's pages to swap; the privileged process (re)locks its memory via mlock/mlockall; the kernel zero-fills the locked region instead of paging it in (vm_fault.c:2327 via TRYPAGER exclusion at vm_fault.c:383-385); key material / auth state / database buffers silently become zeros -> authentication breakage, permanent data loss, availability collapse of the locking process. Not a kernel memory-corruption primitive; no kernel-memory disclosure or overwrite; impact is silent user-memory destruction (integrity/availability) in privileged processes.", "evidence": [ "findings/poc/DF-2685/baseline_stock_run.log - stock run: swapinfo 1026M used, mismatch lines, 'bad=16253937 of 16777216 words', 'BUG REPRODUCED', rc=2", "findings/poc/DF-2685/patched_run.log - patched kernel: swapinfo 1121M used, 'bad=0 of 16777216 words', 'OK: data intact after mlock', rc=0", "findings/poc/DF-2685/build.log - validated kernel identity (#1 Aug 30 19:03:41), build/install record, patch verification", "findings/poc/DF-2685/VERDICT.md - full line-level trace: sys/vm/vm_mmap.c:1036 -> vm_map.c:2654 -> vm_fault.c:2632 -> TRYPAGER vm_fault.c:383-385 -> zero_fill vm_fault.c:2327 -> pmap_enter vm_fault.c:793" ], "kernel_refs": [ "sys/vm/vm_fault.c:383", "sys/vm/vm_fault.c:2327", "sys/vm/vm_fault.c:2632", "sys/vm/vm_map.c:2654", "sys/vm/vm_mmap.c:1029", "sys/vm/vm_mmap.c:1036", "sys/vm/vm_unix.c:151", "sys/dev/virtual/nvmm/nvmm_dragonfly.c:193" ], "poc_changes": "Self-written for this verification (no seed). Pattern-based verifier with page-index-derived words; 4.6GB churn against 4GB RAM + 4GB swap; verifies 64MB earliest-swapped region after mlock. Also probed: file-backed mlock (masked while pages resident), unprivileged reachability (EPERM from SYSCAP_RESTRICTEDROOT on x86-64 stock, documented).", "attempts": 4, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 1450, "guest_dirty": 0, "build_cmd": "cc -O2 -o /tmp/mt/mlockswap mlockswap.c (fix kernel: cd /usr/src && patch -p1 < fix.diff && make -j4 nativekernel && make installkernel && reboot)", "run_cmd": "/tmp/mt/mlockswap (baseline: rc=2 'BUG REPRODUCED'; patched: rc=0 'OK: data intact after mlock')", "code_hash": "23574fbe1562edd1549c3d79cb9bbd489c57e0e7a55133b33602f69625a508e1", "notes": "First reproduction (previous boot): bad=16340977. Kernel-wiring (VM_FAULT_CHANGE_WIRING) still skips the pager after the fix (historical deadlock avoidance); NVMM vm_map_kernel_wiring of guest RAM remains exposed - residual hardening noted. The mlock EPERM gate for plain users is itself a DFly x86-64 quirk (#ifdef pmap_wired_count); on configurations where only RLIMIT applies, any user triggers directly.", "recommended_fix": "TRYPAGER must not exclude user-wiring faults from the pager: allow VM_FAULT_USER_WIRE so mlock/mlockall page data in instead of zero-filling (see fix.diff).", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #1: Sun Aug 30 19:03:41 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Baseline reproduced on stock #0 (16.25M words zeroed, rc=2). Patched kernel #1 with the one-macro TRYPAGER fix returns bad=0 / rc=0 under equivalent swap pressure (1121MB swapped at verify time). Bad behavior eliminated; data paged back in correctly.", "fix_evidence": "findings/poc/DF-2685/patched_run.log; findings/poc/DF-2685/build.log" } |