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)
PoC verification
Evidence pack
findings/poc/DF-4982 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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:
sys/dev/raid/iir/iir_ctrl.c:325-332β the kernel decides the event data size from the user-visibleevent_sourceand writes the size back into user memory:p->dvr.event_data.size = sizeof(...)(one of 8/16/16/16 β all in-bounds).sys/dev/raid/iir/iir_ctrl.c:333-336β it then passes&p->dvr.event_data(still user memory) togdt_store_event().sys/dev/raid/iir/iir.c:1875-1882βgdt_store_event()re-readsevt->sizefrom 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++onebuffer[elastidx], iir.c:1883-1886); mismatch β append (elastidx advances, iir.c:1888-1898). A subsequenterase == 0read (gdt_read_event, iir.c:1911-1945) returns the last event with itssame_countβ MERGE vs APPEND is a 1-bit oracle per probe. - The attacker fully controls the ring seed (any
event_source1-4, anyevent_idxvia 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) throughseverity,event_string[256], the rest ofebuffer[](~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/iiris created only byiir_attach()on an ICP vortex (0x1119) or Intel 0x600 RAID HBA βiir_pci.cprobe 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_dvrwith 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_testableBaseline 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
Confirmed kernel references
- sys/dev/raid/iir/iir_ctrl.c:319
- sys/dev/raid/iir/iir_ctrl.c:326
- sys/dev/raid/iir/iir_ctrl.c:334
- sys/dev/raid/iir/iir.c:145
- sys/dev/raid/iir/iir.c:1875
- sys/dev/raid/iir/iir.c:1877
- sys/dev/raid/iir/iir.c:1879
- sys/dev/raid/iir/iir.c:1880
- sys/dev/raid/iir/iir.c:1883
- sys/dev/raid/iir/iir.h:515
- sys/dev/raid/iir/iir.h:546
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.
No comments yet.