Skip to content

arm64e: rebuild the pointer-authentication patch on Rust 1.100.0 - #52

Open
cypherair wants to merge 13 commits into
base/1.100from
refresh/1.100
Open

cypherair wants to merge 13 commits into
base/1.100from
refresh/1.100

Conversation

@cypherair

@cypherair cypherair commented Nov 10, 2026 •

Copy link
Copy Markdown
Owner

This rebuilds the fork's arm64e pointer-authentication patch on Rust 1.100.0, on top of upstream's own pointer-authentication code. It is not a rebase: the old patch ran its own mechanism beside upstream's, and a plain rebase conflicts in ten files. Here the patch is a Darwin profile in upstream's PointerAuthConfig, plus the fork's wider signing scope, the flag checks and the visionOS target.

  • Base: base/1.100 is upstream's stable branch at 0ef1bc3860, which reads 1.100.0. The 1.100.0 tag does not exist yet; the base moves to the tag when it does.
  • Size: compiler code is +209 −28 in 18 files, 25 lines of which are the visionOS target spec. The old patch was +299 −30 in 26 files.
  • Not in this PR: moving rust-stable-with-arm64e, the stage1 toolchain workflow, and any toolchain build. Those are the next stage.

Commit by commit

1. feat(arm64e): add Apple visionOS target. The old commit, carried over. It is modelled on arm64e-apple-ios. One change: the docs name the maintainer without an @.

2. feat(arm64e): list the pauth features in the arm64e-apple-darwin spec. From the old "model default Apple ptrauth features" commit, only the line that mattered: arm64e-apple-darwin now names +v8.3a,+paca,+pacg like the iOS and tvOS targets do. Pointer authentication no longer depends on the CPU. The arm64e_features helper is gone; each spec just names its features.

3. feat(arm64e): turn on upstream's pointer authentication for arm64e Apple targets. Replaces the old "emit clang-compatible ptrauth metadata" commit.

  • PointerAuthConfig::from_raw returns a new arm64e Apple profile for these targets, built from the target alone.
  • The profile matches clang's Darwin driver: calls, returns, indirect gotos and auth traps on; C function pointers signed with key IA, no diversity. Jump-table hardening, init/fini signing and the ELF GOT stay off.
  • Upstream's existing code then gives every function the four ptrauth attributes, including the C main and __rust_try. The old patch added these in three places of its own.
  • Upstream's ELF module flags (PAuth ABI version, ELF GOT, signed personality) are now emitted only for ELF objects. Without that, upstream's ABI-version code would assert on Apple targets.
  • Dropped: the ptrauth.abi-version module flag. Nothing in LLVM reads it, and Mach-O objects always record ABI version 0. Also dropped: LLVMRustAddModuleFlagMetadata, the attribute helpers, and the paca/pacg symbols.
  • Adds Target::is_apple_arm64e.

At this commit the arm64e targets behave like upstream's pauthtest: only extern "C" pointers are signed. That is a working state, and the next commit widens it.

4. feat(arm64e): sign every function pointer that becomes a value. Replaces the old "authenticate indirect function calls" and "authenticate function pointers used as data" commits.

  • PointerAuthConfig gets a scope. ExternC is upstream's behaviour and stays the pauthtest default. AllFunctions is the arm64e Apple profile.
  • Under AllFunctions, a function's address is signed where it becomes a value, whatever its ABI: fn item and closure casts, the main pointer given to lang_start, constant operands, statics, extern_weak statics and v-table entries.
  • Call sites that produce a direct callee (direct calls, lang-item calls, drop glue called directly, thread-local shims, the personality) take a new pointer_authentication_direct_callees() schema. It is upstream's schema under ExternC and none under AllFunctions. So direct calls stay direct, even at -O0. Upstream signs direct callees too and relies on InstCombine to fold them back; widening its ABI filter alone would have made every direct Rust call an authenticated indirect call in debug builds.
  • A call gets a ptrauth bundle exactly when its callee is not a function symbol and not inline assembly. This covers dyn calls, closures and the try_fn call inside __rust_try, whatever their ABI.
  • Constant operands: upstream's scalar_to_backend passes no schema, so a constant fn pointer used as a value would stay unsigned. The LLVM backend now passes the function schema there under AllFunctions.
  • Dropped: the old SimpleCx field, get_fn_addr_for_data, arm64e_fn_ptr_for_data, the LLVMConstantPtrAuth binding (upstream's LLVMRustConstPtrAuth is used), and the bundle in SBuilder::call (its only caller, the allocator shim, makes direct calls).
  • The old patch changed LLVMRustIsNonGVFunctionPointerTy in C++, which CFI and KCFI also use. The new check is a small Rust helper, llvm::is_indirect_callee, so other targets are untouched.

5. feat(arm64e): leave Mach-O initializer pointers unsigned, as clang does. New. This settles gap I-2 of the assessment. Clang on Darwin does not sign the entries of __mod_init_func and __mod_term_func. Upstream recognises only ELF .init_array/.fini_array, so the old patch signed them; that is why Sequoia's #[ctor] entry was signed in the 1.x app. The Mach-O sections are now recognised, by name or by section type, and take the profile's init/fini schema, which is none. A runtime smoke case checks that such a constructor runs.

6. fix(arm64e): reject flags that would change the arm64e pointer-authentication ABI. Replaces the two old flag-check commits, rebased onto 1.100's restructured target_features.rs and the renamed diagnostics.rs files.

  • -Ctarget-feature=-paca, -pacg or -pauth is an error, one per feature, and is kept out of the backend feature list.
  • -Zbranch-protection with pac-ret is an error; bti and gcs stay allowed.
  • New: -Zpointer-authentication is an error on these targets. Upstream ignores it with a warning on every target but pauthtest; here it would silently do nothing.

7. ci(arm64e): run the fork's validation on pull requests again. See "The validation workflow" below.

8. test(arm64e): check that disabling v8.3a keeps pointer authentication. Added after review. v8.3a implies paca and pacg, but disabling a feature only disables the features that imply it, in rustc and in LLVM. So -Ctarget-feature=-v8.3a keeps pauth, and the flag check rightly allows it. A new revision of the assembly call test uses that flag; it only assembles if the backend still has pauth.

9. ci(arm64e): run the validation on the xcode-27 image. The first run's jobs waited 15 minutes on macos-15 and macos-26 and were dropped without a runner, for lack of GitHub capacity. See "The validation workflow" below.

10. test(arm64e): fix four codegen tests for LLVM 23 and Rust 1.100. The first full run passed every pauthtest codegen test and five arm64e codegen tests. Four arm64e tests failed on their own patterns: LLVM 23 prints a !guid attachment after a function's attributes; the catch_unwind intrinsic changed its signature; a negative check matched the test crate's own name; and LLVM merged two identical test functions. In each case the IR showed the intended result.

11. ci(arm64e): run every test suite even when one fails. Adds --no-fail-fast, so one failing suite no longer hides the others.

The old "close ptrauth IR emission gaps" commit needs nothing new: upstream now attributes the C main and __rust_try itself, and inline assembly is excluded by the new indirect-callee check. Its commit message also described a "late stripping" of bundles that was never in the code.

Where this differs from the assessment's plan

  • Initializer entries are unsigned now (commit 5). The assessment left this open for a device check. I chose clang's layout, since it is the known-good path for dyld. The device run should still confirm that Sequoia's constructor runs.
  • -Zpointer-authentication is an error on arm64e Apple targets, rather than a subset being accepted.
  • The bootstrap fix for shallow fork clones is not carried. The workflow no longer needs it: it gives bootstrap the merge-commit shape it expects in CI.
  • The stage1 toolchain workflow is not carried yet. It builds and publishes the toolchain, and its LLVM provenance pins name LLVM 22.1.6. It belongs with the toolchain build in the next stage.

Tests

All arm64e tests have revisions for darwin, iOS, tvOS and visionOS unless noted.

Ported from the old patch, updated where the code changed:

  • codegen-llvm/arm64e-apple-ptrauth.rs: the four attributes, +pauth, BTI and GCS flags. Now also checks that no jump-table hardening, ELF PAuth flag or ptrauth.abi-version flag appears.
  • codegen-llvm/arm64e-apple-ptrauth-calls.rs and assembly-llvm/arm64e-apple-ptrauth-calls.rs: indirect calls get the bundle and blraaz/braaz; direct calls do not.
  • codegen-llvm/arm64e-apple-ptrauth-fnptr-data.rs: a function pointer passed as an argument is signed.
  • codegen-llvm/arm64e-apple-ptrauth-invoke.rs: __rust_try has the attributes, and its invoke through try_fn has the bundle.
  • codegen-llvm/arm64e-apple-ptrauth-entry.rs: the C main has the attributes. Now also checks that the main pointer given to lang_start is signed.
  • codegen-llvm/arm64e-apple-ptrauth-inline-asm.rs and -callbr.rs: inline assembly and asm goto get no bundle.
  • run-make/arm64e-ptrauth-serialized-output: no direct call keeps a bundle in IR, bitcode or embedded bitcode. The old workflow never ran it.
  • The two UI tests for -Ctarget-feature and -Zbranch-protection, rewritten for the per-feature messages.

New:

  • codegen-llvm/arm64e-apple-ptrauth-scope.rs, at -O0: Rust-ABI fn pointers, closures, constants, statics, extern_weak statics and v-table entries are signed; calls through a Rust fn pointer and through dyn carry the bundle; direct Rust and C calls carry none.
  • codegen-llvm/arm64e-apple-ptrauth-init-term.rs: __mod_init_func and __mod_term_func entries stay unsigned, found by name or by type; the same pointer in an ordinary static is signed.
  • run-make/arm64e-ptrauth-allocator-shim: every function of the allocator shim, which is its own LLVM module, has the four attributes.
  • ui/pointer_authentication/arm64e_apple_fixed_pointer_authentication.rs: -Zpointer-authentication is rejected.

Upstream's pauthtest tests run unchanged, to show the Linux path is untouched: codegen-llvm/pauth/, assembly-llvm/pauth-basic.rs, ui/pointer_authentication/, ui/statics/crt-static-pauthtest.rs, ui/target_modifiers/incompatible_pauth.rs, plus assembly-llvm/targets/targets-elf.rs and targets-macho.rs. (pauth-attr-special-funcs.rs is only-pauthtest, so it is skipped on a macOS host.)

The validation workflow

It last ran its tests on 10 August. Weekly runs then failed at checkout on a branch name that a rename had removed. The fix of 21 October was pushed with a skip-CI marker, and the workflow had no pull_request trigger, so nothing has run since. Now:

  • It runs on pull requests, on pushes to rust-stable-with-arm64e, weekly, and by hand.
  • Every run gets the shape bootstrap expects in CI: a merge commit whose first parent is the newest upstream bors commit. Bootstrap then downloads that commit's prebuilt LLVM (built from the same submodule) instead of building LLVM for hours.
  • It runs x check on the codegen crates, the tests above, tidy, and a stage1 std build for arm64e-apple-darwin.
  • It runs on one image, xcode-27: GitHub's macOS 27 image, in public preview. The standard macos-15 and macos-26 images are short of capacity, and GitHub dropped both jobs of the first run without a runner. So the matrix covers macOS 27 only, no longer macOS 15 and 26. The xcode-27 label may change when the image becomes generally available.
  • Non-blocking smoke cases build and run small arm64e binaries: hello, thread id, a C fn pointer, a thread-local, and a new scope case (a dyn call and a closure through fn pointers, catch_unwind, a thread, a __mod_init_func constructor). They also report whether the binary's fn pointers carry a signature, which tells whether the runner runs arm64e with pointer authentication on.

Upstream's own CI workflow also fires on this PR. It stops at once at its channel check, because it expects a stable-channel PR to target a branch named stable. That failure says nothing about the patch.

Gaps that remain

Unchanged from the assessment unless noted:

  • I-1: Rust's function pointers and v-table entries use the C schema (IA, 0), with no address or type diversity, and the v-table pointer itself is not signed.
  • V-1: pointers that Apple code signs with another schema (__ptrauth fields, blocks, Objective-C, C++ virtuals) crash if called from Rust.
  • V-2 is closed: extern_weak statics are now signed.
  • V-3: a raw address turned into a fn pointer (assembly, or transmute from an integer) fails when called.
  • A function address in a static is signed even when the static's Rust type is a raw data pointer (a fn cast to *const ()). I have not checked what clang does for the same cast in a C static initializer.

Add arm64e-apple-visionos, modelled on arm64e-apple-ios: the same data
layout, the same apple-a12 baseline and the same pointer-authentication
features. Bootstrap's stage 0 compiler does not know the target yet, so
it goes on the sanity check's list of targets missing from stage 0.
arm64e-apple-ios and arm64e-apple-tvos name +v8.3a,+paca,+pacg in their
specs; arm64e-apple-darwin relied on its apple-m1 CPU to imply them. Name
them here too, so that pointer authentication does not depend on the CPU
and survives -Ctarget-cpu=generic.
…ple targets

Give PointerAuthConfig an arm64e Apple profile, built from the target
alone: the platform fixes the ABI, so -Zpointer-authentication does not
change it. The profile matches what clang's Darwin driver turns on for
arm64e: authenticated calls, returns and indirect gotos, auth traps, and
C function pointers signed with key IA and no diversity. It leaves off
what Darwin does not use: jump-table hardening, signed initializer
arrays and the ELF GOT.

With the profile on, upstream's code gives every generated function the
four ptrauth attributes, the C main and __rust_try included, and signs
extern "C" function pointers. Its module flags describe the ELF PAuth
ABI and assert the pauthtest target, so they are now emitted for ELF
objects only. The previous patch's ptrauth.abi-version flag is not carried
over: no part of LLVM reads it, and Mach-O objects record ABI version 0
whatever the module says.

Add Target::is_apple_arm64e for the targets this applies to.
Upstream signs only functions with the C or system ABI, and signs them
wherever codegen takes their address, so direct calls to them carry a
ptrauth bundle until LLVM's InstCombine folds the pair away; in an
unoptimised build they stay authenticated indirect calls. On arm64e
Apple targets the C and Swift code in the same process authenticates
every indirect call, and Rust should not be the soft spot. Widening
upstream's two ABI filters alone would turn every direct Rust call into
a signed indirect one.

Add a scope to PointerAuthConfig instead. ExternC is upstream's
behaviour and stays the pauthtest default. AllFunctions, the arm64e
Apple profile, signs a function's address, whatever its ABI, where it
becomes a value: fn item and closure casts, the main pointer passed to
lang_start, constants, statics, extern_weak statics and v-table entries.
Direct callees, lang-item calls, drop glue called directly, thread-local
shims and the personality take the new direct-callee schema, which is
none under AllFunctions, so direct calls stay direct. A call gets a
ptrauth bundle exactly when its callee is not a function symbol and not
inline assembly; this covers calls through dyn, closures and the
try_fn pointer in __rust_try, whatever their ABI.

Key IA and discriminator 0 are the C function-pointer schema on Darwin,
so pointers cross between Rust, C and Swift in both directions. Direct
calls, asm sym operands and the personality slot stay raw.
Clang's Darwin driver does not turn on -fptrauth-init-fini, so C code on
arm64e leaves the entries of __mod_init_func and __mod_term_func
unsigned. Upstream recognises only the ELF .init_array and .fini_array
names, so on Mach-O such an entry was signed as an ordinary function
pointer; the old patch did the same, which is why Sequoia's #[ctor]
entry was signed in the 1.x app.

Recognise the Mach-O sections too, by name or by section type, and give
them the profile's init/fini schema, which on arm64e Apple targets is
none. The constructor that the ctor crate registers is now laid out
exactly as clang lays out a C constructor.
…tication ABI

-Ctarget-feature=-paca, -pacg or LLVM's -pauth would build arm64e code
that cannot authenticate, while every function still carries the
ptrauth attributes and every indirect call its bundle. Reject them on
arm64e Apple targets, one error per feature, and keep them out of the
backend feature list.

-Zbranch-protection=pac-ret asks for a second return-address scheme on
top of ptrauth-returns. Reject it there too; bti and gcs stay allowed.

-Zpointer-authentication cannot change what the platform fixes. It was
ignored with a warning, as on any target other than pauthtest; on
arm64e Apple targets it is now an error, so that a build does not seem
to have changed pointer authentication when it has not.
The validation workflow last ran its tests on 10 August. Scheduled runs
then failed at checkout every week, on a branch name the 206d116
rename had removed. The fix of 21 October was pushed with a skip-CI
marker, the workflow had no pull_request trigger, and no run has
started since.

Port the workflow onto the refresh and:

- run it on pull requests, alongside pushes to rust-stable-with-arm64e,
  the weekly schedule and manual dispatch;
- give every run the shape bootstrap expects in CI, a merge commit whose
  first parent is the newest upstream bors commit, so that it downloads
  that commit's prebuilt LLVM instead of building LLVM from source. The
  last green run found its base on the fork's retired main branch;
- run upstream's pauthtest tests next to the arm64e tests, to show that
  the Linux path is unchanged, plus targets-elf, both run-make tests and
  tidy;
- add a runtime smoke case for the arm64e scope: a dyn call and a
  closure through fn pointers, catch_unwind, a thread, and a
  constructor in __mod_init_func.

The host-binary smoke cases stay non-blocking, since a runner may not
run arm64e binaries, or may run them with pointer authentication off.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-10T16:52:08.403942Z e43b12f New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 79c51fa689

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@cypherair cypherair closed this Nov 10, 2026
@cypherair cypherair reopened this Nov 10, 2026
v8.3a implies paca and pacg, but disabling a feature only disables the
features that imply it, in rustc and in LLVM alike. So -v8.3a leaves
pauth on, and the flag check rightly lets it through. Add a revision of
the call test with -Ctarget-feature=-v8.3a: it only assembles if the
backend still has pauth, so a change to either side's implication rules
would show here.
GitHub dropped both jobs of the first validation run after 15 minutes
without a runner: "Due to capacity constraints, jobs targeting macOS
arm64 runners may experience longer queue times." The macos-15 and
macos-26 images are the ones short of capacity. xcode-27, the macOS 27
image in public preview, takes jobs at once; the main repository's CI
runs there.

Run the one job on xcode-27. The host triple is now fixed in the job,
since that image may not carry a rustc of its own to ask.
The first validation run built the patch and passed every pauthtest
test and five of the arm64e codegen tests. Four arm64e tests failed on
their own patterns, not on the code they check:

- LLVM 23 prints a !guid attachment after a function's attribute
  group, so `#0 {` no longer follows the attributes.
- The catch_unwind intrinsic is now generic over its data pointer and
  returns bool, and rustc panics on the old declaration.
- `CHECK-NOT: ptrauth` matched the test crate's own name in rustc's IR
  comments. Look for the bundle and a signed callee instead.
- LLVM merged two identical empty functions, so the terminator static
  pointed at the initializer function. Give them different signatures.

In each case the IR showed the intended result: unsigned Mach-O
initializer entries, a signed ordinary static, unsigned direct callees.
x test stops at the first failing suite, so one failing codegen test
hid the assembly, UI and run-make results. Pass --no-fail-fast.
@cypherair cypherair closed this Nov 10, 2026
@cypherair cypherair reopened this Nov 10, 2026
Under CI, bootstrap refuses an x test without --stage ("x.py was run
under CI with an implicit `--stage 1`"). The test step already passes
--stage 1; the tidy step now does too.
A tidy failure skipped the stage1 std build and every runtime smoke
step, the evidence that matters most, and each run takes about twenty
minutes. Tidy now always runs; the std build runs whenever the codegen
crates check, even if a test failed; the smoke steps run whenever the
std build succeeded. Every failure still fails the job.
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