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

ioapic_irqs[] stride invariant (sizeof(struct ioapic_irqinfo) == 1 << IOAPIC_IRQI_SZSHIFT) enforced only by a comment while genassym publishes both halves to different assembly consumers

Summary

genassym emits IOAPIC_IRQI_SIZE (compiler sizeof) and IOAPIC_IRQI_SZSHIFT (hand constant) side by side with nothing relating them: ioapic_ipl.s indexes ioapic_irqs[] with the hand constant while apic_vector.s indexes with sizeof, and the only guard is a comment. Holds today (16 == 1<<4 verified on the shipped assym.s); a one-field drift compiles clean and silently makes the two consumers address different elements (simulated: IRQ 100 -> 1600 vs 2400), corrupting IOAPIC RTE selection. _Static_assert guard validated (passes today, catches the simulated drift with a loud build break).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5597 Β· 15 files
FileTypeDescriptionSize
README.md β€” 2.1 KB ↓ raw
VERDICT.md β€” 2.6 KB ↓ raw
ioapic_drift.c β€” 2.3 KB view raw
guard_ok.c β€” 380 B view raw
guard_drift.c β€” 407 B view raw
build.sh β€” 184 B view raw
run.sh β€” 90 B view raw
drift_now.log β€” 165 B view raw
drift_sim.log β€” 253 B view raw
guard_ok.log β€” 0 B ↓ download
guard_drift.log β€” 213 B view raw
env.txt β€” 196 B view raw
fix.diff β€” 406 B view raw
manifest.json β€” 971 B view raw
verdict.json β€” 2.9 KB view raw

DF-5597 β€” ioapic_irqinfo stride invariant (sizeof == 1<<IOAPIC_IRQI_SZSHIFT) is enforced only by a comment

What

genassym.c:276-277 emits BOTH stride facts into assym.s:

ASSYM(IOAPIC_IRQI_SIZE, sizeof(struct ioapic_irqinfo));   /* compiler truth */
ASSYM(IOAPIC_IRQI_SZSHIFT, IOAPIC_IRQI_SZSHIFT);          /* hand constant  */

