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

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.

--- 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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2741 Β· 15 files
FileTypeDescriptionSize
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).

VERDICT.md
↓ download raw

DF-2741 VERDICT β€” DIOCGSLICEINFO heap overflow

Status: reproduced. Impact: kernel memory corruption (panic; write-capable OOB). Confidence: certain.

What happens

  1. dsmakeslicestruct() (sys/kern/subr_diskslice.c:714-747) allocates a struct diskslices with a runtime slice count. gptinit() (sys/kern/subr_diskgpt.c:175) allocates BASE_SLICE + MAX_GPT_ENTRIES = 2 + 128 = 130 slice records and sets ssp->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).
  2. The DIOCGSLICEINFO ioctl 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).
  3. dsioctl() case DIOCGSLICEINFO (sys/kern/subr_diskslice.c:556-558) does bcopy(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).
  4. Net effect: 29,184 bytes of out-of-bounds heap write per ioctl, with content = 256-byte struct diskslice records 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, fresh vm.sh reset with-src between them): both runs ended in Fatal trap 12: page fault while in kernel mode, fault virtual address = 0x0, instruction pointer = 0x8:0x0, guest dead at db> (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 that diskopen() 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Applied 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.
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Mon Aug 31 13:50:23 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

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.