DragonFlyBSD Kernel Audit
DF-2972 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-2972",
  "status": "reproduced",
  "reproduced": 1,
  "impact": "leak",
  "confidence": "certain",
  "verdict": "exec_shell_imgact() scans the script's live page-cache first page twice (accounting at sys/kern/imgact_shell.c:75-96, copy at :138-162). A MAP_SHARED writable mapping that outlives close() can mutate the interpreter line between the scans -- exec_check_permissions' ETXTBSY (kern_exec.c:1325) only counts open-for-write fds (vfs_default.c:1213-1216 drops v_writecount on close while the mapping persists). Reproduced as an unprivileged user with ~100% divergence rate while racing (tmpfs and hammer2): the kernel reserved space for one interpreter line and copied another, corrupting fname/argv placement inside the 266,240-byte args->buf object (no OOB write possible: scan-2 writes bounded by the page to buf[0..4094]; interpreter_name stays MAXSHELLCMDLEN-bounded) and exposing stale prior-exec content of the recycled exec-args objcache object as interpreter argv strings (kernel-heap bytes inside the copyout block at kern_exec.c:1220 handed out by the argv walk at kern_exec.c:1231-1236). No panic in any run; guest stayed up. Realistic ceiling: unprivileged cross-process snooping of other users' exec argv/env via a raced-exec loop; cross-user snooping itself not demonstrated on the single-user guest.",
  "exploit_chain": "unpriv user: (1) create #!/bin/echo script; (2) open O_RDWR, mmap MAP_SHARED RW, close fd (ETXTBSY disarmed); (3) thread flips interpreter-line bytes while exec'ing the script with ~200KB env to widen the scan1->scan2 bcopy window; (4) scan1!=scan2 => fname/argv overlap or stale gap inside the copyout block; (5) interpreter prints argv incl. stale recycled-object strings (prior execs' argv/env) -- loop to snoop. No escalation: not memory corruption (all writes in-object).",
  "evidence": [
    "run.log: DF2972_HIT iter=3 out=[... /tmp/df2972/t ERARG] (overlap destroys USERARG prefix)",
    "run.log: DF2972_HIT iter=1 out=[... EEEUSERARG] (stale gap merges strings)",
    "run.log: DF2972_HIT iter=2 out=[... /tmp/df2972/t 00=EEEE...] (stale prior-exec env string K00=EEE... from recycled exec-args object delivered as argv)",
    "run.hammer2.log: same on hammer2 root FS (not tmpfs-specific)",
    "VERDICT.md: full mechanism with path:line and bounds proof",
    "leak_sample.txt: stale-byte samples across 4 runs"
  ],
  "kernel_refs": [
    "sys/kern/imgact_shell.c:75",
    "sys/kern/imgact_shell.c:120",
    "sys/kern/imgact_shell.c:123",
    "sys/kern/imgact_shell.c:126",
    "sys/kern/imgact_shell.c:138",
    "sys/kern/imgact_shell.c:160",
    "sys/kern/imgact_shell.c:169",
    "sys/kern/imgact_shell.c:173",
    "sys/kern/kern_exec.c:770",
    "sys/kern/kern_exec.c:1220",
    "sys/kern/kern_exec.c:1231",
    "sys/kern/kern_exec.c:1325",
    "sys/kern/kern_exec.c:137",
    "sys/kern/vfs_default.c:1213",
    "sys/kern/kern_descrip.c:3328"
  ],
  "poc_changes": "written fresh this pass (no prior seed): race_demo.c -- close-after-mmap bypasses ETXTBSY; mutator flips interpreter-arg bytes 12..59 between 1-token/49-byte and 1-token/5-byte states; ~200KB env widens the scan1->scan2 bcopy window; divergence-strict detector (fname/USERARG adjacency) avoids false positives from mid-flip single-state views; plus a hammer2-root-FS variant run.",
  "attempts": 5,
  "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": 95,
  "guest_dirty": 0,
  "build_cmd": "cc -O2 -pthread -Wall -o race_demo race_demo.c",
  "run_cmd": "./race_demo 40000 8",
  "code_hash": "126aa8029d30f39bfc8baee9a092a142c6f2ffebcc99467946f0f7e617c44d8c",
  "notes": "5 positive runs (8/6/4/4 hits + hammer2 4/4), zero consistent/mixed outputs while racing (near-deterministic divergence at full flip rate); no panic ever -- matches in-object bounds proof. DF-0243 (known, not re-reported) re-exercised this pass at argv[0]=256/4096/65536/262140: no panic, script ran normally, confirming the existing false-positive verdict (two's-complement wrap is equivalent to the intended signed adjustment; space truncates back to the correct int).",
  "recommended_fix": "Snapshot the mapped first page once (kmalloc+bcopy) after the interpreted check and run both the accounting and copy passes against the snapshot; see fix.diff.",
  "fix_status": "not_testable",
  "fix_kernel_uname": "",
  "fix_baseline_reproduced": 1,
  "fix_patched_reproduced": 0,
  "fix_verdict": "fix.diff (page snapshot, behavior-preserving for stable content) authored after verification; apply-checked against guest /usr/src with 'patch --dry-run -p1' -- all 6 hunks clean (APPLY_CHECK_OK). Kernel rebuild and patched-behavior rerun not performed: non-corruption-class finding, fix validation not mandated for it; snapshot approach trivially removes the second read of the mutable page.",
  "fix_evidence": [
    "fix.diff",
    "VERDICT.md section 'Fix' (apply-check result)"
  ]
}