dssize()/diskpsize consume dp->d_slice without ds_token β use-after-free walk of struct diskslices/disklabel during forced reprobe, and DSO_RAWPSIZE fallback silently substitutes whole-disk size for partition devices
| Field | Value |
|---|---|
| ID | DF-2831 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:L/A:H |
| CWE | CWE-362 (CWE-125 / CWE-672) |
| File | sys/kern/subr_diskslice.c |
| Lines | 849-870 (caller subr_disk.c:1259-1276; dsopen :761-777) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-31 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
dssize() snapshots dp->d_slice, checks slice bounds and the open mask on that snapshot, may re-open the device (letting a reprobe run), reloads ssp, and then dereferences ssp->dss_slices[slice].ds_label/.ds_ops and calls ops->op_getpartbounds() with no ds_token and no revalidation of the reloaded pointer. Its only caller diskpsize() also takes no token. Meanwhile disk_msg_core processes root-forced reprobes (DIOCSYNCSLICEINFO arg!=0, bypassing the open-busy check) by replacing dp->d_slice and dsgone()ing the old struct plus its labels under ds_token. dsopen() additionally lacks the slice<dss_nslices bounds check every other entry point has. On the stock INVARIANTS guest, swapon racing forced reprobes silently installed whole-disk-sized swap (16384 blocks) for a 4096-block partition in 6-14% of cycles across 4 runs, while a same-instant DIOCGDINFO64 probe proved the in-core label was still correct. With tracing, the same race produced a fatal trap 12 in dssize+0x15c with the walked ssp one generation older than the current one β a freed struct. Provenance pinned 1:1 (219/219): the reprobe's label re-read fails through the unsynchronized diskstrategy (DF-2742 family), disk_probe_slice clears the label, dssize returns -1, and diskpsize's DSO_RAWPSIZE fallback substitutes the raw whole-disk size for a PARTITION device. Distinct sink and fix site from DF-2742 β that fix does not cover the d_psize path.
Threat model & preconditions
Privileged-trigger race (root vs root / forced reprobe vs automated swap management; disk devices require SYSCAP_RESTRICTEDROOT to open): kernel UAF read (panic β observed), silent wrong swap geometry (4Γ oversized swap on a partition β swap accounting corruption, failed page-out I/O), latent worse via same-size-class recycling feeding arbitrary kernel-object content into partition bounds. No unprivβroot chain.
Proof of contest
VERIFIED live (findings/poc/DF-2831/): no-hammer control 2000/2000 correct (size 4096); hammer (150-300k forced reprobes) + swapon loops β ok_WRONG_SIZE 125-287 of 2000 with swapinfo reporting 16384 while the same-instant in-core label says 4096; instrumented kernel dies in dssize+0x15c (panic.txt). Fix (ds_token around diskpsize's dssize walk + never substitute raw size for a partition device + revalidate after re-open reload + the dsopen bounds check) validated on a rebuilt guest kernel: 0/2000 anomalies, clean ENXIO, control 2000/2000, no panic.
Recommended fix
Validated fix.diff in findings/poc/DF-2831/.
References
- DF-2742 (the diskstrategy sibling), DF-0134 (disklabel64 gap β impact upgrade recommended: dscheck turns unvalidated p_boffset into an arbitrary absolute bio_offset incl. 64-bit wrap)
Timeline
- 2026-08-31 Discovered during pass-2 audit of subr_diskslice.c (GLM 5.3); UAF panic + silent wrong-swap-geometry reproduced + fix validated same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2831 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.5 KB | β raw | |
| VERDICT.md | β | 5.4 KB | β raw | |
| env.txt | β | 905 B | view raw | |
| genimg2831.c | β | 4.1 KB | view raw | |
| poc2831.c | β | 2.8 KB | view raw | |
| poc2831b.c | β | 5.0 KB | view raw | |
| build.sh | β | 295 B | view raw | |
| run.sh | β | 596 B | view raw | |
| run.log | β | 1.6 KB | view raw | |
| fix_run.log | β | 763 B | view raw | |
| panic.txt | β | 2.8 KB | view raw | |
| instrument.diff | β | 1.2 KB | view raw | |
| fixinst.diff | β | 2.2 KB | view raw | |
| fix.diff | β | 2.0 KB | view raw | |
| manifest.json | β | 1.4 KB | view raw | |
| verdict.json | β | 6.3 KB | view raw |
DF-2831 β dssize()/diskpsize consume dp->d_slice without ds_token; DSO_RAWPSIZE fallback then substitutes whole-disk size for partitions
- Verified 2026-09-01, DragonFly 6.5-DEVELOPMENT x86_64 guest (QEMU/KVM),
stock INVARIANTS kernel #0 Thu Jul 2 06:02:54 UTC 2026, then instrumented
and fixed kernels built in-guest with
make nativekernel. - File: sys/kern/subr_diskslice.c (dssize :836-871), consumer wired from sys/kern/subr_disk.c diskpsize :1259-1276 (+ DSO_RAWPSIZE fallback :1270-1274), racing disk_probe replacement :367-368/:494 via disk_msg_core under ds_token.
- Bucket: memcorrupt. Severity: Medium. Confidence: certain.
TL;DR verdict
REPRODUCED on the stock guest: racing swapon (d_psize -> dssize) against
root-forced reprobes (DIOCSYNCSLICEINFO arg=1) makes the kernel install a
wrong swap geometry silently: 125-277 of every 2000 swapon cycles
returned the whole-disk size (16384 blocks) for partition vn0s1a whose real
label says 4096 blocks β verified against a same-instant DIOCGDINFO64 probe
showing the in-core label was CORRECT (so the value did not come from the
label). With kprintf instrumentation between the racy reads (build #1) the
same race produced a fatal trap 12 page fault in dssize+0x15c
(movq 0x40(%rax),%rax, fault addr 0x40) β the ENTER/WALK traces show
dssize walking an older generation of struct diskslices
(...6a00 vs current ...7c00) whose label/ops pointers were freed β the
guest died at the ddb prompt. The token fix (diskpsize takes ds_token
around dssize + dssize revalidates slice bounds) removes the UAF walk and,
together with suppressing the DSO_RAWPSIZE whole-disk substitution for
partition devices, drives the wrong-geometry anomalies to 0/2000 and 0/2000
with clean ENXIO failures instead (fix kernel #3).
Evidence
- run.log / fix_run.log β stock vs patched counters
- panic.txt β full serial console trace: DF2831 ENTER/WALK pointer
generations, fatal trap 12,
Stopped at dssize+0x15c - fixinst trace (in VERDICT.md) β 219x "blocks=-1 nolabel" matched 1:1 with 219x "RAWPSIZE fallback -> raw whole-disk size" = provenance of the wrong value on the token-fixed kernel (label loss is the DF-2742 diskstrategy family; the fallback misdesign turns it into silent wrong geometry)
- fix.diff β ds_token in diskpsize + dssize/dsopen bounds revalidation + RAWPSIZE fallback restricted to whole-disk/whole-slice devices
Reproduce
see build.sh / run.sh. Root on the guest; ~90 s per 2000-iteration run.
DF-2831 VERDICT β full narrative
The bug
dssize() (sys/kern/subr_diskslice.c:836-871) snapshots *sspp
(= dp->d_slice), checks slice < ssp->dss_nslices and the open mask on
that snapshot, optionally re-opens the device (which can block and let a
reprobe run), reloads ssp = *sspp, and then dereferences
ssp->dss_slices[slice].ds_label / .ds_ops and calls
ops->op_getpartbounds() β with no ds_token and no revalidation of the
reloaded pointer. Its only caller diskpsize() (sys/kern/subr_disk.c:
1259-1276, the d_psize dispatch) also takes no token. Meanwhile
disk_msg_core processes DISK_DISK_REPROBE (forced by root via
DIOCSYNCSLICEINFO with arg != 0, which skips the open-busy check at
subr_diskslice.c:627-644) by running disk_probe(), which replaces
dp->d_slice with a fresh allocation (subr_disk.c:367-368, mbrinit
realloc, gptinit realloc) and dsgone()s the old one (:494) β freeing the
very struct dssize walks, together with its disklabel.
dsopen() (subr_diskslice.c:761-777) additionally lacks the
slice >= dss_nslices bounds check that every other entry point performs.
Sibling of DF-2742 (diskstrategy, same missing-serialization family) but a distinct sink (d_psize path) with its own fix site; DF-2742's recommended fix does not cover dssize/diskpsize.
How it was proven
- Stock kernel #0, fresh
vm.sh reset with-src: crafted MBR image (genimg2831.c) vnconfig'd to /dev/vn0 β one DFLYBSD slice s1 with a valid disklabel64 whose partition 'a' is 2 MiB (=4096 512-blocks). Racer: swapon("/dev/vn0s1a")+swapoff in a loop; hammer: ioctl DIOCSYNCSLICEINFO(1) on /dev/vn0 (~150-300k reprobes/run). Baseline without hammer: 2000/2000 clean, size always 4096. With hammer (3 independent stock runs): ok_WRONG_SIZE = 206/2000, 285/2000, 277/2000, 125/2000 β swapon succeeded with swapinfo reporting 16384 blocks (= whole-disk ds_size) while a same-instant DIOCGDINFO64 probe of /dev/vn0s1 showed the in-core label still says 'a' = 4096 blocks: the size did NOT come from the label. - Instrumented stock kernel #1 (kprintf between the racy reads only):
serial console captured the two pointer generations diverging live β
ENTER ... ssp=0xfffff8008d5d7c00vsWALK ssp=0xfffff8008d5d6a00with label=0xfffff8008edb5e80 β and thenFatal trap 12: page fault while in kernel mode ... fault virtual address = 0x40 ... Stopped at dssize+0x15c: movq 0x40(%rax),%raxβ a staleds_label/ds_opsread from the freed struct. Guest died atdb>(panic.txt). This is the use-after-free read, caught mid-race. - Provenance of the silent wrong value (fix+instrumentation build #2,
trace counts): 219/1500 anomalies, and dmesg showed exactly
219
DF2831 blocks=-1 nolabel vn0s1amatched 1:1 with 219DF2831 RAWPSIZE fallback vn0s1a -> raw whole-disk size. Chain: the reprobe's own label re-read goes through diskstrategy (no ds_token β the DF-2742 window), hits the transient 2-slice struct, fails, and disk_probe_slice (subr_disk.c:324-332) then clears the slice's label; dssize correctly returns -1 for a labelless partition, and diskpsize's DSO_RAWPSIZE fallback (vn sets DSO_RAWPSIZE, vn.c:579) silently substitutes the raw whole-disk size for a partition device. swapon installs a 4x-oversized swap device on a 2 MiB partition.
Impact
- Unprivileged reachability: NO β both swapon and forced reprobe are root-only (diskopen enforces SYSCAP_RESTRICTEDROOT). This is a privileged-trigger race: root racing root, or root-forced reprobe vs automated swap management.
- Demonstrated: kernel use-after-free read (fatal trap 12), and silent wrong swap geometry (4x oversized swap on a partition; swap I/O past the real partition then fails/clamps at dscheck β corrupted swap bitmap accounting, potential page-loss/DoS). Latent: same-size-class recycling of the freed ssp/label can feed arbitrary kernel-object content into partition bounds (the instrumented trace shows benign recycled-label reads returning 4096 right up until the fault).
- Severity: Medium (local DoS/integrity with privileged trigger; memory-safety class UAF read).
Fix validation (kernel #3, final fix.diff)
make nativekernelin guest, rebooted into #3 (fix_run.log).- Same PoC, same churn: ok_WRONG_SIZE=0/2000 and 0/2000 (stock: 125-277), failures became clean, explicit ENXIO ("Device not configured") during the transient labelless windows β 703/887 occurrences β instead of a silent lie; no panic; no-hammer sanity 2000/2000 swapon OK; guest up throughout.
- Residual: the transient label loss itself is the DF-2742 diskstrategy family (recommended fix there: ds_token around the dp->d_slice fetch + dscheck, and in disk_invalidate). With DF-2742 fixed, the ENXIO transients disappear too; DF-2831's own fix stands alone and correct regardless.
Kernel references
- sys/kern/subr_diskslice.c:849-870 (dssize unsynchronized walk + missing revalidation β the audited file)
- sys/kern/subr_diskslice.c:761-777 (dsopen missing slice bounds check)
- sys/kern/subr_disk.c:1259-1276 (diskpsize, no ds_token), :1270-1274 (DSO_RAWPSIZE whole-disk substitution for partition devices)
- sys/kern/subr_disk.c:367-368,494 (replacement + dsgone under ds_token), :512-598 (disk_msg_core holds ds_token)
- sys/vm/vm_swap.c:307-317 (swaponvp consumer), sys/dev/disk/vn/vn.c:579 (DSO_RAWPSIZE)
Fix verification
fixedfix.diff applied to guest /usr/src, make nativekernel + installkernel, reboot into #3. Baseline (stock, fresh reset): ok_WRONG_SIZE 277/2000 and 125/2000, silent. Patched: ok_WRONG_SIZE 0/2000 and 0/2000, swapon failures under race became clean ENXIO (703/887) instead of a silent 4x-oversized swap device; control (no hammer) 2000/2000 OK; no panic; guest stable. UAF walk eliminated (token-serialized build ran equal churn with zero faults vs stock-instrumented fatal trap 12 within 800 iterations).
findings/poc/DF-2831/fix_run.log; findings/poc/DF-2831/fix.diff; VERDICT.md 'Fix validation'
Confirmed kernel references
Detail
Exploit chain
root: hammer ioctl(DIOCSYNCSLICEINFO,1) on a disk with an open slice partition; concurrently swapon() that partition -> dev_dpsize -> diskpsize -> dssize snapshots *sspp, reopens (mask gone after reprobe), reloads ssp, dereferences dss_slices[slice].ds_label/.ds_ops while the reprobe's dsgone() freed them -> UAF read (fatal trap 12 observed instrumented) or, via label-clear + DSO_RAWPSIZE fallback, silent whole-disk-sized swap installed on a partition (integrity/DoS). No unpriv->root chain: both triggers are privileged (SYSCAP_RESTRICTEDROOT on open).
Evidence (decisive lines)
['findings/poc/DF-2831/run.log: stock counters 206/285/244/277/125 wrong-size of 2000/2000/1500/2000/2000, control 0', "findings/poc/DF-2831/panic.txt: DF2831 ENTER ssp=0xfffff8008d5d7c00 vs WALK ssp=0xfffff8008d5d6a00 then 'Fatal trap 12 ... Stopped at dssize+0x15c: movq 0x40(%rax),%rax'", "findings/poc/DF-2831/VERDICT.md: fix+instrumentation trace 219x 'blocks=-1 nolabel' == 219x 'RAWPSIZE fallback -> raw whole-disk size' (provenance)", 'findings/poc/DF-2831/fix_run.log: fix kernel #3 ok_WRONG_SIZE=0 twice, swapon_err 703/887 clean ENXIO, no-hammer 2000/2000, guest up']
PoC changes
Wrote PoC from scratch (no seed): genimg2831.c builds an MBR+DFLYBSD-slice+valid-disklabel64 image with a 2MiB FS_SWAP partition 'a' (fstype must be non-zero or devfs creates no s1a node); poc2831.c errno-class racer; poc2831b.c size-checking racer with same-instant DIOCGDINFO64 in-core label probe and dual-disk churn variant; instrument.diff/fixinst.diff for kernel provenance tracing.
Verified recommended fix
Take ds_token around the dssize() call in diskpsize(), revalidate slice < dss_nslices after dssize's re-open reload (and add the same bounds check to dsopen), and restrict the DSO_RAWPSIZE fallback to whole-disk/whole-slice devices so partition geometry is never silently replaced by whole-disk size - see fix.diff.
Verdict
REPRODUCED. dssize() (subr_diskslice.c:849-870) walks dp->d_slice and its per-slice disklabel with no ds_token while disk_msg_core's forced reprobes replace and dsgone() those structures; diskpsize() (subr_disk.c:1259-1276) is its only caller and also takes no token. On the stock INVARIANTS guest, swapon on vn0s1a racing root-forced DIOCSYNCSLICEINFO reprobes silently installed whole-disk-sized swap (16384 blocks) for a 4096-block partition in 6-14% of 2000-cycle runs (4 independent runs, one from a fresh vm.sh reset), while a same-instant DIOCGDINFO64 probe proved the in-core label was correct - the value came from state outside the label. With kprintf tracing between the racy reads the same race produced a fatal trap 12 page fault in dssize+0x15c (movq 0x40(%rax),%rax, fault addr 0x40) with the serial trace showing ENTER ssp=...7c00 vs WALK ssp=...6a00 - dssize dereferencing a freed previous-generation struct diskslices; guest died at db> (panic.txt). Provenance of the silent wrong value was then pinned 1:1 (219/219): reprobe label re-reads fail through the unserialized diskstrategy (DF-2742 family) causing disk_probe_slice to clear the slice label, dssize returns -1, and diskpsize's DSO_RAWPSIZE fallback substitutes the raw whole-disk size for a PARTITION device. Privileged trigger (root races root / forced reprobe vs automated swap management); impact ceiling: UAF read (panic), silent wrong swap geometry (oversized swap on a partition, swap accounting corruption). Fix (ds_token around dssize in diskpsize + slice-bounds revalidation in dssize/dsopen + RAWPSIZE fallback restricted to whole-disk/whole-slice devices) validated on rebuilt kernel #3: wrong-size 0/2000 and 0/2000 vs 125-277 stock, failures become clean ENXIO, control run 2000/2000 OK, no panic, guest stable.
No comments yet.