diff --git a/target-arm/cpu.h b/target-arm/cpu.h
index cb6ec5c..be5d022 100644
--- a/target-arm/cpu.h
+++ b/target-arm/cpu.h
@@ -1242,44 +1242,200 @@ bool write_cpustate_to_list(ARMCPU *cpu);
 #  define TARGET_VIRT_ADDR_SPACE_BITS 32
 #endif
 
+/* Physical FIQ exception mask lookup table
+ *
+ * [ From ARM ARMv7 B1.8.6 Async exception masking (table B1-12) ]
+ * [ From ARM ARMv8 G1.11.3 Async exception masking (table G1-18) ]
+ *
+ * The below multi-dimensional table is used for looking up the masking
+ * behavior given the specified state conditions.  The table value are used
+ * for determining whether the PSTATE.AIF/CPSR.AIF bits control enterrupt
+ * masking or not.
+ *
+ *    Dimensions:
+ *    fiq_excp_mask_table[2][2][2][2][2][2][4]
+ *                        |  |  |  |  |  |  +--- Current EL
+ *                        |  |  |  |  |  +------ Non-secure(0)/Secure(1)
+ *                        |  |  |  |  +--------- HCR mask override
+ *                        |  |  |  +------------ SCR exec state control
+ *                        |  |  +--------------- SCR non-secure masking
+ *                        |  +------------------ SCR mask override
+ *                        +--------------------- 32-bit(0)/64-bit(1) EL3
+ *
+ *    The table values are as such:
+ *      0 = Exception is masked depending on PSTATE
+ *      1 = Exception is taken (unmasked) regardless of PSTATE
+ *     -1 = Cannot occur
+ *     -2 = Exception not taken, left pending
+ *
+ * Notes:
+ *    - RW is dont-care when EL3 is AArch32
+ *    - AW/FW are don't care when EL3 is AArch32
+ *    - Exceptions left pending (-2) is informational and should never escape
+ *      as the correct procedure involves first checking current to target EL.
+ *
+ *             SCR         HCR
+ *          64  EA         AMO                 From
+ *         BIT IRQ  AW     IMO      Non-secure         Secure
+ *         EL3 FIQ  FW  RW FMO   EL0 EL1 EL2 EL3   EL0 EL1 EL2 EL3
+ */
+static const int8_t fiq_excp_mask_table[2][2][2][2][2][2][4] = {
+    {{{{{/* 0   0   0   0   0 */{ 0,  0,  0, -1 },{ 0, -1, -1,  0 },},
+        {/* 0   0   0   0   1 */{ 1,  1,  0, -1 },{ 0, -1, -1,  0 },},},
+       {{/* 0   0   0   1   0 */{ 0,  0,  0, -1 },{ 0, -1, -1,  0 },},
+        {/* 0   0   0   1   1 */{ 1,  1,  0, -1 },{ 0, -1, -1,  0 },},},},
+      {{{/* 0   0   1   0   0 */{ 0,  0,  0, -1 },{ 0, -1, -1,  0 },},
+        {/* 0   0   1   0   1 */{ 1,  1,  0, -1 },{ 0, -1, -1,  0 },},},
+       {{/* 0   0   1   1   0 */{ 0,  0,  0, -1 },{ 0, -1, -1,  0 },},
+        {/* 0   0   1   1   1 */{ 1,  1,  0, -1 },{ 0, -1, -1,  0 },},},},},
+     {{{{/* 0   1   0   0   0 */{ 1,  1,  1, -1 },{ 0, -1, -1,  0 },},
+        {/* 0   1   0   0   1 */{ 1,  1,  1, -1 },{ 0, -1, -1,  0 },},},
+       {{/* 0   1   0   1   0 */{ 1,  1,  1, -1 },{ 0, -1, -1,  0 },},
+        {/* 0   1   0   1   1 */{ 1,  1,  1, -1 },{ 0, -1, -1,  0 },},},},
+      {{{/* 0   1   1   0   0 */{ 0,  0,  0, -1 },{ 0, -1, -1,  0 },},
+        {/* 0   1   1   0   1 */{ 1,  1,  1, -1 },{ 0, -1, -1,  0 },},},
+       {{/* 0   1   1   1   0 */{ 0,  0,  0, -1 },{ 0, -1, -1,  0 },},
+        {/* 0   1   1   1   1 */{ 1,  1,  1, -1 },{ 0, -1, -1,  0 },},},},},},
+    {{{{{/* 1   0   0   0   0 */{ 0,  0,  0, -1 },{ 0,  0, -1, -2 },},
+        {/* 1   0   0   0   1 */{ 1,  1,  0, -1 },{ 0,  0, -1, -2 },},},
+       {{/* 1   0   0   1   0 */{ 0,  0, -2, -1 },{ 0,  0, -1, -2 },},
+        {/* 1   0   0   1   1 */{ 1,  1,  0, -1 },{ 0,  0, -1, -2 },},},},
+      {{{/* 1   0   1   0   0 */{ 1,  1,  1, -1 },{ 0,  0, -1, -2 },},
+        {/* 1   0   1   0   1 */{ 1,  1,  0, -1 },{ 0,  0, -1, -2 },},},
+       {{/* 1   0   1   1   0 */{ 0,  0, -2, -1 },{ 0,  0, -1, -2 },},
+        {/* 1   0   1   1   1 */{ 1,  1,  0, -1 },{ 0,  0, -1, -2 },},},},},
+     {{{{/* 1   1   0   0   0 */{ 1,  1,  1, -1 },{ 1,  1, -1,  0 },},
+        {/* 1   1   0   0   1 */{ 1,  1,  1, -1 },{ 1,  1, -1,  0 },},},
+       {{/* 1   1   0   1   0 */{ 1,  1,  1, -1 },{ 1,  1, -1,  0 },},
+        {/* 1   1   0   1   1 */{ 1,  1,  1, -1 },{ 1,  1, -1,  0 },},},},
+      {{{/* 1   1   1   0   0 */{ 1,  1,  1, -1 },{ 1,  1, -1,  0 },},
+        {/* 1   1   1   0   1 */{ 1,  1,  1, -1 },{ 1,  1, -1,  0 },},},
+       {{/* 1   1   1   1   0 */{ 1,  1,  1, -1 },{ 1,  1, -1,  0 },},
+        {/* 1   1   1   1   1 */{ 1,  1,  1, -1 },{ 1,  1, -1,  0 },},},},},},
+};
+
+/* Physical IRQ exception mask lookup table
+ *
+ * [ From ARM ARMv7 B1.8.6 Async exception masking (table B1-13) ]
+ * [ From ARM ARMv8 G1.11.3 Async exception masking (table G1-19) ]
+ *
+ * The below multi-dimensional table is used for looking up the masking
+ * behavior given the specified state conditions.  The table value are used
+ * for determining whether the PSTATE.AIF/CPSR.AIF bits control enterrupt
+ * masking or not.
+ *
+ *    Dimensions:
+ *    irq_excp_mask_table[2][2][2][2][2][4]
+ *                        |  |  |  |  |  +--- Current EL
+ *                        |  |  |  |  +------ Non-secure(0)/Secure(1)
+ *                        |  |  |  +--------- HCR mask override
+ *                        |  |  +------------ SCR exec state control
+ *                        |  +--------------- SCR mask override
+ *                        +------------------ 32-bit(0)/64-bit(1) EL3
+ *
+ *    The table values are as such:
+ *      0 = Exception is masked depending on PSTATE
+ *      1 = Exception is taken (unmasked) regardless of PSTATE
+ *     -1 = Cannot occur
+ *     -2 = Exception not taken, left pending
+ *
+ * Notes:
+ *    - RW is dont-care when EL3 is AArch32
+ *    - Exceptions left pending (-2) is informational and should never escape
+ *      as the correct procedure involves first checking current to target EL.
+ *
+ *             SCR     HCR
+ *          64  EA     AMO                 From
+ *         BIT IRQ     IMO      Non-secure         Secure
+ *         EL3 FIQ  RW FMO   EL0 EL1 EL2 EL3   EL0 EL1 EL2 EL3
+ */
+static const int8_t irq_excp_mask_table[2][2][2][2][2][4] = {
+     {{{{/* 0   0   0   0 */{ 0,  0,  0, -1 },{ 0, -1, -1,  0 },},
+        {/* 0   0   0   1 */{ 1,  1,  0, -1 },{ 0, -1, -1,  0 },},},
+       {{/* 0   0   1   0 */{ 0,  0,  0, -1 },{ 0, -1, -1,  0 },},
+        {/* 0   0   1   1 */{ 1,  1,  0, -1 },{ 0, -1, -1,  0 },},},},
+      {{{/* 0   1   0   0 */{ 0,  0,  0, -1 },{ 0, -1, -1,  0 },},
+        {/* 0   1   0   1 */{ 1,  1,  1, -1 },{ 0, -1, -1,  0 },},},
+       {{/* 0   1   1   0 */{ 0,  0,  0, -1 },{ 0, -1, -1,  0 },},
+        {/* 0   1   1   1 */{ 1,  1,  1, -1 },{ 0, -1, -1,  0 },},},},},
+     {{{{/* 1   0   0   0 */{ 0,  0,  0, -1 },{ 0,  0, -1, -2 },},
+        {/* 1   0   0   1 */{ 1,  1,  0, -1 },{ 0,  0, -1, -2 },},},
+       {{/* 1   0   1   0 */{ 0,  0, -2, -1 },{ 0,  0, -1, -2 },},
+        {/* 1   0   1   1 */{ 1,  1,  0, -1 },{ 0,  0, -1, -2 },},},},
+      {{{/* 1   1   0   0 */{ 1,  1,  1, -1 },{ 1,  1, -1,  0 },},
+        {/* 1   1   0   1 */{ 1,  1,  1, -1 },{ 1,  1, -1,  0 },},},
+       {{/* 1   1   1   0 */{ 1,  1,  1, -1 },{ 1,  1, -1,  0 },},
+        {/* 1   1   1   1 */{ 1,  1,  1, -1 },{ 1,  1, -1,  0 },},},},},
+};
+
 static inline bool arm_excp_unmasked(CPUState *cs, unsigned int excp_idx)
 {
     CPUARMState *env = cs->env_ptr;
     unsigned int cur_el = arm_current_el(env);
-    unsigned int target_el = arm_excp_target_el(cs, excp_idx);
-    /* FIXME: Use actual secure state.  */
-    bool secure = false;
-    /* If in EL1/0, Physical IRQ routing to EL2 only happens from NS state.  */
-    bool irq_can_hyp = !secure && cur_el < 2 && target_el == 2;
-    /* ARMv7-M interrupt return works by loading a magic value
-     * into the PC.  On real hardware the load causes the
-     * return to occur.  The qemu implementation performs the
-     * jump normally, then does the exception return when the
-     * CPU tries to execute code at the magic address.
-     * This will cause the magic PC value to be pushed to
-     * the stack if an interrupt occurred at the wrong time.
-     * We avoid this by disabling interrupts when
-     * pc contains a magic address.
+    bool secure = arm_is_secure(env);
+    uint32_t rw = ((env->cp15.scr_el3 & SCR_RW) == SCR_RW);
+    uint32_t is64 = arm_el_is_aa64(env, 3);
+    uint32_t fw;
+    uint32_t scr;
+    uint32_t hcr;
+    bool pstate_unmasked;
+    int8_t unmasked = 0;
+
+    /* Don't take exceptions if they target a lower EL.
+     * This check should catch any exceptions that would not be taken but left
+     * pending.
      */
-    bool irq_unmasked = !(env->daif & PSTATE_I)
-                        && (!IS_M(env) || env->regs[15] < 0xfffffff0);
-
-    /* Don't take exceptions if they target a lower EL.  */
-    if (cur_el > target_el) {
+    if (cur_el > arm_excp_target_el(cs, excp_idx)) {
         return false;
     }
 
     switch (excp_idx) {
     case EXCP_FIQ:
-        if (irq_can_hyp && (env->cp15.hcr_el2 & HCR_FMO)) {
-            return true;
-        }
-        return !(env->daif & PSTATE_F);
+        scr = ((env->cp15.scr_el3 & SCR_FIQ) == SCR_FIQ);
+        hcr = ((env->cp15.hcr_el2 & HCR_FMO) == HCR_FMO);
+
+        /* The SCR.FW bit only affects masking when the virtualization
+         * extension is present.  The unmasked table assumes that the extension
+         * is present, so when not present we must set FW to 1 to
+         * remain neutral.
+         */
+        fw = (!arm_feature(env, ARM_FEATURE_EL2) |
+              ((env->cp15.scr_el3 & SCR_FW) == SCR_FW));
+
+        /* FIQ are unmasked if PSTATE.F is clear */
+        pstate_unmasked = !(env->daif & PSTATE_F);
+
+        /* Perform a table lookup on whether the current state results in the
+         * exception being masked.  If the table lookup returns true(1) then
+         * PSTATE determines whether the interrupt is unmasked or not.
+         */
+        unmasked = fiq_excp_mask_table[is64][scr][fw][rw][hcr][secure][cur_el];
+        break;
+
     case EXCP_IRQ:
-        if (irq_can_hyp && (env->cp15.hcr_el2 & HCR_IMO)) {
-            return true;
-        }
-        return irq_unmasked;
+        scr = ((env->cp15.scr_el3 & SCR_IRQ) == SCR_IRQ);
+        hcr = ((env->cp15.hcr_el2 & HCR_IMO) == HCR_IMO);
+
+        /* ARMv7-M interrupt return works by loading a magic value
+         * into the PC.  On real hardware the load causes the
+         * return to occur.  The qemu implementation performs the
+         * jump normally, then does the exception return when the
+         * CPU tries to execute code at the magic address.
+         * This will cause the magic PC value to be pushed to
+         * the stack if an interrupt occurred at the wrong time.
+         * We avoid this by disabling interrupts when
+         * pc contains a magic address.
+         */
+        pstate_unmasked = !(env->daif & PSTATE_I)
+                          && (!IS_M(env) || env->regs[15] < 0xfffffff0);
+
+        /* Perform a table lookup on whether the current state results in the
+         * exception being masked.  If the table lookup returns true(1) then
+         * PSTATE determines whether the interrupt is unmasked or not.
+         */
+        unmasked = irq_excp_mask_table[is64][scr][rw][hcr][secure][cur_el];
+        break;
+
     case EXCP_VFIQ:
         if (!secure && !(env->cp15.hcr_el2 & HCR_FMO)) {
             /* VFIQs are only taken when hypervized and non-secure.  */
@@ -1291,10 +1447,16 @@ static inline bool arm_excp_unmasked(CPUState *cs, unsigned int excp_idx)
             /* VIRQs are only taken when hypervized and non-secure.  */
             return false;
         }
-        return irq_unmasked;
+        return !(env->daif & PSTATE_I)
+               && (!IS_M(env) || env->regs[15] < 0xfffffff0);
     default:
         g_assert_not_reached();
     }
+
+    /* We better not have a negative table value or something went wrong */
+    assert(unmasked >= 0);
+
+    return unmasked || pstate_unmasked;
 }
 
 static inline CPUARMState *cpu_init(const char *cpu_model)
