DragonFlyBSD Kernel Audit
← triage · dashboard
DF-1234

Kernel heap info leak via u_int16_t truncation in ASR_fillMessage on user-controlled reply size

Summary

ASR_queue_i (I2OUSRCMD ioctl) at asr.c:3262: ReplySizeInBytes=msgSize<<2 from user-controlled reply MessageSize (up to 0x3FFFC=256KB). kmalloc(M_WAITOK, no M_ZERO) at :3273. ASR_fillMessage(Reply_Ptr,ReplySizeInBytes) at :3273 BUT ASR_fillMessage size param is u_int16_t (asr.c:486) -> ReplySizeInBytes>=0x10000 silently truncated for bzero. Only first 30-60 bytes initialized by header writes. copyout(Reply_Ptr,Reply,ReplySizeInBytes) at :3305/:3575 ships uninitialized slab to userspace. Requires SYSCAP_RESTRICTEDROOT (asr_open :3107). Leaks up to 256KB stale heap: KASLR defeat, slab grooming. Fix: cap ReplySizeInBytes<=MAX_INBOUND_SIZE, change ASR_fillMessage to size_t, add M_ZERO.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1234 · 12 files
FileTypeDescriptionSize
VERDICT.md verdict full path:line trace, threat model, fix rationale 5.3 KB ↓ raw
README.md readme claim, verdict, runnable-PoC instructions 2.4 KB ↓ raw
asr_fillmsg_leak.c trigger-source PoC: reachability check for /dev/asrN 2.9 KB view raw
build.sh build-script cc -O -Wall -o asr_fillmsg_leak asr_fillmsg_leak.c 193 B view raw
run.sh run-script ./asr_fillmsg_leak 104 B view raw
build.log build-log PoC build, full output 127 B view raw
run.log run-log PoC run on this guest (open /dev/asrN ENOENT) 514 B view raw
fix.diff suggested-fix cap ReplySizeInBytes to MAX_INBOUND_SIZE; widen ASR_fillMessage size to u_int32_t 988 B view raw
fix_build.log build-log kernel build rc=0 with all 5 fixes applied; asr.c compiled clean under -Werror 2.1 KB view raw
env.txt environment guest uname, cc version, PCI topology 2.0 KB view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme claim, verdict, runnable-PoC instructions
↓ download raw

DF-1234 — README

Finding

ASR_queue_i (the I2OUSRCMD ioctl handler) at sys/dev/raid/asr/asr.c ships up to ~256 KB of uninitialized kernel slab to userspace because:

  • ASR_fillMessage(void *Message, u_int16_t size) at asr.c:486 truncates size to 16 bits before bzero (asr.c:491).
  • ReplySizeInBytes at asr.c:3262 is (userMsgSize << 2), up to 0xFFFF<<2 = ~256 KB.
  • The reply buffer is kmalloc(M_WAITOK) without M_ZERO (asr.c:3273-3276).
  • ASR_fillMessage(Reply_Ptr, ReplySizeInBytes) at asr.c:3282 silently truncates -> bzero does nothing for ReplySizeInBytes ≥ 64 KB.
  • copyout(Reply_Ptr, Reply, ReplySizeInBytes) at asr.c:3305 / :3575 ships the unzeroed slab to userspace.

Verdict

NOT REPRODUCED on this guest (latent): no DPT SmartRAID controller on the audit guest → no /dev/asrN device node → ioctl not reachable. The driver is statically linked into X86_64_GENERIC (kldstat -v shows pci/asr), so the bug path is in the running kernel and fires on real DragonFly installs with the matching controller.

Confidence (bug is real): certain — traced line-by-line in sys/. Impact ceiling: root→kernel info-leak (gated by SYSCAP_RESTRICTEDROOT at asr.c:3107); up to ~256 KB of stale kernel heap per call. Useful for slab-grooming state disclosure / KASLR-defeat, not unprivileged.

How to reproduce

./build.sh && ./run.sh

Expected on this guest: PoC builds clean, prints "No /dev/asrN found: No such file or directory" and reachability analysis. On a host with an asr(4) controller and SYSCAP_RESTRICTEDROOT, sending I2OUSRCMD with a crafted reply frame leaks up to 256 KB of kernel slab.

Files

Path Purpose
asr_fillmsg_leak.c PoC: reachability check for /dev/asrN
build.sh / run.sh exact build/run commands
fix.diff cap ReplySizeInBytes to MAX_INBOUND_SIZE; widen ASR_fillMessage size param
VERDICT.md full path:line trace, threat model, fix rationale
build.log / run.log PoC build + run outputs
fix_build.log kernel-build compile validation of fix.diff
env.txt guest uname / cc / device topology
VERDICT.md verdict full path:line trace, threat model, fix rationale
↓ download raw

DF-1234 — VERDICT

Finding: ASR_queue_i (I2OUSRCMD ioctl) at sys/dev/raid/asr/asr.c ships uninitialized kernel slab memory to userspace because ASR_fillMessage() takes a u_int16_t size parameter that silently truncates the (user-controlled) ReplySizeInBytes, which can be up to ~256 KB.

Status: NOT REPRODUCED (latent — no DPT SmartRAID controller on this guest). Confidence (bug is real): certain (traced line-by-line in sys/). Impact ceiling: up to ~256 KB of stale kernel heap leaked to a privileged caller per I2OUSRCMD invocation. Useful for slab-grooming state disclosure / KASLR defeat — but gated by SYSCAP_RESTRICTEDROOT (asr_open at asr.c:3107), so this is root→kernel info-leak, not unprivileged.

