โฌข DragonFlyBSD Kernel Audit
โ† dashboard
DF-0074

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.

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
FileTypeDescriptionSize
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
README.md readme build/run/expected + reachability notes
โ†“ download 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 zone in _kmalloc <- fork1
  • Fatal trap 12 ... slab_cleanup+0x1c9 (NULL deref, idle reclaimer)
  • panic: ... hammer2_chain_create preceded by dscheck(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

  • python3 is not on the DragonFly guest by default; generate overflow.img on a host and ship it.
  • Only >= 15 GPT entries are needed for dss_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 the DIOCGSLICEINFO destination is undersized.
VERDICT.md verdict full narrative: root cause line-by-line, trigger, three panics, reachability/severity, PoC changes
โ†“ download raw

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: DIOCGSLICEINFO is _IOR('d', 111, struct diskslices) (sys/sys/diskslice.h:96). mapped_ioctl allocates the data buffer with size = IOCPARM_LEN(com) = sizeof(struct diskslices) = offsetof(dss_slices) + MAX_SLICES * sizeof(struct diskslice) = 32 + 16 * 256 = 4128 bytes (sys/kern/sys_generic.c:668,675).
  • Source size for a GPT disk: subr_diskgpt.c:175 calls dsmakeslicestruct(BASE_SLICE + MAX_GPT_ENTRIES, info) = 130 slots (BASE_SLICE = 2, MAX_GPT_ENTRIES = 128), and subr_diskgpt.c:222 sets ssp->dss_nslices = BASE_SLICE + i (= 130 for any GPT whose header entries >= 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 bcopy length becomes offsetof(dss_slices) + dss_nslices * sizeof(struct diskslice) = 32 + 130 * 256 = 33312 bytes, written into the 4128-byte data buffer โ†’ 29184 bytes (~28 KB) of overrun past the end of an M_IOCTLOPS slab object, on every call. The overrun bytes are attacker-influenced (ds_offset, ds_size, ds_type_uuid, ds_stor_uuid come straight from the crafted GPT entries โ€” subr_diskgpt.c:237+).
  • The trailing copyout (sys_generic.c:730) only copies the declared sizeof(struct diskslices) back, so the corruption is confined to kernel heap (the user merely observes dss_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*) are root:operator crw-r-----, and the operator group contains only root (/etc/group: operator:*:5:root). An unprivileged user (maxx, uid 1001) gets open: 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 operator group, 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 the dss_nslices inflation; only the DIOCGSLICEINFO (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 the M_IOCTLOPS neighborhood 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

  1. trigger_stress.c (new): the original trigger.c only issues a single DIOCGSLICEINFO. On master DEV the single-shot overflow did not synchronously panic (the corrupted neighbor wasn't exercised in-band). trigger_stress.c issues 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 original trigger.c is retained as the minimal proof (it returns nslices=130, proving the overflow).
  2. build.sh / run.sh (new): self-contained, runnable reproduce scripts. run.sh documents that it must run as root (or operator) and that the panic is asynchronous (proof in dfbsd-qemu/boot.log).
  3. Image build clarified: python3 is not installed on the DragonFly guest, so build_gpt.py runs on a host with python3 and the resulting overflow.img is shipped to the guest. build.sh warns if the image is missing.
  4. 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 โ€” mdconfig is 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.

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

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