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.
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2875 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 viabio_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 own0x22+rpattern - 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.
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)
- Premature free / UAF on in-flight
bp->b_dataβdiskiodonefor W1's bio freesiost->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 showingaa aa aa aawhere 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 f0and 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). - Memory leak β
vmstat -m dmsg_diskin-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). - 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 inkdmsg_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)
- Peer streams pipelined BLK_WRITEs in one transaction with aux size class C (victim object's zone).
- 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).
- 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
fixedFix 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']
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.
No comments yet.