The asker is writing a hobby OS and just migrated from the legacy 8259 PIC to the modern APIC architecture. Under QEMU everything works; on real hardware they see three distinct symptoms: spurious INT 2 firings, no keyboard or mouse interrupts at all, and (with PIT enabled and its source override honored) a flood of vector 39.
Why this is hard. QEMU is famously forgiving. It tolerates missing EOIs, sloppy redirection-table programming, and skipping bus quirks. Real chipsets do not. Three independent things have to be correct simultaneously for IOAPIC to work on bare metal:
- The legacy PIC must be fully masked and disabled — not just "ignored." If you don't mask all 16 lines on both 8259s and ideally remap them out of the CPU exception range (0x20–0x2F), spurious IRQ7/IRQ15 can still surface as INT 2 or other low vectors, especially on hardware that delivers a "ghost" interrupt before mask takes effect.
- MADT/ACPI parsing must honor Interrupt Source Overrides. On most x86 boards the PIT is wired through IOAPIC pin 2, not pin 0 — exactly what the asker noticed. But ISO entries also carry polarity and trigger-mode flags. Getting those wrong means edges are missed or levels never deassert, which matches the "flood of vector 39" symptom (level-triggered without EOI behaves like a stuck interrupt).
- Local APIC must be enabled and EOI'd. Every interrupt — including spurious ones — needs a write to the LAPIC EOI register. Forgetting this freezes the priority register at a non-zero TPR/ISR level and silently blocks subsequent IRQs (which matches "no keyboard or mouse").
Direction to investigate. I'd suggest a layered bring-up:
- Dump the full MADT and print every ISO, NMI source, and IOAPIC entry. Compare against what's actually programmed in the redirection table after init — many bugs are simply a mismatch between "what ACPI said" and "what got written."
- For each IOAPIC RTE, explicitly set delivery mode (000 = fixed), destination mode, polarity (high for ISA, low for PCI), and trigger (edge for ISA, level for PCI). Don't rely on reset defaults.
- Add a vector-127 spurious handler in the LAPIC SVR and verify it isn't firing constantly — that's often where INT 2-like surprises actually originate.
- Check that the PS/2 controller is actually emitting IRQ1/12. Some modern boards route the keyboard through USB legacy emulation; if the BIOS handed off USB, the PS/2 IRQ never comes.
Gotchas. The asker should test on a machine with real PS/2 ports, not USB-emulated ones. They should also confirm the IOAPIC base address from MADT rather than assuming 0xFEC00000 — most boards use it, but it's not guaranteed.