# 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: BADFREE2` after ~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: BADFREE2` after ~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+0x11e
  ```
  Guest 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->freelist` via 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 — while `VF_LOCKED` stays 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, both `close_locked_drive()` executions and both
  `Free(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.
