Skip to content

test(tests): pin the bti j split that no bundled binary can show - #331

Merged
danielplohmann merged 1 commit into
danielplohmann:masterfrom
r0ny123:test/aarch64-bti-target-type
Sep 11, 2026
Merged

test(tests): pin the bti j split that no bundled binary can show#331
danielplohmann merged 1 commit into
danielplohmann:masterfrom
r0ny123:test/aarch64-bti-target-type

Conversation

@r0ny123

@r0ny123 r0ny123 commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Item 2 of #322, the bti j row.

What changed since the issue was written

no bundled fixture contains a single bti j word, and every bundled AArch64 fixture is bit-identical across that change

The first clause is no longer true. #300 bundled elf_cxx_landing_pads_arm64_xored, which carries five bti j words, and instrumenting a run shows all five reach _isLikelyInteriorBtiCandidate.

The second clause still holds, and the reason is worth recording rather than just working around. All five sit inside exception landing pads: isDeclaredLandingPad is true for every one, and the shape test independently returns "interior" for every one. So USE_AARCH64_BTI_TARGET_TYPE decides nothing there — the address is refused with the flag on or off, by two other rules, one of which (#300's LSDA pass) is documented as having to precede the shape test precisely because under -mbranch-protection every pad opens with a bti.

That is why bundling a BTI-built binary would not have closed this row either. A compiler emits bti j at exception landing pads and jump-table cases, and on anything with unwind data the landing pads are already refused by the time this rule is consulted.

What the flag needs to decide by itself

A bti j the shape test would otherwise accept: after alignment padding, opening a block that looks like an entry, in an image that declares no landing pads at all. A raw buffer is exactly that image, so this builds one instead of asking for a binary that cannot be bundled.

hint word flag off flag on flag decides
bti j hint booked at +0x20 hint refused, block at +0x24 booked yes
bti c booked booked no
bti jc booked booked no
bare bti booked booked no

The three that do not move are the point of the test as much as the one that does: they are what makes it measure the jump-only split rather than bti handling in general. bti jc admits a call and a bare bti admits both, so neither says jump-only, and neither should be refused.

Tests

tests/testAArch64BtiTargetType.py, six cases: the default, the refusal, the flag-off control, bti c, the combined and bare hints, and a case asserting the first function is recovered in all eight arrangements so no other assertion can pass by reading an empty set.

Verified to fail when the rule is disabled: forcing the word == BTI_J branch false fails testAJumpOnlyHintIsNotAnEntry and nothing else.

What this does not close

The corpus magnitude in #310 — 803 false / 0 true for bti j against bti c's 505/0 — is still not reproducible from anything in this repo, and this PR does not claim to change that. It makes the mechanism regression-testable, which is the durable half of the two options the issue lists; the number remains a figure measured elsewhere.

Gates

  • python -m pytest tests/2,066 passed, 1 skipped, 2,593 subtests
  • ruff check . and ruff format --check . → clean
  • make typecheck → exit 0, 261 diagnostics, identical to master
  • Test-only: no src/ change, no fixture added, no baseline moves.

You marked these as yours in the issue, so this is offered rather than assumed.

danielplohmann#310 refuses a `bti j` word as a function start and keeps `bti c`, on the
grounds that a jump-only hint marks an indirect branch target inside a
routine where a call hint marks a callable entry. danielplohmann#322 records the figure
behind it as unverifiable here, because no bundled fixture contained a
`bti j` word and every AArch64 fixture was bit-identical across the
change.

The first half of that has since stopped being true: danielplohmann#300 bundled
elf_cxx_landing_pads_arm64_xored, which carries five `bti j` words, and
they do reach the rule. The second half still holds, and now for a
reason worth writing down. All five sit inside exception landing pads,
where the shape test refuses them on its own account and the LSDA rule
refuses them earlier still, so the flag decides nothing on that fixture
and toggling it moves no address.

What the flag needs in order to decide by itself is a `bti j` the shape
test would otherwise accept: after alignment padding, opening a block
that looks like an entry, in an image declaring no landing pads at all.
A raw buffer is exactly that image, so this builds one rather than asking
for a binary that cannot be bundled.

With the flag off the hint word is booked as a function and the block
behind it is not; with it on the hint is refused and the block is
recovered in its place. `bti c`, `bti jc` and a bare `bti` are asserted
unaffected in both settings, which is what makes the case measure the
jump-only split rather than bti handling in general.
@danielplohmann

Copy link
Copy Markdown
Owner

Reviewed here and good to go. The raw buffer with no .eh_frame is the one arrangement where the flag decides by itself, and building that rather than asking for a BTI-built binary is the correct reading of why bundling one would not have closed this row: on anything carrying unwind data the landing pads are refused before this rule is consulted.

The three controls are what make the test measure the jump-only split rather than BTI handling in general, and testTheSameWordIsAnEntryWithTheFlagOff asserting both directions is what stops it passing on a rule that quietly stopped running.

With this and #333 in, both rows of #322 item 2 have their mechanism pinned in-repo. The magnitudes stay measured-and-posted rather than reproducible, and I agree that is what the fixtures owe.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants