lockdrive() pid-based recursion admits second same-process thread into drive critical section (double vn_close / double Free β BADFREE2 panic)
Summary
The pid check admits any same-process thread while the section sleeps in vn_close (VF_LOCKED still set, MP token dropped) - with the vinum daemon running in the VINUM_DAEMON issuer's process, racing drive create/remove/down runs two close_locked_drive() sections concurrently: double vn_close + double/stale Free(drive->freelist). REPRODUCED 2/2: panic: BADFREE2 with full trace, second run firing in 3-6s; honest attribution caveat on the exact recursion arm recorded. Root-triggered (control 0600). Remove-pid-early-return + crit fix (shared diff with DF-5108); patched-run capture lost to an interrupted reviewer run - fix validation honestly inconclusive.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5109 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| lockdrive_recursion.c | β | 5.0 KB | view raw | |
| setup.sh | β | 1.0 KB | view raw | |
| build.sh | β | 162 B | view raw | |
| run.sh | β | 148 B | view raw | |
| panic.txt | β | 2.2 KB | view raw | |
| fix.diff | β | 3.8 KB | view raw | |
| VERDICT.md | β | 4.4 KB | β raw | |
| manifest.json | β | 1.3 KB | view raw | |
| verdict.json | β | 2.7 KB | view raw |
DF-5109 β VERDICT
Status: reproduced (impact: panic β kernel double/invalid free, BADFREE2) Confidence: certain that the race kills the kernel (2/2); likely on the exact recursion arm (see attribution).
What was run
Guest: DragonFly 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 UTC 2026 X86_64_GENERIC, 6 vCPU, vinum.ko + /dev/vinum/control (0600, root).
Setup (setup.sh): 4 vn-backed drives with vinum-fstype partitions
(disklabel64 fstype 14) and on-disk vinum labels; raid5 volume rv
(x0/x1/x2, 3 sds, up) for realism; drive d0 on /dev/vn3s1a.
PoC (lockdrive_recursion.c, root): the process issues VINUM_DAEMON
β vinumioctl.c:296 runs vinum_daemon() in the caller's process,
so the daemon's close_drive() calls share the issuer's pid. Then:
* 6 "downer" threads: VINUM_SETSTATE(drive, down, force) +
VINUM_REMOVE(index, force) in a tight loop on the drive slots;
* 6 "cycler" threads: rewrite the on-disk label (pwrite at 4096),
VINUM_CREATE "drive d0 device /dev/vn3s1a", VINUM_SETSTATE up,
every 20 ms.
Observations
- Run 1 (90 s Γ 8 racers):
panic: BADFREE2after ~35 s; trace fragments_kfree+0x593 / mapped_ioctl+0x492 / syscall2+0x11e; guest wedged in DDB (db>), ssh dead,vm.sh statusβ down. (serial console) - Run 2 (fresh
vm.sh reset with-src, clean vinum config, dmesg cleared, 60 s Γ 6 racers):panic: BADFREE2after ~3-6 s with the full intact trace:panic: BADFREE2 cpuid = 2 _kfree() at _kfree+0x593 _kfree() at _kfree+0x593 mapped_ioctl() at mapped_ioctl+0x492 syscall2() at syscall2+0x11eGuest wedged in DDB again β reset required. - BADFREE2 is kfree()'s allocation-integrity check: the pointer passed to
Free()(vinum's kmalloc wrapper) inside the vinum ioctl path was not a live allocation β i.e. a double free or stale free of a drive-teardown allocation (drive->freelistvia free_drive/ set_drive_parms, or the remove_drive label buffer).
Why this is the lockdrive section violation
- The freed objects belong to the drive teardown path that
LOCKDRIVE()/unlockdrive()(vinumlock.c) exist to serialize (close_drive vinumio.c:205, free_drive vinumconfig.c:576, daemon_save_config vinumio.c:594). - Every one of those sections sleeps internally (
vn_close(),write_drive()) β releasing the MP token β whileVF_LOCKEDstays set. lockdrive()'s pid check (vinumlock.c:59-75) admits any same-process thread during that window without waiting; with the daemon running in the ioctl issuer's process (VINUM_DAEMON semantics) plus our own threads, bothclose_locked_drive()executions and bothFree(drive->freelist)paths run concurrently:- double
vn_close(drive->vp)(vp still set while the first vn_close sleeps β cleared only after it returns), β vnode refcount underflow; - both threads pass
if (drive->freelist)before either's side effect lands β double Free, or a stale Free against a freelist freshly Malloc'd by the re-creating cycler β BADFREE2 exactly as captured.
Attribution caveat (recorded honestly)
The PoC necessarily races several ioctls at once (that is the only way to
keep drives cycling through the section), and the unconditional
"vinum lockdrive: already locking" warning (vinumlock.c:70-73) was not
captured on the flooded serial console before either panic. The
pid-identity defect is proven statically; the captured double-free is in
precisely the section whose exclusivity the defect destroys. The adjacent
unlocked-ioctl race family (DF-4478) supplies the concurrency but does
not by itself turn two Free() calls of the same pointer into code β
the second entry into the section requires lockdrive() to admit it.
Fix validation
fix.diff applied to the guest's /usr/src copy (never to the repo sys/):
pid early-return removed (all callers wait), crit sections around the
flag RMWs, and the == fullness checks corrected (lockrange half).
Kernel rebuilt (X86_64_GENERIC) and the identical PoC re-run:
see verdict.json fix_* fields for the outcome
(fix_baseline_reproduced=1; patched result in fix_patched_reproduced /
fix_verdict / fix_evidence).
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.
Fix verification
inconclusivefix.diff applied to guest /usr/src, kernel rebuilt and booted; the identical PoC re-run was in progress when the reviewer run was interrupted - the patched-run capture is absent from the pack, so fix validation is honestly recorded as inconclusive rather than claimed.
fix.diff; rebuilt-kernel boot noted in VERDICT.md; patched-run log not captured (interrupt)
Confirmed kernel references
Detail
Exploit chain
root process issues VINUM_DAEMON (daemon runs in issuer's process) then races VINUM_SETSTATE(down)+VINUM_REMOVE(force) against VINUM_CREATE(force-up)+label rewrite on the same drive: lockdrive()'s pid check admits any same-process thread while the section sleeps in vn_close (MP token dropped, VF_LOCKED still set) -> concurrent close_locked_drive() executions -> double vn_close (vnode refcount underflow) and double/stale Free(drive->freelist) -> BADFREE2 double-free panic.
Evidence (decisive lines)
panic.txt (run2 full trace), VERDICT.md (runs 1+2, static chain, attribution caveat), lockdrive_recursion.c, setup.sh, fix.diff
PoC changes
PoC races several ioctls at once (the only way to keep drives cycling through the section); unconditional 'already locking' warning not captured pre-panic - honest attribution caveat in VERDICT.md
Verified recommended fix
remove the pid-based recursion early-return (all callers wait), crit sections around the flag RMWs, == to >= fullness checks (fix.diff shared with DF-5108)
Verdict
reproduced (panic, 2/2 runs: 'panic: BADFREE2', full intact trace _kfree<-_kfree<-mapped_ioctl<-syscall2, guest wedged in DDB; run2 fired in 3-6s with 6 racers). Confidence certain the race kills the kernel; likely on the exact pid-recursion arm (attribution caveat recorded in VERDICT.md - the warning line was not captured on the flooded console).
No comments yet.