but nothing at build time relates them. The hand constant (sys/platform/pc64/apic/ioapic_abi.h:58, #define IOAPIC_IRQI_SZSHIFT 4) is guarded only by a comment (ioapic_abi.h:50-51: "Keep size of ioapic_irqinfo power of 2 / Update IOAPIC_IRQI_SZSHIFT after changing ioapic_irqinfo size"). There is no CTASSERT/_Static_assert anywhere in the apic subsystem (verified by grep).

The two consumers use different facts of the same contract:

If a field is ever added to struct ioapic_irqinfo (the plausible future edit: an owner-cpumask for IRQ balancing) and SZSHIFT is not bumped, ioapic_ipl.s computes wrong element addresses for ioapic_irqs[] β€” mask/unmask IMCR writes land on the wrong IOAPIC redirection entries β€” while apic_vector.s stays correct. The build stays green; the divergence is silent. (Today the invariant holds: live assym.s shows IOAPIC_IRQI_SIZE 0x10 == 1 << IOAPIC_IRQI_SZSHIFT 0x4.)

Reproduce

  • ./run.sh (after cc): ioapic_drift.c without -D β†’ invariant HOLDS (16 == 16, matching the shipped kernel's assym.s); with -DDRIFT (one added uint32_t field) β†’ 24 != 16, and the two assembly consumers would index the array differently (IRQ 100: offset 1600 vs 2400) β€” with no build error today.
  • guard_ok.c / guard_drift.c: the proposed _Static_assert compiles clean on the current layout and fails the build on the drifted layout (guard_drift.log: error: static assertion failed).

Fix

One _Static_assert in ioapic_abi.h right under the SZSHIFT define β€” see fix.diff.

VERDICT.md
↓ download raw

DF-5597 VERDICT

Status: reproduced (hardening-gap demonstration, build-diff evidence class). Impact: none today β€” the invariant currently holds; this finding documents the unenforced invariant whose future drift is silent and interrupt-routing-corrupting. Confidence: certain.

Facts established on the guest

  1. Current state is consistent. The shipped kernel's generated assym.s contains IOAPIC_IRQI_SIZE 0x10 and IOAPIC_IRQI_SZSHIFT 0x4; ioapic_drift.c (compiled from a verbatim copy of the struct) confirms sizeof(struct ioapic_irqinfo) == 16 == 1 << 4 (drift_now.log). The independent TU cross-check (findings/poc/DF-5596/out.kernelview.raw.txt) reproduces both values from the real headers.
  2. Enforcement is comment-only. grep over ioapic_abi.h and the apic directory finds no CTASSERT/_Static_assert/static_assert tying IOAPIC_IRQI_SZSHIFT to the struct size. genassym.c emits both numbers side by side (lines 276-277) but nothing compares them.
  3. Drift is silent today. ioapic_drift.c -DDRIFT (one added uint32_t io_owner_cpumask) β†’ sizeof = 24, stride stays 16 (drift_sim.log): ioapic_ipl.s:80,97 (shll $IOAPIC_IRQI_SZSHIFT) and apic_vector.s:55-59 (IOAPIC_IRQI_SIZE * irq) would index ioapic_irqs[] differently β€” under-walked elements, wrong io_addr/io_idx/io_flags β€” and the kernel still builds and boots with no diagnostic.
  4. The proposed guard works. With _Static_assert added (fix.diff): guard_ok.log — clean compile on the current layout (rc=0); guard_drift.log — error: static assertion failed: "ioapic_irqinfo size drifted from IOAPIC_IRQI_SZSHIFT" (rc≠0). The failure mode converts from silent runtime divergence to a loud build break.

Why this is filed against genassym.c

genassym.c is the file that publishes both halves of this contract to the assembly world; it is the natural place the audit asks "do the sizes emitted match what consumers assume?" (checklist item d). The struct and the hand constant live in ioapic_abi.h, which is where the one-line fix belongs (fix.diff included).

Classification

No runtime defect exists today (hence impact=none); this is a verified hardening finding with a tested one-line fix. Severity Info.

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 reproduced

Baseline: drifted layout compiles with zero diagnostics (invariant unenforced). Patched guard: current layout compiles clean, drifted layout fails with 'static assertion failed: ioapic_irqinfo size drifted from IOAPIC_IRQI_SZSHIFT'.

guard_ok.log, guard_drift.log, drift_now.log, drift_sim.log
↓ fix.diffper-fix-DF-5597

Confirmed kernel references

Detail

Evidence (decisive lines)

drift_now.log (16==16 holds, matches shipped assym.s IOAPIC_IRQI_SIZE 0x10 / SZSHIFT 0x4); drift_sim.log (24 vs 16, consumer divergence, no build error today); guard_ok.log (rc=0 current layout); guard_drift.log (static assertion failed: stride drifted)

PoC changes

authored from scratch: ioapic_drift.c (verbatim struct copy + DRIFT variant), guard_ok.c/guard_drift.c (proposed _Static_assert against current/drifted layout)

Verified recommended fix

Add _Static_assert(sizeof(struct ioapic_irqinfo) == 1u << IOAPIC_IRQI_SZSHIFT, ...) under the SZSHIFT define in ioapic_abi.h (fix.diff).

Verdict

genassym.c:276-277 publishes both halves of the ioapic_irqs[] stride contract (IOAPIC_IRQI_SIZE=sizeof and IOAPIC_IRQI_SZSHIFT=hand constant from ioapic_abi.h:58) but nothing at build time relates them; the invariant is enforced only by a comment (ioapic_abi.h:50-51). Demonstrated on the guest: today sizeof==16==1<<4 (drift_now.log, matching the shipped kernel's assym.s), while a one-field drift (drift_sim.log, 24 vs 16) makes the two assembly consumers index ioapic_irqs[] differently (ioapic_ipl.s:80,97 vs apic_vector.s:55-59) with the kernel still building cleanly -- a silent interrupt-routing corruption mode. The proposed _Static_assert (fix.diff) compiles clean on the current layout and fails the build on the drifted layout (guard_drift.log), converting the drift to a loud build break. Impact today: none (invariant holds); filed as verified hardening.