icu_ioapic_extint() accepts negative irq: guard lacks signed lower bound -> C-undefined shift feeds the live 8259 IMR; IMR written lockless and without icu_imen shadow sync (caller-gated, live-verified benign)
Summary
The guard covers only the upper bound and the slave link, never irq<0: a negative irq reaches ~(1<<irq) (shift by negative count, C99 UB) and a 5-outb ICW sequence on the live master 8259, without imen_spinlock or shadow sync. Live-verified on the QEMU/i440fx guest: icu_ioapic_extint(-1) returned 0, the UB materialized as mask=0xff (all-masked - exactly what icu_init writes deliberately), shadow untouched, state restored exactly, guest healthy. Sole in-tree caller pins irq==0 with an always-on KKASSERT at boot - kernel-code-only reach, benign sink. Closes the family's validation perimeter (DF-5464/5465/5503 class). One-line lower-bound fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5616 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.2 KB | β raw | |
| VERDICT.md | β | 7.1 KB | β raw | |
| icuintro.c | β | 7.1 KB | view raw | |
| Makefile | β | 84 B | β download | |
| build.sh | β | 94 B | view raw | |
| run.sh | β | 118 B | view raw | |
| build.log | β | 6.0 KB | view raw | |
| run.log | β | 3.6 KB | view raw | |
| panic.txt | β | 1.9 KB | view raw | |
| env.txt | β | 918 B | view raw | |
| fix.diff | β | 359 B | view raw | |
| manifest.json | β | 1.0 KB | view raw | |
| verdict.json | β | 3.8 KB | view raw |
DF-5616 β icu_ioapic_extint() validation gap (sys/platform/pc64/icu/icu.c:158-183)
WHAT The exported kernel routine icu_ioapic_extint(irq, vec) validates its
irq argument with if (irq >= 8 || irq == 2) return EOPNOTSUPP;
(icu.c:167) β the check has NO lower bound. A negative irq passes
validation and reaches mask = ~(1 << irq) (icu.c:170), which is C
undefined behavior (shift by negative count), and then a 5-outb ICW
reprogram of the live master 8259 including outb(IO_ICU1+1, mask)
(icu.c:176-180). The same routine writes the PIC's IMR without the
imen_spinlock protocol and without syncing the icu_imen shadow β
safe today only because its sole in-tree caller
(ioapic_conf_legacy_extint, ioapic_abi.c:1056-1075) runs at boot
with IF=0 and pins irq==0 via an always-on KKASSERT (ioapic_abi.c:1064).
SEVERITY Info (hardening / defense-in-depth β no demonstrated security impact). Reachable only from kernel code (root/KLD); the only in-tree caller pins irq=0 with an always-on KKASSERT.
EVIDENCE (live, QEMU/i440fx guest, kernel X86_64_GENERIC of 2026-07-02, 6 CPUs, MachIntrABI=IOAPIC(2) with the legacy 8259 present):
D0: ncpu=6 mycpuid=5 MachIntrABI.type=2 (MACHINTR_ICU=1) D1: icu_imen(shadow)=fffb hw_imr=fffb coherent=YES D2: ELCR=0c00 (bit=1 -> level) P3: (uint8_t)~(1 << -1) = 0xff (C99 UB shift; x86: 1<<31) P3: (uint8_t)~(1 << 3) = 0xf7 (well-defined case) P2: irq0 mask-cycle IMR1 fb -> fa -> fb; shadow fffb; ELCR 0c00->0c00 persist=YES P5: ICU2 ICW cycle, ELCR_HI 0c -> 0c (persists across ICW) P4: ioapic_enable=1 icu_ioapic_extint(-1,0x20)=0 IMR1_after=0xff (expect 0xff: all-masked, benign) shadow_untouched=YES P4: restored IMR={fb,ff} want={fb,ff} shadow=fffb ELCR 0c00->0c00->0c00 P6: alive, ticks delta=199; final icu_imen=fffb hw=fffb coherent=YES
i.e. executed LIVE: the function accepts irq=-1 (returns 0 where its
own comment/contract says reject), performs the UB shift, writes the
all-masked IMR β and nothing corrupts (mask lands 0xff on this
compiler/hardware), the icu_imen shadow is untouched, state restores
exactly, guest stays up.
BUILD guest # sh build.sh (bsd.kmod.mk, -Werror clean; see build.log) RUN guest # sh run.sh (kldload, capture, kldunload; see run.log)
panic.txt documents the FIRST harness revision's guest panic β a
HARNESS bug (lwkt_setcpu_self() from a process thread violates
usched_dfly's lwp_qcpu invariant at usched_dfly.c:453), NOT a kernel
finding of this audit. v2 removed the pinning; run.log is the
decisive clean run.
PASS-2 CROSS-CHECKS settled by this pack / by source proof (see VERDICT.md): (a) IMEN 16-bit RMW SMP safety β correct (lock.h SPIN_LOCK cli protocol; live coherence fffb==fffb before/after) (b) ELCR persistence across masks/ICW β verified live (0c00 stable) (c) pending-vector lost-update windows β none reachable (boot-order proof) (d) DF-5464 KASSERT-class / DF-1102 subsumption β no twin in icu.c beyond this weaker, caller-gated, benign cousin (this finding)
DF-5616 β VERDICT
Finding: icu_ioapic_extint() (sys/platform/pc64/icu/icu.c:158-183) β
signed lower-bound validation gap (if (irq >= 8 || irq == 2) at icu.c:167
does not reject irq < 0), leading to mask = ~(1 << irq) at icu.c:170 β
a C-undefined shift β feeding outb(IO_ICU1 + 1, mask) at icu.c:180.
Secondary hygiene: the IMR/ICW writes bypass imen_spinlock and never sync
the icu_imen shadow.
Reproduced? YES (defect manifests live; impact demonstrably none)
On the audit QEMU guest (DragonFly 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 2026,
X86_64_GENERIC, 6 vCPUs, i440fx), a KLD harness (icuintro.c, root-loaded)
called the real kernel routine with irq = -1, vec = 0x20:
- return value
0β the function ACCEPTS the negative irq; per its own comment ("Only first 8 interrupt is supported", icu.c:163-166) it must reject out-of-range input with EOPNOTSUPP. - IMR1 after the call =
0xffβ the UB shift1 << -1materialized as1 << 31on this compiler/target, somask = (uint8_t)~0x80000000 = 0xff(all-masked). P3 of the harness computed the exact icu.c:170 expression live:0xff. No wild mask, no OOB, no corruption. icu_imenshadow untouched by the routine (shadow_untouched=YES) β confirming the missing shadow sync (cosmetic here; the ICU ABI is retired on IOAPIC systems).- Full state restore (manual ICW sequence mirroring icu_init(), icu.c:62-102) returned IMR to {0xfb,0xff} exactly; ELCR stable 0x0c00 throughout; guest alive, interrupts flowing, module unloaded cleanly.
Severity is Info: the only in-tree caller pins irq == 0 with an
always-on KKASSERT (ioapic_abi.c:1064 KKASSERT(irq == 0)), there is no
userspace/ioctl/sysctl reachability β kernel-code-only (root/KLD) β and the
sink is provably benign on x86 (mask truncates to 0xff = "mask everything",
the same value icu_init writes at icu.c:83 anyway). No uid0, no panic, no
leak, no DoS: impact = none.
Why this is still worth fixing
It is the same validation-hygiene class the orchestrator flagged for this PIC family this pass (icu_abi.c DF-5464/DF-5465, ioapic.c DF-5503), but it is the only remaining instance where the guard itself exists yet is incomplete (a signed lower bound), rather than merely KASSERT-gated. Two tokens fix it.
Pass-2 checklist results (the rest of the file: clean)
- (a) IMEN mask arithmetic / SMP RMW β CORRECT.
ICU_INTRDIS/ICU_INTREN(icu_ipl.s:108-144) do the shadow RMW + port write underimen_spinlock, and the locoreSPIN_LOCKmacro (lock.h:61-78) doespushfq/cli/cmpxchg(unlockpopfq) β so the lock is local-IRQ-safe as well.MASK_IRQ/UNMASK_IRQ(icu_vector.s:100-116) both holdICU_IMASK_LOCK; the EOI is issued inside the lock (icu_vector.s:137-140), preserving the mask+EOI atomicity needed for level-triggered shared IRQs. Byte-split writes always rewrite the full byte from the shadow under the lock β no lost-update window. LIVE:icu_imenshadow == hardware IMR (fffb) before and after mask cycles (run.log D1/P2/P6). - (b) ELCR trigger persistence β icu.c never touches ports 0x4d0/0x4d1.
LIVE: ELCR (0x0c00 β irqs 10,11 level, PCI-style) unchanged across both
INTRDIS/INTREN mask cycles (P2) and a full ICW1..ICW4+IMR reprogram of
ICU2 (P5).
elcr_write_trigger's unlocked shadow RMW (elcr.c:100-118) has only boot-time callers β no runtime race (and elcr.c is not this file). - (c) reprogram state machine / pending vectors β there is no
icu_reprogramin DragonFly; the closest machinery isgd_ipending+ doreti replay (icu_vector.s:141-166) β outside this file. The icu.c-adjacent lost-update window (MachIntrABI.cleanup()bzeroinggd_ipendingafter IF is restored β icu.c:202-206, init_main.c:306-307) is unreachable: at every point cleanup runs, all device IRQs are masked in hardware (boot-order proof: IF first enabled at SI_BOOT2_LEAVE_CRIT 0x1900000 with IMR=0xfffb from icu_abi_stabilize; the only post-IF call site, ioapic.c:623, runs at 0x1a70000 with zero registered ICU sources β bus enumeration is later β and slave IMR 0xff so not even the cascade can assert). No production lost-update. - (d) DF-5464 KASSERT-class twin check β icu.c contains NO INVARIANTS-only
KASSERT at all; its three assertions are always-on KKASSERTs of internal
boot invariants (icu.c:109, 190, 191), and each provably holds at every
call site:
icu_definitruns at machdep.c:2775 afterMachIntrABI = MachIntrABI_ICU(machdep.c:2682); foricu_reinit_noioapic, ioapic_config() can only return an error BEFOREMachIntrABI = MachIntrABI_IOAPIC(ioapic.c:103, :113 vs :125; post-switch failures are inline panics and the tail returns 0 unconditionally at ioapic.c:211), and lapic.c:1287 runs at 0x1a50000 before any switch;ioapic_enableis forced to 0 at both call sites before the call (ioapic.c:622, lapic.c:1286). No production-bypassable assertion in this file. - DF-1102 subsumption β icu.c never calls msi_setup/msi_map; the ICU-side
consumer of those unvalidated sinks is icu_abi.c:499 (covered by DF-5466's
negative
hw.icu.msi_startchain). Nothing to add. - Dead code β
icu_reinit()(icu.c:127-141) andelcr_resume()(elcr.c:120-126) have zero callers in the tree; the ACPI resume path they were written for does not exist (acpi_sleep_machdepis a stub returning 0, acpi_wakeup.c:50-54;acpi_install_wakeup_handlerempty at :56-59). The#ifdef foobranch (never defined) shows the re-enable-of-registered- IRQs logic was abandoned. Consequence:icu_init()provably never runs after boot β no runtime SMP race is reachable on any current path.
Fix
--- a/sys/platform/pc64/icu/icu.c
+++ b/sys/platform/pc64/icu/icu.c
@@ -163,7 +163,7 @@ icu_ioapic_extint(int irq, int vec)
/*
* Only first 8 interrupt is supported.
* Don't allow setup for the slave link.
*/
- if (irq >= 8 || irq == 2)
+ if (irq < 0 || irq >= 8 || irq == 2)
return EOPNOTSUPP;
Fix validation status: not_applicable (Info finding; single-fix kernel
rebuild not warranted β the change is a two-token guard whose behavioral
effect is exactly "return EOPNOTSUPP for irq<0", verified by inspection; no
memory-corruption baseline exists to regression-test).
Harness incident (documented for honesty)
Harness v1 used lwkt_setcpu_self(globaldata_find(0)) to pin to CPU0 and
panicked the guest afterwards in dfly_acquire_curproc
(usched_dfly.c:453 lp->lwp_qcpu == dd->cpuid) β raw LWKT thread migration
from a process thread is out-of-contract for usched_dfly. This is a harness
bug, not an audit finding (requires root KLD; no unprivileged trigger).
See panic.txt. v2 removed the pinning; run.log (v2) is the decisive run on
a fresh vm.sh reset with-src guest.
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_testableInfo/hardening finding β single-fix kernel rebuild not warranted. fix.diff is a two-token guard whose entire behavioral delta is 'return EOPNOTSUPP for irq < 0', verified against the live P4 result by inspection; no memory-corruption baseline exists to regression-test.
fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log P4 lines: 'icu_ioapic_extint(-1,0x20)=0 IMR1_after=0xff ... shadow_untouched=YES' and 'restored IMR={fb,ff} want={fb,ff}'; P3 '(uint8_t)~(1 << -1) = 0xff'; D1/P6 'icu_imen=fffb hw=fffb coherent=YES'; VERDICT.md carries the full pass-2 negative results (IMEN SMP protocol correct, ELCR persistence proven live, boot-order proofs killing the cleanup/KKASSERT race candidates, dead icu_reinit/elcr_resume).
PoC changes
Authored harness from scratch (no seed). v1 pinned to CPU0 via lwkt_setcpu_self() and panicked the guest afterwards in dfly_acquire_curproc (usched_dfly.c:453) β a HARNESS bug (out-of-contract primitive use), documented in panic.txt, not a finding. v2 dropped the pinning and replaced the icu_definit()-based restore (would KKASSERT-panic under the live IOAPIC ABI, icu.c:109) with a manual ICW sequence mirroring icu_init() (icu.c:62-102).
Verified recommended fix
Add the signed lower bound: if (irq < 0 || irq >= 8 || irq == 2) return EOPNOTSUPP; (icu.c:167)
Verdict
Live-verified on the QEMU/i440fx guest: icu_ioapic_extint(-1, 0x20) is ACCEPTED (returns 0) because the guard at icu.c:167 (irq >= 8 || irq == 2) lacks the signed lower bound, executes the C-undefined shift 1 << irq at icu.c:170 (materializes as 1 << 31 on this target), and writes IMR1 = 0xff (all-masked, benign) to the live master 8259 at icu.c:180 without imen_spinlock and without syncing icu_imen. Nothing corrupts: the mask truncates to 0xff, the shadow is untouched, state restores exactly, guest stays healthy. Only in-tree caller pins irq==0 via always-on KKASSERT (ioapic_abi.c:1064); kernel-code-only reach (root/KLD). Info/hardening; two-token fix.
No comments yet.