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.
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2742 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_nslicespath, 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.
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) readsdp->d_sliceand passes it todscheck()with no ds_token (line 1246).disk_probe()on the disk_msg_core thread replacesdp->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 argumentstorms β 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_testableConfirmed 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.
No comments yet.