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

KKASSERT panic in DAIOCTRIM from non-512-aligned user-supplied offsets

Summary

daioctl() enforces 512-byte alignment of user-supplied TRIM range via two KKASSERT() calls instead of returning EINVAL. Any caller that passes del_num not multiple of 512 triggers kernel assertion failure -> panic. KKASSERT in DragonFly expands to panic on failure. User-controlled alignment enforced by fatal assert rather than EINVAL return. Reachable directly: open /dev/daXsY (requires SYSCAP_RESTRICTEDROOT) then ioctl(fd DAIOCTRIM &args) with any non-multiple-of-512 value. [VERIFIED pass2: reproduced - real daioctl dispatch panics at both verbatim assert lines with full backtrace daioctl dev_dioctl devfs_fo_ioctl mapped_ioctl syscall2; fix-validated - misaligned EINVAL, aligned control byte-identical TRIM CDB; privileged reach]

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2275 Β· 45 files
FileTypeDescriptionSize
README.md β€” 4.1 KB ↓ raw
VERDICT.md β€” 5.5 KB ↓ raw
panic.txt β€” 2.0 KB view raw
run.log β€” 1.6 KB view raw
run_baseline_full.log β€” 5.2 KB view raw
build.log β€” 5.2 MB ↓ download
fix.diff β€” 1.0 KB view raw
fix_build.log β€” 612.9 KB ↓ download
fix_run.log β€” 2.6 KB view raw
env.txt β€” 465 B view raw
serial_mlen_full.log β€” 27.8 KB view raw
serial_moff_full.log β€” 13.8 KB view raw
rig/df_pre.c β€” 4.5 KB view raw
rig/df_post.c β€” 18.0 KB view raw
rig/gen.sh β€” 972 B view raw
rig/check_verbatim.sh β€” 724 B view raw
rig/build.sh β€” 3.1 KB view raw
rig/quick.sh β€” 1.6 KB view raw
rig/make_fix.sh β€” 527 B view raw
rig/run_baseline.sh β€” 2.3 KB view raw
rig/run_panic.sh β€” 642 B view raw
rig/run_fixed.sh β€” 1.4 KB view raw
rig/da_user.c β€” 2.5 KB view raw
rig/fix2275_2276.diff β€” 1.0 KB view raw
rig/verbatim.log β€” 247 B view raw
rig/gen.log β€” 207 B view raw
rig/build_outer.log β€” 13.8 KB view raw
rig/retry1.log β€” 665 B view raw
rig/installkernel.log β€” 80.4 KB view raw
rig/build_user.log β€” 188 B view raw
rig/run_baseline_outer.log β€” 5.2 KB view raw
rig/run_panic_mlen.log β€” 192 B view raw
rig/quick_outer.log β€” 834 B view raw
rig/run_fixed_outer.log β€” 2.6 KB view raw
rig/fix_build.log β€” 612.9 KB ↓ download
rig/fix_installkernel.log β€” 80.4 KB view raw
rig/gen_fix.log β€” 207 B view raw
rig/verbatim_fix.log β€” 247 B view raw
rig/disasm_daioctl_fixed.txt β€” 10.6 KB view raw
rig/panic_mlen.txt β€” 947 B view raw
rig/panic_moff.txt β€” 606 B view raw
rig/serial_mlen_full.log β€” 27.8 KB view raw
rig/serial_moff_full.log β€” 13.8 KB view raw
rig/env.txt β€” 465 B view raw
verdict.json β€” 4.4 KB view raw

DF-2275 verification pack β€” DAIOCTRIM KKASSERT panic on misaligned user range

Bug: sys/bus/cam/scsi/scsi_da.c:443-444 β€” the DAIOCTRIM handler enforces 512-byte alignment of the USER-SUPPLIED off_t pair with two KKASSERT()s instead of returning EINVAL. On the stock INVARIANTS kernel any successful opener of a /dev/daX node (gated by SYSCAP_RESTRICTEDROOT at daopen, scsi_da.c:505) can panic the machine with one ioctl.

Verdict: REPRODUCED (privileged-reach kernel panic / local DoS) β€” fix validated A/B.

Both asserts were tripped independently from an unprivileged-looking userland program through the REAL dispatch:

