Skip to content

Nothing fails when an arm64 library loses its branch protection - #232

Merged
xroche merged 4 commits into
masterfrom
check-branch-protection
Sep 21, 2026
Merged

xroche merged 4 commits into
masterfrom
check-branch-protection

Conversation

@xroche

@xroche xroche commented Sep 21, 2026 •

Copy link
Copy Markdown
Owner

Two ways an arm64 library can lose its branch protection, both silent. A library built without -mbranch-protection still loads and passes every test. An -march bump to armv8.3-a turns the PAC hints into retaa, which faults on an ARMv8.0 core.

On every arm64 .so the check wants no retaa or retab, one paciasp in the instruction column, and one feature note naming BTI and PAC. It refuses an empty walk, a directory it could not read, and a failing tool, because each of those otherwise reads as a pass.

I ran it against real libraries on both sides. The ones built before #231 fail on libhttrack.so, whose note the unmarked OpenSSL statics cleared, and the ones built after it pass.

A real build only produces the passing case, so the test drives the failures through stubs that copy the real tools' output, lowercase aarch64 feature included. Six mutations of the checker each turn exactly one case red.

The note assertion is right only now. Before #231 it was absent from a correctly built libhttrack.so.

xroche and others added 4 commits September 21, 2026 21:44
Two regressions here are silent. A library built without -mbranch-protection
still loads and still passes every test. An -march bump to armv8.3-a turns the
PAC hints into retaa, which faults on an ARMv8.0 core.

The check asserts both on every arm64 .so. No retaa or retab, and a feature
note carrying BTI and PAC. It refuses an empty directory, because a build that
produced no library would otherwise pass every assertion it makes.

I ran it against real artifacts on both sides. The libraries built before #231
fail on libhttrack.so, whose note the unmarked OpenSSL statics cleared, and the
ones built after it pass.

A real build only ever produces the passing case, so the test drives the
failures through stub tools. Two mutations of the checker each turn exactly one
case red, which is what says the cases are not covering for each other.

Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The check asserts a BTI and PAC note on every arm64 library, which only holds
once #231's OpenSSL carries one.

Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A failing llvm-objdump piped into grep -c reports zero matches, which the retaa
check read as a pass. It now captures the disassembly first and exits when that
fails.

The note assertion alone passes a library that carries the note and signs no
function, so the check also requires a paciasp. The test gains that case, plus
one that pins the count in the pass line, because a walk that silently shrank
would otherwise read as a clean run.

Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The paciasp assertion counted its mnemonic anywhere in the disassembly, and
objdump prints the file path and every branch target. The same library passed
or failed depending on the directory holding it, and a library exporting a
function called paciasp passed while signing nothing. The match is now anchored
to the instruction column.

Two more ways through. llvm-readelf prints one feature line per note section,
and bash globs span newlines, so a good line covered for a bad one. The check
now wants exactly one. And find's status is lost through a process
substitution, so a directory it could not read shrank the walk in silence.

The stubs now copy the real tools, down to the path header and the lowercase
"aarch64 feature". They printed what the script expected, which cannot catch
the script expecting the wrong thing. Dropping the -i from that grep passed
every case and turned every real library red.

Six mutations, each killed by its own case. Retab dropped, the note match made
case-sensitive, the instruction match unanchored. Then the disassembler no
longer fail-closed, the two-note guard removed, and find's status ignored.

The test moves into the native job, which is a required check, because the job
it was in is not and it guards the gate. A failed check keeps the libraries now,
since that is when they are worth reading.

Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@xroche
xroche enabled auto-merge (squash) September 21, 2026 20:15
@xroche
xroche merged commit 9a051f4 into master Sep 21, 2026
7 checks passed
@xroche
xroche deleted the check-branch-protection branch September 21, 2026 20:27
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.

1 participant