DIOCGSLICEINFO: dsioctl bcopy uses runtime dss_nslices (GPT disks: up to 130) against the 16-record ioctl type β 29,184-byte kernel heap OOB write per ioctl
| Field | Value |
|---|---|
| ID | DF-2741 |
| Status | new |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-787 Out-of-bounds Write |
| File | sys/kern/subr_disk.c |
| Lines | 1196-1205 (sink subr_diskslice.c:556-558) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-30 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
dsioctl()'s DIOCGSLICEINFO handler bcopy()s
offsetof(struct diskslices, dss_slices[ssp->dss_nslices]) bytes into
the kernel ioctl buffer that sys_ioctl() kmallocs at exactly
IOCPARM_LEN(DIOCGSLICEINFO) = sizeof(struct diskslices) = 4,128 bytes
(16 slice records, MAX_SLICES). gptinit() allocates
BASE_SLICE+MAX_GPT_ENTRIES=130 slice records and sets dss_nslices for
any GPT with >16 entries, so the copy is 33,312 bytes β a 29,184-byte
linear heap overflow past an M_IOCTLOPS allocation, with
per-256-byte-record attacker-controlled content (GPT lba_start/lba_end
β ds_offset/ds_size, two 16-byte UUIDs) plus kernel heap pointers and
long zero runs. DIOCGSLICEINFO also leaks kernel heap pointers
(ds_dev/ds_label/ds_ops) in-bounds for the first 16 slices (aggravator).
Threat model & preconditions
Root-class local attacker (diskopen demands SYSCAP_RESTRICTEDROOT β verified live: operator-group users get EPERM at open; ceiling on default configs is root-classβkernel code execution) with any open fd on a cooked disk node whose media carries a β₯17-entry GPT (attacker-craftable image via vn or any writable disk β GPT images are untrusted data, so the overflow content is fully attacker-chosen). On this no-KASLR/no-SMAP/no-SMEP target a controlled function-pointer overwrite yields kernel code execution.
Proof of concept
Reproduced twice on the stock guest (findings/poc/DF-2741/): mkimages.py
crafts a 128-entry GPT image; vnconfig β dss_nslices=130; a single
ioctl(fd, DIOCGSLICEINFO) already overflows (33312 > 4128); hammer
β Fatal trap 12, fault va 0x0, ip 0x8:0x0 (call through an
overflow-zeroed function pointer), guest dead 2/2. Fix (clamp the copy
to MAX_SLICES) validated by in-guest nativekernel rebuild: 51M+ hammer
iterations + 18M adjacent-disk cross-checks survived where stock
panicked 2/2.
Recommended fix
--- a/sys/kern/subr_diskslice.c
+++ b/sys/kern/subr_diskslice.c
@@ -553,10 +553,22 @@
}
return (0);
- case DIOCGSLICEINFO:
- bcopy(ssp, data, (char *)&ssp->dss_slices[ssp->dss_nslices] -
- (char *)ssp);
+ case DIOCGSLICEINFO: {
+ u_int n;
+
+ /*
+ * The DIOCGSLICEINFO ioctl buffer is only sized for
+ * MAX_SLICES slice records, but GPT disks allocate
+ * more (up to BASE_SLICE + MAX_GPT_ENTRIES). Clamp
+ * the copy to the declared size of the ioctl to avoid
+ * overflowing the kernel ioctl buffer.
+ */
+ n = ssp->dss_nslices;
+ if (n > MAX_SLICES)
+ n = MAX_SLICES;
+ bcopy(ssp, data, (char *)&ssp->dss_slices[n] - (char *)ssp);
return (0);
+ }
Timeline
- 2026-08-30 Discovered during pass-2 audit of subr_disk.c (GLM 5.3); reproduced 2/2 panic + fix validated same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2741 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc2741.c | β | 3.9 KB | view raw | |
| mkimages.py | β | 4.5 KB | view raw | |
| README.md | β | 2.5 KB | β raw | |
| VERDICT.md | β | 4.6 KB | β raw | |
| build.sh | β | 80 B | view raw | |
| run.sh | β | 219 B | view raw | |
| build.log | β | 59 B | view raw | |
| run.log | β | 393 B | view raw | |
| run.2.log | β | 422 B | view raw | |
| run.3.log | β | 942 B | view raw | |
| panic.txt | β | 1.5 KB | view raw | |
| panic_boot.log | β | 14.1 KB | view raw | |
| panic2_boot.log | β | 14.1 KB | view raw | |
| env.txt | β | 392 B | view raw | |
| fix.diff | β | 737 B | view raw |
DF-2741 β DIOCGSLICEINFO kernel heap overflow via GPT dss_nslices
Impact
Kernel heap out-of-bounds write of up to 29,184 bytes past a 4,128-byte
M_IOCTLOPS allocation, with partially attacker-controlled content
(GPT lba_start/lba_end + two 16-byte UUIDs per 256-byte record) and
kernel heap pointers. Reproduced: two identical kernel panics
(call through an overflow-zeroed function pointer, instruction pointer =
0x8:0x0) within ~30 s of ioctl hammering.
Trigger: ioctl(fd, DIOCGSLICEINFO, buf) on any cooked disk device whose
media contains a GPT with more than 16 entries (dss_nslices becomes
BASE_SLICE + entries, up to 130, while the ioctl type declares
MAX_SLICES = 16 slice records).
Reachability gate (verified live): opening any cooked disk node goes through
diskopen() (sys/kern/subr_disk.c:1072) which requires
SYSCAP_RESTRICTEDROOT β i.e. a root-class credential. Group operator
cannot open the nodes (0640 root:operator notwithstanding) β verified: an
operator-group user gets EPERM at open. So the trigger requires root; the
bug is nevertheless a genuine kernel memory-safety defect (kernel must not
corrupt its heap regardless of caller privilege) and would become directly
security-relevant if the open gate is ever relaxed (jail/devfs rulesets).
Reproduce (guest)
# 1. build cc -O -o poc2741 poc2741.c -lpthread python3 mkimages.py . # host-side; or use bundled artifacts # 2. attach the crafted GPT image (128 entries) to a vn device vnconfig -c /dev/vn0 /root/gpt.img ls /dev/vn0* | wc -l # 129 nodes appear # 3. single-shot proof of the length mismatch (the copy already overflowed) ./poc2741 /dev/vn0 info # dss_nslices (runtime) = 130 # declared type capacity = 16 # kernel bcopy length = 33312 -> 29184 bytes past the 4128-byte buffer # 4. hammer (expected: kernel panic within ~1 min) ./poc2741 /dev/vn0 hammer # cross-allocation variant: on a second, plain MBR disk vnconfig -c /dev/vn1 /root/mbr.img ./poc2741 /dev/vn1 cross & # flags any foreign byte in its zero tail ./poc2741 /dev/vn0 hammer
Expected output
Step 3 prints VULNERABLE: runtime nslices 130 > declared 16 ....
Step 4 ends in Fatal trap 12: page fault while in kernel mode,
fault virtual address = 0x0, instruction pointer = 0x8:0x0
(call through a zeroed function pointer), guest down at the db> prompt.
Fix
fix.diff clamps the bcopy to MAX_SLICES records β validated by
rebuilding the kernel in the guest and re-running this PoC (see VERDICT.md).
DF-2741 VERDICT β DIOCGSLICEINFO heap overflow
Status: reproduced. Impact: kernel memory corruption (panic; write-capable OOB). Confidence: certain.
What happens
dsmakeslicestruct()(sys/kern/subr_diskslice.c:714-747) allocates astruct disksliceswith a runtime slice count.gptinit()(sys/kern/subr_diskgpt.c:175) allocatesBASE_SLICE + MAX_GPT_ENTRIES= 2 + 128 = 130 slice records and setsssp->dss_nslices = BASE_SLICE + i(subr_diskgpt.c:222) where i can be 128 (any GPT whose header declares β₯ 128 entries; bound checked only against MAX_GPT_ENTRIES, never against MAX_SLICES).- The
DIOCGSLICEINFOioctl type is_IOR('d', 111, struct diskslices)(sys/sys/diskslice.h:96) β its buffer capacity is fixed by the declared type:offsetof(dss_slices) + MAX_SLICES * sizeof(struct diskslice)= 32 + 16256 = 4,128 bytes* (MAX_SLICES = 16, sys/sys/diskslice.h:122). dsioctl()case DIOCGSLICEINFO (sys/kern/subr_diskslice.c:556-558) doesbcopy(ssp, data, (char *)&ssp->dss_slices[ssp->dss_nslices] - (char *)ssp)β a runtime-length copy of 32 + 130256 = 33,312 bytes* into the 4,128-byte kernel ioctl buffer (sys_ioctl()kmallocs exactly IOCPARM_LEN bytes, sys/kern/sys_generic.c:668-676; 4128 > STK_PARAMS=128 so it is a heap allocation from M_IOCTLOPS).- Net effect: 29,184 bytes of out-of-bounds heap write per ioctl, with
content = 256-byte
struct diskslicerecords 16..129: per record 8-byte kernel heap pointer (ds_dev), two fully attacker-controlled u64s (ds_offset/ds_size= the GPT entry's lba_start/lba_end+1), two attacker-controlled 16-byte UUIDs, and ~200 zero bytes. A GPT with fewer entries overflows proportionally (e.g. 20 entries β 1,024 bytes OOB).
Reproduction (guest, DragonFly 6.5-DEVELOPMENT #0, stock INVARIANTS)
- Crafted GPT image (
mkimages.py): protective MBR (type 0xEE), header at LBA1 with entries=128/entsz=128 and valid CRCs, 128 non-nil entries. vnconfig -c /dev/vn0 gpt.imgβ 129 device nodes; runtime probe sets dss_nslices = 130../poc2741 /dev/vn0 info(run.log): kernel bcopy length 33312 vs buffer 4128 β the overflow already happened on this single ioctl../poc2741 /dev/vn0 hammer(two independent runs, freshvm.sh reset with-srcbetween them): both runs ended inFatal trap 12: page fault while in kernel mode,fault virtual address = 0x0,instruction pointer = 0x8:0x0, guest dead atdb>(panic.txt, panic_boot.log, panic2_boot.log). A call to address 0 is a function pointer zeroed by the mostly-zero overflow β the write-capable primitive is proven end-to-end.
Exploit chain assessment (why not uid0 here)
- Reachability:
ioctl(DIOCGSLICEINFO)needs an open fd on a cooked disk node. Verified live thatdiskopen()requires SYSCAP_RESTRICTEDROOT (subr_disk.c:1072): an operator-group (gid 5) user gets EPERM at open, despite the 0640 root:operator node bits. So only root-class credentials can trigger it on a default system β no unprivβroot chain exists via this bug today; the ceiling is rootβkernel corruption (still a kernel memory-safety defect; a sandboxed/jailed root or relaxed devfs ruleset re-opens the question). - Primitive quality: 29 KB linear heap overflow with per-record controlled u64s and predictable zero runs, on a guest/kernel with no KASLR/SMAP/SMEP β an operator-class or jailed-root attacker would have everything needed to convert this into code execution (groom adjacent M_IOCTLOPS/malloc-type objects, smash a function pointer with a controlled 8-byte ds_offset value instead of the zero that happened naturally). The naturally-occurring NULL-call demonstrates control-path takeover without any grooming at all.
Fix validation
fix.diff clamps the bcopy to MAX_SLICES records. Applied to the guest's
/usr/src, kernel rebuilt (make nativekernel), rebooted, PoC re-run:
see fix section of manifest.json / verdict.json (fix_status) β patched
kernel survived the same hammer + cross-allocation runs that panicked the
stock kernel twice, and the info mode still reports the (correct,
informational) nslices=130 header. The overflow is gone.
Kernel references
sys/kern/subr_diskslice.c:556-558 (bug), :715-747 (runtime-sized alloc), sys/kern/subr_diskgpt.c:136-144,175,222 (nslices up to 130), sys/sys/diskslice.h:96 (declared 16-record ioctl type), :122 (MAX_SLICES), :106-109 (DKMAXSLICES/DKMAXPARTITIONS=128/256 vs MAX_SLICES=16), sys/kern/sys_generic.c:668-676 (exact-size heap ioctl buffer), sys/kern/subr_disk.c:1196-1205 (dispatch to dsioctl).
Fix verification
fixedApplied fix.diff to guest /usr/src, make nativekernel (BUILD_RC=0), installkernel, reboot into #1. Baseline stock #0 panicked 2/2 within ~30s of identical hammering; patched #1 survived 51M+ hammer iterations plus 18M adjacent-disk DIOCGSLICEINFO cross-checks with zero foreign bytes and clean dmesg. The OOB write is gone; nslices=130 still reported in the (in-bounds) header as before.
run.3.log; build completed 14:03:09 UTC (guest /root/build.log); uptime intact after hammer on patched kernel.
Confirmed kernel references
Detail
Exploit chain
root-class fd on cooked disk node -> ioctl(DIOCGSLICEINFO) on a >=17-entry GPT disk -> 29KB mostly-zero heap overflow with controlled 8-byte values at fixed 256-byte strides -> adjacent heap object corruption; naturally-zeroed function pointer caused ip=0 kernel call in both reproductions. Ceiling on default config: root->kernel code execution (no unpriv reach: diskopen SYSCAP_RESTRICTEDROOT gate).
Evidence (decisive lines)
['run.log: info mode -- runtime dss_nslices=130, kernel bcopy length 33312 vs ioctl buffer 4128 (overflow 29184 bytes)', 'panic.txt + panic_boot.log + panic2_boot.log: two identical Fatal trap 12, ip=0x8:0x0 within ~30s of hammering', 'run.3.log: fix validation -- patched kernel #1 survived 51M+ hammers + 18M clean cross ioctls', 'fix.diff: one-clamp fix in sys/kern/subr_diskslice.c DIOCGSLICEINFO']
PoC changes
seed PoC written from scratch: crafted-GPT generator (mkimages.py, 128 entries), vn-based attach, info/hammer/cross modes; no changes needed after first build (offsetof header fix only).
Verified recommended fix
Clamp the DIOCGSLICEINFO bcopy to MAX_SLICES slice records (as in fix.diff); longer term, size the ioctl type for DKMAXSLICES.
Verdict
dsioctl()'s DIOCGSLICEINFO handler bcopy()s offsetof(dss_slices[dss_nslices]) bytes using the runtime slice count, which gptinit() sets up to BASE_SLICE+MAX_GPT_ENTRIES=130, into the fixed 4,128-byte kernel ioctl buffer (sizeof(struct diskslices) holds only MAX_SLICES=16 records) -- a 29,184-byte linear heap OOB write per ioctl with per-record attacker-controlled u64s (GPT lba fields), two 16-byte UUIDs, kernel heap pointers, and long zero runs. Reproduced twice on the stock guest: within ~30s of ioctl hammering the kernel called through an overflow-zeroed function pointer (Fatal trap 12, fault va 0x0, ip 0x8:0x0) and died at db>. Trigger requires an open cooked-disk fd; diskopen() demands SYSCAP_RESTRICTEDROOT, so root-class credentials are needed on default configs (operator-group users get EPERM at open -- verified live); hence no unpriv->uid0 chain exists via this bug today, and the primitive itself is fully characterized (write-capable, controlled content, no KASLR/SMAP/SMEP on target). Fix (clamp copy to MAX_SLICES) validated by in-guest nativekernel rebuild: 51M+ hammer iterations + 18M adjacent-disk cross-checks survived with zero corruption where the stock kernel panicked twice.
No comments yet.