You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Classify jump tables to de-noise the raw-BR PAC audit
The raw-BR/BLR audit flagged every plain indirect branch uniformly,
and on arm64e the overwhelming majority are compiler switch dispatch,
not JOP hazards. Recognize the clang jump-table idiom and dismiss it:
adrp xB,#pg ; add xB,xB,#off ; ldrsw xE,[xB,xI,lsl #2] ;
adr xA,#. ; add xT,xA,xE ; br xT
The target is a PC-relative base plus a signed offset read from a
table at a statically materialized (read-only) address -- not a value
loaded from a corruptible pointer -- so a BR to exactly that xT is
benign. jt_advance walks the five producers under strict adjacency
(a fresh ADRP restarts, anything else resets), and check_pac_raw_-
indirect consults it before the BR advances the machine past stage 5.
This inverts armlint's usual bias. Everywhere else a false negative
(missed fold) is the safe error; here the audit's dangerous error is
hiding a real hazard, so the classifier is deliberately narrow. Only
this exact five-producer shape suppresses, and only a BR: a BLR has no
jump-table form and is never dismissed, nor is a BR whose register the
idiom did not just compute. Linker veneers (adr+br, no table load),
the compact ldrb-scaled table variant, and any unclassified branch
stay on the worklist. The surviving-BR detail now reads "verify: not a
recognized jump table" rather than the old blanket "jump tables are
benign".
Recon: the idiom is 100% uniform across the arm64e system binaries.
Every raw BR in ls (1), ssh (3), sshd (3), zsh (22), and bash (18) is
this exact shape -- all 47 byte-verified against the disassembly by an
independent decoder, zero mismatches -- so the classifier empties
their raw-BR worklists completely while leaving every LR-spill and
BLR finding untouched. (This corrects the prior calibration note,
which mislabeled ssh's three as veneers.) On plain-arm64 libcapstone
run with -a pac, nothing is suppressed: its 151 tail-call BRs and 198
BLRs all survive, since none match the ldrsw idiom -- the conservative
result.
Forward-declared decode_add_imm_x/decode_add_x_shifted_lsl for the
recognizer, which sits far above their definitions. Unit test
test_pac_jump_table: the canonical idiom and its commuted final add
suppress; a bare BR, a BLR after the full idiom, a BR to the wrong
register, a one-instruction gap, a missing ADRP, and a mismatched ADD
base all stay flagged. Fixture pac_jump_table (plain arm64 + -a pac
sidecar, so it runs on Linux CI too) holds the dismissed table beside
a surviving BR and BLR. Two existing PAC snapshots absorb the detail
wording change, findings unchanged. Capstone 5.0.9.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: TODO.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -75,7 +75,7 @@ The largest untouched family; none of these need liveness machinery.
75
75
| Split fusion pairs (cmp+b.cond, aese+aesmc same-dest, adrp+add) | Informational: "these should be adjacent"; per-core tables from the SOGs |
76
76
| Render `mov xd, #0` (not `mov xd, xzr`) and `movi v0.2d, #0` (not `movi d0, #0`) | Apple eliminates only those spellings at rename; rendering tweaks to existing checks |
77
77
| Loaded value as base not offset (`[x9, x8]` → `[x8, x9]` when x8 was just loaded) | Apple guide §4.6.7: 1 cycle of address-generation latency |
78
-
| PAC audit v2: jump-table classification to auto-dismiss benign raw `br` (the clang `adrp`/`add __const` + `ldrsw` + `add` + `br` idiom), non-SP LR stores (jmp_buf/context saves; rare -- 0 in bash/dyld, lives in libsystem_c), zero-discriminator forward-edge worklist (`braaz`/`blraaz`; dyld 169, bash 85, ssh 109)| v1 (`-a pac`) covers SP-based spills and flags every raw BR/BLR uniformly. Auto-arm on arm64e slices: **done** for both `-a pac` and `-m pauth` (one cpusubtype gate in scan_macho) |
78
+
| PAC audit v2: non-SP LR stores (jmp_buf/context saves; rare -- 0 in bash/dyld, lives in libsystem_c), zero-discriminator forward-edge worklist (`braaz`/`blraaz`; dyld 169, bash 85, ssh 109), the compact `ldrb`-scaled jump-table variant (`adr` + `ldrb` + `add …, lsl #2` + `br`; seen in Homebrew arm64 libcapstone, unmatched by the ldrsw classifier) |Auto-arm on arm64e slices: **done** for both `-a pac` and `-m pauth`. Jump-table classification for the dominant `ldrsw` idiom: **done** (jt_advance in check_pac_raw_indirect empties the arm64e raw-BR worklist) |
79
79
| LDP/STP synthesized through a scratch ADD (`add x27, xN, #big ; ldp x3, x4, [x27]`) → two plain `ldr`/`str` with the offset folded in | Size-neutral 2-for-2 that drops the ADD from the address dependency chain and frees the scratch; gc emits it whenever a pair offset exceeds ±504 or is 8-misaligned (~13k in go), LLVM for big Q-register spill offsets (~9k in librustc_driver); requires the split offsets to encode (scaled imm12, or LDUR/STUR range) |
0 commit comments