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

diskstrategy consumes dp->d_slice with no serialization while disk_probe/disk_invalidate replace and free it β€” use-after-free window for every in-flight bio

Field Value
ID DF-2742
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:L/I:L/A:L
CWE CWE-416 UAF (CWE-362 race)
File sys/kern/subr_disk.c
Lines 1246 (replacements :367-368, :494, :950-954)
Area kern
Confidence likely
Discovered 2026-08-30
Pass 2 (GLM 5.3 second pass)
Bucket memcorrupt
Reported pending
Known CVE none
CVE match novel

Summary

diskstrategy() reads dp->d_slice and passes it to dscheck() without ds_token or a refcount, while disk_probe() on the disk_msg_core thread replaces dp->d_slice and dsgone()s the old struct, and disk_invalidate() frees it from driver media-change/detach paths with no token at all. dscheck() dereferences and writes the object (sp->ds_flags |= DSF_REPROBE), so a stale pointer is a UAF read/write for bios on any open slice. Live demonstration: 660k forced reprobes (DIOCSYNCSLICEINFO) with 8 concurrent readers produced EINVAL storms β€” readers consuming the transient 2-slice replacement struct mid-swap β€” proving the unsynchronized window; no corruption observed because dsgone's kfree(4128B M_DEVBUF) is followed in the same probe by an equal-size realloc that immediately recycles the chunk. Latent UAF: any allocator change or interposer allocation of that size class turns it into corruption (raw-device I/O at stale/garbage offsets).

Threat model & preconditions

Root-forced reprobe or a removable-media change event racing ordinary buffered I/O on a mounted slice (mounted-FS I/O needs no privilege to be the victim); today's allocator masks it, so realistic impact is transient EINVAL I/O failures.

Serialize: take ds_token around the dp->d_slice fetch + dscheck() in diskstrategy() (the following dev_dstrategy is an async queue op and can run outside the token); take ds_token in disk_invalidate() around dsgone().

Timeline

  • 2026-08-30 Discovered during pass-2 audit of subr_disk.c (GLM 5.3); race window live-proven, corruption honestly not reproduced.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2742 Β· 8 files
FileTypeDescriptionSize
poc2742.c β€” 2.3 KB view raw
README.md β€” 1.7 KB ↓ raw
VERDICT.md β€” 2.8 KB ↓ raw
build.sh β€” 47 B view raw
run.sh β€” 92 B view raw
run.log β€” 459 B view raw
run.2.log β€” 404 B view raw
env.txt β€” 392 B view raw

DF-2742 β€” unsynchronized dp->d_slice replacement vs in-flight bios (UAF-class race)

Impact

disk_probe() (runs on the disk_msg_core thread, sys/kern/subr_disk.c:367-368, 494) replaces dp->d_slice and frees the old struct diskslices via dsgone(), while diskstrategy() (sys/kern/subr_disk.c:1246) reads dp->d_slice and hands it to dscheck() with no ds_token and no refcount β€” a use-after-free window for every in-flight bio on any open slice (e.g. I/O on a mounted filesystem). Same for disk_invalidate() (subr_disk.c:950-954) called from driver media-change/detach paths (sys/bus/cam/scsi/scsi_da.c:534, sys/dev/disk/fd/fd.c:1087, nata). dscheck() also WRITES into the object (sp->ds_flags |= DSF_REPROBE, subr_diskslice.c:182).

Reproduce (guest, root)

cc -O -o poc2742 poc2742.c -lpthread
vnconfig -c /dev/vn2 /root/mbr.img     # plain MBR, one DFLYBSD slice
./poc2742 /dev/vn2 /dev/vn2s1 50       # 8 reader threads + reprobe hammer

Success criterion: kernel panic in dscheck/dsgone/malloc paths.

Observed (2 runs, 660k + 25k reprobes)

  • No memory-corruption crash. The allocator's same-size immediate reuse (old ssp 4128 B freed by dsgone -> dsmakeslicestruct reallocates the same size class) keeps the stale pointer pointing at valid memory in practice.
  • The unsynchronized window itself IS demonstrated live: readers receive pread: Invalid argument (EINVAL) in storms β€” dscheck observing the transient 2-slice replacement struct (slice >= dss_nslices path, subr_diskslice.c:115-119) while disk_probe is mid-replacement. In the first run 7 of 8 reader threads died with EINVAL after 3-241 reads while the reprobe hammer ran.
VERDICT.md
↓ download raw

DF-2742 VERDICT β€” dp->d_slice replacement/free vs unserialized diskstrategy

