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)
PoC verification
Evidence pack
findings/poc/DF-5597 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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:
sys/platform/pc64/apic/ioapic_ipl.s:80,97βshll $IOAPIC_IRQI_SZSHIFT, %eax(indexesioapic_irqs[]with the hand constant)sys/platform/pc64/apic/apic_vector.s:55-59β `IOAPIC_IRQI_SIZE * (irq_num)- IOAPIC_IRQI_{ADDR,IDX,FLAGS}` (indexes with sizeof)
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(aftercc):ioapic_drift.cwithout -D β invariant HOLDS (16 == 16, matching the shipped kernel's assym.s); with-DDRIFT(one addeduint32_tfield) β 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_assertcompiles 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.
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
- Current state is consistent. The shipped kernel's generated assym.s
contains
IOAPIC_IRQI_SIZE 0x10andIOAPIC_IRQI_SZSHIFT 0x4;ioapic_drift.c(compiled from a verbatim copy of the struct) confirmssizeof(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. - 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.
- Drift is silent today.
ioapic_drift.c -DDRIFT(one addeduint32_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 indexioapic_irqs[]differently β under-walked elements, wrong io_addr/io_idx/io_flags β and the kernel still builds and boots with no diagnostic. - The proposed guard works. With
_Static_assertadded (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
fixedBaseline: 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
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.
No comments yet.