open("/dev/da99") -> daopen (real, SYSCAP gate passed as root)
ioctl(fd, DAIOCTRIM, {0, 4104})
  -> syscall2 -> mapped_ioctl (16-byte copyin)
  -> devfs_fo_ioctl -> dev_dioctl -> daioctl (real d_ioctl handler)
  -> panic: assertion "(bytes_left % 512) == 0" failed in daioctl

ioctl(fd, DAIOCTRIM, {520, 4096})
  -> panic: assertion "(bytes_start% 512) == 0" failed in daioctl

Panic traces in panic.txt / serial_mlen_full.log / serial_moff_full.log. The panic cites scsi_da.c:558 / :559 β€” that is the CONCATENATED harness file; subtract the 115-line preamble and you land exactly on verbatim lines 443 / 444 of the audited file (sha256-gated byte-for-byte, rig/verbatim.log).

Rig (kernel-compiled harness β€” CAM verbatim-interposition pattern)

The guest build tree copy of scsi_da.c is replaced IN PLACE by df_pre.c + VERBATIM scsi_da.c + df_post.c (rig/gen.sh); device da stays configured so the harness is the one and only compiled instance of the driver. The guest boots from vbd0 (virtio-blk) and has no real da device, so nothing else changes at boot. Only EXTERNAL symbols are interposed, per-object pass-through (fabricated da99 only): dev_dstrategy, xpt_schedule, xpt_action, xpt_setup_ccb, cam_periph_getccb, cam_periph_runccb, xpt_release_ccb, xpt_path_periph, xpt_path_serialno.

Everything between the syscall and the panic is REAL kernel code: mapped_ioctl copyin (sys/kern/sys_generic.c:681-724), devfs_fo_ioctl, dev_dioctl, daioctl, and (for the functional control) daopen, dacheckmedia, dagetcapacity, dasetgeom, dastrategy, dastart, dadone, getnewbuf, biowait, biodone, brelse, cam_extend, make_dev(&da_ops).

The fabricated SECOND disk da99 (real make_dev, real da_ops, real periph in the real daperiphs array, fake transport answering READ CAPACITY) never touches the boot disk.

Build

cd rig && ./build.sh          # full kernel + install + reboot (~30 min)

Run (baseline = vulnerable)

./run_baseline.sh             # non-destructive controls + DF-2276 cases
./run_panic.sh mlen           # PANIC at scsi_da.c:443
./run_panic.sh moff           # PANIC at scsi_da.c:444

Expected (vulnerable kernel)

panic: assertion "(bytes_left % 512) == 0" failed in daioctl ...
panic: assertion "(bytes_start% 512) == 0" failed in daioctl ...
backtrace: daioctl -> dev_dioctl -> devfs_fo_ioctl -> mapped_ioctl -> syscall2

Fixed kernel (A/B)

scp fix2275_2276.diff make_fix.sh dfbsd:/root/
ssh dfbsd 'sh /root/make_fix.sh' && cd rig && ./quick.sh && ./run_fixed.sh

mlen/moff -> ioctl returned r=-1 errno=22 (Invalid argument), guest stays up
aligned {0,4096} -> SUCCESS with identical TRIM CDB (1 range, lba 0, 8 blks)

Impact ceiling: honest note β€” the ioctl is only reachable after opening a da device, and daopen rejects everyone lacking SYSCAP_RESTRICTEDROOT (scsi_da.c:505), so this is a PRIVILEGED-reach denial of service (panic), not an unprivileged privesc. It is still a kernel-diagnostic-as-input- validation defect: a maintenance ioctl must never be able to panic a production (non-INVARIANTS) kernel β€” note on non-INVARIANTS kernels KKASSERT compiles to a no-op and the misaligned values fall through to the DF-2276 loop machinery instead (see ../DF-2276).

Artifacts: panic.txt, run.log, build.log, fix.diff, fix_build.log, fix_run.log, env.txt, rig/ (df_pre.c, df_post.c, gen.sh, check_verbatim.sh, build.sh, quick.sh, run_baseline.sh, run_panic.sh, run_fixed.sh, da_user.c, full logs, disasm).

VERDICT.md
↓ download raw

DF-2275 VERDICT β€” REPRODUCED (privileged-reach panic), fix validated

What was claimed

