β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2875

Pipelined BLK_WRITE in one transaction: single-slot iost->data aux tracking frees the most-recent aux while its bio is still in flight (deterministic UAF on bp->b_data) and leaks all earlier aux

Field Value
ID DF-2875
Status new
Severity High
CVSS 3.1 CVSS:3.1/AV:A/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
CWE CWE-416 / CWE-401
File sys/kern/subr_diskiocom.c
Lines 440, 445, 606
Area kern
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket memcorrupt
Reported pending
Known CVE none
CVE match novel

Summary

The server explicitly supports streaming multiple ops inside one BLK transaction, but disk_blk_write tracks detached aux buffers in a single per-state slot: each kdmsg_detach_aux_data overwrites the previous pointer and diskiodone's kdmsg_free_aux_data on ANY completion frees whatever the slot currently holds β€” the MOST RECENT buffer, whose bio may still be queued β€” while all earlier detached buffers are orphaned (leak). The free is also racy between concurrent diskiodone invocations (check-then-free-then-NULL β†’ potential double free).

Threat model & preconditions

Any dmsg peer of a disk iocom: a remote HAMMER2 cluster node routed by the boot-time hammer2 service daemon (which DIOCRECLUSTERs every disk) or the local holder of the cluster pipe. Attacker controls message count, aux sizes (512B..1MB), contents and timing — i.e. which kmalloc zone is freed under an in-flight DMA and when reclamation happens. User→root route: repeat with the victim zone's size class → kernel heap contents/pointer harvest onto disk (BLK_READ back), write-side grooming for full kernel memory corruption.

Proof of contest

VERIFIED on the stock guest (findings/poc/DF-2875/dfpeer.c): one BLK_WRITE transaction W1(4KB) → W2(128KB) → R1..R4(128KB); readback shows W2 slots containing 0xAA — the freed buffer reclaimed by a later message's aux before the victim's DMA — and ASCII/kernel bytes persisted to attacker-readable raw disk; vmstat -m dmsg_disk grows 2.19K→4.67M over 20 rounds (leak). Fix (per-bio bio_caller_info2.ptr ownership) validated on a rebuilt kernel: zero leak, byte-exact writes.

Validated fix.diff in findings/poc/DF-2875/.

Timeline

  • 2026-09-02 Discovered during pass-2 audit of subr_diskiocom.c (GLM 5.3); UAF + leak + persisted-disclosure reproduced + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2875 Β· 12 files
FileTypeDescriptionSize
dfpeer.c β€” 15.4 KB view raw
run_f2.sh β€” 1.1 KB view raw
run.log β€” 3.2 KB view raw
f2_scan.txt β€” 2.1 KB view raw
fix.diff β€” 3.9 KB view raw
build.sh β€” 51 B view raw
run.sh β€” 143 B view raw
env.txt β€” 373 B view raw
VERDICT.md β€” 3.7 KB ↓ raw
run.fix.log β€” 887 B view raw
verdict.json β€” 3.8 KB view raw
README.md β€” 1.8 KB ↓ raw

DF-2875 β€” pipelined BLK_WRITE: single-slot iost->data aux tracking β†’

premature kfree of in-flight bp->b_data (UAF during disk I/O) + leak

What this pack contains

  • dfpeer.c β€” dmsg wire peer (socketpair + DIOCRECLUSTER + hand-crafted DMSG_BLK_* frames; the kernel reader verifies no CRCs, so frames are hand-built).
  • run_f2.sh β€” the trigger + readback scan + malloc accounting.
  • f2_scan.txt β€” readback of all written offsets after 10 rounds:
  • W2 slots containing aa aa aa aa (the victim bio DMA'd a reclaimer's buffer contents β€” the freed slot was reallocated before the DMA),
  • a W2 slot containing 53 ff 00 f0 (raw allocator/kernel memory persisted to disk),
  • several slots containing ASCII "cals" (kernel heap string data persisted to disk).
  • f2_mem_before.txt / f2_mem_after.txt β€” vmstat -m dmsg_disk: in-use 2.19K β†’ 2.28M across 10 rounds (orphaned detached aux).
  • fix.diff (kern_dmsg.c/subr_disk.c hunks included for completeness) β€” per-bio aux ownership via bio_caller_info2.ptr.

Build & run (root on the DF guest; root == stand-in for the

authenticated dmsg cluster peer, the same threat position as DF-0117)

cc -O -Wall -o /tmp/dfpeer dfpeer.c        # build.sh
pkill -f hammer2                            # free boot-time disk iocoms
sh run_f2.sh                                # run.sh (~2 min)

Expected output (stock kernel)

  • pipelines: 10 rounds done
  • readscan anomalies: W2 slots (off+005/007/009... odd 6*i+1 offsets) showing aa aa aa aa, 53 ff 00 f0, or ASCII garbage instead of their own 0x22+r pattern
  • dmsg_disk in-use growth ~2.3MB (leak)

On the fix kernel, the same run shows every slot with exactly its own pattern (W1 0x11+r, W2 0x22+r, reclaimers 0xAA) and no dmsg_disk growth.

VERDICT.md
↓ download raw

DF-2875 VERDICT

Status: reproduced (memcorrupt β€” deterministic premature free of an

in-flight disk-I/O buffer; peer-controlled)

Guest: DragonFly 6.5-DEVELOPMENT #0 X86_64_GENERIC (see env.txt), QEMU virtio boot disk /dev/vbd0 (30GB; 7.5MB unused tail gap used as the write sandbox).

What was run

run_f2.sh: 10 rounds of a single BLK_WRITE transaction streaming W1(4KB, pattern 0x11+r) β†’ W2(128KB, 0x22+r) β†’ R1..R4(128KB, 0xAA, last carries DELETE), all pipelined while earlier bios are still in flight; then a full readback scan of all 60 slots (readscan).

Observed (stock kernel)

  1. Premature free / UAF on in-flight bp->b_data β€” diskiodone for W1's bio frees iost->data, which by then contains the aux of the most recent write (R4), while R4's bio is still queued (sys/kern/subr_diskiocom.c:440 detach-overwrite, :606 free-on-any- completion). Readback: - W2/reclaimer slots showing aa aa aa aa where their own pattern should be β€” the freed buffer was reallocated (reclaimer aux, same kmalloc size class, M_DMSG_DISK) before the victim bio's DMA, so the disk received another message's payload (f2_scan.txt: off+003/005/009... aa aa aa aa). - off+014: 53 ff 00 f0 and several slots with ASCII "cals" β€” kernel heap contents persisted onto the raw disk through the freed-then-reused buffer (info-leak-onto-disk primitive; readable back by the same peer via BLK_READ).
  2. Memory leak β€” vmstat -m dmsg_disk in-use grew 2.19K β†’ 2.28M across the 10 rounds: every detached aux except the one freed by the first completion is orphaned (aux1 of each round + intermediates).
  3. No panic on this (non-INVARIANTS) kernel; the racy double-free variant (two concurrent diskiodone on the same iost between the if (data->aux_data) check and the NULL store in kdmsg_free_aux_data) was not needed to prove the bug β€” the sequential premature-free is fully deterministic.

Threat model

Any dmsg peer of a disk iocom: a remote HAMMER2 cluster node routed by the hammer2 service daemon (which DIOCRECLUSTERs every disk at boot, sbin/hammer2/cmd_service.c:898), or any local process holding the peer end of the cluster pipe. Same position as DF-0117. The attacker controls message contents, sizes (aux 512B..1MB), count, and timing β†’ controls which kmalloc zone is freed-under-DMA and when reclamation happens. On this permissive guest (no SMAP/SMEP/KASLR) the primitive is a classic exploitable UAF: content-controlled kernel heap object freed while referenced by in-flight DMA, with attacker-driven reallocation.

Exploit chain (memcorrupt β†’ uid0 route)

  1. Peer streams pipelined BLK_WRITEs in one transaction with aux size class C (victim object's zone).
  2. First completion frees the newest C-sized buffer under the in-flight bio; subsequent message aux allocations (same zone, same thread) reclaim it; the pending DMA then reads the replacement object's bytes and writes them to attacker-chosen disk offsets β€” kernel heap disclosure to disk (proven: "cals", 53 ff 00 f0).
  3. Repeating with target zones yields pointer/contents disclosure; combined with write-side grooming (the same freed slot can be re-detached), full kernel-memory corruption is reachable. uid0 was not attempted end-to-end on this guest because the peer already runs as root here; on a real cluster the peer is a remote node and the primitive is remote-kernel-memory corruption.

Fix validation

fix.diff (per-bio aux ownership via bio_caller_info2.ptr) applied to the guest /usr/src, kernel rebuilt (make nativekernel), PoC re-run: readback shows every slot with exactly its own pattern and dmsg_disk usage returns to baseline (see run.fix.log). Bug dead.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Fix kernel (all four diffs applied, rebuilt, rebooted): pipelines run shows dmsg_disk 320B before AND after (no leak) and every readback slot byte-exact - no cross-writes, no kernel heap on disk. Premature free and leak both dead.

['run.fix.log']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Wed Sep 2 18:43:16 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (fix.diff applied, make nativekernel)

Confirmed kernel references

Detail

Exploit chain

dmsg peer -> BLK_WRITE|CREATE txn -> stream W1(small)+W2(big)+reclaimers(big) -> W1 completion kfree()s W2/R4's aux under its in-flight bio -> peer's subsequent message aux allocations (same kmalloc zone, reader thread) reclaim the slot -> victim bio's DMA reads replacement object bytes and persists them to attacker-chosen disk offsets (kernel heap disclosure onto disk, proven with 'cals'/53ff00f0) -> repeat with target zone for contents/pointer harvest; write-side grooming yields full kernel memory corruption (uid0 route on hardening-free hosts; peer here already ran privileged, so uid0 was not the demo target)

Evidence (decisive lines)

['run.log (readscan anomalies: aa-cross-writes at W2 slots; 63 61 6c 73 at off+004/011/013/037/055; 53 ff 00 f0 in first run)', 'f2_mem_before/after.txt (dmsg_disk 2.19K -> 2.28M / 4.67M)', 'VERDICT.md', 'run.fix.log (fix kernel: zero leak, byte-exact slots)']

PoC changes

Seed sketch was a hypothetical; wrote a full dmsg wire peer (dfpeer.c): socketpair + DIOCRECLUSTER + hand-crafted DMSG_BLK frames (kernel rx path verifies no CRCs); pipeline/readscan/hold modes; fixed header padding to hbytes=128 for 96-byte blk structs and enlarged the header parser buffer to DMSG_HDR_MAX after the LNK_CONN frame desynced it

Verified recommended fix

Track detached aux per-bio (bio_caller_info2.ptr owns a kdmsg_data_t; diskiodone frees exactly that) instead of the shared iost->data slot

Verdict

Pipelined BLK_WRITE messages in one dmsg transaction hit a single-slot aux tracker (iost->data): each kdmsg_detach_aux_data overwrites the slot (subr_diskiocom.c:440) and ANY READ/WRITE completion frees whatever the slot currently holds (:606) - i.e. the MOST RECENT aux while its bio is still in flight. Verified on the stock guest: 3/10 W2 slots and several reclaimer slots on the raw disk contained other messages' payloads (0xAA) or raw kernel heap bytes ('63 61 6c 73', '53 ff 00 f0') instead of the data actually sent - a deterministic premature-free/UAF on bp->b_data with attacker-controlled sizes (512B..1MB) and timing, plus a linear memory leak (dmsg_disk 2.19K->4.67M over 20 rounds). Fixed kernel shows zero leak and byte-exact writes.