diff --git a/config/config-x86-common.mak b/config/config-x86-common.mak
index 8fc3490..d45c9a8 100644
--- a/config/config-x86-common.mak
+++ b/config/config-x86-common.mak
@@ -69,6 +69,8 @@ $(TEST_DIR)/tsc_adjust.elf: $(cstart.o) $(TEST_DIR)/tsc_adjust.o
 
 $(TEST_DIR)/apic.elf: $(cstart.o) $(TEST_DIR)/apic.o
 
+$(TEST_DIR)/ioapic.elf: $(cstart.o) $(TEST_DIR)/ioapic.o
+
 $(TEST_DIR)/tscdeadline_latency.elf: $(cstart.o) $(TEST_DIR)/tscdeadline_latency.o
 
 $(TEST_DIR)/init.elf: $(cstart.o) $(TEST_DIR)/init.o
diff --git a/config/config-x86_64.mak b/config/config-x86_64.mak
index d62c1e9..dfdeed6 100644
--- a/config/config-x86_64.mak
+++ b/config/config-x86_64.mak
@@ -10,5 +10,6 @@ tests = $(TEST_DIR)/access.flat $(TEST_DIR)/apic.flat \
 tests += $(TEST_DIR)/svm.flat
 tests += $(TEST_DIR)/vmx.flat
 tests += $(TEST_DIR)/tscdeadline_latency.flat
+tests += $(TEST_DIR)/ioapic.flat
 
 include config/config-x86-common.mak
diff --git a/lib/x86/apic.c b/lib/x86/apic.c
index 9c42c4d..80b96d8 100644
--- a/lib/x86/apic.c
+++ b/lib/x86/apic.c
@@ -17,6 +17,11 @@ static void outb(unsigned char data, unsigned short port)
     asm volatile ("out %0, %1" : : "a"(data), "d"(port));
 }
 
+void eoi(void)
+{
+    apic_write(APIC_EOI, 0);
+}
+
 static u32 xapic_read(unsigned reg)
 {
     return *(volatile u32 *)(g_apic + reg);
@@ -117,6 +122,12 @@ int enable_x2apic(void)
     }
 }
 
+u32 ioapic_read_reg(unsigned reg)
+{
+    *(volatile u32 *)g_ioapic = reg;
+    return *(volatile u32 *)(g_ioapic + 0x10);
+}
+
 void ioapic_write_reg(unsigned reg, u32 value)
 {
     *(volatile u32 *)g_ioapic = reg;
@@ -129,6 +140,24 @@ void ioapic_write_redir(unsigned line, ioapic_redir_entry_t e)
     ioapic_write_reg(0x10 + line * 2 + 1, ((u32 *)&e)[1]);
 }
 
+ioapic_redir_entry_t ioapic_read_redir(unsigned line)
+{
+    ioapic_redir_entry_t e;
+
+    ((u32 *)&e)[0] = ioapic_read_reg(0x10 + line * 2 + 0);
+    ((u32 *)&e)[1] = ioapic_read_reg(0x10 + line * 2 + 1);
+    return e;
+
+}
+
+void set_mask(unsigned line, int mask)
+{
+    ioapic_redir_entry_t e = ioapic_read_redir(line);
+
+    e.mask = mask;
+    ioapic_write_redir(line, e);
+}
+
 void enable_apic(void)
 {
     printf("enabling apic\n");
diff --git a/lib/x86/apic.h b/lib/x86/apic.h
index e325e9a..216b98d 100644
--- a/lib/x86/apic.h
+++ b/lib/x86/apic.h
@@ -20,8 +20,14 @@ typedef struct {
 
 void mask_pic_interrupts(void);
 
+void eoi(void);
+
 void ioapic_write_redir(unsigned line, ioapic_redir_entry_t e);
 void ioapic_write_reg(unsigned reg, uint32_t value);
+ioapic_redir_entry_t ioapic_read_redir(unsigned line);
+uint32_t ioapic_read_reg(unsigned reg);
+
+void set_mask(unsigned line, int mask);
 
 void enable_apic(void);
 uint32_t apic_read(unsigned reg);
diff --git a/x86/apic.c b/x86/apic.c
index 365c281..d4eec52 100644
--- a/x86/apic.c
+++ b/x86/apic.c
@@ -140,11 +140,6 @@ static void test_apicbase(void)
     report_prefix_pop();
 }
 
-static void eoi(void)
-{
-    apic_write(APIC_EOI, 0);
-}
-
 static int ipi_count;
 
 static void self_ipi_isr(isr_regs_t *regs)
@@ -165,79 +160,6 @@ static void test_self_ipi(void)
     report("self ipi", ipi_count == 1);
 }
 
-static void set_ioapic_redir(unsigned line, unsigned vec)
-{
-    ioapic_redir_entry_t e = {
-        .vector = vec,
-        .delivery_mode = 0,
-        .trig_mode = 0,
-    };
-
-    ioapic_write_redir(line, e);
-}
-
-static void set_irq_line(unsigned line, int val)
-{
-    asm volatile("out %0, %1" : : "a"((u8)val), "d"((u16)(0x2000 + line)));
-}
-
-static void toggle_irq_line(unsigned line)
-{
-    set_irq_line(line, 1);
-    set_irq_line(line, 0);
-}
-
-static int g_isr_77;
-
-static void ioapic_isr_77(isr_regs_t *regs)
-{
-    ++g_isr_77;
-    eoi();
-}
-
-static void test_ioapic_intr(void)
-{
-    handle_irq(0x77, ioapic_isr_77);
-    set_ioapic_redir(0x0e, 0x77);
-    toggle_irq_line(0x0e);
-    asm volatile ("nop");
-    report("ioapic interrupt", g_isr_77 == 1);
-}
-
-static int g_78, g_66, g_66_after_78;
-static ulong g_66_rip, g_78_rip;
-
-static void ioapic_isr_78(isr_regs_t *regs)
-{
-    ++g_78;
-    g_78_rip = regs->rip;
-    eoi();
-}
-
-static void ioapic_isr_66(isr_regs_t *regs)
-{
-    ++g_66;
-    if (g_78)
-        ++g_66_after_78;
-    g_66_rip = regs->rip;
-    eoi();
-}
-
-static void test_ioapic_simultaneous(void)
-{
-    handle_irq(0x78, ioapic_isr_78);
-    handle_irq(0x66, ioapic_isr_66);
-    set_ioapic_redir(0x0e, 0x78);
-    set_ioapic_redir(0x0f, 0x66);
-    irq_disable();
-    toggle_irq_line(0x0f);
-    toggle_irq_line(0x0e);
-    irq_enable();
-    asm volatile ("nop");
-    report("ioapic simultaneous interrupt",
-           g_66 && g_78 && g_66_after_78 && g_66_rip == g_78_rip);
-}
-
 volatile int nmi_counter_private, nmi_counter, nmi_hlt_counter, sti_loop_active;
 
 void sti_nop(char *p)
@@ -390,8 +312,6 @@ int main()
 
     test_self_ipi();
 
-    test_ioapic_intr();
-    test_ioapic_simultaneous();
     test_sti_nmi();
     test_multiple_nmi();
 
diff --git a/x86/eventinj.c b/x86/eventinj.c
index 32de6f0..bddedce 100644
--- a/x86/eventinj.c
+++ b/x86/eventinj.c
@@ -32,11 +32,6 @@ void apic_self_nmi(void)
 	apic_icr_write(APIC_DEST_PHYSICAL | APIC_DM_NMI | APIC_INT_ASSERT, 0);
 }
 
-static void eoi(void)
-{
-    apic_write(APIC_EOI, 0);
-}
-
 #define flush_phys_addr(__s) outl(0xe4, __s)
 #define flush_stack() do {						\
 		int __l;						\
diff --git a/x86/ioapic.c b/x86/ioapic.c
new file mode 100644
index 0000000..2afdaa2
--- /dev/null
+++ b/x86/ioapic.c
@@ -0,0 +1,99 @@
+#include "libcflat.h"
+#include "apic.h"
+#include "vm.h"
+#include "smp.h"
+#include "desc.h"
+#include "isr.h"
+
+#define EDGE_TRIGGERED 0
+#define LEVEL_TRIGGERED 1
+
+static void set_ioapic_redir(unsigned line, unsigned vec, unsigned trig_mode)
+{
+	ioapic_redir_entry_t e = {
+		.vector = vec,
+		.delivery_mode = 0,
+		.trig_mode = trig_mode,
+	};
+
+	ioapic_write_redir(line, e);
+}
+
+static void set_irq_line(unsigned line, int val)
+{
+	asm volatile("out %0, %1" : : "a"((u8)val), "d"((u16)(0x2000 + line)));
+}
+
+static void toggle_irq_line(unsigned line)
+{
+	set_irq_line(line, 1);
+	set_irq_line(line, 0);
+}
+
+static volatile int g_isr_77;
+
+static void ioapic_isr_77(isr_regs_t *regs)
+{
+	++g_isr_77;
+	eoi();
+}
+
+static void test_ioapic_intr(void)
+{
+	handle_irq(0x77, ioapic_isr_77);
+	set_ioapic_redir(0x0e, 0x77, EDGE_TRIGGERED);
+	toggle_irq_line(0x0e);
+	asm volatile ("nop");
+	report("ioapic interrupt", g_isr_77 == 1);
+}
+
+static int g_78, g_66, g_66_after_78;
+static ulong g_66_rip, g_78_rip;
+
+static void ioapic_isr_78(isr_regs_t *regs)
+{
+	++g_78;
+	g_78_rip = regs->rip;
+	eoi();
+}
+
+static void ioapic_isr_66(isr_regs_t *regs)
+{
+	++g_66;
+	if (g_78)
+		++g_66_after_78;
+	g_66_rip = regs->rip;
+	eoi();
+}
+
+static void test_ioapic_simultaneous(void)
+{
+	handle_irq(0x78, ioapic_isr_78);
+	handle_irq(0x66, ioapic_isr_66);
+	set_ioapic_redir(0x0e, 0x78, EDGE_TRIGGERED);
+	set_ioapic_redir(0x0f, 0x66, EDGE_TRIGGERED);
+	irq_disable();
+	toggle_irq_line(0x0f);
+	toggle_irq_line(0x0e);
+	irq_enable();
+	asm volatile ("nop");
+	report("ioapic simultaneous interrupt",
+		g_66 && g_78 && g_66_after_78 && g_66_rip == g_78_rip);
+}
+
+int main(void)
+{
+	setup_vm();
+	smp_init();
+	setup_idt();
+
+	mask_pic_interrupts();
+	enable_apic();
+
+	irq_enable();
+
+	test_ioapic_intr();
+	test_ioapic_simultaneous();
+
+	return report_summary();
+}
diff --git a/x86/unittests.cfg b/x86/unittests.cfg
index badb08a..a38544f 100644
--- a/x86/unittests.cfg
+++ b/x86/unittests.cfg
@@ -12,6 +12,11 @@ smp = 2
 extra_params = -cpu qemu64,+x2apic,+tsc-deadline
 arch = x86_64
 
+[ioapic]
+file = ioapic.flat
+extra_params = -cpu qemu64
+arch = x86_64
+
 [smptest]
 file = smptest.flat
 smp = 2
