Skip to content

Inverted-branch fold flags toolchain-forced conditional tail calls (actionability false positive) #7

Description

@gaul

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

  1. Reclassify as opt-in/informational (post-link audience), alongside
    the planned -a audit class and the adrp+add → adr row.
  2. 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.)
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions