Diagnosing ARM64 Memory Tagging Extension Faults on Linux and Android
A diagnostic guide for ARM64 Memory Tagging Extension (MTE) faults on Linux and Android: recognizing synchronous/asynchronous tag-check failures, decoding ESR_ELx syndrome codes, and applying targeted fixes for heap, stack, and global violations.
17 Jan 2026, 13:35 UTC

Recognizable Condition
A process crashes with SIGSEGV (si_code=SEGV_MTEAERR) or SIGBUS (si_code=BUS_MTEAERR) on ARMv8.5-A or newer hardware (Neoverse N2/V1, Cortex-A710/X2/X3, Google Tensor G2+). Kernel logs show arm64_mte: asynchronous tag check fault or a synchronous fault at the program counter with ESR_ELx.ISS bits [5:0]=0b010101 (0x15), indicating a tag-check failure. These faults appear non-deterministically for asynchronous cases, often at an instruction unrelated to the actual memory error.
Cause and Diagnostic Reference
| Symptom Pattern | Likely Cause | Fault Type | Typical Location |
|---|---|---|---|
| Crash at unrelated PC, tag mismatch in 16-byte granule | Heap buffer overflow or underflow | Asynchronous | Later load/store after corruption |
| Fault when freed memory reallocated with new tag | Use-after-free | Asynchronous | Access to dangling pointer |
| Synchronous fault on function prologue/epilogue | Stack tag mismatch (SP-relative access) | Synchronous | Stack frame setup/teardown |
| Sync fault on first access after dynamic loader runs | Global/data section tag mismatch | Synchronous | First use of global variable |
| Faults in legacy pointer arithmetic or integer-to-pointer casts | Top-byte ignore (TBI=0) or missing tag preservation | Either | Pointer manipulation code |
Ordered Diagnostic Checks
1. Confirm MTE Is Enabled
Run on the target device (requires shell access, no special permissions):
cat /proc/cpuinfo | grep -i mte
# Expected: "Features" line includes "mte"
dmesg | grep -i mte
# Expected: "MTE enabled" or "arm64_mte: MTE enabled"
If absent, the CPU or kernel lacks MTE support. Minimum kernel: 5.17 for synchronous faults, 6.1+ for full asynchronous handling.
2. Inspect the Fault ESR
Extract the syndrome from kernel logs (requires dmesg access, typically root or CAP_SYSLOG):
dmesg -T | grep -A5 'tag check fault'
# Look for ESR_ELx value, e.g., ESR_EL1=0x96000015
# ISS[5:0] = 0x15 → synchronous tag check fault
# ISS[5:0] = 0x16 → asynchronous tag check fault
# Fault address = (ESR >> 12) & 0xFFFFFFFFF << 12
Decode manually if needed:
echo 'obase=16; ibase=16; 96000015' | bc
# Output: 15 → confirms synchronous tag check fault
3. Check VMA Tag State
Verify the faulting region is MTE-tagged (run as the process owner or root):
pmap -x
# Check permissions column for "mt" flag
cat /proc//smaps | grep -A2 'VmFlags:'
# "mt" in VmFlags indicates MTE-tagged VMA
4. Force Synchronous Faults for Precise Origin
Reproduce with synchronous mode to pinpoint the offending instruction (requires application restart):
LD_PRELOAD=libmtemalloc.so MALLOC_MTE=1 PR_MTE_TCF=1 ./app
# PR_MTE_TCF=1 enables synchronous fault delivery via prctl
# Adds ~5-10% overhead; use only for debugging
5. Enable Hardware-Assisted Sanitizer
Compile with HWASAN for precise origin tracking (requires rebuild):
clang -fsanitize=hwaddress -fno-omit-frame-pointer -O1 -g app.c -o app
# Run normally; faults report exact source line
Fixes Mapped to Findings
Heap Buffer Overflow/Underflow
Add explicit bounds checks before pointer arithmetic. Use malloc_usable_size(ptr) or __builtin_object_size(ptr, 0) to validate accesses. Enable allocator-assisted detection:
export MALLOC_MTE=2 # async detection (default on Android 13+)
export MALLOC_MTE=3 # sync detection for debugging
Use-After-Free
Set pointers to NULL immediately after free(). Use sized deallocation APIs where available: free_sized(ptr, size), free_aligned_sized(ptr, align, size). Enable quarantine to delay reuse:
mallopt(M_QUARANTINE, 64 * 1024); // 64 KB quarantine buffer
Stack Tag Mismatch
Ensure compiler flags are compatible: -fstack-protector-strong and -mstack-protector-guard=tls work with MTE. Avoid inline assembly that clobbers the stack pointer without preserving its top-byte tag. If hand-written assembly manipulates SP, tag the new value with irg (insert random tag) or copy the tag from the original SP.
Global/Data Section Mismatch
Relink with a tag-aware linker. Use lld with packed dynamic relocations:
clang -fuse-ld=lld -Wl,--pack-dyn-relocs=relr ...
Avoid __attribute__((section(".mysec"))) without a linker script that marks the section MTE-compatible (aligned to 16 bytes, tagged at load time).
TBI/Pointer Cast Issues
Compile with -mno-ignore-top-byte (default on MTE kernels). For integer-to-pointer round-trips, use uintptr_t and preserve tags:
void *p = malloc(16);
uintptr_t i = (uintptr_t)p;
// ... arithmetic on i ...
void *q = __builtin_preserve_tag((void *)i);
Legacy code relying on TBI=0 (prctl(PR_SET_TAGGED_ADDR_CTRL, PR_TAGGED_ADDR_ENABLE)) conflicts with MTE; MTE implies TBI=1. Such code must be updated.
Escalation Criteria
| Condition | Likely Root Cause | Action |
|---|---|---|
| Fault persists after clean HWASAN build | CPU erratum (e.g., Cortex-A710 r0p0 spurious async faults, fixed in r0p1) | Check CPU revision: cat /proc/cpuinfo | grep -i revision; consult vendor errata; disable MTE for affected cores via kernel cmdline arm64_mte=off as last resort |
Kernel panic in do_mte_fault | Kernel < 5.17 (sync) or < 6.1 (async) | Upgrade to 6.1+ LTS; backport MTE patches if stuck on older LTS |
| Performance regression >15% with sync mode | Overhead on hot paths | Profile: perf stat -e armv8_pmuv3_0/mte_*/ ./app; apply selective tagging: prctl(PR_MTE_TCF, PR_MTE_TCF_SYNC, 0, 0, 0) only on critical threads; use async (PR_MTE_TCF_ASYNC) elsewhere |
| False positives in JIT/interpreted runtimes (V8, ART) | Runtime emits untagged pointers or mismatched tags | File runtime bug; workaround: restrict MTE to async for JIT regions via mprotect(ptr, size, PROT_MTE | PROT_READ | PROT_WRITE) |
Limitations and Verification
MTE granules are 16-byte aligned. Structure padding or alignment changes can shift tag boundaries unexpectedly. Use alignas(16) on critical structures to control tag placement. Low entropy at early boot (before rngd or virtio-rng starts) can cause tag collisions, yielding false negatives. Ensure a hardware RNG is active.
Verify the fix by re-running with synchronous detection and confirming the fault moves to the allocation site:
MALLOC_MTE=3 ./app
# Fault should now occur at the exact overflow/write-after-free line
perf record -e armv8_pmuv3_0/mte_tag_check_fault/ ./app
perf report
# Compare fault counts before and after fix
Cross-validate on x86_64 with HWASAN emulation: the same bug should surface with a similar stack trace, confirming the logic error is architecture-independent.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.