Why can TF-A code still run in an address space that it itself has set to be "Execute-Never"?

2026-05-30

Stack Overflow: View Question

Tags: arm, cpu-registers, trustzone, fvp

Score: 0 | Views: 130

The asker is running ARM Trusted Firmware (BL2/BL31) at EL3 on an FVP Base_Revc_2xAEMvA model. They observe that even after the translation tables explicitly mark a memory region as UXN/PXN (Execute-Never), the CPU keeps fetching and executing instructions from that region. From a pure "MMU enforces permissions on every access" mental model, this looks impossible — so what gives?

Why it's interesting: this sits at the intersection of three subtle ARMv8-A behaviours that are easy to overlook when you only read the descriptor format tables:

Approach to debug:

  1. Confirm which EL the offending fetches happen at. In Arm DS, check CurrentEL at the moment of execution.
  2. Dump the actual descriptor the MMU is using by walking TTBR0_EL3 manually for the faulting VA — don't trust the C struct you think you wrote. The FVP's "Memory" view lies if you read the source table instead of following the walk.
  3. After modifying tables, issue the canonical sequence: DSB ISHST; TLBI ALLE3; DSB ISH; ISB. Skipping the trailing ISB is the classic bug.
  4. Verify SCTLR_EL3.M = 1 and SCTLR_EL3.WXN. If M=0, the MMU is off and every region is effectively RWX flat-mapped — XN bits are dead text.
  5. Check HCR_EL2.{TGE,E2H} if a transition occurred; the effective regime can shift mid-flow.

Gotcha: on FVP specifically, the model is very forgiving about caches but very strict about TLB semantics. A bug that "works" on silicon because of timing can be exposed on FVP — or vice versa. Also, WXN only promotes writable pages to XN; it doesn't retroactively invalidate cached translations.

The challenge: "The MMU isn't enforcing my XN bit" almost always means the translation the core is actually using isn't the one you just wrote — TLB invalidation and translation regime, not descriptor encoding, are the real suspects.

All newsletters