Status: not_reproduced (no memory corruption observed). The unsynchronized access itself was demonstrated live. Impact: none demonstrated beyond transient I/O failure. Confidence: likely (race is statically certain; allocator behavior currently masks the UAF).

The defect (by inspection)

  • diskstrategy() (sys/kern/subr_disk.c:1222-1252) reads dp->d_slice and passes it to dscheck() with no ds_token (line 1246).
  • disk_probe() on the disk_msg_core thread replaces dp->d_slice (subr_disk.c:367-368) and frees the old struct (dsgone(&osp), subr_disk.c:373,494) while label reads inside the probe block and release ds_token β€” so strategy-path bios can interleave.
  • disk_invalidate() (subr_disk.c:950-954) β†’ dsgone(&disk->d_slice) with no token at all, from driver media-change/detach contexts (sys/bus/cam/scsi/scsi_da.c:534, sys/dev/disk/fd/fd.c:1087, sys/dev/disk/nata/*).
  • dscheck() dereferences and even writes the object (sp->ds_flags |= DSF_REPROBE, subr_diskslice.c:182) β€” a stale pointer is a use-after-free read/write.

What was observed (live)

  • Run 1: 660,136 forced reprobes (DIOCSYNCSLICEINFO with nonzero arg on /dev/vn2) over 50 s with 8 concurrent reader threads on /dev/vn2s1: 7 of 8 readers hit pread: Invalid argument storms β€” dscheck reading the transient 2-slice replacement struct mid-probe (slice >= dss_nslices, subr_diskslice.c:115-119). This is direct evidence that strategy-path bios consume dp->d_slice values that disk_probe is concurrently swapping.
  • Run 2 (dual-disk interleave, vn2+vn3 both reprobed to make the freed 4128-byte ssp claimable by the other disk's dsmakeslicestruct): 25,210 reprobes, again no corruption.
  • Why no UAF crash: dsgone's kfree(old ssp, 4128 B, M_DEVBUF) is followed within the same probe by dsmakeslicestruct(MAX_SLICES) = same type/size class, so the chunk is immediately recycled into the new ssp; the stale pointer therefore almost always still points at validly-initialized memory. Corruption requires an interposer allocation of exactly that size class between free and reuse, which we could not arrange from the available knobs.

Honesty note

This is a real missing-serialization defect (any future allocator change, heap pressure pattern, or different nslices mix turns it into a UAF), but on this kernel and allocator we could not convert it into observed memory corruption, so it is filed at Low severity with not_reproduced status.

Suggested fix

Serialize the strategy path against slice-struct replacement: in diskstrategy(), acquire ds_token around the dp->d_slice fetch + dscheck() (the subsequent dev_dstrategy() is an async queue operation and can run outside the token), and make disk_invalidate() take ds_token around dsgone().

Fix verification

not_testable
per-fix-DF-2742

Confirmed kernel references

Detail

Evidence (decisive lines)

['run.log: 7 of 8 readers failed with pread EINVAL during 660,136 forced reprobes (transient ssp observed by strategy path)', 'run.2.log: dual-disk interleave variant, 25,210 reprobes, no corruption', 'VERDICT.md: full lock-protocol trace of why the window exists and what masks it']

PoC changes

written from scratch; threaded readers + DIOCSYNCSLICEINFO(nonzero) reprobe hammer

Verified recommended fix

Take ds_token in diskstrategy() around the dp->d_slice fetch + dscheck() (dev_dstrategy is an async queue op and can stay outside), and in disk_invalidate() around dsgone().

Verdict

diskstrategy() reads dp->d_slice and passes it to dscheck() with no ds_token (subr_disk.c:1246) while disk_probe() (msg thread) replaces dp->d_slice and dsgone()s the old struct (subr_disk.c:367-368,494) and disk_invalidate() frees it from driver media-change paths (subr_disk.c:950-954) -- a statically-certain unsynchronized lifetime window (CWE-416-class). Live runs (660k + 25k forced reprobes with 8 concurrent reader threads) demonstrated the window itself: readers hit EINVAL storms from dscheck consuming the transient 2-slice replacement struct mid-probe (slice >= dss_nslices). No memory corruption could be produced: dsgone's kfree of the 4128-byte ssp is followed in the same probe by an equal-size M_DEVBUF realloc that immediately recycles the chunk, so the stale pointer keeps pointing at validly-initialized memory on this allocator. Filed Low: latent UAF, root/media-change trigger, transient I/O failure side effect observed.