Mechanism (confirmed line-by-line in sys/)

  1. ASR_fillMessage is declared at sys/dev/raid/asr/asr.c:485-486: c static PI2O_MESSAGE_FRAME ASR_fillMessage(void *Message, u_int16_t size) { PI2O_MESSAGE_FRAME Message_Ptr; Message_Ptr = (I2O_MESSAGE_FRAME *)Message; bzero(Message_Ptr, size); /* :491 */ ... The size argument is u_int16_t. Any caller passing a value > 0xFFFF has it silently truncated to the low 16 bits before bzero runs.

  2. ASR_queue_i (servicing I2OUSRCMD ioctl, asr.c:3788) computes the reply-buffer size from the user-supplied reply message-size field: c /* asr.c:3262-3263 */ ReplySizeInBytes = (I2O_MESSAGE_FRAME_getMessageSize( &(Reply_Ptr->StdReplyFrame.StdMessageFrame)) << 2); MessageSize is a U16 in the I2O frame, so ReplySizeInBytes can be up to 0xFFFF << 2 = 0x3FFFC (~256 KB). Message_Ptr (the request) is bounded to MAX_INBOUND_SIZE = 512 at asr.c:3223-3224, but the reply size is not bounded.

  3. The reply buffer is allocated without M_ZERO at asr.c:3273-3276: c if ((Reply_Ptr = (PI2O_SCSI_ERROR_REPLY_MESSAGE_FRAME)kmalloc ( ((ReplySizeInBytes > sizeof(I2O_SCSI_ERROR_REPLY_MESSAGE_FRAME)) ? ReplySizeInBytes : sizeof(I2O_SCSI_ERROR_REPLY_MESSAGE_FRAME)), M_TEMP, M_WAITOK)) == NULL) { ... } Then ASR_fillMessage is called at asr.c:3282: c (void)ASR_fillMessage((void *)Reply_Ptr, ReplySizeInBytes); When ReplySizeInBytes == 0x10000 (e.g. MessageSize == 0x4000), the u_int16_t parameter truncates to 0 and bzero does nothing.

  4. Only the I2O header fields are subsequently written: InitiatorContext, TransactionContext, MsgFlags | REPLY, MessageSize (asr.c:3283-3291). These touch only the first ~30-60 bytes. The remaining ~64 KB of the M_TEMP slab allocation contains uninitialized kernel heap residue.

  5. copyout((caddr_t)Reply_Ptr, (caddr_t)Reply, ReplySizeInBytes) at asr.c:3305 (special-case path) and asr.c:3575 (general path) ships the full ReplySizeInBytes back to userspace — leaking the unzeroed slab.

  6. The user-supplied ReplySizeInBytes reaches copyout verbatim, so the leak size is attacker-controlled up to ~256 KB per call.

Why it is NOT REPRODUCED on this guest

  • pciconf -lv shows no DPT SmartRAID adapter. asr_attach never runs, so no /dev/asrN cdev is created.
  • kldstat -v confirms pci/asr is statically linked into X86_64_GENERIC (the driver IS in the running kernel), so the bug path exists and would fire on a host with the matching controller.
  • ls /dev/asr* returns no device nodes. PoC asr_fillmsg_leak.c confirms this at runtime.

Threat model & privilege boundary

asr_open at sys/dev/raid/asr/asr.c:3107 enforces caps_priv_check(ap->a_cred, SYSCAP_RESTRICTEDROOT). Only root (or a process explicitly granted the restricted-root capability) can open the device. So this is a root→kernel info-leak primitive, useful for:

  • defeating slab randomization (leak adjacent object pointers),
  • leaking previously-freed slab contents (e.g. crypto keys, auth tokens freed into the same M_TEMP bucket),
  • bridging into a separate write-capable primitive via the leaked pointers.

It is not an unprivileged-user vector. The hardening gap is that a root operator expects I2OUSRCMD to round-trip a well-defined reply frame, not to be handed the kernel heap.

Fix (authored in fix.diff, applied + compile-validated)

Two-part defense-in-depth:

  1. Cap ReplySizeInBytes to MAX_INBOUND_SIZE (512) right after computation at asr.c:3264. This matches the cap already applied to the request size at asr.c:3223-3224, is consistent with the I2O limit, and immediately eliminates both the truncation and the leak.

  2. Widen ASR_fillMessage's size from u_int16_t to u_int32_t at asr.c:486, so the (now bounded but still up to 512) value cannot be silently truncated even if a future caller passes a larger size. This is signature-only — the body is unchanged because bzero and the setMessageSize math both accept any unsigned int.

Validation

  • fix.diff applies cleanly with patch -p1 --forward (verified).
  • All 5 audit fixes applied together; make -j6 nativekernel KERNCONF=X86_64_GENERIC returned rc=0 with no errors / warnings under -Werror. asr.c was compiled cleanly into both the kernel and the asr.ko module.
  • Fix is not_testable at runtime on this guest (no DPT controller).

Fix verification

not_testable

compile validated

nativekernel rc=0

Confirmed kernel references

Detail

Exploit chain

none

Evidence (decisive lines)

Verdict

Source-confirmed. ASR_fillMessage u16 truncation -> ~256KB heap info leak. asr in GENERIC, no DPT HW. Root-only.