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

iir(4) GDT_IOCTL_EVENT double-fetch: raced event_data.size drives memcmp/strcmp length against the 16-byte eu union in static ebuffer[] β€” OOB kernel read with byte-wise equality-oracle disclosure

Summary

The ioctl treats the raw user pointer as kernel memory: it writes size INTO user memory then gdt_store_event re-fetches it three times and uses the fresh value as the memcmp length - a racing thread flipping size to 0xFFFF makes the kernel compare up to 65KB against the 16-byte union (through ebuffer[256] and adjacent statics), and the merge-vs-append outcome is observable via a follow-up read: a 1-bit-per-probe oracle disclosing kernel statics byte-wise. No copyin anywhere in the path. Root/operator-gated + rare ICP/Intel HBA; hw-gated on guest. Copyin+clamp fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4982 Β· 9 files
FileTypeDescriptionSize
README.md β€” 1.6 KB ↓ raw
VERDICT.md β€” 5.4 KB ↓ raw
df4982_iir_evt_oracle.c β€” 6.2 KB view raw
build.sh β€” 116 B view raw
run.sh β€” 114 B view raw
env.txt β€” 1.1 KB view raw
verdict.json β€” 3.4 KB view raw
fix.diff β€” 2.2 KB view raw
manifest.json β€” 823 B view raw

DF-4982 β€” iir(4) GDT_IOCTL_EVENT double-fetch β†’ OOB kernel read (memcmp/strcmp) with equality-oracle

Build / run (on a host that actually HAS an iir HBA)

cc -O2 -Wall -o df4982_iir_evt_oracle df4982_iir_evt_oracle.c
./df4982_iir_evt_oracle /dev/iir0        # root, or operator once devfs rule grants the node

Expected output (hardware present)

in-bounds  (size=16): MERGE
in-bounds  (mismatch): APPEND
OOB raced  (size=0xFFFF): APPEND/MERGE per probe   <-- 1-bit oracle per probe

The oracle: a probe where the first k bytes of the user buffer equal ebuffer[elastidx].event_data.eu[0..k-1] makes memcmp read kernel byte eu[k] (k may be far past the 16-byte union β€” through severity, event_string[256], the remaining ~73 KB of ebuffer[], and adjacent module statics). MERGE ⇔ guess at byte k was correct. Byte-wise kernel .data/.bss disclosure at ≀256 probes/byte.

Status on the reference guest

Not runnable β€” no iir HBA. Verified on the audit guest (see env.txt): the iir driver is compiled into X86_64_GENERIC, but the QEMU PCI bus has no vendor 0x1119 (ICP vortex) / Intel device 0x600 RAID controller, so iir_pci_probe() never matches, iir_attach() never runs, gdt_make_dev() is never called and /dev/iir does not exist. Every iir_ctrl.c ioctl path, including this one, is unreachable there. The finding is proven by source trace (VERDICT.md); severity Low because the primitive is additionally gated by the root-only 0600 device node.

Fix

fix.diff β€” copy the event struct in ONCE, clamp size to sizeof(eu), operate on the kernel copy, copy back out on exit.

VERDICT.md
↓ download raw

DF-4982 VERDICT β€” iir(4) GDT_IOCTL_EVENT double-fetch β†’ OOB kernel read

Status: untested at runtime (hardware-gated). Primitive and reachability proven by source trace.

The bug (path:line)

iir_ioctl() treats ap->a_data as a kernel pointer β€” on DragonFly it is the raw user pointer (the same fact DF-2028 was filed on). For GDT_IOCTL_EVENT with erase == 0xff:

  1. sys/dev/raid/iir/iir_ctrl.c:325-332 β€” the kernel decides the event data size from the user-visible event_source and writes the size back into user memory: p->dvr.event_data.size = sizeof(...) (one of 8/16/16/16 β€” all in-bounds).
  2. sys/dev/raid/iir/iir_ctrl.c:333-336 β€” it then passes &p->dvr.event_data (still user memory) to gdt_store_event().
  3. sys/dev/raid/iir/iir.c:1875-1882 β€” gdt_store_event() re-reads evt->size from user memory three separate times: - evt->size != 0 (iir.c:1877) β€” branch decision, - memcmp(&ebuffer[elastidx].event_data.eu, &evt->eu, evt->size) (iir.c:1878-1879) β€” length is the freshly re-fetched value, - evt->size == 0 (iir.c:1880) β€” strcmp(&ebuffer[...].event_string, &evt->event_string) branch (unterminated 256-byte arrays on both sides if a prior store left no NUL β†’ unbounded strcmp walk).

eu is a 16-byte packed union (iir.h:517-543; hand-checked: sync = 2+2+2+4+2+3+1 = 16, stream[16] = 16). size is u_int16_t β†’ a racing thread flipping size to 0xFFFF between step 1's store and step 3's load makes memcmp compare up to 65,535 bytes against a 16-byte object β€” an OOB read of the static ebuffer[GDT_MAX_EVENTS] (256 Γ— 294 = 73,504 bytes, iir.c:145) and of whatever module statics the linker placed after it. memcmp stops at the first mismatch β€” the read of kernel byte eu[k] happens for any k the attacker can reach with a matching prefix, which is exactly what makes it an oracle.

Why it is exploitable as a disclosure (not just a crash)

  • Match outcome is observable: equality β‡’ merge (same_count++ on ebuffer[elastidx], iir.c:1883-1886); mismatch β‡’ append (elastidx advances, iir.c:1888-1898). A subsequent erase == 0 read (gdt_read_event, iir.c:1911-1945) returns the last event with its same_count β€” MERGE vs APPEND is a 1-bit oracle per probe.
  • The attacker fully controls the ring seed (any event_source 1-4, any event_idx via erase==0xff stores) so the memcmp branch preconditions (ebuffer[elastidx].event_source == source && event_idx == idx, ebuffer[elastidx].event_data.size != 0) are trivially satisfied.
  • Byte k is learned with ≀256 probes (guess-and-check with learned prefix); the OOB side runs from offset 17 (past eu) through severity, event_string[256], the rest of ebuffer[] (~73 KB) and adjacent module statics β€” potentially function/data pointers in the module's .data/.bss (KASLR-relevant on hardened deployments).

The same double-fetch also breaks the size == 0 strcmp path (iir.c:1880-1882): both strings can be non-NUL-terminated (event_string[256] filled by a prior 0xff-store), so strcmp itself walks out of bounds until it hits a zero byte.

Threat model / gating (why Low, not higher)

  • /dev/iir is created only by iir_attach() on an ICP vortex (0x1119) or Intel 0x600 RAID HBA β€” iir_pci.c probe table; rare hardware.
  • Node perms: UID_ROOT, GID_OPERATOR, S_IRUSR|S_IWUSR (iir_ctrl.c:105-106) β†’ root-only by default. The GID_OPERATOR choice shows operator-group access is the intended deployment; a devfs rule granting the group makes any operator-group member an attacker. With root-only perms the bug is nearly worthless (root reading kernel .bss).
  • Driver-internal callers of gdt_store_event() (iir.c:1574, 1590, 1699, 1842-1844) all pass &gdt->sc_dvr with driver-set sizes β€” unaffected.

Why untested (hardware gate, verified on guest)

Reference guest (DragonFly 6.5-DEVELOPMENT x86_64, X86_64_GENERIC): - kldload iir β†’ "module already loaded or in kernel" (driver resident); - /dev/iir* β†’ No such file or directory; - pciconf -l β†’ only the 7 standard QEMU i440fx devices, no 0x1119/0x600 RAID controller β‡’ iir_pci_probe never matches β‡’ gdt_make_dev never called β‡’ ioctl interface unreachable. See env.txt. The PoC source in this pack is complete and runnable the moment an HBA is present; no assertion in this finding rests on runtime behavior.

Fix validation

Not applicable β€” baseline could not be reproduced on the available guest (no HBA), so there is no A/B possible. fix.diff is authored from the line-accurate root cause: copy the struct in once (copyin), clamp size <= sizeof(eu), use the kernel copy, copyout on the way out. That eliminates both the double-fetch and the unterminated-strcmp variant (a clamped, kernel-owned copy has no race window).

Bottom line

Certain OOB-read primitive (attacker-controlled compare length of up to 65535 against a 16-byte kernel object, three separate user-memory re-fetches) with a workable equality-oracle exploitation strategy; severity capped Low by double gating (HBA presence + 0600 root-only node).

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

not_testable
baseline no→ patch + rebuild →patched clean

Baseline not reproducible on available guest (no iir HBA -> no /dev/iir), so no A/B fix validation possible; fix.diff authored from line-accurate root cause and reviewed to close both the memcmp length race and the unterminated strcmp variant

fix.diff (copyin/clamp/copyout) in this pack
↓ fix.diffper-fix-DF-4982

Confirmed kernel references

Detail

Exploit chain

seed ring via erase=0xff store (attacker source/idx) -> second 0xff ioctl with racer flipping event_data.size to 0xFFFF -> memcmp(ebuffer[elastidx].eu, user_buf, 0xFFFF) reads kernel bytes past the 16-byte union -> MERGE (same_count>=2) vs APPEND observable via erase=0 gdt_read_event -> learn byte k with <=256 guesses keeping learned prefix -> disclose ebuffer tail + adjacent module statics

Evidence (decisive lines)

VERDICT.md (full trace with iir.c/iir_ctrl.c line cites); env.txt (guest: /dev/iir absent, driver in-kernel, no 0x1119/0x600 PCI dev); df4982_iir_evt_oracle.c (runnable race+oracle PoC for HW-equipped hosts); fix.diff (copyin-once + clamp + copyout)

PoC changes

PoC authored fresh (no pass-1 seed): mirrors packed gdt_event_t layout, races only the 2-byte size field, observes merge via same_count on erase=0 read

Verified recommended fix

Copyin gdt_event_t once, clamp event_data.size to sizeof(eu), run the event helpers on the kernel copy, copyout on exit

Verdict

Certain OOB kernel-read primitive by source trace: iir_ctrl.c:325-332 writes event_data.size into USER memory, then gdt_store_event (iir.c:1875-1882) re-fetches evt->size three times and uses it as the memcmp length against the 16-byte eu union inside the 73.5KB static ebuffer[] β€” a racing thread flipping size to 0xFFFF yields up to ~65KB OOB comparison, with a merge-vs-append (same_count) equality oracle turning it into byte-wise kernel .data/.bss disclosure; the size==0 branch has the same flaw via unterminated strcmp on event_string[256]. Not runnable on the reference guest: the iir driver is compiled in but no ICP/Intel-0x600 RAID HBA exists, so /dev/iir is never created (verified, env.txt); additionally gated by the 0600 root-only device node, hence Low.