DIOCGSLICEINFO heap buffer overflow via crafted GPT disk image (dss_nslices > MAX_SLICES)
| Field | Value |
|---|---|
| ID | DF-0074 |
| Status | new |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:H |
| CWE | CWE-122 Heap-based Buffer Overflow |
| File | sys/kern/subr_diskslice.c |
| Lines | 556-559 |
| Area | kern (disk slice / disklabel parsing) |
| Confidence | certain |
| Discovered | 2026-06-30 |
| Reported | pending |
Summary
The DIOCGSLICEINFO handler in dsioctl copies the live kernel struct
diskslices into the ioctl data buffer using a length derived from the
actual slice count dss_nslices:
case DIOCGSLICEINFO:
bcopy(ssp, data, (char *)&ssp->dss_slices[ssp->dss_nslices] -
(char *)ssp); /* sys/kern/subr_diskslice.c:557 */
return (0);
But the destination data buffer is sized sizeof(struct diskslices) โ encoded
into the ioctl command by _IOR('d', 111, struct diskslices) (diskslice.h:96)
โ which only has room for MAX_SLICES = 16 slice slots (diskslice.h:122,176).
The kernel allocates exactly that many bytes in mapped_ioctl
(sys_generic.c:668).
For a GPT-formatted disk, dsmakeslicestruct allocates
BASE_SLICE + MAX_GPT_ENTRIES = 2 + 128 = 130 slots (subr_diskgpt.c:175) and
sets dss_nslices = BASE_SLICE + i up to 130 (:222, where i iterates the
on-disk entry count up to MAX_GPT_ENTRIES). The on-disk entries field only
needs to be >= 15 for dss_nslices to exceed MAX_SLICES.
Net result: a GPT disk with >= 15 partition entries causes the bcopy to
write (dss_nslices โ 16) ร sizeof(struct diskslice) bytes past the end of the
~4 KB data buffer โ up to ~29 KB of kernel-heap overrun when
dss_nslices = 130. The overrun bytes include attacker-controlled fields
(ds_offset, ds_size, ds_type_uuid, ds_stor_uuid) from each valid GPT
entry (subr_diskgpt.c:258-262). The source side stays in-bounds (130-slot
alloc); only the destination overflows.
Reachability: an attacker presents a crafted GPT disk image (USB mass
storage, mdconfig/virtual disk, etc.). The auto-probe populates
dss_nslices. Any subsequent DIOCGSLICEINFO ioctl on a slice device of that
disk corrupts the kernel heap. Reachable by any local principal that can open the
slice device node and issue the ioctl (common disk-inspection utilities do this).
Impact: kernel heap corruption suitable for local privilege escalation /
kernel-mode code execution via heap grooming; reliable kernel-panic DoS is
trivial. The copyout in mapped_ioctl only copies sizeof(struct diskslices)
bytes back, so the corruption is confined to kernel heap.
Recommended fix
Cap the copy at the destination size:
--- a/sys/kern/subr_diskslice.c
+++ b/sys/kern/subr_diskslice.c
@@ case DIOCGSLICEINFO:
- bcopy(ssp, data, (char *)&ssp->dss_slices[ssp->dss_nslices] -
- (char *)ssp);
+ {
+ u_int n = (ssp->dss_nslices > MAX_SLICES) ?
+ MAX_SLICES : ssp->dss_nslices;
+ bcopy(ssp, data, (char *)&ssp->dss_slices[n] - (char *)ssp);
+ }
return (0);
Better: replace the raw-struct ioctl with a structured copy that does not expose kernel pointers (see DF-0075) and uses a dedicated output structure sized to the actual returned data.
Proof of concept
See findings/poc/DF-0074/. A script builds a minimal GPT disk image with 128
partition entries (all nil-typed except a few), attaches it, and issues
DIOCGSLICEINFO to trigger the heap overflow.
Timeline
- 2026-06-30 Discovered during automated file-by-file audit of
sys/kern/subr_diskslice.c. - pending Reported to DragonFlyBSD security contact.
PoC verification
Evidence pack
findings/poc/DF-0074 ยท 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| build_gpt.py | trigger-source | host-side generator: minimal valid GPT image, header entries=128, 17 non-nil entries -> dss_nslices=130 | 4.2 KB | view raw |
| trigger.c | trigger-source | minimal proof: opens slice dev, issues DIOCGSLICEINFO, prints nslices (=130 => overflow) | 1.0 KB | view raw |
| trigger_stress.c | exploit-trigger | ioctl + slab churn (open/close fds, fork/exit) + parallel flood to surface the async slab-corruption panic | 2.1 KB | view raw |
| build.sh | build-script | cc -O2 trigger.c and trigger_stress.c; warns if overflow.img missing | 692 B | view raw |
| run.sh | run-script | vnconfig -c vn0 overflow.img; single trigger + stress + flood; root/operator only | 1.3 KB | view raw |
| build.log | build-log | final successful build, full compiler output (BUILD_SCRIPT_EXIT=0) | 244 B | view raw |
| run.log | run-log | decisive run: nslices=130 returned, then ssh dies (RC=124) when panic hits during the flood | 1.6 KB | view raw |
| panic.txt | panic-signature | three distinct panic signatures from dfbsd-qemu/boot.log across three fresh-boot runs (slab_cleanup, slaballoc corrupted zone, hammer2 cascade) | 5.4 KB | view raw |
| env.txt | environment | uname, cc 8.3, vnconfig, devfs perms (root:operator crw-r-----), operator group = root only, maxx EACCES | 1.6 KB | view raw |
| VERDICT.md | verdict | full narrative: root cause line-by-line, trigger, three panics, reachability/severity, PoC changes | 8.3 KB | โ raw |
| fix.diff | suggested-fix | cap DIOCGSLICEINFO bcopy at MAX_SLICES; git apply --check + patch --dry-run pass; matches finding proposal | 766 B | view raw |
| PANIC.txt | archive | archived capture from the prior 6.4.2-RELEASE verification (historical) | 4.9 KB | view raw |
| README.md | readme | build/run/expected + reachability notes | 3.9 KB | โ raw |
DF-0074 PoC โ DIOCGSLICEINFO heap overflow via crafted GPT disk image
What this proves
The DIOCGSLICEINFO handler (sys/kern/subr_diskslice.c:557) bcopy()s
dss_nslices-worth of struct diskslice (256 B each) into the ioctl data
buffer, which mapped_ioctl sizes at sizeof(struct diskslices) = 4128 B
(only MAX_SLICES = 16 slots). A GPT disk whose header advertises 128 entries
makes the kernel set dss_nslices = BASE_SLICE + 128 = 130
(sys/kern/subr_diskgpt.c:175,222), so the bcopy writes
32 + 130*256 = 33312 bytes into the 4128-byte buffer โ a deterministic
29184-byte (~28 KB) overrun of kernel heap on every call. With slab
churn the overrun reliably surfaces as a kernel panic; the underlying memory
corruption is weaponizable for local privilege escalation.
Verified on
DragonFly 6.5-DEVELOPMENT master DEV
(v6.5.0.1712.g89e6a-DEVELOPMENT #1, X86_64_GENERIC). See VERDICT.md for
the full narrative and env.txt for the guest environment.
Reproduce
# 1. On a host with python3: build the crafted GPT image (128 entries).
python3 build_gpt.py overflow.img
# 2. Copy the whole folder + overflow.img to the guest (as maxx).
# scp -r . dfbsd-maxx:poc/DF-0074/
# 3. On the guest, build the triggers (as maxx).
sh ./build.sh # -> trigger, trigger_stress
# 4. Attach the image and fire the overflow (as root; see Reachability).
# sh ./run.sh vn0
#
# The single trigger prints "DIOCGSLICEINFO returned nslices=130"
# (proof the oversized bcopy executed). The stress/flood section then
# forces the slab corruption to surface. The panic is ASYNCHRONOUS and
# may land a few seconds after the script returns; capture it from the
# serial console log (dfbsd-qemu/boot.log on the QEMU host).
run.sh must run as root (or a principal in the operator group / a
devfs class that exposes slice devices). On the default devfs ruleset,
/dev/vn0* are root:operator crw-r----- and operator contains only
root, so an unprivileged user gets EACCES.
Expected result (bug present)
- The trigger returns
nslices=130(the 28 KB heap overflow executes). - The guest panics within the run (asynchronous, heap-layout dependent).
Observed signatures on master DEV (all in
panic.txt): panic: slaballoc: corrupted zonein_kmalloc <- fork1Fatal trap 12 ... slab_cleanup+0x1c9(NULL deref, idle reclaimer)panic: ... hammer2_chain_createpreceded bydscheck(vbd0s1d): slice too large(the overrun corrupted the root disk's slice metadata)
Expected result (bug fixed)
With fix.diff applied, the bcopy is capped at MAX_SLICES; the trigger
returns nslices=130 (the live count is unchanged) but no heap overrun
occurs and no panic follows under any amount of churn.
Files
| File | Purpose |
|---|---|
build_gpt.py |
host-side GPT image generator (128 entries) |
trigger.c |
minimal proof: single DIOCGSLICEINFO, prints nslices |
trigger_stress.c |
ioctl + slab churn + fork/flood to surface the panic |
build.sh / run.sh |
self-contained reproduce scripts |
build.log / run.log |
full untrimmed build / decisive-run output |
panic.txt |
the three panic signatures from dfbsd-qemu/boot.log |
env.txt |
guest uname, cc, devfs perms, reachability caveat |
VERDICT.md |
full root-cause + reproduction narrative |
fix.diff |
git apply-able bounds fix (cap copy at MAX_SLICES) |
manifest.json |
machine-readable artifact catalog |
PANIC.txt |
archived capture from the prior 6.4.2-RELEASE run |
Notes
python3is not on the DragonFly guest by default; generateoverflow.imgon a host and ship it.- Only
>= 15GPT entries are needed fordss_nslices > MAX_SLICES(dss_nslices = 2 + i); the PoC uses the maximum 128 for the largest overrun. - The overflow source (
dsmakeslicestruct, 130 slots) is correctly allocated; only theDIOCGSLICEINFOdestination is undersized.
DF-0074 โ VERDICT
Verdict: REPRODUCED (kernel heap overflow -> panic) on master DEV
The DIOCGSLICEINFO heap-buffer overflow described in the finding is real,
present in the current DragonFlyBSD master DEV kernel, and was reproduced as a
kernel panic on every fresh-boot attempt. The overflow is mathematically
deterministic on every invocation; the resulting crash site is heap-layout
dependent (three distinct panic signatures observed across three fresh-boot
runs, all from the same 28 KB overrun).
Guest tested: DragonFly dfbsd 6.5-DEVELOPMENT DragonFly
v6.5.0.1712.g89e6a-DEVELOPMENT #1: Mon Jun 29 14:18:01 UTC 2026
x86_64 / X86_64_GENERIC (the audit's master DEV build).
Root cause (confirmed line-by-line in sys/)
The DIOCGSLICEINFO handler copies the live kernel struct diskslices
into the ioctl data buffer using a length derived from the actual slice
count, but the destination buffer is only sized for MAX_SLICES = 16 slots:
/* sys/kern/subr_diskslice.c:556-559 */
case DIOCGSLICEINFO:
bcopy(ssp, data, (char *)&ssp->dss_slices[ssp->dss_nslices] -
(char *)ssp);
return (0);
- Destination size:
DIOCGSLICEINFOis_IOR('d', 111, struct diskslices)(sys/sys/diskslice.h:96).mapped_ioctlallocates thedatabuffer withsize = IOCPARM_LEN(com)=sizeof(struct diskslices)=offsetof(dss_slices) + MAX_SLICES * sizeof(struct diskslice)=32 + 16 * 256 = 4128bytes (sys/kern/sys_generic.c:668,675). - Source size for a GPT disk:
subr_diskgpt.c:175callsdsmakeslicestruct(BASE_SLICE + MAX_GPT_ENTRIES, info)= 130 slots (BASE_SLICE = 2,MAX_GPT_ENTRIES = 128), andsubr_diskgpt.c:222setsssp->dss_nslices = BASE_SLICE + i(= 130 for any GPT whose headerentries >= 128).dsmakeslicestruct(subr_diskslice.c:720-723)kmallocs the full 130-slot object, so the source is in-bounds โ only the destination overflows. - The overflow: the
bcopylength becomesoffsetof(dss_slices) + dss_nslices * sizeof(struct diskslice)=32 + 130 * 256 = 33312bytes, written into the 4128-bytedatabuffer โ 29184 bytes (~28 KB) of overrun past the end of anM_IOCTLOPSslab object, on every call. The overrun bytes are attacker-influenced (ds_offset,ds_size,ds_type_uuid,ds_stor_uuidcome straight from the crafted GPT entries โsubr_diskgpt.c:237+). - The trailing
copyout(sys_generic.c:730) only copies the declaredsizeof(struct diskslices)back, so the corruption is confined to kernel heap (the user merely observesdss_nslices = 130).
Struct sizes were verified on the guest with a small probe:
sizeof(struct diskslice) = 256, sizeof(struct diskslices) = 4128,
offsetof(dss_slices) = 32, overflow = (130-16)*256 = 29184 (~28 KB).
Trigger & evidence
A crafted 1 MiB GPT image (build_gpt.py, header entries = 128, 17 non-nil
entries) is attached with vnconfig -c vn0 overflow.img. The kernel
auto-probes the GPT and creates /dev/vn0s0../dev/vn0s16 (already proving
dss_nslices > 16). Issuing DIOCGSLICEINFO on /dev/vn0s1 returns
nslices=130 โ proof the oversized bcopy executed โ and writes 28 KB past
the data buffer into adjacent kernel heap.
Three fresh-boot runs (via vm.sh reset -> clean-install) all panicked, each
at a different downstream site of the same slab corruption (see panic.txt):
| Run | Panic site | Mechanism |
|---|---|---|
| 1 | slab_cleanup+0x1c9 (NULL deref, trap 12, idle) |
async slab reclaimer walked corrupted zone metadata |
| 2 | panic: slaballoc: corrupted zone in _kmalloc <- fork1 <- sys_fork |
slab allocator's own KKASSERT caught the corrupted zone during a flood-induced fork |
| 3 | panic: assertion "parent->error == 0" in hammer2_chain_create preceded by dscheck(vbd0s1d): slice too large |
the 28 KB overrun corrupted the root disk's slice metadata in heap, breaking hammer2 |
Run 3 is especially telling: the overflow corrupted slab objects describing
the unrelated root disk (vbd0s1d), demonstrating the overrun reaches
arbitrary adjacent kernel state โ not just the crafted vn0 disk. The
panic: slaballoc: corrupted zone (Run 2) is the most direct signature: the
slab allocator's zone-consistency assertion fired because of the overflow.
The crash is asynchronous and probabilistic in exact site/timing (the
overrun corrupts whatever slab object happens to be adjacent at runtime), but
the underlying 28 KB heap overflow is 100% deterministic on every call โ
nslices=130 is returned on every single invocation, including the ~15 runs
that did not immediately panic.
Reachability / privilege (severity calibration)
- Default devfs: slice device nodes (
/dev/vn0*,/dev/md0*,/dev/serno/*/s*,/dev/vbd0*) areroot:operator crw-r-----, and theoperatorgroup contains onlyroot(/etc/group:operator:*:5:root). An unprivileged user (maxx, uid 1001) getsopen: Permission denied(EACCES) โ confirmed on the guest. So on a default single-user/workstation box the bug is root-only to trigger directly. - However, the same defect is reachable by any principal that can open a
slice device and issue the ioctl: any user in the
operatorgroup, any devfs ruleset that exposes slice devices to a group/class (common on multi-user servers, build/jail hosts, disk-inspection utilities, container/VM hosts that pass through storage), and any disk-probing utility that runs with such access. Presenting the crafted image via USB mass storage reaches the auto-probe path with no ioctl needed from the attacker for thedss_nslicesinflation; only theDIOCGSLICEINFO(or any consumer of the raw-struct ioctl) needs to fire. - Primitive: a deterministic ~28 KB attacker-influenced kernel-heap write
(CWE-122). Reliable kernel-panic DoS is trivially demonstrated. With slab
grooming (spray victim objects carrying function pointers /
ucred */ refcounts into theM_IOCTLOPSneighborhood adjacent to the 4128-byte allocation, punch a hole, trigger the overflow into it), the corruption is convertible to kernel-mode code execution / local privilege escalation. The overflow size (28 KB) and the attacker-controlled content make this a strong primitive; full LPE was not developed in this verification pass (time-bounded), but the path is open.
PoC changes made during verification
trigger_stress.c(new): the originaltrigger.conly issues a singleDIOCGSLICEINFO. On master DEV the single-shot overflow did not synchronously panic (the corrupted neighbor wasn't exercised in-band).trigger_stress.cissues the ioctl, then churns the slab allocator (open/close 64 fds, fork/exit 40 children) and a 16-process parallel flood to force a corrupted neighbor to be allocated/freed/validated, reliably surfacing the panic within a run. The originaltrigger.cis retained as the minimal proof (it returnsnslices=130, proving the overflow).build.sh/run.sh(new): self-contained, runnable reproduce scripts.run.shdocuments that it must run as root (or operator) and that the panic is asynchronous (proof indfbsd-qemu/boot.log).- Image build clarified:
python3is not installed on the DragonFly guest, sobuild_gpt.pyruns on a host with python3 and the resultingoverflow.imgis shipped to the guest.build.shwarns if the image is missing. - Device naming: DragonFly uses
/dev/vn0s1+vnconfig -c vn0(the original PoC text referenced NetBSD-style/dev/vnd0s1/vnd0/mdconfig, none of which apply here โmdconfigis absent on this guest).
No attack-logic changes were needed; build_gpt.py produced a valid GPT
(header entries=128, correct CRCs) on the first host-side run and the kernel
probed it correctly.
Recommended fix
fix.diff (validates with both git apply --check and patch --dry-run):
cap the bcopy length at MAX_SLICES so it never exceeds the destination
buffer size. This matches the finding markdown's ## Recommended fix
proposal (the same one-line clamp), with an added explanatory comment and a
local u_int ncap to keep the bcopy expression readable. The deeper fix
(replacing the raw-struct ioctl with a structured, pointer-free output โ see
DF-0075) remains desirable but is out of scope for this minimal bounds fix.
Confirmed kernel references
- sys/kern/subr_diskslice.c:557
- sys/kern/subr_diskslice.c:556
- sys/kern/subr_diskslice.c:720
- sys/kern/subr_diskslice.c:723
- sys/kern/subr_diskgpt.c:175
- sys/kern/subr_diskgpt.c:222
- sys/kern/subr_diskgpt.c:50
- sys/sys/diskslice.h:96
- sys/sys/diskslice.h:122
- sys/sys/diskslice.h:176
- sys/kern/sys_generic.c:668
- sys/kern/sys_generic.c:675
- sys/kern/sys_generic.c:730
Detail
Exploit chain
Primitive = deterministic ~28KB attacker-influenced kernel-heap write past an M_IOCTLOPS kmalloc(4128) buffer; overrun bytes carry attacker-controlled GPT fields (ds_offset/ds_size/ds_type_uuid/ds_stor_uuid from subr_diskgpt.c:237+). The 4128-byte alloc lands in the slab page-zone/KVA region; adjacent objects observed to include the live root-disk diskslices (Run 3 corrupted vbd0) and slab zone headers (Run 2 tripped the slaballoc zone-consistency KKASSERT). Conversion path: groom the heap to place a victim object carrying a function pointer / ucred* / refcount adjacent to the 4128-byte data buffer (fill slabs, punch a hole, trigger DIOCGSLICEINFO to overwrite the victim), then invoke the victim for control-flow hijack or credential substitution -> uid0. Full LPE not developed in this pass; reliable kernel-panic DoS trivially achieved (demonstrated 3x). Chain trigger lives in trigger_stress.c.
Evidence (decisive lines)
findings/poc/DF-0074/panic.txt holds three panic signatures from dfbsd-qemu/boot.log: 'panic: slaballoc: corrupted zone' (trace _kmalloc<-fork1<-sys_fork<-syscall2), 'Fatal trap 12 ... Stopped at slab_cleanup+0x1c9: cmpq %rbx,(%rcx)' (NULL deref, current process Idle), and 'panic: assertion parent->error==0 failed in hammer2_chain_create' preceded by 'dscheck(vbd0s1d): slice too large'. run.log shows 'DIOCGSLICEINFO returned nslices=130' six times then ssh is killed (RC 124) by the panic during the flood. env.txt documents maxx EACCES on /dev/vn0s1 and operator:*:5:root. VERDICT.md has the line-by-line root cause.
PoC changes
Added trigger_stress.c: the original trigger.c only issues a single DIOCGSLICEINFO which did NOT synchronously panic on master DEV (the corrupted neighbor wasn't exercised in-band); trigger_stress adds slab churn (open/close 64 fds), fork/exit churn (40 children), and a 16-proc parallel flood that reliably surfaces the async slab-corruption panic within a run. Added build.sh/run.sh as self-contained reproduce scripts. Clarified that python3 is host-side only (NOT installed on the DragonFly guest -- build_gpt.py runs on the host and overflow.img is shipped) and that DragonFly uses /dev/vn0s1 + 'vnconfig -c vn0' (original PoC text referenced NetBSD-style /dev/vnd0s1, vnd0, and mdconfig which is absent here). Shipped a prebuilt overflow.img. Updated README.md to the verified reproduce flow.
Verified recommended fix
fix.diff caps the DIOCGSLICEINFO bcopy at MAX_SLICES (u_int ncap = min(dss_nslices, MAX_SLICES); bcopy ... dss_slices[ncap]) so the copy can never exceed the sizeof(struct diskslices)=4128B destination; validates with both 'git apply --check' and 'patch --dry-run'; matches the finding markdown's Recommended fix proposal (added an explanatory comment and a local ncap var for readability). Full diff at findings/poc/DF-0074/fix.diff.
Verdict
REPRODUCED on master DEV (6.5.0.1712.g89e6a). The DIOCGSLICEINFO handler at sys/kern/subr_diskslice.c:557 bcopy()s dss_nslices (=130 for a 128-entry GPT, set at subr_diskgpt.c:175,222) slots of struct diskslice (256B each) into a sizeof(struct diskslices)=4128B (16-slot) data buffer allocated at sys_generic.c:675, overrunning kernel heap by (130-16)256=29184 bytes on every call. The trigger returns nslices=130, proving the oversized copy executed; across three fresh-boot (vm.sh reset) runs the overrun surfaced as panics at three different downstream sites -- 'panic: slaballoc: corrupted zone' in _kmalloc<-fork1<-sys_fork, 'Fatal trap 12 ... slab_cleanup+0x1c9' (NULL deref in idle reclaimer), and a hammer2_chain_create assertion after the overrun corrupted the live ROOT disk's slice metadata ('dscheck(vbd0s1d): slice too large') -- all direct consequences of the same 28KB slab corruption. On default devfs, /dev/vn0 are root:operator crw-r----- with operator=root-only, so direct unprivileged trigger requires operator-group membership or a permissive devfs ruleset (confirmed: maxx uid 1001 gets EACCES); the deterministic attacker-influenced heap write is convertible to local privilege escalation with slab grooming (chain not developed in this time-bounded pass).