Commit 119c22e added check_inverted_branch: a one-instruction conditional
skip over an unconditional B (b.eq .+8 ; b L, likewise CBZ/CBNZ and
TBZ/TBNZ) folds to the single inverted branch (b.ne L). The fold is
architecturally sound — every one of the 103 findings across the six
reference binaries was byte-verified, and the range/degenerate/side-entry
gates all held. The check is nevertheless reverted, because on clang/Mach-O
binaries the shape it flags is dominated by spellings the toolchain is
forced to emit, which no recompilation can improve.
The evidence: /bin/bash
armlint /bin/bash reports 12 findings of this class. Classifying every
site against LC_FUNCTION_STARTS:
| site (__text) |
containing fn size |
B displacement |
B target |
| 0x263f4 |
52 B |
−45 insns |
function entry |
| 0x2fd00 |
36 B |
−10466 |
function entry |
| 0x454ac |
24 B |
+126 |
function entry |
| 0x47448 |
12 B |
+45103 |
__auth_stubs (imported fn) |
| 0x54b68 |
116 B |
+17795 |
function entry |
| 0x5a354 |
56 B |
+704 |
function entry |
| 0x66c40 |
44 B |
−406 |
function entry |
| 0x66d44 |
44 B |
−471 |
function entry (same target as above) |
| 0x6b3a8 |
64 B |
+20 |
function entry |
| 0x6b484 |
64 B |
+20 |
function entry |
| 0x72db0 |
20 B |
−742 |
function entry |
| 0x72dc4 |
20 B |
−1660 |
function entry |
12 of 12 are conditional tail calls: tiny guard/delegate wrappers
(12–116 bytes, several adjacent near-identical twins) whose b lands
exactly on another function's entry — one of them on an imported
function's dyld auth stub. A representative site (0x10006baa4): load a
global flag byte, tbnz w8, #3 into the local path (clear a PAC-signed
saved function pointer, return 0), otherwise tail-call the neighboring
function, whose pacibsp prologue confirms the entry.
The mechanism
When clang tail-call-optimizes if (cond) return f(...);, the ideal
emission is a single b.cond _f / cbz Rn, _f. It cannot: _f is
another symbol whose distance is unknown at assembly time, and Mach-O
arm64 defines no relocation for conditional branches — only
ARM64_RELOC_BRANCH26 for B/BL (contrast ELF's R_AARCH64_CONDBR19 and
R_AARCH64_TSTBR14). The only legal spelling is to invert the condition
into a .+8 skip and put the relocation on an unconditional b _f
(which ld64 can also island). After layout the target usually lands
within imm19 reach — bash's whole __text is 460 KB — but ld64 performs
no branch relaxation, so the conservative pair ships in every Mach-O
binary. The compiler and linker did nothing improvable.
Why that is a false positive here
armlint's default finding classes promise recompile-level actionability:
the code, or the compiler, left something on the table. These sites are
already optimal for the object format; only a post-link rewriter could
apply the fold. That is the same actionability class as adrp+add → adr,
which TODO.md has always carried with a "relink-level suggestion; likely
opt-in" caveat — this check should have been held to the same bar.
The recon classified the unfoldable population precisely (out-of-range
pairs are the compilers' far-branch overflow splits; 98% of
librustc_driver's TBZ-over-B pairs) but never asked who produces the
in-range residue. bash answers: for clang/Mach-O, forced conditional
tail calls. The Go-built population (gh: 89 findings) needs its own
census — Go does not emit conditional tail calls, and its shapes look
like hand-written runtime assembly (e.g. the tbnz x4, #63, .+8 ; b
sign-test at runtime offset 0x7dc0, identical in go and gh) plus
gc's own conservative branch layout; some of those may be genuinely
actionable, but that split was never measured.
Paths to reinstatement
- Reclassify as opt-in/informational (post-link audience), alongside
the planned -a audit class and the adrp+add → adr row.
- Suppress cross-symbol transfers: parse
LC_FUNCTION_STARTS and
drop findings whose B target is a function entry or lands in
__stubs/__auth_stubs. What remains — intra-function pairs — is a
genuine compiler miss, since intra-function distances are known at
assembly time. (Watch for cold-section splitting, where an
intra-function transfer is still cross-symbol.)
- Both: suppress by default, flag to include the forced population.
The full implementation (check, 24 unit asserts, fixture, docs) lives in
119c22e and its revert; git revert of the revert commit resurrects it
when one of the above is designed.
🤖 Generated with Claude Code
Commit 119c22e added
check_inverted_branch: a one-instruction conditionalskip over an unconditional B (
b.eq .+8 ; b L, likewise CBZ/CBNZ andTBZ/TBNZ) folds to the single inverted branch (
b.ne L). The fold isarchitecturally sound — every one of the 103 findings across the six
reference binaries was byte-verified, and the range/degenerate/side-entry
gates all held. The check is nevertheless reverted, because on clang/Mach-O
binaries the shape it flags is dominated by spellings the toolchain is
forced to emit, which no recompilation can improve.
The evidence: /bin/bash
armlint /bin/bashreports 12 findings of this class. Classifying everysite against
LC_FUNCTION_STARTS:__auth_stubs(imported fn)12 of 12 are conditional tail calls: tiny guard/delegate wrappers
(12–116 bytes, several adjacent near-identical twins) whose
blandsexactly on another function's entry — one of them on an imported
function's dyld auth stub. A representative site (0x10006baa4): load a
global flag byte,
tbnz w8, #3into the local path (clear a PAC-signedsaved function pointer, return 0), otherwise tail-call the neighboring
function, whose
pacibspprologue confirms the entry.The mechanism
When clang tail-call-optimizes
if (cond) return f(...);, the idealemission is a single
b.cond _f/cbz Rn, _f. It cannot:_fisanother symbol whose distance is unknown at assembly time, and Mach-O
arm64 defines no relocation for conditional branches — only
ARM64_RELOC_BRANCH26for B/BL (contrast ELF'sR_AARCH64_CONDBR19andR_AARCH64_TSTBR14). The only legal spelling is to invert the conditioninto a
.+8skip and put the relocation on an unconditionalb _f(which ld64 can also island). After layout the target usually lands
within imm19 reach — bash's whole
__textis 460 KB — but ld64 performsno branch relaxation, so the conservative pair ships in every Mach-O
binary. The compiler and linker did nothing improvable.
Why that is a false positive here
armlint's default finding classes promise recompile-level actionability:
the code, or the compiler, left something on the table. These sites are
already optimal for the object format; only a post-link rewriter could
apply the fold. That is the same actionability class as
adrp+add → adr,which TODO.md has always carried with a "relink-level suggestion; likely
opt-in" caveat — this check should have been held to the same bar.
The recon classified the unfoldable population precisely (out-of-range
pairs are the compilers' far-branch overflow splits; 98% of
librustc_driver's TBZ-over-B pairs) but never asked who produces the
in-range residue. bash answers: for clang/Mach-O, forced conditional
tail calls. The Go-built population (gh: 89 findings) needs its own
census — Go does not emit conditional tail calls, and its shapes look
like hand-written runtime assembly (e.g. the
tbnz x4, #63, .+8 ; bsign-test at runtime offset 0x7dc0, identical in
goandgh) plusgc's own conservative branch layout; some of those may be genuinely
actionable, but that split was never measured.
Paths to reinstatement
the planned
-aaudit class and theadrp+add → adrrow.LC_FUNCTION_STARTSanddrop findings whose B target is a function entry or lands in
__stubs/__auth_stubs. What remains — intra-function pairs — is agenuine compiler miss, since intra-function distances are known at
assembly time. (Watch for cold-section splitting, where an
intra-function transfer is still cross-symbol.)
The full implementation (check, 24 unit asserts, fixture, docs) lives in
119c22e and its revert;
git revertof the revert commit resurrects itwhen one of the above is designed.
🤖 Generated with Claude Code