sys/bus/cam/scsi/scsi_da.c:443-444:

case DAIOCTRIM:
{
    bytes_left = del_num[1];
    bytes_start = del_num[0];

    /* TRIM occurs on 512-byte sectors. */
    KKASSERT((bytes_left % 512) == 0);     /* :443 */
    KKASSERT((bytes_start% 512) == 0);     /* :444 */

User-supplied alignment is enforced by kernel DIAGNOSTIC assertions; a bad value must produce EINVAL, not a panic.

How it was verified (mechanically, through the real d_ioctl dispatch)

  1. Rig. A DragonFly 6.5-DEVELOPMENT X86_64_GENERIC INVARIANTS kernel was built in which the driver file is df_pre.c + byte-for-byte scsi_da.c + df_post.c (rig/gen.sh; verbatim gate rig/verbatim.log: embedded copy sha256 3c795c0f8aa93c9f2833b46727a64adbc8b50df02ef6cde2b616755a7160a148 == pristine guest copy == audit tree). The harness fabricates a SECOND disk, da99: real make_dev(&da_ops, dkmakeunit(99), root:operator 0600) creating /dev/da99, a real cam_periph registered in the real daperiphs, and a fake transport that answers READ CAPACITY(10) during the real daopen/dacheckmedia/dagetcapacity/dasetgeom chain. The boot disk (vbd0) is untouched; the real da driver is the ONLY compiled copy (device da stays configured).

  2. Gate check (source line-accurate). daopen rejects openers with caps_priv_check_self(SYSCAP_RESTRICTEDROOT) (scsi_da.c:505). daioctl has NO capability check, NO softc/d_trimflag check, and no lock between entry and the asserts β€” only dkunit(dev) + cam_extend_get(daperiphs, unit) (scsi_da.c:429-431). DAIOCTRIM is _IOW('C', 128, off_t[2]) (sys/bus/cam/scsi/scsi_daio.h:38); mapped_ioctl copyins the 16 bytes into a kernel buffer before the driver sees them (sys/kern/sys_generic.c:681-724), so del_num[] is fully attacker data.

  3. Panic 1 (bytes_left). Userland ioctl(fd, DAIOCTRIM, {0, 4104}) on /dev/da99 (opened through the REAL daopen β€” root passes the caps gate):

    panic: assertion "(bytes_left % 512) == 0" failed in daioctl at /usr/src/sys/bus/cam/scsi/scsi_da.c:558 daioctl() at daioctl+0x34d dev_dioctl() at dev_dioctl+0x65 devfs_fo_ioctl() at devfs_fo_ioctl+0x95 mapped_ioctl() at mapped_ioctl+0x5fa syscall2() at syscall2+0x11e

558 - 115 (preamble lines) = 443 β€” the exact audited line.

  1. Panic 2 (bytes_start). ioctl(fd, DAIOCTRIM, {520, 4096}):

    panic: assertion "(bytes_start% 512) == 0" failed in daioctl at /usr/src/sys/bus/cam/scsi/scsi_da.c:559 [= verbatim 444] (same backtrace)

Full serial console output: panic.txt, serial_mlen_full.log, serial_moff_full.log. Both runs took the whole guest down (DDB), i.e. the panic is a full machine DoS from one syscall.

  1. Controls on the same kernel. Aligned {0,4096} completes the ENTIRE real TRIM path successfully (see ../DF-2276 run.log: XPT_TRIM CDB, 1 range, lba 0 count 8, dxfer 512) β€” proving the panics are caused by the misalignment itself, not by harness damage.

Honest impact assessment

  • Reach: the trigger must first OPEN a da device; daopen gates on SYSCAP_RESTRICTEDROOT (scsi_da.c:505). So: privileged-reach (root or RESTRICTEDROOT-capable) local DoS. No unprivileged path exists on a default system; no privesc.
  • On production kernels built WITHOUT INVARIANTS the KKASSERTs vanish and the misaligned request falls into the DF-2276 loop machinery instead (misaligned start advances by full 512 blocks; misaligned length loops forever issuing TRIMs β€” see ../DF-2276). The two findings are the INVARIANTS and non-INVARIANTS faces of the same missing validation.

Fix validation (A/B, single-fix kernel)

fix.diff replaces both asserts with EINVAL checks plus a negative-range reject (the negative part belongs to DF-2276; one combined diff validated for both). Baseline kernel (uname #1, Oct 18 01:07) vs fixed kernel (uname #2, Oct 18 01:35), identical harness:

case baseline fixed
mlen PANIC at :443 errno 22 EINVAL, guest up
moff PANIC at :444 errno 22 EINVAL, guest up
aligned SUCCESS, TRIM CDB 1 range lba0 len8 SUCCESS, identical CDB

fix_status: fixed (bad behavior gone, functional path preserved).

Upstream comparison

FreeBSD has no DAIOCTRIM; its equivalent, DIOCGDELETE _IOW('d',136, off_t[2]), is validated in the GEOM layer BEFORE any work: sys/geom/geom_dev.c:609-618 rejects (offset % sectorsize) != 0 || (length % sectorsize) != 0 || length <= 0 with EINVAL and iterates while (length > 0). The validated fix mirrors that canonical shape (EINVAL for misalignment, reject non-positive lengths) while keeping the DragonFly dispatch structure.

Files

panic.txt (both console traces), run.log (decisive excerpts), build.log (full kernel build), fix.diff, fix_build.log, fix_run.log, env.txt, serial_mlen_full.log, serial_moff_full.log, rig/ (all sources + raw logs: run_baseline_outer.log, run_panic_mlen.log, run_fixed_outer.log, disasm_daioctl_fixed.txt shows the patched daioctl sign-check branches).

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Single-fix kernel (fix.diff: reject negative range, then EINVAL for misaligned length and offset) built with quickkernel and A/B tested: mlen and moff both return errno 22 with the guest staying up (baseline panicked at scsi_da.c:443/:444), and the aligned control {0,4096} still completes the full real TRIM path with a byte-identical CDB (1 range, lba 0, count 8, dxfer 512).

fix_run.log (mlen/moff EINVAL lines and aligned control); rig/run_fixed_outer.log (full suite); rig/disasm_daioctl_fixed.txt (patched daioctl shows the js sign-check branches to the EINVAL path).
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #2: Sun Oct 18 01:35:38 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

panic.txt has both console traces with the assertion text and backtraces; serial_mlen_full.log and serial_moff_full.log are the raw serial consoles; run.log pairs each userland trigger with its panic and the fixed EINVAL results; rig/verbatim.log proves the compiled driver is byte-for-byte the audited scsi_da.c; rig/run_baseline_outer.log has the functional aligned control through the real TRIM path.

PoC changes

No seed PoC existed for this finding; the rig was written from scratch following the established DF-2263..2273 CAM verbatim-interposition pattern (scsi_da.c replaced in place on the guest by df_pre.c + verbatim + df_post.c because device da must stay configured; fabricated second disk da99; per-object pass-through shims). First build failed on three compile errors (opaque struct cam_path replaced by an identity-token array, void* ccb_bio cast, ccb timeout_ch pointer allocation); after those fixes the harness built and every run reproduced first try.

Verified recommended fix

Replace both KKASSERTs in daioctl with EINVAL returns for misaligned or negative user ranges (fix.diff, validated A/B).

Verdict

REPRODUCED on the CAM verbatim-interposition harness kernel (DragonFly 6.5-DEVELOPMENT X86_64_GENERIC, INVARIANTS): a userland ioctl(DAIOCTRIM) with a non-512-aligned length {0,4104} or offset {520,4096} on the fabricated but fully real /dev/da99 (real make_dev + real daopen SYSCAP gate) panics the machine exactly at scsi_da.c:443 and :444 - the panic strings cite harness lines 558/559 which are verbatim 443/444 after the 115-line preamble, and the backtrace is the full real dispatch daioctl <- dev_dioctl <- devfs_fo_ioctl <- mapped_ioctl <- syscall2. Both panics captured on serial console. Honest reach: openers must pass SYSCAP_RESTRICTEDROOT at daopen (scsi_da.c:505), so this is a privileged-reach local DoS (panic), not privesc; on non-INVARIANTS kernels the asserts vanish and the same bad input falls into the DF-2276 loop machinery. Fix validated A/B on a single-fix kernel: both misaligned cases now return EINVAL with the guest staying up while the aligned control still succeeds with a byte-identical TRIM CDB.