From e005b8bb971fe5893078c3b45947534226816354 Mon Sep 17 00:00:00 2001 From: cypherair <262752927+cypherair@users.noreply.github.com> Date: Tue, 10 Nov 2026 06:46:02 -0800 Subject: [PATCH 01/23] feat(arm64e): add Apple visionOS target 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. --- compiler/rustc_target/src/spec/mod.rs | 1 + .../src/spec/targets/arm64e_apple_visionos.rs | 25 ++++++++++ src/bootstrap/src/core/sanity.rs | 1 + src/doc/rustc/src/SUMMARY.md | 1 + src/doc/rustc/src/platform-support.md | 1 + .../platform-support/arm64e-apple-visionos.md | 46 +++++++++++++++++++ tests/assembly-llvm/targets/targets-macho.rs | 3 ++ 7 files changed, 78 insertions(+) create mode 100644 compiler/rustc_target/src/spec/targets/arm64e_apple_visionos.rs create mode 100644 src/doc/rustc/src/platform-support/arm64e-apple-visionos.md diff --git a/compiler/rustc_target/src/spec/mod.rs b/compiler/rustc_target/src/spec/mod.rs index 2d7cd049eb61c..b017bbb1f6012 100644 --- a/compiler/rustc_target/src/spec/mod.rs +++ b/compiler/rustc_target/src/spec/mod.rs @@ -1470,6 +1470,7 @@ supported_targets! { ("aarch64-apple-visionos", aarch64_apple_visionos), ("aarch64-apple-visionos-sim", aarch64_apple_visionos_sim), + ("arm64e-apple-visionos", arm64e_apple_visionos), ("armebv7r-none-eabi", armebv7r_none_eabi), ("armebv7r-none-eabihf", armebv7r_none_eabihf), diff --git a/compiler/rustc_target/src/spec/targets/arm64e_apple_visionos.rs b/compiler/rustc_target/src/spec/targets/arm64e_apple_visionos.rs new file mode 100644 index 0000000000000..b1000c2c1c7eb --- /dev/null +++ b/compiler/rustc_target/src/spec/targets/arm64e_apple_visionos.rs @@ -0,0 +1,25 @@ +use crate::spec::base::apple::{Arch, TargetEnv, base}; +use crate::spec::{Os, SanitizerSet, Target, TargetMetadata, TargetOptions}; + +pub(crate) fn target() -> Target { + let (opts, llvm_target, arch) = base(Os::VisionOs, Arch::Arm64e, TargetEnv::Normal); + Target { + llvm_target, + metadata: TargetMetadata { + description: Some("ARM64e Apple visionOS".into()), + tier: Some(3), + host_tools: Some(false), + std: Some(true), + }, + pointer_width: 64, + data_layout: "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-n32:64-S128-Fn32" + .into(), + arch, + options: TargetOptions { + features: "+neon,+apple-a12,+v8.3a,+paca,+pacg".into(), + max_atomic_width: Some(128), + supported_sanitizers: SanitizerSet::ADDRESS | SanitizerSet::THREAD, + ..opts + }, + } +} diff --git a/src/bootstrap/src/core/sanity.rs b/src/bootstrap/src/core/sanity.rs index 6983030fedd0a..775b3c691ec9c 100644 --- a/src/bootstrap/src/core/sanity.rs +++ b/src/bootstrap/src/core/sanity.rs @@ -36,6 +36,7 @@ pub struct Finder { /// when the newly-bumped stage 0 compiler now knows about the formerly-missing targets. const STAGE0_MISSING_TARGETS: &[&str] = &[ // just a dummy comment so the list doesn't get onelined + "arm64e-apple-visionos", ]; /// Minimum version threshold for libstdc++ required when using prebuilt LLVM diff --git a/src/doc/rustc/src/SUMMARY.md b/src/doc/rustc/src/SUMMARY.md index f15c712c32b91..e040c00c69cbe 100644 --- a/src/doc/rustc/src/SUMMARY.md +++ b/src/doc/rustc/src/SUMMARY.md @@ -46,6 +46,7 @@ - [arm64e-apple-tvos](platform-support/arm64e-apple-tvos.md) - [\*-apple-watchos](platform-support/apple-watchos.md) - [\*-apple-visionos](platform-support/apple-visionos.md) + - [arm64e-apple-visionos](platform-support/arm64e-apple-visionos.md) - [aarch64-nintendo-switch-freestanding](platform-support/aarch64-nintendo-switch-freestanding.md) - [aarch64-unknown-linux-gnu](platform-support/aarch64-unknown-linux-gnu.md) - [aarch64-unknown-linux-musl](platform-support/aarch64-unknown-linux-musl.md) diff --git a/src/doc/rustc/src/platform-support.md b/src/doc/rustc/src/platform-support.md index c2a86d85dfcde..18ef3ba5556cc 100644 --- a/src/doc/rustc/src/platform-support.md +++ b/src/doc/rustc/src/platform-support.md @@ -303,6 +303,7 @@ target | std | host | notes [`arm64e-apple-darwin`](platform-support/arm64e-apple-darwin.md) | ✓ | ✓ | ARM64e Apple Darwin [`arm64e-apple-ios`](platform-support/arm64e-apple-ios.md) | ✓ | | ARM64e Apple iOS [`arm64e-apple-tvos`](platform-support/arm64e-apple-tvos.md) | ✓ | | ARM64e Apple tvOS +[`arm64e-apple-visionos`](platform-support/arm64e-apple-visionos.md) | ✓ | | ARM64e Apple visionOS [`armeb-unknown-linux-gnueabi`](platform-support/armeb-unknown-linux-gnueabi.md) | ✓ | ? | Arm BE8 the default Arm big-endian architecture since [Armv6](https://developer.arm.com/documentation/101754/0616/armlink-Reference/armlink-Command-line-Options/--be8?lang=en). [`armebv7r-none-eabi`](platform-support/armebv7r-none-eabi.md) | * | | Bare Armv7-R, Big Endian [`armebv7r-none-eabihf`](platform-support/armebv7r-none-eabi.md) | * | | Bare Armv7-R, Big Endian, hardfloat diff --git a/src/doc/rustc/src/platform-support/arm64e-apple-visionos.md b/src/doc/rustc/src/platform-support/arm64e-apple-visionos.md new file mode 100644 index 0000000000000..fcecca12049f3 --- /dev/null +++ b/src/doc/rustc/src/platform-support/arm64e-apple-visionos.md @@ -0,0 +1,46 @@ +# `arm64e-apple-visionos` + +**Tier: 3** + +Apple arm64e visionOS. + +## Target maintainers + +[cypherair](https://github.com/cypherair) + +## Requirements + +This target is cross-compiled and supports `std`. + +This target requires the visionOS SDK provided by Xcode. + +The deployment target can be configured with `XROS_DEPLOYMENT_TARGET`. + +## Building the target + +You can build Rust with support for the target by adding it to the `target` +list in `bootstrap.toml`: +```toml +[build] +target = ["arm64e-apple-visionos"] +``` + +## Building Rust programs + +Rust does not yet ship pre-compiled artifacts for this target. +To compile for this target, you will need to build Rust with the target enabled. + +## Testing + +This target is not tested by Rust CI. + +Binaries are intended to run on visionOS devices with `arm64e` architecture. + +The visionOS simulator targets use `arm64`, not `arm64e`. + +## Cross-compilation toolchains and C code + +C code should be built with Apple's Clang from Xcode, using the visionOS SDK and +matching deployment target. + +The Clang target is suffixed with `-xros`. diff --git a/tests/assembly-llvm/targets/targets-macho.rs b/tests/assembly-llvm/targets/targets-macho.rs index 037f3c0093619..00383a6028d3e 100644 --- a/tests/assembly-llvm/targets/targets-macho.rs +++ b/tests/assembly-llvm/targets/targets-macho.rs @@ -34,6 +34,9 @@ //@ revisions: aarch64_apple_visionos //@ [aarch64_apple_visionos] compile-flags: --target aarch64-apple-visionos //@ [aarch64_apple_visionos] needs-llvm-components: aarch64 +//@ revisions: arm64e_apple_visionos +//@ [arm64e_apple_visionos] compile-flags: --target arm64e-apple-visionos +//@ [arm64e_apple_visionos] needs-llvm-components: aarch64 //@ revisions: aarch64_apple_visionos_sim //@ [aarch64_apple_visionos_sim] compile-flags: --target aarch64-apple-visionos-sim //@ [aarch64_apple_visionos_sim] needs-llvm-components: aarch64 From 474fdfe1dae4c9cff220a017f6fc573dbbcd8213 Mon Sep 17 00:00:00 2001 From: cypherair <262752927+cypherair@users.noreply.github.com> Date: Tue, 10 Nov 2026 06:46:10 -0800 Subject: [PATCH 02/23] feat(arm64e): list the pauth features in the arm64e-apple-darwin spec 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. --- compiler/rustc_target/src/spec/targets/arm64e_apple_darwin.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/compiler/rustc_target/src/spec/targets/arm64e_apple_darwin.rs b/compiler/rustc_target/src/spec/targets/arm64e_apple_darwin.rs index ab8f369f1bee2..9e14526513af3 100644 --- a/compiler/rustc_target/src/spec/targets/arm64e_apple_darwin.rs +++ b/compiler/rustc_target/src/spec/targets/arm64e_apple_darwin.rs @@ -16,6 +16,7 @@ pub(crate) fn target() -> Target { .into(), arch, options: TargetOptions { + features: "+v8.3a,+paca,+pacg".into(), mcount: "\u{1}mcount".into(), cpu: "apple-m1".into(), max_atomic_width: Some(128), From 37ba468fc1d54ab3b0f12c973bfc87f3914ffe8a Mon Sep 17 00:00:00 2001 From: cypherair <262752927+cypherair@users.noreply.github.com> Date: Tue, 10 Nov 2026 06:52:10 -0800 Subject: [PATCH 03/23] feat(arm64e): turn on upstream's pointer authentication for arm64e Apple 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. --- compiler/rustc_codegen_llvm/src/base.rs | 7 ++- compiler/rustc_session/src/session.rs | 23 +++++++- compiler/rustc_target/src/spec/mod.rs | 5 ++ .../arm64e-apple-ptrauth-entry.rs | 32 ++++++++++ .../arm64e-apple-ptrauth-invoke.rs | 37 ++++++++++++ tests/codegen-llvm/arm64e-apple-ptrauth.rs | 36 +++++++++++ .../arm64e-ptrauth-allocator-shim/rmake.rs | 59 +++++++++++++++++++ .../arm64e-ptrauth-allocator-shim/shim.rs | 5 ++ 8 files changed, 201 insertions(+), 3 deletions(-) create mode 100644 tests/codegen-llvm/arm64e-apple-ptrauth-entry.rs create mode 100644 tests/codegen-llvm/arm64e-apple-ptrauth-invoke.rs create mode 100644 tests/codegen-llvm/arm64e-apple-ptrauth.rs create mode 100644 tests/run-make/arm64e-ptrauth-allocator-shim/rmake.rs create mode 100644 tests/run-make/arm64e-ptrauth-allocator-shim/shim.rs diff --git a/compiler/rustc_codegen_llvm/src/base.rs b/compiler/rustc_codegen_llvm/src/base.rs index 6e7c31f7743d7..9b497e69b0123 100644 --- a/compiler/rustc_codegen_llvm/src/base.rs +++ b/compiler/rustc_codegen_llvm/src/base.rs @@ -25,7 +25,7 @@ use rustc_middle::mono::Visibility; use rustc_middle::ty::TyCtxt; use rustc_session::config::{DebugInfo, Offload}; use rustc_span::Symbol; -use rustc_target::spec::SanitizerSet; +use rustc_target::spec::{BinaryFormat, SanitizerSet}; use crate::builder::Builder; use crate::builder::gpu_offload::OffloadGlobals; @@ -163,7 +163,10 @@ pub(crate) fn compile_codegen_unit( cx.add_objc_module_flags(); } - if cx.sess().pointer_authentication() { + // These flags describe the ELF PAuth ABI; LLVM reads them only for ELF objects. + if cx.sess().pointer_authentication() + && cx.sess().target.binary_format == BinaryFormat::Elf + { let cfg = cx.sess().pointer_auth_config.as_ref().unwrap(); let aarch64_elf_pauthabi_version = diff --git a/compiler/rustc_session/src/session.rs b/compiler/rustc_session/src/session.rs index 060e614e82180..716a57f30bebc 100644 --- a/compiler/rustc_session/src/session.rs +++ b/compiler/rustc_session/src/session.rs @@ -123,7 +123,7 @@ pub struct PointerAuthSchema { } impl PointerAuthSchema { pub fn function_pointers_default(target: &Target) -> Self { - assert!(target.cfg_abi == CfgAbi::Pauthtest); + assert!(target.cfg_abi == CfgAbi::Pauthtest || target.is_apple_arm64e()); return Self { is_address_discriminated: PointerAuthAddressDiscriminator::HardwareAddress(false), discrimination_kind: PointerAuthDiscrimination::None, @@ -183,6 +183,23 @@ impl PointerAuthConfig { vt_ptr_type_discrimination: true, }; } + /// The arm64e ABI of Apple's platforms, as clang's Darwin driver sets it up. C function + /// pointers use key IA with no diversity. + fn arm64e_apple(target: &Target) -> Self { + Self { + return_addresses: true, + auth_traps: true, + indirect_gotos: true, + elf_got: false, + aarch64_jump_table_hardening: false, + function_pointers: Some(PointerAuthSchema::function_pointers_default(target)), + init_fini: None, + intrinsics: true, + typeinfo_vt_ptr_discrimination: false, + vt_ptr_addr_discrimination: true, + vt_ptr_type_discrimination: true, + } + } pub fn calculate_pauth_abi_version(&self, target: &Target) -> u32 { assert!(target.cfg_abi == CfgAbi::Pauthtest); // Bit positions of version flags for AARCH64_PAUTH_PLATFORM_LLVM_LINUX. @@ -229,6 +246,10 @@ impl PointerAuthConfig { pauth_abi_version } pub fn from_raw(raw: &[(PointerAuthOption, bool)], target: &Target) -> Option { + // The platform fixes the arm64e ABI, so `-Zpointer-authentication` does not apply. + if target.is_apple_arm64e() { + return Some(Self::arm64e_apple(target)); + } if target.cfg_abi != CfgAbi::Pauthtest { return None; } diff --git a/compiler/rustc_target/src/spec/mod.rs b/compiler/rustc_target/src/spec/mod.rs index b017bbb1f6012..2813650e7c5b4 100644 --- a/compiler/rustc_target/src/spec/mod.rs +++ b/compiler/rustc_target/src/spec/mod.rs @@ -2129,6 +2129,11 @@ impl Target { } } + /// Whether this is an arm64e target on an Apple platform, whose ABI signs code pointers. + pub fn is_apple_arm64e(&self) -> bool { + self.is_like_darwin && self.llvm_target.starts_with("arm64e") + } + /// Is this target single-threaded? /// /// This affects both optimizations (e.g., atomics can be lowered to regular operations) and diff --git a/tests/codegen-llvm/arm64e-apple-ptrauth-entry.rs b/tests/codegen-llvm/arm64e-apple-ptrauth-entry.rs new file mode 100644 index 0000000000000..6da5ffce6c420 --- /dev/null +++ b/tests/codegen-llvm/arm64e-apple-ptrauth-entry.rs @@ -0,0 +1,32 @@ +// The C `main` that rustc generates carries the pointer-authentication attributes too. +//@ add-minicore +//@ revisions: DARWIN IOS TVOS VISIONOS +//@ [DARWIN] compile-flags: --target arm64e-apple-darwin -Copt-level=0 +//@ [DARWIN] needs-llvm-components: aarch64 +//@ [IOS] compile-flags: --target arm64e-apple-ios -Copt-level=0 +//@ [IOS] needs-llvm-components: aarch64 +//@ [TVOS] compile-flags: --target arm64e-apple-tvos -Copt-level=0 +//@ [TVOS] needs-llvm-components: aarch64 +//@ [VISIONOS] compile-flags: --target arm64e-apple-visionos -Copt-level=0 +//@ [VISIONOS] needs-llvm-components: aarch64 + +#![feature(no_core, lang_items)] +#![no_core] + +extern crate minicore; + +#[lang = "start"] +fn lang_start( + _main: fn() -> T, + _argc: isize, + _argv: *const *const u8, + _sigpipe: u8, +) -> isize { + 0 +} + +fn main() {} + +// CHECK: define i32 @main({{.*}}) unnamed_addr [[MAIN_ATTR:#[0-9]+]] +// CHECK: attributes [[MAIN_ATTR]] = {{.*}} "ptrauth-auth-traps" "ptrauth-calls" +// CHECK-SAME: "ptrauth-indirect-gotos" "ptrauth-returns" diff --git a/tests/codegen-llvm/arm64e-apple-ptrauth-invoke.rs b/tests/codegen-llvm/arm64e-apple-ptrauth-invoke.rs new file mode 100644 index 0000000000000..ac724d090383f --- /dev/null +++ b/tests/codegen-llvm/arm64e-apple-ptrauth-invoke.rs @@ -0,0 +1,37 @@ +// ignore-tidy-linelength +// `__rust_try`, which rustc generates for `catch_unwind`, carries the pointer-authentication +// attributes. +//@ add-minicore +//@ revisions: DARWIN IOS TVOS VISIONOS +//@ [DARWIN] compile-flags: --target arm64e-apple-darwin -Copt-level=0 -Cpanic=unwind -Zverify-llvm-ir +//@ [DARWIN] needs-llvm-components: aarch64 +//@ [IOS] compile-flags: --target arm64e-apple-ios -Copt-level=0 -Cpanic=unwind -Zverify-llvm-ir +//@ [IOS] needs-llvm-components: aarch64 +//@ [TVOS] compile-flags: --target arm64e-apple-tvos -Copt-level=0 -Cpanic=unwind -Zverify-llvm-ir +//@ [TVOS] needs-llvm-components: aarch64 +//@ [VISIONOS] compile-flags: --target arm64e-apple-visionos -Copt-level=0 -Cpanic=unwind -Zverify-llvm-ir +//@ [VISIONOS] needs-llvm-components: aarch64 + +#![crate_type = "lib"] +#![feature(intrinsics, no_core, lang_items)] +#![no_core] + +extern crate minicore; + +#[rustc_intrinsic] +pub unsafe fn catch_unwind( + try_fn: fn(*mut u8), + data: *mut u8, + catch_fn: fn(*mut u8, *mut u8), +) -> i32; + +fn try_fn(_: *mut u8) {} +fn catch_fn(_: *mut u8, _: *mut u8) {} + +#[unsafe(no_mangle)] +pub unsafe fn invoke_catch() -> i32 { + unsafe { catch_unwind(try_fn, 0 as *mut u8, catch_fn) } +} + +// CHECK: define internal i32 @__rust_try({{.*}}) unnamed_addr [[TRY_ATTR:#[0-9]+]] personality +// CHECK: attributes [[TRY_ATTR]] = {{.*}} "ptrauth-auth-traps" "ptrauth-calls" "ptrauth-indirect-gotos" "ptrauth-returns" diff --git a/tests/codegen-llvm/arm64e-apple-ptrauth.rs b/tests/codegen-llvm/arm64e-apple-ptrauth.rs new file mode 100644 index 0000000000000..c1aa5f78056bd --- /dev/null +++ b/tests/codegen-llvm/arm64e-apple-ptrauth.rs @@ -0,0 +1,36 @@ +// ignore-tidy-linelength +// Every function on an arm64e Apple target carries the four attributes that clang's Darwin driver +// turns on, and none of the ELF PAuth ABI's attributes or module flags. +//@ add-minicore +//@ filecheck-flags: --implicit-check-not=aarch64-jump-table-hardening +//@ filecheck-flags: --implicit-check-not=aarch64-elf-pauthabi --implicit-check-not=ptrauth-elf-got +//@ filecheck-flags: --implicit-check-not=ptrauth-sign-personality +//@ filecheck-flags: --implicit-check-not=ptrauth.abi-version +//@ revisions: DARWIN DARWIN_BTI DARWIN_GCS IOS TVOS VISIONOS +//@ [DARWIN] compile-flags: --target arm64e-apple-darwin +//@ [DARWIN] needs-llvm-components: aarch64 +//@ [DARWIN_BTI] compile-flags: --target arm64e-apple-darwin -Zbranch-protection=bti +//@ [DARWIN_BTI] needs-llvm-components: aarch64 +//@ [DARWIN_GCS] compile-flags: --target arm64e-apple-darwin -Zbranch-protection=gcs +//@ [DARWIN_GCS] needs-llvm-components: aarch64 +//@ [IOS] compile-flags: --target arm64e-apple-ios +//@ [IOS] needs-llvm-components: aarch64 +//@ [TVOS] compile-flags: --target arm64e-apple-tvos +//@ [TVOS] needs-llvm-components: aarch64 +//@ [VISIONOS] compile-flags: --target arm64e-apple-visionos +//@ [VISIONOS] needs-llvm-components: aarch64 + +#![crate_type = "lib"] +#![feature(no_core, lang_items)] +#![no_core] + +extern crate minicore; + +// CHECK: @test(){{.*}} [[ATTR:#[0-9]+]] { +#[unsafe(no_mangle)] +pub fn test() {} + +// CHECK: attributes [[ATTR]] = {{.*}} "ptrauth-auth-traps" "ptrauth-calls" "ptrauth-indirect-gotos" "ptrauth-returns" +// CHECK-SAME: "target-features"="{{.*}}+pauth{{.*}}" +// DARWIN_BTI: !{{[0-9]+}} = !{i32 8, !"branch-target-enforcement", i32 1} +// DARWIN_GCS: !{{[0-9]+}} = !{i32 8, !"guarded-control-stack", i32 1} diff --git a/tests/run-make/arm64e-ptrauth-allocator-shim/rmake.rs b/tests/run-make/arm64e-ptrauth-allocator-shim/rmake.rs new file mode 100644 index 0000000000000..b80cc502ab95a --- /dev/null +++ b/tests/run-make/arm64e-ptrauth-allocator-shim/rmake.rs @@ -0,0 +1,59 @@ +//! rustc builds the allocator shim outside the usual function path, as its own LLVM module. On +//! arm64e Apple targets its functions must still carry the pointer-authentication attributes. + +//@ needs-llvm-components: aarch64 +//@ ignore-backends: gcc + +use run_make_support::{llvm_dis, rfs, rustc}; + +const PTRAUTH_ATTRS: [&str; 4] = [ + "\"ptrauth-auth-traps\"", + "\"ptrauth-calls\"", + "\"ptrauth-indirect-gotos\"", + "\"ptrauth-returns\"", +]; + +fn main() { + // Only `-Csave-temps` keeps the shim's module, as bitcode. + rustc() + .input("shim.rs") + .target("arm64e-apple-darwin") + .emit("obj") + .arg("-Csave-temps") + .arg("-Ccodegen-units=1") + .run(); + + let bitcode: Vec<_> = rfs::read_dir(".") + .map(|entry| entry.unwrap().path()) + .filter(|path| path.extension().is_some_and(|ext| ext == "bc")) + .collect(); + let mut shims = 0; + for bc in bitcode { + let ll = bc.with_extension("ll"); + llvm_dis().input(&bc).arg("-o").arg(&ll).run(); + let ir = rfs::read_to_string(&ll); + if ir.lines().any(|line| { + line.starts_with("define ") && line.contains("__rust_no_alloc_shim_is_unstable_v2") + }) { + shims += 1; + check_functions(&ir); + } + } + assert!(shims > 0, "no bitcode file holds the allocator shim"); +} + +fn check_functions(ir: &str) { + for define in ir.lines().filter(|line| line.starts_with("define ")) { + let group = define + .split_whitespace() + .find(|word| word.starts_with('#')) + .unwrap_or_else(|| panic!("no attribute group on `{define}`")); + let attrs = ir + .lines() + .find(|line| line.starts_with(&format!("attributes {group} = "))) + .unwrap_or_else(|| panic!("attribute group {group} is not defined")); + for attr in PTRAUTH_ATTRS { + assert!(attrs.contains(attr), "`{define}` lacks {attr}: `{attrs}`"); + } + } +} diff --git a/tests/run-make/arm64e-ptrauth-allocator-shim/shim.rs b/tests/run-make/arm64e-ptrauth-allocator-shim/shim.rs new file mode 100644 index 0000000000000..76cd0dee1ccdc --- /dev/null +++ b/tests/run-make/arm64e-ptrauth-allocator-shim/shim.rs @@ -0,0 +1,5 @@ +#![feature(allocator_internals, no_core)] +#![crate_type = "cdylib"] +#![default_lib_allocator] +#![needs_allocator] +#![no_core] From bd42d025d54eb4574b3dfb4fbc575f50f03d946c Mon Sep 17 00:00:00 2001 From: cypherair <262752927+cypherair@users.noreply.github.com> Date: Tue, 10 Nov 2026 07:00:08 -0800 Subject: [PATCH 04/23] feat(arm64e): sign every function pointer that becomes a value 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. --- compiler/rustc_codegen_llvm/src/builder.rs | 9 +- compiler/rustc_codegen_llvm/src/common.rs | 40 ++++-- compiler/rustc_codegen_llvm/src/consts.rs | 5 +- compiler/rustc_codegen_llvm/src/context.rs | 4 +- compiler/rustc_codegen_llvm/src/llvm/ffi.rs | 1 + compiler/rustc_codegen_llvm/src/llvm/mod.rs | 8 ++ compiler/rustc_codegen_ssa/src/base.rs | 2 +- compiler/rustc_codegen_ssa/src/common.rs | 2 +- compiler/rustc_codegen_ssa/src/mir/block.rs | 6 +- compiler/rustc_codegen_ssa/src/mir/rvalue.rs | 2 +- compiler/rustc_session/src/session.rs | 31 ++++- .../arm64e-apple-ptrauth-calls.rs | 45 +++++++ .../arm64e-apple-ptrauth-callbr.rs | 28 ++++ .../arm64e-apple-ptrauth-calls.rs | 51 +++++++ .../arm64e-apple-ptrauth-entry.rs | 4 +- .../arm64e-apple-ptrauth-fnptr-data.rs | 39 ++++++ .../arm64e-apple-ptrauth-inline-asm.rs | 28 ++++ .../arm64e-apple-ptrauth-invoke.rs | 3 +- .../arm64e-apple-ptrauth-scope.rs | 126 ++++++++++++++++++ .../ptrauth.rs | 31 +++++ .../arm64e-ptrauth-serialized-output/rmake.rs | 54 ++++++++ 21 files changed, 494 insertions(+), 25 deletions(-) create mode 100644 tests/assembly-llvm/arm64e-apple-ptrauth-calls.rs create mode 100644 tests/codegen-llvm/arm64e-apple-ptrauth-callbr.rs create mode 100644 tests/codegen-llvm/arm64e-apple-ptrauth-calls.rs create mode 100644 tests/codegen-llvm/arm64e-apple-ptrauth-fnptr-data.rs create mode 100644 tests/codegen-llvm/arm64e-apple-ptrauth-inline-asm.rs create mode 100644 tests/codegen-llvm/arm64e-apple-ptrauth-scope.rs create mode 100644 tests/run-make/arm64e-ptrauth-serialized-output/ptrauth.rs create mode 100644 tests/run-make/arm64e-ptrauth-serialized-output/rmake.rs diff --git a/compiler/rustc_codegen_llvm/src/builder.rs b/compiler/rustc_codegen_llvm/src/builder.rs index abc71f450a515..20633213e620e 100644 --- a/compiler/rustc_codegen_llvm/src/builder.rs +++ b/compiler/rustc_codegen_llvm/src/builder.rs @@ -23,6 +23,7 @@ use rustc_middle::ty::layout::{ }; use rustc_middle::ty::{self, Instance, Ty, TyCtxt}; use rustc_sanitizers::{cfi, kcfi}; +use rustc_session::PointerAuthScope; use rustc_session::config::OptLevel; use rustc_span::Span; use rustc_target::callconv::{FnAbi, PassMode}; @@ -2184,9 +2185,15 @@ impl<'a, 'll, 'tcx> Builder<'a, 'll, 'tcx> { if self.sess().pointer_authentication_functions().is_none() { return None; } + let scope = self.sess().pointer_authentication_scope(); + // When every function pointer is signed and no direct callee is, exactly the indirect + // calls authenticate their callee, whatever their ABI. + if scope == Some(PointerAuthScope::AllFunctions) && !llvm::is_indirect_callee(llfn) { + return None; + } // Pointer authentication support is currently limited to extern "C" calls; filter out other // ABIs. - if fn_abi?.conv != CanonAbi::C { + if scope == Some(PointerAuthScope::ExternC) && fn_abi?.conv != CanonAbi::C { return None; } // Filter out LLVM intrinsics. diff --git a/compiler/rustc_codegen_llvm/src/common.rs b/compiler/rustc_codegen_llvm/src/common.rs index 7a05bf718663c..8d71f96b9f157 100644 --- a/compiler/rustc_codegen_llvm/src/common.rs +++ b/compiler/rustc_codegen_llvm/src/common.rs @@ -15,7 +15,7 @@ use rustc_hir::def::DefKind; use rustc_hir::def_id::DefId; use rustc_middle::mir::interpret::{GlobalAlloc, PointerArithmetic, Scalar}; use rustc_middle::ty::{Instance, TyCtxt}; -use rustc_session::{PointerAuthAddressDiscriminator, PointerAuthSchema}; +use rustc_session::{PointerAuthAddressDiscriminator, PointerAuthSchema, PointerAuthScope}; use rustc_span::bug; use tracing::debug; @@ -36,23 +36,25 @@ pub(crate) fn maybe_sign_fn_ptr<'ll, 'tcx>( return llfn; } - // Only free functions or methods - let def_id = instance.def_id(); - if !matches!(cx.tcx.def_kind(def_id), DefKind::Fn | DefKind::AssocFn) { - return llfn; - } - // Only C ABI - let abi = cx.tcx.fn_sig(def_id).skip_binder().abi(); - if !matches!(abi, ExternAbi::C { .. } | ExternAbi::System { .. }) { - return llfn; + if cx.tcx.sess.pointer_authentication_scope() == Some(PointerAuthScope::ExternC) { + // Only free functions or methods + let def_id = instance.def_id(); + if !matches!(cx.tcx.def_kind(def_id), DefKind::Fn | DefKind::AssocFn) { + return llfn; + } + // Only C ABI + let abi = cx.tcx.fn_sig(def_id).skip_binder().abi(); + if !matches!(abi, ExternAbi::C { .. } | ExternAbi::System { .. }) { + return llfn; + } + if Some(def_id) == cx.tcx.lang_items().eh_personality() { + return llfn; + } } // Ignore LLVM intrinsics if llvm::get_value_name(llfn).starts_with(b"llvm.") { return llfn; } - if Some(def_id) == cx.tcx.lang_items().eh_personality() { - return llfn; - } let addr_diversity = match schema.is_address_discriminated { PointerAuthAddressDiscriminator::HardwareAddress(true) => Some(llfn), @@ -400,6 +402,18 @@ impl<'ll, 'tcx> ConstCodegenMethods for CodegenCx<'ll, 'tcx> { }) } + fn scalar_to_backend(&self, cv: Scalar, layout: abi::Scalar, llty: &'ll Type) -> &'ll Value { + // A function address in a constant operand is a value: sign it if every function pointer + // is signed. + let schema = match self.tcx.sess.pointer_authentication_scope() { + Some(PointerAuthScope::AllFunctions) => { + self.tcx.sess.pointer_authentication_functions() + } + Some(PointerAuthScope::ExternC) | None => None, + }; + self.scalar_to_backend_with_pac(cv, layout, llty, schema) + } + fn scalar_to_backend_with_pac( &self, cv: Scalar, diff --git a/compiler/rustc_codegen_llvm/src/consts.rs b/compiler/rustc_codegen_llvm/src/consts.rs index bbb1141a92e89..66cb6e64ea774 100644 --- a/compiler/rustc_codegen_llvm/src/consts.rs +++ b/compiler/rustc_codegen_llvm/src/consts.rs @@ -15,6 +15,7 @@ use rustc_middle::mir::interpret::{ use rustc_middle::mono::MonoItem; use rustc_middle::ty::layout::{HasTypingEnv, LayoutOf}; use rustc_middle::ty::{self, Instance}; +use rustc_session::PointerAuthScope; use rustc_span::{Symbol, bug, span_bug}; use rustc_target::spec::Arch; use tracing::{debug, instrument, trace}; @@ -213,7 +214,9 @@ fn check_and_apply_linkage<'ll, 'tcx>( let fn_abi = cx.fn_abi_of_fn_ptr(fn_sig, ty::List::empty()); // Decide if the initializer needs to be signed if cx.sess().pointer_authentication() - && matches!(fn_sig.abi(), ExternAbi::C { .. } | ExternAbi::System { .. }) + && (cx.sess().pointer_authentication_scope() + == Some(PointerAuthScope::AllFunctions) + || matches!(fn_sig.abi(), ExternAbi::C { .. } | ExternAbi::System { .. })) { should_sign = true; } diff --git a/compiler/rustc_codegen_llvm/src/context.rs b/compiler/rustc_codegen_llvm/src/context.rs index 0ce3bfe07c413..f74bb9e857eec 100644 --- a/compiler/rustc_codegen_llvm/src/context.rs +++ b/compiler/rustc_codegen_llvm/src/context.rs @@ -1046,6 +1046,8 @@ impl<'ll, 'tcx> MiscCodegenMethods<'tcx> for CodegenCx<'ll, 'tcx> { }; let tcx = self.tcx; + // Unwind tables refer to the personality by symbol. Rust code never calls it through a + // pointer, so it is treated like a direct callee. let llfn = match tcx.lang_items().eh_personality() { Some(def_id) if name.is_none() => self.get_fn_addr( ty::Instance::expect_resolve( @@ -1055,7 +1057,7 @@ impl<'ll, 'tcx> MiscCodegenMethods<'tcx> for CodegenCx<'ll, 'tcx> { ty::List::empty(), DUMMY_SP, ), - tcx.sess.pointer_authentication_functions(), + tcx.sess.pointer_authentication_direct_callees(), ), _ => { let name = name.unwrap_or("rust_eh_personality"); diff --git a/compiler/rustc_codegen_llvm/src/llvm/ffi.rs b/compiler/rustc_codegen_llvm/src/llvm/ffi.rs index 655a79e341bd1..e2d6afe1a1234 100644 --- a/compiler/rustc_codegen_llvm/src/llvm/ffi.rs +++ b/compiler/rustc_codegen_llvm/src/llvm/ffi.rs @@ -1069,6 +1069,7 @@ unsafe extern "C" { pub(crate) fn LLVMGetAggregateElement(ConstantVal: &Value, Idx: c_uint) -> Option<&Value>; pub(crate) fn LLVMGetConstOpcode(ConstantVal: &Value) -> Opcode; pub(crate) fn LLVMIsAConstantExpr(Val: &Value) -> Option<&Value>; + pub(crate) safe fn LLVMIsAInlineAsm(Val: &Value) -> Option<&Value>; // Operations on global variables, functions, and aliases (globals) pub(crate) fn LLVMIsDeclaration(Global: &Value) -> Bool; diff --git a/compiler/rustc_codegen_llvm/src/llvm/mod.rs b/compiler/rustc_codegen_llvm/src/llvm/mod.rs index 89e4d60656d34..3a8fcbe1b2046 100644 --- a/compiler/rustc_codegen_llvm/src/llvm/mod.rs +++ b/compiler/rustc_codegen_llvm/src/llvm/mod.rs @@ -510,6 +510,14 @@ pub(crate) fn add_alias<'ll>( unsafe { LLVMAddAlias2(module, ty, address_space.0, aliasee, name.as_ptr()) } } +/// Whether a call to `callee` goes through a pointer: the callee is neither a function symbol +/// nor inline assembly. +pub(crate) fn is_indirect_callee(callee: &Value) -> bool { + let callee = unsafe { LLVMRustStripPointerCasts(callee) }; + let not_a_function_symbol = unsafe { LLVMRustIsNonGVFunctionPointerTy(callee) }; + not_a_function_symbol && LLVMIsAInlineAsm(callee).is_none() +} + /// Safe wrapper for `LLVMRustConstPtrAuth`. pub(crate) fn const_ptr_auth<'ll>( ptr: &'ll Value, diff --git a/compiler/rustc_codegen_ssa/src/base.rs b/compiler/rustc_codegen_ssa/src/base.rs index 8743d703940ec..79461ff990c21 100644 --- a/compiler/rustc_codegen_ssa/src/base.rs +++ b/compiler/rustc_codegen_ssa/src/base.rs @@ -578,7 +578,7 @@ pub fn maybe_create_entry_wrapper<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>>( DUMMY_SP, ); let start_fn = - cx.get_fn_addr(start_instance, cx.sess().pointer_authentication_functions()); + cx.get_fn_addr(start_instance, cx.sess().pointer_authentication_direct_callees()); let i8_ty = cx.type_i8(); let arg_sigpipe = bx.const_u8(sigpipe); diff --git a/compiler/rustc_codegen_ssa/src/common.rs b/compiler/rustc_codegen_ssa/src/common.rs index 4b54235cf71bc..3c961c77f4368 100644 --- a/compiler/rustc_codegen_ssa/src/common.rs +++ b/compiler/rustc_codegen_ssa/src/common.rs @@ -119,7 +119,7 @@ pub(crate) fn build_langcall<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>>( let instance = ty::Instance::mono(tcx, def_id); ( bx.fn_abi_of_instance(instance, ty::List::empty()), - bx.get_fn_addr(instance, tcx.sess.pointer_authentication_functions()), + bx.get_fn_addr(instance, tcx.sess.pointer_authentication_direct_callees()), instance, ) } diff --git a/compiler/rustc_codegen_ssa/src/mir/block.rs b/compiler/rustc_codegen_ssa/src/mir/block.rs index f99009a0f4243..d09195adea2e8 100644 --- a/compiler/rustc_codegen_ssa/src/mir/block.rs +++ b/compiler/rustc_codegen_ssa/src/mir/block.rs @@ -714,7 +714,7 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> { } _ => ( false, - bx.get_fn_addr(drop_fn, bx.sess().pointer_authentication_functions()), + bx.get_fn_addr(drop_fn, bx.sess().pointer_authentication_direct_callees()), bx.fn_abi_of_instance(drop_fn, ty::List::empty()), drop_fn, ), @@ -1138,7 +1138,7 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> { None, Some(bx.get_fn_addr( instance, - bx.sess().pointer_authentication_functions(), + bx.sess().pointer_authentication_direct_callees(), )), ) } @@ -1461,7 +1461,7 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> { let fn_ptr = match (instance, llfn) { (Some(instance), None) => { - bx.get_fn_addr(instance, bx.sess().pointer_authentication_functions()) + bx.get_fn_addr(instance, bx.sess().pointer_authentication_direct_callees()) } (_, Some(llfn)) => llfn, _ => span_bug!(fn_span, "no instance or llfn for call"), diff --git a/compiler/rustc_codegen_ssa/src/mir/rvalue.rs b/compiler/rustc_codegen_ssa/src/mir/rvalue.rs index 3b33cf5a17602..274692280c315 100644 --- a/compiler/rustc_codegen_ssa/src/mir/rvalue.rs +++ b/compiler/rustc_codegen_ssa/src/mir/rvalue.rs @@ -768,7 +768,7 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> { args: ty::GenericArgs::empty(), }; let fn_ptr = - bx.get_fn_addr(instance, bx.sess().pointer_authentication_functions()); + bx.get_fn_addr(instance, bx.sess().pointer_authentication_direct_callees()); let fn_abi = bx.fn_abi_of_instance(instance, ty::List::empty()); let fn_ty = bx.fn_decl_backend_type(fn_abi); let fn_attrs = if bx.tcx().def_kind(instance.def_id()).has_codegen_attrs() { diff --git a/compiler/rustc_session/src/session.rs b/compiler/rustc_session/src/session.rs index 716a57f30bebc..a28f5a7e7157b 100644 --- a/compiler/rustc_session/src/session.rs +++ b/compiler/rustc_session/src/session.rs @@ -143,7 +143,20 @@ impl PointerAuthSchema { } } +/// Which function pointers are signed, and which calls authenticate their callee. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PointerAuthScope { + /// Functions with the C or system ABI. Their address is signed wherever codegen takes it, so + /// direct calls to them carry a `ptrauth` bundle too, until LLVM folds the pair away. + ExternC, + /// Every function. An address is signed where it becomes a value: a function pointer, a + /// v-table entry, a constant or a static. Every indirect call is authenticated, and direct + /// calls stay direct. + AllFunctions, +} + pub struct PointerAuthConfig { + pub scope: PointerAuthScope, /// Should return addresses be authenticated? pub return_addresses: bool, /// Do authentication failures cause a trap? @@ -170,6 +183,7 @@ impl PointerAuthConfig { fn default(target: &Target) -> Self { assert!(target.cfg_abi == CfgAbi::Pauthtest); return Self { + scope: PointerAuthScope::ExternC, return_addresses: true, auth_traps: true, indirect_gotos: true, @@ -184,9 +198,12 @@ impl PointerAuthConfig { }; } /// The arm64e ABI of Apple's platforms, as clang's Darwin driver sets it up. C function - /// pointers use key IA with no diversity. + /// pointers use key IA with no diversity. Rust function pointers and v-table entries use the + /// same schema, so that Rust's indirect calls are as protected as those of the C and Swift + /// code in the same process. fn arm64e_apple(target: &Target) -> Self { Self { + scope: PointerAuthScope::AllFunctions, return_addresses: true, auth_traps: true, indirect_gotos: true, @@ -1285,6 +1302,18 @@ impl Session { self.pointer_auth_config.as_ref().and_then(|cfg| cfg.function_pointers.as_ref()) } + /// The schema for a function address taken as the callee of a direct call. + pub fn pointer_authentication_direct_callees(&self) -> Option<&PointerAuthSchema> { + match self.pointer_auth_config.as_ref()?.scope { + PointerAuthScope::ExternC => self.pointer_authentication_functions(), + PointerAuthScope::AllFunctions => None, + } + } + + pub fn pointer_authentication_scope(&self) -> Option { + self.pointer_auth_config.as_ref().map(|cfg| cfg.scope) + } + pub fn pointer_authentication_init_fini(&self) -> Option<&PointerAuthSchema> { self.pointer_auth_config.as_ref().and_then(|cfg| cfg.init_fini.as_ref()) } diff --git a/tests/assembly-llvm/arm64e-apple-ptrauth-calls.rs b/tests/assembly-llvm/arm64e-apple-ptrauth-calls.rs new file mode 100644 index 0000000000000..8a0e1a06c0d2b --- /dev/null +++ b/tests/assembly-llvm/arm64e-apple-ptrauth-calls.rs @@ -0,0 +1,45 @@ +// Indirect calls authenticate with `blraaz` or `braaz`; direct calls do not. +//@ add-minicore +//@ assembly-output: emit-asm +//@ compile-flags: --target arm64e-apple-darwin -Copt-level=2 +//@ needs-llvm-components: aarch64 + +#![crate_type = "lib"] +#![feature(no_core, lang_items)] +#![no_core] + +extern crate minicore; + +#[inline(never)] +#[unsafe(no_mangle)] +pub extern "C" fn target(x: i32) -> i32 { + x +} + +#[unsafe(no_mangle)] +pub extern "C" fn direct(x: i32) -> i32 { + target(x) +} + +#[unsafe(no_mangle)] +pub extern "C" fn indirect_tail(f: extern "C" fn(i32) -> i32, x: i32) -> i32 { + f(x) +} + +#[unsafe(no_mangle)] +pub extern "C" fn indirect_non_tail(f: extern "C" fn(i32) -> i32, x: i32) -> i32 { + match f(x) { + 0 => target(x), + y => y, + } +} + +// CHECK-LABEL: _direct: +// CHECK-NOT: braaz +// CHECK-NOT: blraaz + +// CHECK-LABEL: _indirect_tail: +// CHECK: braaz + +// CHECK-LABEL: _indirect_non_tail: +// CHECK: blraaz diff --git a/tests/codegen-llvm/arm64e-apple-ptrauth-callbr.rs b/tests/codegen-llvm/arm64e-apple-ptrauth-callbr.rs new file mode 100644 index 0000000000000..ef3db87c7812b --- /dev/null +++ b/tests/codegen-llvm/arm64e-apple-ptrauth-callbr.rs @@ -0,0 +1,28 @@ +// `asm goto` lowers to `callbr`, whose callee is inline assembly: it carries no `ptrauth` bundle. +//@ add-minicore +//@ revisions: DARWIN IOS TVOS VISIONOS +//@ [DARWIN] compile-flags: --target arm64e-apple-darwin +//@ [DARWIN] needs-llvm-components: aarch64 +//@ [IOS] compile-flags: --target arm64e-apple-ios +//@ [IOS] needs-llvm-components: aarch64 +//@ [TVOS] compile-flags: --target arm64e-apple-tvos +//@ [TVOS] needs-llvm-components: aarch64 +//@ [VISIONOS] compile-flags: --target arm64e-apple-visionos +//@ [VISIONOS] needs-llvm-components: aarch64 + +#![crate_type = "lib"] +#![feature(asm_goto_with_outputs, no_core, lang_items)] +#![no_core] + +extern crate minicore; +use minicore::*; + +#[unsafe(no_mangle)] +pub unsafe fn inline_asm_goto() { + unsafe { asm!("b {}", label {}) }; +} + +// CHECK-LABEL: define{{.*}} @inline_asm_goto( +// CHECK-NOT: callbr{{.*}}"ptrauth" +// CHECK: callbr void asm sideeffect alignstack "b ${0:l}", "!i{{.*}}"() #{{[0-9]+}}{{$}} +// CHECK-NEXT: to label %{{.*}} [label %{{.*}}] diff --git a/tests/codegen-llvm/arm64e-apple-ptrauth-calls.rs b/tests/codegen-llvm/arm64e-apple-ptrauth-calls.rs new file mode 100644 index 0000000000000..94707d65e108a --- /dev/null +++ b/tests/codegen-llvm/arm64e-apple-ptrauth-calls.rs @@ -0,0 +1,51 @@ +// Indirect calls carry a `ptrauth` bundle with key IA and discriminator 0; direct calls carry none. +//@ add-minicore +//@ revisions: DARWIN IOS TVOS VISIONOS +//@ [DARWIN] compile-flags: --target arm64e-apple-darwin -Copt-level=2 +//@ [DARWIN] needs-llvm-components: aarch64 +//@ [IOS] compile-flags: --target arm64e-apple-ios -Copt-level=2 +//@ [IOS] needs-llvm-components: aarch64 +//@ [TVOS] compile-flags: --target arm64e-apple-tvos -Copt-level=2 +//@ [TVOS] needs-llvm-components: aarch64 +//@ [VISIONOS] compile-flags: --target arm64e-apple-visionos -Copt-level=2 +//@ [VISIONOS] needs-llvm-components: aarch64 + +#![crate_type = "lib"] +#![feature(no_core, lang_items)] +#![no_core] + +extern crate minicore; + +#[inline(never)] +#[unsafe(no_mangle)] +pub extern "C" fn target(x: i32) -> i32 { + x +} + +#[unsafe(no_mangle)] +pub extern "C" fn direct(x: i32) -> i32 { + target(x) +} + +#[unsafe(no_mangle)] +pub extern "C" fn indirect_tail(f: extern "C" fn(i32) -> i32, x: i32) -> i32 { + f(x) +} + +#[unsafe(no_mangle)] +pub extern "C" fn indirect_non_tail(f: extern "C" fn(i32) -> i32, x: i32) -> i32 { + match f(x) { + 0 => target(x), + y => y, + } +} + +// CHECK-LABEL: define{{.*}} @direct( +// CHECK-NOT: [ "ptrauth"(i32 0, i64 0) ] +// CHECK: ret i32 + +// CHECK-LABEL: define{{.*}} @indirect_tail( +// CHECK: {{(tail )?}}call{{.*}} {{%.*}}(i32 {{.*}}){{.*}} [ "ptrauth"(i32 0, i64 0) ]{{$}} + +// CHECK-LABEL: define{{.*}} @indirect_non_tail( +// CHECK: {{(tail )?}}call{{.*}} {{%.*}}(i32 {{.*}}){{.*}} [ "ptrauth"(i32 0, i64 0) ]{{$}} diff --git a/tests/codegen-llvm/arm64e-apple-ptrauth-entry.rs b/tests/codegen-llvm/arm64e-apple-ptrauth-entry.rs index 6da5ffce6c420..4fc8f233de286 100644 --- a/tests/codegen-llvm/arm64e-apple-ptrauth-entry.rs +++ b/tests/codegen-llvm/arm64e-apple-ptrauth-entry.rs @@ -1,4 +1,5 @@ -// The C `main` that rustc generates carries the pointer-authentication attributes too. +// The C `main` that rustc generates carries the pointer-authentication attributes too, and the +// pointer to the Rust `main` that it hands to `lang_start` is signed. //@ add-minicore //@ revisions: DARWIN IOS TVOS VISIONOS //@ [DARWIN] compile-flags: --target arm64e-apple-darwin -Copt-level=0 @@ -28,5 +29,6 @@ fn lang_start( fn main() {} // CHECK: define i32 @main({{.*}}) unnamed_addr [[MAIN_ATTR:#[0-9]+]] +// CHECK: call {{.*}}@{{.*}}lang_start{{.*}}(ptr ptrauth (ptr @{{.*}}4main{{.*}}, i32 0), // CHECK: attributes [[MAIN_ATTR]] = {{.*}} "ptrauth-auth-traps" "ptrauth-calls" // CHECK-SAME: "ptrauth-indirect-gotos" "ptrauth-returns" diff --git a/tests/codegen-llvm/arm64e-apple-ptrauth-fnptr-data.rs b/tests/codegen-llvm/arm64e-apple-ptrauth-fnptr-data.rs new file mode 100644 index 0000000000000..a8dbf3e03562c --- /dev/null +++ b/tests/codegen-llvm/arm64e-apple-ptrauth-fnptr-data.rs @@ -0,0 +1,39 @@ +// A function pointer passed as an argument is signed, and the callee authenticates it. +//@ add-minicore +//@ revisions: DARWIN IOS TVOS VISIONOS +//@ [DARWIN] compile-flags: --target arm64e-apple-darwin -Copt-level=0 -Zverify-llvm-ir +//@ [DARWIN] needs-llvm-components: aarch64 +//@ [IOS] compile-flags: --target arm64e-apple-ios -Copt-level=0 -Zverify-llvm-ir +//@ [IOS] needs-llvm-components: aarch64 +//@ [TVOS] compile-flags: --target arm64e-apple-tvos -Copt-level=0 -Zverify-llvm-ir +//@ [TVOS] needs-llvm-components: aarch64 +//@ [VISIONOS] compile-flags: --target arm64e-apple-visionos -Copt-level=0 -Zverify-llvm-ir +//@ [VISIONOS] needs-llvm-components: aarch64 + +#![crate_type = "lib"] +#![feature(no_core, lang_items)] +#![no_core] + +extern crate minicore; + +#[unsafe(no_mangle)] +pub extern "C" fn invoke_main(main_fn: extern "C" fn(*const u8) -> i32, state: *const u8) -> i32 { + main_fn(state) +} + +#[unsafe(no_mangle)] +pub extern "C" fn sample_main(_: *const u8) -> i32 { + 7 +} + +#[unsafe(no_mangle)] +pub extern "C" fn call_sample() -> i32 { + let f = sample_main; + invoke_main(f, 0 as *const u8) +} + +// CHECK-LABEL: define{{.*}} i32 @invoke_main( +// CHECK: call i32 %{{.*}}(ptr %{{.*}}){{.*}} [ "ptrauth"(i32 0, i64 0) ] + +// CHECK-LABEL: define{{.*}} i32 @call_sample( +// CHECK: call i32 @invoke_main(ptr ptrauth (ptr @sample_main, i32 0), ptr null) diff --git a/tests/codegen-llvm/arm64e-apple-ptrauth-inline-asm.rs b/tests/codegen-llvm/arm64e-apple-ptrauth-inline-asm.rs new file mode 100644 index 0000000000000..2da0081c94b29 --- /dev/null +++ b/tests/codegen-llvm/arm64e-apple-ptrauth-inline-asm.rs @@ -0,0 +1,28 @@ +// A call to inline assembly carries no `ptrauth` bundle: its callee is not a pointer. +//@ add-minicore +//@ revisions: DARWIN IOS TVOS VISIONOS +//@ [DARWIN] compile-flags: --target arm64e-apple-darwin +//@ [DARWIN] needs-llvm-components: aarch64 +//@ [IOS] compile-flags: --target arm64e-apple-ios +//@ [IOS] needs-llvm-components: aarch64 +//@ [TVOS] compile-flags: --target arm64e-apple-tvos +//@ [TVOS] needs-llvm-components: aarch64 +//@ [VISIONOS] compile-flags: --target arm64e-apple-visionos +//@ [VISIONOS] needs-llvm-components: aarch64 + +#![crate_type = "lib"] +#![feature(no_core, lang_items)] +#![no_core] + +extern crate minicore; +use minicore::*; + +#[unsafe(no_mangle)] +pub unsafe fn inline_asm() { + unsafe { asm!("nop") }; +} + +// CHECK-LABEL: define{{.*}} @inline_asm( +// CHECK: call void asm sideeffect alignstack "nop" +// CHECK-NOT: [ "ptrauth"(i32 0, i64 0) ] +// CHECK: ret void diff --git a/tests/codegen-llvm/arm64e-apple-ptrauth-invoke.rs b/tests/codegen-llvm/arm64e-apple-ptrauth-invoke.rs index ac724d090383f..bf828c7b9c5fb 100644 --- a/tests/codegen-llvm/arm64e-apple-ptrauth-invoke.rs +++ b/tests/codegen-llvm/arm64e-apple-ptrauth-invoke.rs @@ -1,6 +1,6 @@ // ignore-tidy-linelength // `__rust_try`, which rustc generates for `catch_unwind`, carries the pointer-authentication -// attributes. +// attributes, and its call through the `try_fn` pointer authenticates the callee. //@ add-minicore //@ revisions: DARWIN IOS TVOS VISIONOS //@ [DARWIN] compile-flags: --target arm64e-apple-darwin -Copt-level=0 -Cpanic=unwind -Zverify-llvm-ir @@ -34,4 +34,5 @@ pub unsafe fn invoke_catch() -> i32 { } // CHECK: define internal i32 @__rust_try({{.*}}) unnamed_addr [[TRY_ATTR:#[0-9]+]] personality +// CHECK: invoke void %{{.*}}(ptr %{{.*}}){{.*}} [ "ptrauth"(i32 0, i64 0) ] // CHECK: attributes [[TRY_ATTR]] = {{.*}} "ptrauth-auth-traps" "ptrauth-calls" "ptrauth-indirect-gotos" "ptrauth-returns" diff --git a/tests/codegen-llvm/arm64e-apple-ptrauth-scope.rs b/tests/codegen-llvm/arm64e-apple-ptrauth-scope.rs new file mode 100644 index 0000000000000..88d7abe40d833 --- /dev/null +++ b/tests/codegen-llvm/arm64e-apple-ptrauth-scope.rs @@ -0,0 +1,126 @@ +// ignore-tidy-linelength +// On arm64e Apple targets every function address that becomes a value is signed with key IA and +// discriminator 0, whatever the function's ABI: function pointers, closures, constants, statics, +// `extern_weak` statics and v-table entries. Every indirect call authenticates its callee, and +// direct calls stay direct, even unoptimised. +//@ add-minicore +//@ revisions: DARWIN IOS TVOS VISIONOS +//@ [DARWIN] compile-flags: --target arm64e-apple-darwin -Copt-level=0 -Zverify-llvm-ir +//@ [DARWIN] needs-llvm-components: aarch64 +//@ [IOS] compile-flags: --target arm64e-apple-ios -Copt-level=0 -Zverify-llvm-ir +//@ [IOS] needs-llvm-components: aarch64 +//@ [TVOS] compile-flags: --target arm64e-apple-tvos -Copt-level=0 -Zverify-llvm-ir +//@ [TVOS] needs-llvm-components: aarch64 +//@ [VISIONOS] compile-flags: --target arm64e-apple-visionos -Copt-level=0 -Zverify-llvm-ir +//@ [VISIONOS] needs-llvm-components: aarch64 + +#![crate_type = "lib"] +#![feature(no_core, lang_items, linkage)] +#![no_core] + +extern crate minicore; +use minicore::*; + +pub trait Shape { + fn area(&self) -> u32; +} + +pub struct Square(pub u32); + +impl Shape for Square { + fn area(&self) -> u32 { + self.0 + } +} + +#[inline(never)] +fn rust_target() -> u32 { + 7 +} + +#[inline(never)] +#[unsafe(no_mangle)] +pub extern "C" fn c_target(x: u32) -> u32 { + x +} + +const RUST_TARGET: fn() -> u32 = rust_target; + +// rustc recognises an `extern_weak` function by the `Option` lang item, which minicore lacks. +#[lang = "Option"] +pub enum Weak { + Missing, + Present(T), +} +impl Copy for Weak {} + +#[allow(improper_ctypes)] +extern "C" { + #[linkage = "extern_weak"] + static weak_rust_fn: Weak u32>; +} + +// CHECK-DAG: @RUST_TARGET_STATIC = {{.*}}constant ptr ptrauth (ptr @{{.*}}rust_target{{.*}}, i32 0) +// CHECK-DAG: @_rust_extern_with_linkage_{{.*}}weak_rust_fn = internal {{.*}}global ptr ptrauth (ptr @weak_rust_fn, i32 0) +// CHECK-DAG: constant {{.*}}ptrauth (ptr @{{.*}}Square{{.*}}area{{.*}}, i32 0) +#[unsafe(no_mangle)] +pub static RUST_TARGET_STATIC: fn() -> u32 = rust_target; + +// CHECK-LABEL: @rust_fn_ptr( +// CHECK: ret ptr ptrauth (ptr @{{.*}}rust_target{{.*}}, i32 0) +#[unsafe(no_mangle)] +pub fn rust_fn_ptr() -> fn() -> u32 { + rust_target +} + +// CHECK-LABEL: @closure_fn_ptr( +// CHECK: ret ptr ptrauth (ptr @{{.*}}, i32 0) +#[unsafe(no_mangle)] +pub fn closure_fn_ptr() -> fn() -> u32 { + || 7 +} + +// CHECK-LABEL: @const_fn_ptr( +// CHECK: ret ptr ptrauth (ptr @{{.*}}rust_target{{.*}}, i32 0) +#[unsafe(no_mangle)] +pub fn const_fn_ptr() -> fn() -> u32 { + RUST_TARGET +} + +#[unsafe(no_mangle)] +pub fn weak_fn_ptr() -> Weak u32> { + unsafe { weak_rust_fn } +} + +// CHECK-LABEL: @call_rust_fn_ptr( +// CHECK: call {{.*}}i32 %{{.*}}(){{.*}} [ "ptrauth"(i32 0, i64 0) ] +#[unsafe(no_mangle)] +pub fn call_rust_fn_ptr(f: fn() -> u32) -> u32 { + f() +} + +// CHECK-LABEL: @call_dyn( +// CHECK: call {{.*}}i32 %{{.*}}(ptr {{.*}}%{{.*}}){{.*}} [ "ptrauth"(i32 0, i64 0) ] +#[unsafe(no_mangle)] +pub fn call_dyn(shape: &dyn Shape) -> u32 { + shape.area() +} + +// CHECK-LABEL: @make_dyn( +#[unsafe(no_mangle)] +pub fn make_dyn(square: &Square) -> u32 { + call_dyn(square) +} + +// CHECK-LABEL: @direct_calls( +// CHECK-NOT: ptrauth +// CHECK: call {{.*}}i32 @{{.*}}rust_target{{.*}}() +// CHECK-NOT: ptrauth +// CHECK: call {{.*}}i32 @c_target(i32 {{.*}}%{{.*}}) +// CHECK-NOT: ptrauth +// CHECK: ret i32 +#[unsafe(no_mangle)] +pub fn direct_calls(x: u32) -> u32 { + rust_target(); + c_target(x) +} diff --git a/tests/run-make/arm64e-ptrauth-serialized-output/ptrauth.rs b/tests/run-make/arm64e-ptrauth-serialized-output/ptrauth.rs new file mode 100644 index 0000000000000..f06a241238e4b --- /dev/null +++ b/tests/run-make/arm64e-ptrauth-serialized-output/ptrauth.rs @@ -0,0 +1,31 @@ +#![crate_type = "lib"] +#![feature(lang_items, no_core)] +#![no_core] + +#[lang = "pointee_sized"] +pub trait PointeeSized {} + +#[lang = "meta_sized"] +pub trait MetaSized: PointeeSized {} + +#[lang = "sized"] +pub trait Sized: MetaSized {} + +#[lang = "copy"] +pub trait Copy: Sized {} + +#[inline(never)] +#[no_mangle] +pub extern "C" fn target(x: i32) -> i32 { + x +} + +#[inline(always)] +fn call_indirect(f: extern "C" fn(i32) -> i32, x: i32) -> i32 { + f(x) +} + +#[no_mangle] +pub extern "C" fn devirtualized(x: i32) -> i32 { + call_indirect(target, x) +} diff --git a/tests/run-make/arm64e-ptrauth-serialized-output/rmake.rs b/tests/run-make/arm64e-ptrauth-serialized-output/rmake.rs new file mode 100644 index 0000000000000..dd69ec9e176fa --- /dev/null +++ b/tests/run-make/arm64e-ptrauth-serialized-output/rmake.rs @@ -0,0 +1,54 @@ +//@ needs-llvm-components: aarch64 + +use std::path::{Path, PathBuf}; + +use run_make_support::{llvm_ar, llvm_dis, llvm_objcopy, rfs, rustc}; + +fn assert_no_illegal_direct_ptrauth(path: &str) { + let ir = rfs::read_to_string(path); + for (line_idx, line) in ir.lines().enumerate() { + if line.contains("@target(") && line.contains("\"ptrauth\"") { + panic!( + "{path}:{} contains a direct call with a ptrauth operand bundle:\n{line}", + line_idx + 1 + ); + } + } +} + +fn main() { + rustc() + .input("ptrauth.rs") + .target("arm64e-apple-darwin") + .opt_level("2") + .arg("--emit=llvm-ir=module.ll,llvm-bc=module.bc") + .run(); + assert_no_illegal_direct_ptrauth("module.ll"); + + llvm_dis().input("module.bc").arg("-o").arg("module-bc.ll").run(); + assert_no_illegal_direct_ptrauth("module-bc.ll"); + + rustc() + .input("ptrauth.rs") + .target("arm64e-apple-darwin") + .opt_level("2") + .arg("-Cembed-bitcode=yes") + .crate_type("rlib") + .arg("-o") + .arg("libptrauth.rlib") + .run(); + + if Path::new("extract").exists() { + rfs::recursive_remove("extract"); + } + rfs::create_dir("extract"); + llvm_ar().extract().arg("../libptrauth.rlib").current_dir("extract").run(); + let object = rfs::read_dir("extract") + .map(|entry| entry.unwrap().path()) + .find(|path| path.extension().is_some_and(|ext| ext == "o")) + .expect("embedded-bitcode rlib should contain an object file"); + let embedded_bc = PathBuf::from("extract").join("embedded.bc"); + llvm_objcopy().dump_section("__LLVM,__bitcode", &embedded_bc).arg(&object).run(); + llvm_dis().input(&embedded_bc).arg("-o").arg("embedded.ll").run(); + assert_no_illegal_direct_ptrauth("embedded.ll"); +} From 7fe49fb98060dedbcfd7857a7d4b9f55db700a30 Mon Sep 17 00:00:00 2001 From: cypherair <262752927+cypherair@users.noreply.github.com> Date: Tue, 10 Nov 2026 07:01:03 -0800 Subject: [PATCH 05/23] feat(arm64e): leave Mach-O initializer pointers unsigned, as clang does 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. --- compiler/rustc_codegen_llvm/src/consts.rs | 18 ++++++- compiler/rustc_session/src/session.rs | 2 +- .../arm64e-apple-ptrauth-init-term.rs | 47 +++++++++++++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 tests/codegen-llvm/arm64e-apple-ptrauth-init-term.rs diff --git a/compiler/rustc_codegen_llvm/src/consts.rs b/compiler/rustc_codegen_llvm/src/consts.rs index 66cb6e64ea774..3c59678b4ab74 100644 --- a/compiler/rustc_codegen_llvm/src/consts.rs +++ b/compiler/rustc_codegen_llvm/src/consts.rs @@ -17,7 +17,7 @@ use rustc_middle::ty::layout::{HasTypingEnv, LayoutOf}; use rustc_middle::ty::{self, Instance}; use rustc_session::PointerAuthScope; use rustc_span::{Symbol, bug, span_bug}; -use rustc_target::spec::Arch; +use rustc_target::spec::{Arch, BinaryFormat}; use tracing::{debug, instrument, trace}; use crate::common::CodegenCx; @@ -168,7 +168,11 @@ fn codegen_static_initializer<'ll, 'tcx>( .link_section .map(|link_section| { let s = link_section.as_str(); - if s.starts_with(".init_array") || s.starts_with(".fini_array") { + if s.starts_with(".init_array") + || s.starts_with(".fini_array") + || (cx.sess().target.binary_format == BinaryFormat::MachO + && is_macho_init_term_section(s)) + { IsInitOrFini::Yes } else { IsInitOrFini::No @@ -178,6 +182,16 @@ fn codegen_static_initializer<'ll, 'tcx>( Ok((const_alloc_to_llvm(cx, alloc.inner(), IsStatic::Yes, is_in_init_fini), alloc)) } +/// Whether a Mach-O section specifier, `segment,section[,type[,attributes]]`, names a section of +/// initializer or terminator pointers. +fn is_macho_init_term_section(specifier: &str) -> bool { + let mut parts = specifier.split(',').map(str::trim).skip(1); + let name = parts.next(); + let section_type = parts.next(); + matches!(name, Some("__mod_init_func" | "__mod_term_func")) + || matches!(section_type, Some("mod_init_funcs" | "mod_term_funcs")) +} + fn set_global_alignment<'ll>(cx: &CodegenCx<'ll, '_>, gv: &'ll Value, mut align: Align) { // The target may require greater alignment for globals than the type does. // Note: GCC and Clang also allow `__attribute__((aligned))` on variables, diff --git a/compiler/rustc_session/src/session.rs b/compiler/rustc_session/src/session.rs index a28f5a7e7157b..b3292e481ed14 100644 --- a/compiler/rustc_session/src/session.rs +++ b/compiler/rustc_session/src/session.rs @@ -200,7 +200,7 @@ impl PointerAuthConfig { /// The arm64e ABI of Apple's platforms, as clang's Darwin driver sets it up. C function /// pointers use key IA with no diversity. Rust function pointers and v-table entries use the /// same schema, so that Rust's indirect calls are as protected as those of the C and Swift - /// code in the same process. + /// code in the same process. Initializer and terminator entries are not signed. fn arm64e_apple(target: &Target) -> Self { Self { scope: PointerAuthScope::AllFunctions, diff --git a/tests/codegen-llvm/arm64e-apple-ptrauth-init-term.rs b/tests/codegen-llvm/arm64e-apple-ptrauth-init-term.rs new file mode 100644 index 0000000000000..a2e43e7bd69d6 --- /dev/null +++ b/tests/codegen-llvm/arm64e-apple-ptrauth-init-term.rs @@ -0,0 +1,47 @@ +// ignore-tidy-linelength +// Clang on Darwin does not sign initializer or terminator pointers, so on arm64e Apple targets the +// entries of `__mod_init_func` and `__mod_term_func` stay unsigned, whether the section is named +// or typed. The same function pointer in an ordinary static is signed. +//@ add-minicore +//@ revisions: DARWIN IOS TVOS VISIONOS +//@ [DARWIN] compile-flags: --target arm64e-apple-darwin +//@ [DARWIN] needs-llvm-components: aarch64 +//@ [IOS] compile-flags: --target arm64e-apple-ios +//@ [IOS] needs-llvm-components: aarch64 +//@ [TVOS] compile-flags: --target arm64e-apple-tvos +//@ [TVOS] needs-llvm-components: aarch64 +//@ [VISIONOS] compile-flags: --target arm64e-apple-visionos +//@ [VISIONOS] needs-llvm-components: aarch64 + +#![crate_type = "lib"] +#![feature(no_core)] +#![no_core] + +extern crate minicore; + +extern "C" fn init_fn() {} +extern "C" fn term_fn() {} + +// CHECK-DAG: @{{.*}}CTOR_CRATE = {{.*}}constant ptr @{{.*}}init_fn{{.*}}, section "__DATA,__mod_init_func,mod_init_funcs" +#[used] +#[unsafe(link_section = "__DATA,__mod_init_func,mod_init_funcs")] +static CTOR_CRATE: extern "C" fn() = init_fn; + +// CHECK-DAG: @{{.*}}BY_NAME = {{.*}}constant ptr @{{.*}}init_fn{{.*}}, section "__DATA,__mod_init_func" +#[used] +#[unsafe(link_section = "__DATA,__mod_init_func")] +static BY_NAME: extern "C" fn() = init_fn; + +// CHECK-DAG: @{{.*}}BY_TYPE = {{.*}}constant ptr @{{.*}}init_fn{{.*}}, section "__DATA, __inits , mod_init_funcs" +#[used] +#[unsafe(link_section = "__DATA, __inits , mod_init_funcs")] +static BY_TYPE: extern "C" fn() = init_fn; + +// CHECK-DAG: @{{.*}}TERM = {{.*}}constant ptr @{{.*}}term_fn{{.*}}, section "__DATA,__mod_term_func,mod_term_funcs" +#[used] +#[unsafe(link_section = "__DATA,__mod_term_func,mod_term_funcs")] +static TERM: extern "C" fn() = term_fn; + +// CHECK-DAG: @{{.*}}ORDINARY = {{.*}}constant ptr ptrauth (ptr @{{.*}}init_fn{{.*}}, i32 0) +#[used] +static ORDINARY: extern "C" fn() = init_fn; From cc7aa33c4796d985f704391fab361ff03482d758 Mon Sep 17 00:00:00 2001 From: cypherair <262752927+cypherair@users.noreply.github.com> Date: Tue, 10 Nov 2026 07:04:20 -0800 Subject: [PATCH 06/23] fix(arm64e): reject flags that would change the arm64e pointer-authentication 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. --- compiler/rustc_codegen_ssa/src/diagnostics.rs | 8 ++++ .../rustc_codegen_ssa/src/target_features.rs | 16 +++++++ compiler/rustc_session/src/diagnostics.rs | 14 ++++++ compiler/rustc_session/src/session.rs | 11 ++++- .../src/compiler-flags/branch-protection.md | 6 ++- ...-arm64e-apple-pac-ret.DARWIN_PACRET.stderr | 4 ++ ...e-apple-pac-ret.DARWIN_PAUTHLR_LEAF.stderr | 4 ++ ...ion-arm64e-apple-pac-ret.IOS_PACRET.stderr | 4 ++ ...m64e-apple-pac-ret.IOS_PAUTHLR_LEAF.stderr | 4 ++ ...on-arm64e-apple-pac-ret.TVOS_PACRET.stderr | 4 ++ ...64e-apple-pac-ret.TVOS_PAUTHLR_LEAF.stderr | 4 ++ ...rm64e-apple-pac-ret.VISIONOS_PACRET.stderr | 4 ++ ...apple-pac-ret.VISIONOS_PAUTHLR_LEAF.stderr | 4 ++ .../branch-protection-arm64e-apple-pac-ret.rs | 43 +++++++++++++++++++ ...e-apple-ptrauth-disable.DARWIN_BOTH.stderr | 6 +++ ...e-apple-ptrauth-disable.DARWIN_PACA.stderr | 4 ++ ...e-apple-ptrauth-disable.DARWIN_PACG.stderr | 4 ++ ...-apple-ptrauth-disable.DARWIN_PAUTH.stderr | 4 ++ ...m64e-apple-ptrauth-disable.IOS_BOTH.stderr | 6 +++ ...64e-apple-ptrauth-disable.TVOS_BOTH.stderr | 6 +++ ...apple-ptrauth-disable.VISIONOS_BOTH.stderr | 6 +++ ...et-feature-arm64e-apple-ptrauth-disable.rs | 43 +++++++++++++++++++ ...m64e_apple_fixed_pointer_authentication.rs | 14 ++++++ ..._apple_fixed_pointer_authentication.stderr | 4 ++ 24 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.DARWIN_PACRET.stderr create mode 100644 tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.DARWIN_PAUTHLR_LEAF.stderr create mode 100644 tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.IOS_PACRET.stderr create mode 100644 tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.IOS_PAUTHLR_LEAF.stderr create mode 100644 tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.TVOS_PACRET.stderr create mode 100644 tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.TVOS_PAUTHLR_LEAF.stderr create mode 100644 tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.VISIONOS_PACRET.stderr create mode 100644 tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.VISIONOS_PAUTHLR_LEAF.stderr create mode 100644 tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.rs create mode 100644 tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.DARWIN_BOTH.stderr create mode 100644 tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.DARWIN_PACA.stderr create mode 100644 tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.DARWIN_PACG.stderr create mode 100644 tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.DARWIN_PAUTH.stderr create mode 100644 tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.IOS_BOTH.stderr create mode 100644 tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.TVOS_BOTH.stderr create mode 100644 tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.VISIONOS_BOTH.stderr create mode 100644 tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.rs create mode 100644 tests/ui/pointer_authentication/arm64e_apple_fixed_pointer_authentication.rs create mode 100644 tests/ui/pointer_authentication/arm64e_apple_fixed_pointer_authentication.stderr diff --git a/compiler/rustc_codegen_ssa/src/diagnostics.rs b/compiler/rustc_codegen_ssa/src/diagnostics.rs index 09faa71468bf7..341d80c17e1c0 100644 --- a/compiler/rustc_codegen_ssa/src/diagnostics.rs +++ b/compiler/rustc_codegen_ssa/src/diagnostics.rs @@ -1198,6 +1198,14 @@ pub(crate) struct Aarch64SoftfloatNeon; )] pub(crate) struct X86SoftfloatSse; +#[derive(Diagnostic)] +#[diag( + "`-Ctarget-feature` cannot disable `{$feature}` on arm64e Apple targets, whose ABI requires pointer authentication" +)] +pub(crate) struct Arm64eApplePtrauthDisableForbidden<'a> { + pub feature: &'a str, +} + #[derive(Diagnostic)] #[diag("ignoring feature with missing prefix in `-Ctarget-feature`: `{$feature}`")] #[note("features must begin with a `+` to enable or `-` to disable it")] diff --git a/compiler/rustc_codegen_ssa/src/target_features.rs b/compiler/rustc_codegen_ssa/src/target_features.rs index b630155c144ea..5c7f73817aa7e 100644 --- a/compiler/rustc_codegen_ssa/src/target_features.rs +++ b/compiler/rustc_codegen_ssa/src/target_features.rs @@ -268,6 +268,12 @@ fn parse_rust_feature_list<'a>( } } +/// Whether disabling `base_feature` would turn pointer authentication off on an arm64e Apple +/// target. `pauth` is LLVM's name for the pair that Rust calls `paca` and `pacg`. +fn disables_arm64e_apple_ptrauth(target: &Target, base_feature: &str, enable: bool) -> bool { + !enable && target.is_apple_arm64e() && matches!(base_feature, "paca" | "pacg" | "pauth") +} + /// Utility function for a codegen backend to compute the set of all actually enabled Rust target /// features (which will be stored in `sess.internal_target_features`). /// @@ -321,6 +327,12 @@ pub fn internal_target_features<'a, const N: usize>( sess.dcx().emit_warn(diagnostics::UnknownCTargetFeaturePrefix { feature }); }, |base_feature, new_features, enable| { + if disables_arm64e_apple_ptrauth(&sess.target, base_feature, enable) { + sess.dcx().emit_err(diagnostics::Arm64eApplePtrauthDisableForbidden { + feature: base_feature, + }); + return; + } match features_map.get(base_feature) { None => { // This is definitely not a valid Rust feature name. We do not add it to @@ -487,6 +499,10 @@ pub fn flag_to_backend_features<'a>( // Errors are already emitted in `internal_target_features`; avoid duplicates. }, |base_feature, new_features, enable| { + // Rejected in `internal_target_features`. + if disables_arm64e_apple_ptrauth(&sess.target, base_feature, enable) { + return; + } // Forward unknown features to the backend as that's what we have always done. let new_features = new_features.unwrap_or_else(|| FxHashSet::from_iter(std::iter::once(base_feature))); diff --git a/compiler/rustc_session/src/diagnostics.rs b/compiler/rustc_session/src/diagnostics.rs index 4ec8ca42edbee..71d60c693baef 100644 --- a/compiler/rustc_session/src/diagnostics.rs +++ b/compiler/rustc_session/src/diagnostics.rs @@ -398,6 +398,14 @@ pub(crate) struct PointerAuthenticationNotSupportedForTarget<'a> { pub(crate) target_triple: &'a TargetTuple, } +#[derive(Diagnostic)] +#[diag( + "`-Z pointer-authentication` cannot change pointer authentication for target {$target_triple}, whose ABI fixes it" +)] +pub(crate) struct PointerAuthenticationFixedByTarget<'a> { + pub(crate) target_triple: &'a TargetTuple, +} + #[derive(Diagnostic)] #[diag( "`-Z small-data-threshold` is not supported for target {$target_triple} and will be ignored" @@ -410,6 +418,12 @@ pub(crate) struct SmallDataThresholdNotSupportedForTarget<'a> { #[diag("`-Zbranch-protection` is only supported on aarch64")] pub(crate) struct BranchProtectionRequiresAArch64; +#[derive(Diagnostic)] +#[diag( + "`-Zbranch-protection` cannot use `pac-ret` on arm64e Apple targets, which already sign return addresses" +)] +pub(crate) struct BranchProtectionPacRetOnArm64eApple; + #[derive(Diagnostic)] #[diag("`-Csplit-debuginfo={$debuginfo}` is unstable on this platform")] pub(crate) struct SplitDebugInfoUnstablePlatform { diff --git a/compiler/rustc_session/src/session.rs b/compiler/rustc_session/src/session.rs index b3292e481ed14..e454dc128a5d3 100644 --- a/compiler/rustc_session/src/session.rs +++ b/compiler/rustc_session/src/session.rs @@ -1612,7 +1612,11 @@ fn validate_commandline_args_with_session_available(sess: &Session) { ); } - if sess.target.cfg_abi != CfgAbi::Pauthtest + if sess.target.is_apple_arm64e() && !sess.opts.unstable_opts.pointer_authentication.is_empty() { + sess.dcx().emit_err(diagnostics::PointerAuthenticationFixedByTarget { + target_triple: &sess.opts.target_triple, + }); + } else if sess.target.cfg_abi != CfgAbi::Pauthtest && !sess.opts.unstable_opts.pointer_authentication.is_empty() { sess.dcx().emit_warn(diagnostics::PointerAuthenticationNotSupportedForTarget { @@ -1780,6 +1784,11 @@ fn validate_commandline_args_with_session_available(sess: &Session) { if sess.opts.unstable_opts.branch_protection.is_some() && sess.target.arch != Arch::AArch64 { sess.dcx().emit_err(diagnostics::BranchProtectionRequiresAArch64); } + if sess.target.is_apple_arm64e() + && sess.opts.unstable_opts.branch_protection.is_some_and(|bp| bp.pac_ret.is_some()) + { + sess.dcx().emit_err(diagnostics::BranchProtectionPacRetOnArm64eApple); + } if let Some(dwarf_version) = sess.opts.cg.dwarf_version.or(sess.opts.unstable_opts.dwarf_version) diff --git a/src/doc/unstable-book/src/compiler-flags/branch-protection.md b/src/doc/unstable-book/src/compiler-flags/branch-protection.md index c15567dcac2ee..fc03a4b944a04 100644 --- a/src/doc/unstable-book/src/compiler-flags/branch-protection.md +++ b/src/doc/unstable-book/src/compiler-flags/branch-protection.md @@ -19,6 +19,10 @@ It takes some combination of the following values, separated by a `,`. For example, `-Z branch-protection=bti,pac-ret,leaf` is valid, but `-Z branch-protection=bti,leaf,pac-ret` is not. -Rust's standard library does not ship with BTI or pointer authentication enabled by default. +arm64e Apple targets already sign return addresses as part of their ABI, so they reject `pac-ret`. +They accept `bti` and `gcs`. + +Outside arm64e Apple targets, Rust's standard library does not ship with BTI or pointer +authentication enabled by default. In Cargo projects the standard library can be recompiled with pointer authentication using the nightly [build-std](../../cargo/reference/unstable.html#build-std) feature. diff --git a/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.DARWIN_PACRET.stderr b/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.DARWIN_PACRET.stderr new file mode 100644 index 0000000000000..f1edbbf2be921 --- /dev/null +++ b/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.DARWIN_PACRET.stderr @@ -0,0 +1,4 @@ +error: `-Zbranch-protection` cannot use `pac-ret` on arm64e Apple targets, which already sign return addresses + +error: aborting due to 1 previous error + diff --git a/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.DARWIN_PAUTHLR_LEAF.stderr b/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.DARWIN_PAUTHLR_LEAF.stderr new file mode 100644 index 0000000000000..f1edbbf2be921 --- /dev/null +++ b/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.DARWIN_PAUTHLR_LEAF.stderr @@ -0,0 +1,4 @@ +error: `-Zbranch-protection` cannot use `pac-ret` on arm64e Apple targets, which already sign return addresses + +error: aborting due to 1 previous error + diff --git a/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.IOS_PACRET.stderr b/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.IOS_PACRET.stderr new file mode 100644 index 0000000000000..f1edbbf2be921 --- /dev/null +++ b/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.IOS_PACRET.stderr @@ -0,0 +1,4 @@ +error: `-Zbranch-protection` cannot use `pac-ret` on arm64e Apple targets, which already sign return addresses + +error: aborting due to 1 previous error + diff --git a/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.IOS_PAUTHLR_LEAF.stderr b/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.IOS_PAUTHLR_LEAF.stderr new file mode 100644 index 0000000000000..f1edbbf2be921 --- /dev/null +++ b/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.IOS_PAUTHLR_LEAF.stderr @@ -0,0 +1,4 @@ +error: `-Zbranch-protection` cannot use `pac-ret` on arm64e Apple targets, which already sign return addresses + +error: aborting due to 1 previous error + diff --git a/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.TVOS_PACRET.stderr b/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.TVOS_PACRET.stderr new file mode 100644 index 0000000000000..f1edbbf2be921 --- /dev/null +++ b/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.TVOS_PACRET.stderr @@ -0,0 +1,4 @@ +error: `-Zbranch-protection` cannot use `pac-ret` on arm64e Apple targets, which already sign return addresses + +error: aborting due to 1 previous error + diff --git a/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.TVOS_PAUTHLR_LEAF.stderr b/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.TVOS_PAUTHLR_LEAF.stderr new file mode 100644 index 0000000000000..f1edbbf2be921 --- /dev/null +++ b/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.TVOS_PAUTHLR_LEAF.stderr @@ -0,0 +1,4 @@ +error: `-Zbranch-protection` cannot use `pac-ret` on arm64e Apple targets, which already sign return addresses + +error: aborting due to 1 previous error + diff --git a/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.VISIONOS_PACRET.stderr b/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.VISIONOS_PACRET.stderr new file mode 100644 index 0000000000000..f1edbbf2be921 --- /dev/null +++ b/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.VISIONOS_PACRET.stderr @@ -0,0 +1,4 @@ +error: `-Zbranch-protection` cannot use `pac-ret` on arm64e Apple targets, which already sign return addresses + +error: aborting due to 1 previous error + diff --git a/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.VISIONOS_PAUTHLR_LEAF.stderr b/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.VISIONOS_PAUTHLR_LEAF.stderr new file mode 100644 index 0000000000000..f1edbbf2be921 --- /dev/null +++ b/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.VISIONOS_PAUTHLR_LEAF.stderr @@ -0,0 +1,4 @@ +error: `-Zbranch-protection` cannot use `pac-ret` on arm64e Apple targets, which already sign return addresses + +error: aborting due to 1 previous error + diff --git a/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.rs b/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.rs new file mode 100644 index 0000000000000..c91e13fd7f650 --- /dev/null +++ b/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.rs @@ -0,0 +1,43 @@ +// ignore-tidy-linelength +// arm64e Apple targets sign return addresses through their ABI, so they reject `pac-ret`. +//@ ignore-backends: gcc +//@ revisions: DARWIN_PACRET DARWIN_PAUTHLR_LEAF IOS_PACRET IOS_PAUTHLR_LEAF TVOS_PACRET TVOS_PAUTHLR_LEAF VISIONOS_PACRET VISIONOS_PAUTHLR_LEAF +//@ [DARWIN_PACRET] compile-flags: --target=arm64e-apple-darwin -Zbranch-protection=pac-ret +//@ [DARWIN_PACRET] check-fail +//@ [DARWIN_PACRET] needs-llvm-components: aarch64 +//@ [DARWIN_PAUTHLR_LEAF] compile-flags: --target=arm64e-apple-darwin -Zbranch-protection=pac-ret,pc,leaf +//@ [DARWIN_PAUTHLR_LEAF] check-fail +//@ [DARWIN_PAUTHLR_LEAF] needs-llvm-components: aarch64 +//@ [IOS_PACRET] compile-flags: --target=arm64e-apple-ios -Zbranch-protection=pac-ret +//@ [IOS_PACRET] check-fail +//@ [IOS_PACRET] needs-llvm-components: aarch64 +//@ [IOS_PAUTHLR_LEAF] compile-flags: --target=arm64e-apple-ios -Zbranch-protection=pac-ret,pc,leaf +//@ [IOS_PAUTHLR_LEAF] check-fail +//@ [IOS_PAUTHLR_LEAF] needs-llvm-components: aarch64 +//@ [TVOS_PACRET] compile-flags: --target=arm64e-apple-tvos -Zbranch-protection=pac-ret +//@ [TVOS_PACRET] check-fail +//@ [TVOS_PACRET] needs-llvm-components: aarch64 +//@ [TVOS_PAUTHLR_LEAF] compile-flags: --target=arm64e-apple-tvos -Zbranch-protection=pac-ret,pc,leaf +//@ [TVOS_PAUTHLR_LEAF] check-fail +//@ [TVOS_PAUTHLR_LEAF] needs-llvm-components: aarch64 +//@ [VISIONOS_PACRET] compile-flags: --target=arm64e-apple-visionos -Zbranch-protection=pac-ret +//@ [VISIONOS_PACRET] check-fail +//@ [VISIONOS_PACRET] needs-llvm-components: aarch64 +//@ [VISIONOS_PAUTHLR_LEAF] compile-flags: --target=arm64e-apple-visionos -Zbranch-protection=pac-ret,pc,leaf +//@ [VISIONOS_PAUTHLR_LEAF] check-fail +//@ [VISIONOS_PAUTHLR_LEAF] needs-llvm-components: aarch64 + +#![crate_type = "lib"] +#![feature(no_core, lang_items)] +#![no_core] + +#[lang = "pointee_sized"] +pub trait PointeeSized {} + +#[lang = "meta_sized"] +pub trait MetaSized: PointeeSized {} + +#[lang = "sized"] +pub trait Sized: MetaSized {} + +//~? ERROR `-Zbranch-protection` cannot use `pac-ret` on arm64e Apple targets, which already sign return addresses diff --git a/tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.DARWIN_BOTH.stderr b/tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.DARWIN_BOTH.stderr new file mode 100644 index 0000000000000..8a622a357df98 --- /dev/null +++ b/tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.DARWIN_BOTH.stderr @@ -0,0 +1,6 @@ +error: `-Ctarget-feature` cannot disable `paca` on arm64e Apple targets, whose ABI requires pointer authentication + +error: `-Ctarget-feature` cannot disable `pacg` on arm64e Apple targets, whose ABI requires pointer authentication + +error: aborting due to 2 previous errors + diff --git a/tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.DARWIN_PACA.stderr b/tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.DARWIN_PACA.stderr new file mode 100644 index 0000000000000..569d2f28027b5 --- /dev/null +++ b/tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.DARWIN_PACA.stderr @@ -0,0 +1,4 @@ +error: `-Ctarget-feature` cannot disable `paca` on arm64e Apple targets, whose ABI requires pointer authentication + +error: aborting due to 1 previous error + diff --git a/tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.DARWIN_PACG.stderr b/tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.DARWIN_PACG.stderr new file mode 100644 index 0000000000000..c32981e61baca --- /dev/null +++ b/tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.DARWIN_PACG.stderr @@ -0,0 +1,4 @@ +error: `-Ctarget-feature` cannot disable `pacg` on arm64e Apple targets, whose ABI requires pointer authentication + +error: aborting due to 1 previous error + diff --git a/tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.DARWIN_PAUTH.stderr b/tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.DARWIN_PAUTH.stderr new file mode 100644 index 0000000000000..fccd848ab06d4 --- /dev/null +++ b/tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.DARWIN_PAUTH.stderr @@ -0,0 +1,4 @@ +error: `-Ctarget-feature` cannot disable `pauth` on arm64e Apple targets, whose ABI requires pointer authentication + +error: aborting due to 1 previous error + diff --git a/tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.IOS_BOTH.stderr b/tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.IOS_BOTH.stderr new file mode 100644 index 0000000000000..8a622a357df98 --- /dev/null +++ b/tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.IOS_BOTH.stderr @@ -0,0 +1,6 @@ +error: `-Ctarget-feature` cannot disable `paca` on arm64e Apple targets, whose ABI requires pointer authentication + +error: `-Ctarget-feature` cannot disable `pacg` on arm64e Apple targets, whose ABI requires pointer authentication + +error: aborting due to 2 previous errors + diff --git a/tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.TVOS_BOTH.stderr b/tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.TVOS_BOTH.stderr new file mode 100644 index 0000000000000..8a622a357df98 --- /dev/null +++ b/tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.TVOS_BOTH.stderr @@ -0,0 +1,6 @@ +error: `-Ctarget-feature` cannot disable `paca` on arm64e Apple targets, whose ABI requires pointer authentication + +error: `-Ctarget-feature` cannot disable `pacg` on arm64e Apple targets, whose ABI requires pointer authentication + +error: aborting due to 2 previous errors + diff --git a/tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.VISIONOS_BOTH.stderr b/tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.VISIONOS_BOTH.stderr new file mode 100644 index 0000000000000..8a622a357df98 --- /dev/null +++ b/tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.VISIONOS_BOTH.stderr @@ -0,0 +1,6 @@ +error: `-Ctarget-feature` cannot disable `paca` on arm64e Apple targets, whose ABI requires pointer authentication + +error: `-Ctarget-feature` cannot disable `pacg` on arm64e Apple targets, whose ABI requires pointer authentication + +error: aborting due to 2 previous errors + diff --git a/tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.rs b/tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.rs new file mode 100644 index 0000000000000..3bef9f9ed0186 --- /dev/null +++ b/tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.rs @@ -0,0 +1,43 @@ +// ignore-tidy-linelength +// `-Ctarget-feature` cannot turn pointer authentication off on arm64e Apple targets, under +// Rust's feature names or LLVM's. +//@ ignore-backends: gcc +//@ revisions: DARWIN_PACA DARWIN_PACG DARWIN_PAUTH DARWIN_BOTH IOS_BOTH TVOS_BOTH VISIONOS_BOTH +//@ [DARWIN_PACA] compile-flags: --target=arm64e-apple-darwin -Ctarget-feature=-paca +//@ [DARWIN_PACA] check-fail +//@ [DARWIN_PACA] needs-llvm-components: aarch64 +//@ [DARWIN_PACG] compile-flags: --target=arm64e-apple-darwin -Ctarget-feature=-pacg +//@ [DARWIN_PACG] check-fail +//@ [DARWIN_PACG] needs-llvm-components: aarch64 +//@ [DARWIN_PAUTH] compile-flags: --target=arm64e-apple-darwin -Ctarget-feature=-pauth +//@ [DARWIN_PAUTH] check-fail +//@ [DARWIN_PAUTH] needs-llvm-components: aarch64 +//@ [DARWIN_BOTH] compile-flags: --target=arm64e-apple-darwin -Ctarget-feature=-paca,-pacg +//@ [DARWIN_BOTH] check-fail +//@ [DARWIN_BOTH] needs-llvm-components: aarch64 +//@ [IOS_BOTH] compile-flags: --target=arm64e-apple-ios -Ctarget-feature=-paca,-pacg +//@ [IOS_BOTH] check-fail +//@ [IOS_BOTH] needs-llvm-components: aarch64 +//@ [TVOS_BOTH] compile-flags: --target=arm64e-apple-tvos -Ctarget-feature=-paca,-pacg +//@ [TVOS_BOTH] check-fail +//@ [TVOS_BOTH] needs-llvm-components: aarch64 +//@ [VISIONOS_BOTH] compile-flags: --target=arm64e-apple-visionos -Ctarget-feature=-paca,-pacg +//@ [VISIONOS_BOTH] check-fail +//@ [VISIONOS_BOTH] needs-llvm-components: aarch64 + +#![crate_type = "lib"] +#![feature(no_core, lang_items)] +#![no_core] + +#[lang = "pointee_sized"] +pub trait PointeeSized {} + +#[lang = "meta_sized"] +pub trait MetaSized: PointeeSized {} + +#[lang = "sized"] +pub trait Sized: MetaSized {} + +//[DARWIN_PACA,DARWIN_BOTH,IOS_BOTH,TVOS_BOTH,VISIONOS_BOTH]~? ERROR `-Ctarget-feature` cannot disable `paca` on arm64e Apple targets, whose ABI requires pointer authentication +//[DARWIN_PACG,DARWIN_BOTH,IOS_BOTH,TVOS_BOTH,VISIONOS_BOTH]~? ERROR `-Ctarget-feature` cannot disable `pacg` on arm64e Apple targets, whose ABI requires pointer authentication +//[DARWIN_PAUTH]~? ERROR `-Ctarget-feature` cannot disable `pauth` on arm64e Apple targets, whose ABI requires pointer authentication diff --git a/tests/ui/pointer_authentication/arm64e_apple_fixed_pointer_authentication.rs b/tests/ui/pointer_authentication/arm64e_apple_fixed_pointer_authentication.rs new file mode 100644 index 0000000000000..04b6b4439e768 --- /dev/null +++ b/tests/ui/pointer_authentication/arm64e_apple_fixed_pointer_authentication.rs @@ -0,0 +1,14 @@ +// ignore-tidy-linelength +// The arm64e ABI fixes pointer authentication on Apple targets, so `-Z pointer-authentication` is +// an error there rather than an option that is quietly ignored. +//@ ignore-backends: gcc +//@ check-fail +//@ needs-llvm-components: aarch64 +//@ compile-flags: -Zpointer-authentication=-calls --crate-type=lib --target arm64e-apple-darwin + +#![feature(no_core)] +#![no_std] +#![no_main] +#![no_core] + +//~? ERROR `-Z pointer-authentication` cannot change pointer authentication for target arm64e-apple-darwin, whose ABI fixes it diff --git a/tests/ui/pointer_authentication/arm64e_apple_fixed_pointer_authentication.stderr b/tests/ui/pointer_authentication/arm64e_apple_fixed_pointer_authentication.stderr new file mode 100644 index 0000000000000..4635fd039cced --- /dev/null +++ b/tests/ui/pointer_authentication/arm64e_apple_fixed_pointer_authentication.stderr @@ -0,0 +1,4 @@ +error: `-Z pointer-authentication` cannot change pointer authentication for target arm64e-apple-darwin, whose ABI fixes it + +error: aborting due to 1 previous error + From 79c51fa689dcd43043a7b845590919ea7b9fdeea Mon Sep 17 00:00:00 2001 From: cypherair <262752927+cypherair@users.noreply.github.com> Date: Tue, 10 Nov 2026 07:08:02 -0800 Subject: [PATCH 07/23] ci(arm64e): run the fork's validation on pull requests again The validation workflow last ran its tests on 10 August. Scheduled runs then failed at checkout every week, on a branch name the 206d116e0f 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. --- .github/workflows/fork-arm64e.yml | 516 ++++++++++++++++++++++++++++++ 1 file changed, 516 insertions(+) create mode 100644 .github/workflows/fork-arm64e.yml diff --git a/.github/workflows/fork-arm64e.yml b/.github/workflows/fork-arm64e.yml new file mode 100644 index 0000000000000..426e2aadcd5f4 --- /dev/null +++ b/.github/workflows/fork-arm64e.yml @@ -0,0 +1,516 @@ +name: Fork arm64e validation + +# Validates the fork's arm64e pointer-authentication patch: its own tests, upstream's pauthtest +# tests, tidy, and an arm64e std build. Fork only; not part of upstream. + +on: + workflow_dispatch: + pull_request: + push: + branches: + - rust-stable-with-arm64e + schedule: + - cron: "43 11 * * 2" + +permissions: + contents: read + +concurrency: + group: fork-arm64e-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + arm64e: + if: github.repository == 'cypherair/rust' + strategy: + fail-fast: false + matrix: + runner: [macos-15, macos-26] + name: arm64e validation (${{ matrix.runner }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: 240 + defaults: + run: + shell: bash + steps: + - name: Checkout source + uses: actions/checkout@v5 + with: + fetch-depth: 2 + submodules: recursive + + # In CI, bootstrap takes HEAD^1 as the upstream commit and downloads its prebuilt LLVM, as on + # a pull request to rust-lang/rust. Give every run that shape: a merge commit whose first + # parent is the newest upstream (bors) commit in the history, and whose tree is the tip's. + - name: Put the upstream base at HEAD^1 + run: | + set -euo pipefail + email="$(sed -n 's/^git_merge_commit_email=//p' src/stage0)" + tip="$(git rev-parse HEAD)" + base="" + for depth in 2 64 512; do + if [ "$depth" -gt 2 ]; then + git fetch --no-tags --depth="$depth" origin "$tip" + fi + base="$(git log --first-parent --author="$email" -n1 --format=%H "$tip")" + if [ -n "$base" ]; then + break + fi + done + if [ -z "$base" ]; then + echo "no upstream commit within 512 commits of $tip" >&2 + exit 1 + fi + if [ "$(git rev-parse -q --verify HEAD^1 || true)" != "$base" ]; then + merge="$(git -c user.name='fork CI' -c user.email=fork-ci@invalid \ + commit-tree "$tip^{tree}" -p "$base" -p "$tip" -m "Validate $tip on $base")" + git checkout -q --detach "$merge" + fi + git log --oneline -n2 --first-parent HEAD + + - name: Show environment + run: | + mkdir -p /tmp/arm64e-diagnostics + { + echo "github.event_name=${GITHUB_EVENT_NAME}" + echo "runner=${{ matrix.runner }}" + sw_vers + uname -a + sysctl -n kern.osproductversion kern.osversion + sysctl -n machdep.cpu.brand_string 2>/dev/null || true + git show -s --format='HEAD: %H %P %ae %s' HEAD + python3 --version + clang --version + rustc -vV || true + } | tee /tmp/arm64e-diagnostics/runner-fingerprint.txt + + - name: Check arm64e codegen crates + id: check_codegen + run: | + python3 x.py check compiler/rustc_codegen_ssa compiler/rustc_codegen_llvm --stage 1 + + - name: Run arm64e and pauthtest tests + id: targeted_tests + run: | + python3 x.py test --stage 1 --force-rerun \ + tests/codegen-llvm/arm64e-apple-ptrauth*.rs \ + tests/assembly-llvm/arm64e-apple-ptrauth-calls.rs \ + tests/assembly-llvm/targets/targets-macho.rs \ + tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.rs \ + tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.rs \ + tests/run-make/arm64e-ptrauth-allocator-shim \ + tests/run-make/arm64e-ptrauth-serialized-output \ + tests/codegen-llvm/pauth \ + tests/assembly-llvm/pauth-basic.rs \ + tests/assembly-llvm/targets/targets-elf.rs \ + tests/ui/pointer_authentication \ + tests/ui/statics/crt-static-pauthtest.rs \ + tests/ui/target_modifiers/incompatible_pauth.rs + + - name: Run tidy + id: tidy + run: | + python3 x.py test tidy + + - name: Build stage1 arm64e std toolchain + id: build_stage1 + run: | + python3 x.py build compiler/rustc library/std --stage 1 --target arm64e-apple-darwin + + - name: Prepare arm64e smoke helpers + run: | + mkdir -p /tmp/arm64e-diagnostics + cat > /tmp/arm64e_collect_smoke.py <<'PY' + import argparse + import json + import pathlib + import re + import subprocess + import sys + + + def run_capture(cmd, timeout=None): + proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout) + return { + "cmd": cmd, + "returncode": proc.returncode, + "signal": -proc.returncode if proc.returncode < 0 else None, + "stdout": proc.stdout, + "stderr": proc.stderr, + } + + + def load_text(path): + try: + return pathlib.Path(path).read_text() + except FileNotFoundError: + return None + + + parser = argparse.ArgumentParser() + parser.add_argument("--name", required=True) + parser.add_argument("--rustc", required=True) + parser.add_argument("--source", required=True) + parser.add_argument("--output-base", required=True) + parser.add_argument("--json-out", required=True) + parser.add_argument("--target", default="arm64e-apple-darwin") + parser.add_argument("--emit", default="link") + parser.add_argument("--run-binary", action="store_true") + parser.add_argument("--expected-stdout") + parser.add_argument("--expected-stdout-regex") + parser.add_argument("--asm-pattern") + args = parser.parse_args() + + output_base = pathlib.Path(args.output_base) + compile_cmd = [ + args.rustc, + args.source, + "--target", + args.target, + "-O", + "--emit", + args.emit, + "-o", + str(output_base), + ] + payload = { + "name": args.name, + "target": args.target, + "emit": args.emit, + "compile": run_capture(compile_cmd), + "output_base": str(output_base), + "paths": { + "source": args.source, + "binary": str(output_base), + "asm": str(output_base.with_suffix(".s")), + "ir": str(output_base.with_suffix(".ll")), + }, + } + + overall_ok = payload["compile"]["returncode"] == 0 + + if overall_ok: + payload["file"] = run_capture(["/usr/bin/file", str(output_base)]) + else: + payload["file"] = None + + asm_path = output_base.with_suffix(".s") + asm_text = load_text(asm_path) + if asm_text is not None and args.asm_pattern: + payload["asm_matches"] = re.findall(args.asm_pattern, asm_text) + else: + payload["asm_matches"] = [] + + if args.run_binary and overall_ok: + payload["run"] = run_capture([str(output_base)], timeout=30) + if payload["run"]["returncode"] != 0: + overall_ok = False + stdout = payload["run"]["stdout"] + if args.expected_stdout is not None: + if stdout.rstrip("\n") != args.expected_stdout.rstrip("\n"): + payload["stdout_mismatch"] = { + "expected": args.expected_stdout, + "actual": stdout, + } + overall_ok = False + if args.expected_stdout_regex is not None: + if re.fullmatch(args.expected_stdout_regex, stdout.strip()) is None: + payload["stdout_regex_mismatch"] = { + "expected_regex": args.expected_stdout_regex, + "actual": stdout, + } + overall_ok = False + else: + payload["run"] = None + + json_path = pathlib.Path(args.json_out) + json_path.write_text(json.dumps(payload, indent=2, sort_keys=True)) + + print(f"== {args.name} compile ==") + print(f"returncode={payload['compile']['returncode']}") + if payload["compile"]["stdout"]: + print("-- stdout --") + print(payload["compile"]["stdout"], end="") + if payload["compile"]["stderr"]: + print("-- stderr --") + print(payload["compile"]["stderr"], end="") + if payload["file"] is not None: + print("== file ==") + print(payload["file"]["stdout"], end="") + if payload["run"] is not None: + print("== run ==") + print(f"returncode={payload['run']['returncode']}") + if payload["run"]["signal"] is not None: + print(f"signal={payload['run']['signal']}") + print("-- stdout --") + print(payload["run"]["stdout"], end="") + print("-- stderr --") + print(payload["run"]["stderr"], end="") + if payload["asm_matches"]: + print("== asm matches ==") + for match in payload["asm_matches"]: + print(match) + + sys.exit(0 if overall_ok else 1) + PY + + - name: Diagnose arm64e hello runtime + id: hello_diag + continue-on-error: true + run: | + HOST_TRIPLE="$(rustc -vV | sed -n 's/^host: //p')" + STAGE1="build/${HOST_TRIPLE}/stage1/bin/rustc" + + cat > /tmp/arm64e-diagnostics/arm64e_hello.rs <<'EOF' + fn main() { + println!("hello-stage1"); + } + EOF + + python3 /tmp/arm64e_collect_smoke.py \ + --name hello \ + --rustc "$STAGE1" \ + --source /tmp/arm64e-diagnostics/arm64e_hello.rs \ + --output-base /tmp/arm64e-diagnostics/arm64e_hello \ + --json-out /tmp/arm64e-diagnostics/arm64e_hello.json \ + --emit link \ + --run-binary \ + --expected-stdout 'hello-stage1' + + - name: Diagnose arm64e thread id runtime + id: thread_id_diag + continue-on-error: true + run: | + HOST_TRIPLE="$(rustc -vV | sed -n 's/^host: //p')" + STAGE1="build/${HOST_TRIPLE}/stage1/bin/rustc" + + cat > /tmp/arm64e-diagnostics/arm64e_thread_id.rs <<'EOF' + fn main() { + println!("{:?}", std::thread::current().id()); + } + EOF + + python3 /tmp/arm64e_collect_smoke.py \ + --name thread_id \ + --rustc "$STAGE1" \ + --source /tmp/arm64e-diagnostics/arm64e_thread_id.rs \ + --output-base /tmp/arm64e-diagnostics/arm64e_thread_id \ + --json-out /tmp/arm64e-diagnostics/arm64e_thread_id.json \ + --emit link \ + --run-binary \ + --expected-stdout-regex '^ThreadId\([0-9]+\)$' + + - name: Diagnose arm64e function pointer runtime + id: fnptr_diag + continue-on-error: true + run: | + HOST_TRIPLE="$(rustc -vV | sed -n 's/^host: //p')" + STAGE1="build/${HOST_TRIPLE}/stage1/bin/rustc" + + cat > /tmp/arm64e-diagnostics/arm64e_fnptr.rs <<'EOF' + #[inline(never)] + pub extern "C" fn target(x: i32) -> i32 { x + 1 } + + fn main() { + let f: extern "C" fn(i32) -> i32 = target; + println!("{}", f(41)); + } + EOF + + python3 /tmp/arm64e_collect_smoke.py \ + --name fnptr \ + --rustc "$STAGE1" \ + --source /tmp/arm64e-diagnostics/arm64e_fnptr.rs \ + --output-base /tmp/arm64e-diagnostics/arm64e_fnptr \ + --json-out /tmp/arm64e-diagnostics/arm64e_fnptr.json \ + --emit llvm-ir,asm,link \ + --run-binary \ + --expected-stdout '42' \ + --asm-pattern 'braaz|blraaz|blr' + + echo "LLVM IR excerpt:" + sed -n '1,40p' /tmp/arm64e-diagnostics/arm64e_fnptr.ll || true + echo + echo "Assembly excerpt:" + sed -n '1,40p' /tmp/arm64e-diagnostics/arm64e_fnptr.s || true + + - name: Diagnose arm64e TLS runtime + id: tls_diag + continue-on-error: true + run: | + HOST_TRIPLE="$(rustc -vV | sed -n 's/^host: //p')" + STAGE1="build/${HOST_TRIPLE}/stage1/bin/rustc" + + cat > /tmp/arm64e-diagnostics/arm64e_tls_smoke.rs <<'EOF' + use std::cell::RefCell; + + fn main() { + thread_local! { + static S: RefCell = RefCell::default(); + } + + S.with(|x| *x.borrow_mut() = "pika pika".to_string()); + S.with(|x| println!("{}", x.borrow())); + } + EOF + + python3 /tmp/arm64e_collect_smoke.py \ + --name tls \ + --rustc "$STAGE1" \ + --source /tmp/arm64e-diagnostics/arm64e_tls_smoke.rs \ + --output-base /tmp/arm64e-diagnostics/arm64e_tls_smoke \ + --json-out /tmp/arm64e-diagnostics/arm64e_tls_smoke.json \ + --emit llvm-ir,asm,link \ + --run-binary \ + --expected-stdout 'pika pika' \ + --asm-pattern 'braaz|blraaz|blr' + + echo "LLVM IR excerpt:" + sed -n '1,40p' /tmp/arm64e-diagnostics/arm64e_tls_smoke.ll || true + echo + echo "Assembly excerpt:" + sed -n '1,60p' /tmp/arm64e-diagnostics/arm64e_tls_smoke.s || true + + - name: Diagnose arm64e pointer-authentication scope runtime + id: scope_diag + continue-on-error: true + run: | + HOST_TRIPLE="$(rustc -vV | sed -n 's/^host: //p')" + STAGE1="build/${HOST_TRIPLE}/stage1/bin/rustc" + + cat > /tmp/arm64e-diagnostics/arm64e_scope.rs <<'EOF' + use std::hint::black_box; + use std::sync::atomic::{AtomicBool, Ordering}; + + static CONSTRUCTOR_RAN: AtomicBool = AtomicBool::new(false); + + extern "C" fn constructor() { + CONSTRUCTOR_RAN.store(true, Ordering::SeqCst); + } + + #[used] + #[unsafe(link_section = "__DATA,__mod_init_func,mod_init_funcs")] + static CONSTRUCTOR: extern "C" fn() = constructor; + + trait Shape { + fn area(&self) -> u32; + } + + struct Square(u32); + + impl Shape for Square { + fn area(&self) -> u32 { + self.0 * self.0 + } + } + + fn twice(f: fn(u32) -> u32, x: u32) -> u32 { + f(f(x)) + } + + fn main() { + let shape = black_box(Box::new(Square(3)) as Box); + let add_one = black_box((|x| x + 1) as fn(u32) -> u32); + let signed = (add_one as usize) >> 47 != 0; + let caught = std::panic::catch_unwind(|| panic!("caught on purpose")).is_err(); + let area = std::thread::spawn(move || shape.area()).join().unwrap(); + println!( + "constructor={} area={} twice={} caught={} signed={}", + CONSTRUCTOR_RAN.load(Ordering::SeqCst), + area, + twice(add_one, 40), + caught, + signed, + ); + } + EOF + + python3 /tmp/arm64e_collect_smoke.py \ + --name scope \ + --rustc "$STAGE1" \ + --source /tmp/arm64e-diagnostics/arm64e_scope.rs \ + --output-base /tmp/arm64e-diagnostics/arm64e_scope \ + --json-out /tmp/arm64e-diagnostics/arm64e_scope.json \ + --emit llvm-ir,asm,link \ + --run-binary \ + --expected-stdout-regex '^constructor=true area=9 twice=42 caught=true signed=(true|false)$' \ + --asm-pattern 'braaz|blraaz' + + - name: Upload arm64e diagnostics + if: always() + uses: actions/upload-artifact@v4 + with: + name: arm64e-diagnostics-${{ matrix.runner }} + if-no-files-found: ignore + path: /tmp/arm64e-diagnostics + + - name: Summarize arm64e validation + if: always() + run: | + python3 - <<'PY' >> "$GITHUB_STEP_SUMMARY" + import json + import pathlib + + root = pathlib.Path("/tmp/arm64e-diagnostics") + cases = [ + ("hello", "arm64e_hello.json"), + ("thread_id", "arm64e_thread_id.json"), + ("fnptr", "arm64e_fnptr.json"), + ("tls", "arm64e_tls_smoke.json"), + ("scope", "arm64e_scope.json"), + ] + + def sanitize(text): + if text is None: + return "" + text = text.strip() + if not text: + return "" + return text.replace("|", "\\|").replace("\n", "
") + + print("## arm64e validation (${{ matrix.runner }})") + print() + print("- check_codegen: ${{ steps.check_codegen.outcome }}") + print("- targeted_tests: ${{ steps.targeted_tests.outcome }}") + print("- tidy: ${{ steps.tidy.outcome }}") + print("- build_stage1: ${{ steps.build_stage1.outcome }}") + print("- hello_diag (non-blocking): ${{ steps.hello_diag.outcome }}") + print("- thread_id_diag (non-blocking): ${{ steps.thread_id_diag.outcome }}") + print("- fnptr_diag (non-blocking): ${{ steps.fnptr_diag.outcome }}") + print("- tls_diag (non-blocking): ${{ steps.tls_diag.outcome }}") + print("- scope_diag (non-blocking): ${{ steps.scope_diag.outcome }}") + print() + print("| case | compile | run | rc/signal | file | stdout | asm hits |") + print("| --- | --- | --- | --- | --- | --- | --- |") + for case_name, json_name in cases: + path = root / json_name + if not path.exists(): + print(f"| {case_name} | missing | missing | | | | |") + continue + data = json.loads(path.read_text()) + compile_status = "ok" if data["compile"]["returncode"] == 0 else "fail" + run = data.get("run") + if run is None: + run_status = "not-run" + rc_signal = "" + stdout = "" + else: + run_status = "ok" if run["returncode"] == 0 else "fail" + rc_signal = f"rc={run['returncode']}" + if run.get("signal") is not None: + rc_signal += f" sig={run['signal']}" + stdout = sanitize(run.get("stdout")) + file_output = sanitize((data.get("file") or {}).get("stdout")) + asm_hits = sanitize(",".join(data.get("asm_matches") or [])) + print( + f"| {case_name} | {compile_status} | {run_status} | {sanitize(rc_signal)} | " + f"{file_output} | {stdout} | {asm_hits} |" + ) + print() + print("The host-binary smoke steps are non-blocking: a runner may not run arm64e binaries, or may run them with pointer authentication off. The tests, tidy and the stage1 build are blocking.") + PY + + { + echo "Smoke diagnostic JSON files:" + ls -1 /tmp/arm64e-diagnostics/*.json + } From c580abdfef9f93ed3807c862107509c204382389 Mon Sep 17 00:00:00 2001 From: cypherair <262752927+cypherair@users.noreply.github.com> Date: Tue, 10 Nov 2026 07:28:10 -0800 Subject: [PATCH 08/23] test(arm64e): check that disabling v8.3a keeps pointer authentication 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. --- tests/assembly-llvm/arm64e-apple-ptrauth-calls.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/assembly-llvm/arm64e-apple-ptrauth-calls.rs b/tests/assembly-llvm/arm64e-apple-ptrauth-calls.rs index 8a0e1a06c0d2b..d47bd83012cfb 100644 --- a/tests/assembly-llvm/arm64e-apple-ptrauth-calls.rs +++ b/tests/assembly-llvm/arm64e-apple-ptrauth-calls.rs @@ -1,8 +1,13 @@ // Indirect calls authenticate with `blraaz` or `braaz`; direct calls do not. +// Disabling `v8.3a` does not disable `paca` and `pacg`, which it implies: the backend keeps `pauth` +// and still emits these instructions. //@ add-minicore //@ assembly-output: emit-asm -//@ compile-flags: --target arm64e-apple-darwin -Copt-level=2 -//@ needs-llvm-components: aarch64 +//@ revisions: DEFAULT NO_V83A +//@ [DEFAULT] compile-flags: --target arm64e-apple-darwin -Copt-level=2 +//@ [DEFAULT] needs-llvm-components: aarch64 +//@ [NO_V83A] compile-flags: --target arm64e-apple-darwin -Copt-level=2 -Ctarget-feature=-v8.3a +//@ [NO_V83A] needs-llvm-components: aarch64 #![crate_type = "lib"] #![feature(no_core, lang_items)] From 1022ec983b68c8bc76ea97eb3ecd19b3dbb1e2e2 Mon Sep 17 00:00:00 2001 From: cypherair <262752927+cypherair@users.noreply.github.com> Date: Tue, 10 Nov 2026 07:47:23 -0800 Subject: [PATCH 09/23] ci(arm64e): run the validation on the xcode-27 image 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. --- .github/workflows/fork-arm64e.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/fork-arm64e.yml b/.github/workflows/fork-arm64e.yml index 426e2aadcd5f4..ca145b4d332c9 100644 --- a/.github/workflows/fork-arm64e.yml +++ b/.github/workflows/fork-arm64e.yml @@ -22,13 +22,18 @@ concurrency: jobs: arm64e: if: github.repository == 'cypherair/rust' + # The standard macOS arm64 images (`macos-15`, `macos-26`) are short of capacity: their jobs + # can wait and then be dropped without a runner. `xcode-27`, the macOS 27 image in public + # preview, gets runners. Its label may change when it becomes generally available. strategy: fail-fast: false matrix: - runner: [macos-15, macos-26] + runner: [xcode-27] name: arm64e validation (${{ matrix.runner }}) runs-on: ${{ matrix.runner }} timeout-minutes: 240 + env: + HOST_TRIPLE: aarch64-apple-darwin defaults: run: shell: bash @@ -258,7 +263,6 @@ jobs: id: hello_diag continue-on-error: true run: | - HOST_TRIPLE="$(rustc -vV | sed -n 's/^host: //p')" STAGE1="build/${HOST_TRIPLE}/stage1/bin/rustc" cat > /tmp/arm64e-diagnostics/arm64e_hello.rs <<'EOF' @@ -281,7 +285,6 @@ jobs: id: thread_id_diag continue-on-error: true run: | - HOST_TRIPLE="$(rustc -vV | sed -n 's/^host: //p')" STAGE1="build/${HOST_TRIPLE}/stage1/bin/rustc" cat > /tmp/arm64e-diagnostics/arm64e_thread_id.rs <<'EOF' @@ -304,7 +307,6 @@ jobs: id: fnptr_diag continue-on-error: true run: | - HOST_TRIPLE="$(rustc -vV | sed -n 's/^host: //p')" STAGE1="build/${HOST_TRIPLE}/stage1/bin/rustc" cat > /tmp/arm64e-diagnostics/arm64e_fnptr.rs <<'EOF' @@ -338,7 +340,6 @@ jobs: id: tls_diag continue-on-error: true run: | - HOST_TRIPLE="$(rustc -vV | sed -n 's/^host: //p')" STAGE1="build/${HOST_TRIPLE}/stage1/bin/rustc" cat > /tmp/arm64e-diagnostics/arm64e_tls_smoke.rs <<'EOF' @@ -375,7 +376,6 @@ jobs: id: scope_diag continue-on-error: true run: | - HOST_TRIPLE="$(rustc -vV | sed -n 's/^host: //p')" STAGE1="build/${HOST_TRIPLE}/stage1/bin/rustc" cat > /tmp/arm64e-diagnostics/arm64e_scope.rs <<'EOF' From d1c10fa8375449c0c41ff3dcd8cce840ae4cfb16 Mon Sep 17 00:00:00 2001 From: cypherair <262752927+cypherair@users.noreply.github.com> Date: Tue, 10 Nov 2026 08:11:49 -0800 Subject: [PATCH 10/23] test(arm64e): fix four codegen tests for LLVM 23 and Rust 1.100 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. --- .../arm64e-apple-ptrauth-init-term.rs | 5 +++-- .../arm64e-apple-ptrauth-invoke.rs | 18 +++++++++--------- .../codegen-llvm/arm64e-apple-ptrauth-scope.rs | 9 ++++++--- tests/codegen-llvm/arm64e-apple-ptrauth.rs | 2 +- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/tests/codegen-llvm/arm64e-apple-ptrauth-init-term.rs b/tests/codegen-llvm/arm64e-apple-ptrauth-init-term.rs index a2e43e7bd69d6..9fd9a61133d31 100644 --- a/tests/codegen-llvm/arm64e-apple-ptrauth-init-term.rs +++ b/tests/codegen-llvm/arm64e-apple-ptrauth-init-term.rs @@ -19,8 +19,9 @@ extern crate minicore; +// Different signatures, so that LLVM does not merge the two functions. extern "C" fn init_fn() {} -extern "C" fn term_fn() {} +extern "C" fn term_fn(_: i32) {} // CHECK-DAG: @{{.*}}CTOR_CRATE = {{.*}}constant ptr @{{.*}}init_fn{{.*}}, section "__DATA,__mod_init_func,mod_init_funcs" #[used] @@ -40,7 +41,7 @@ static BY_TYPE: extern "C" fn() = init_fn; // CHECK-DAG: @{{.*}}TERM = {{.*}}constant ptr @{{.*}}term_fn{{.*}}, section "__DATA,__mod_term_func,mod_term_funcs" #[used] #[unsafe(link_section = "__DATA,__mod_term_func,mod_term_funcs")] -static TERM: extern "C" fn() = term_fn; +static TERM: extern "C" fn(i32) = term_fn; // CHECK-DAG: @{{.*}}ORDINARY = {{.*}}constant ptr ptrauth (ptr @{{.*}}init_fn{{.*}}, i32 0) #[used] diff --git a/tests/codegen-llvm/arm64e-apple-ptrauth-invoke.rs b/tests/codegen-llvm/arm64e-apple-ptrauth-invoke.rs index bf828c7b9c5fb..6f55e87d94e9e 100644 --- a/tests/codegen-llvm/arm64e-apple-ptrauth-invoke.rs +++ b/tests/codegen-llvm/arm64e-apple-ptrauth-invoke.rs @@ -19,20 +19,20 @@ extern crate minicore; #[rustc_intrinsic] -pub unsafe fn catch_unwind( - try_fn: fn(*mut u8), - data: *mut u8, - catch_fn: fn(*mut u8, *mut u8), -) -> i32; +pub unsafe fn catch_unwind( + try_fn: unsafe fn(*mut Data), + data: *mut Data, + catch_fn: unsafe fn(*mut Data, *mut u8), +) -> bool; -fn try_fn(_: *mut u8) {} -fn catch_fn(_: *mut u8, _: *mut u8) {} +unsafe fn try_fn(_: *mut u8) {} +unsafe fn catch_fn(_: *mut u8, _: *mut u8) {} #[unsafe(no_mangle)] -pub unsafe fn invoke_catch() -> i32 { +pub unsafe fn invoke_catch() -> bool { unsafe { catch_unwind(try_fn, 0 as *mut u8, catch_fn) } } -// CHECK: define internal i32 @__rust_try({{.*}}) unnamed_addr [[TRY_ATTR:#[0-9]+]] personality +// CHECK: define internal {{.*}}@__rust_try({{.*}}) unnamed_addr [[TRY_ATTR:#[0-9]+]] personality // CHECK: invoke void %{{.*}}(ptr %{{.*}}){{.*}} [ "ptrauth"(i32 0, i64 0) ] // CHECK: attributes [[TRY_ATTR]] = {{.*}} "ptrauth-auth-traps" "ptrauth-calls" "ptrauth-indirect-gotos" "ptrauth-returns" diff --git a/tests/codegen-llvm/arm64e-apple-ptrauth-scope.rs b/tests/codegen-llvm/arm64e-apple-ptrauth-scope.rs index 88d7abe40d833..5847325250319 100644 --- a/tests/codegen-llvm/arm64e-apple-ptrauth-scope.rs +++ b/tests/codegen-llvm/arm64e-apple-ptrauth-scope.rs @@ -113,11 +113,14 @@ pub fn make_dyn(square: &Square) -> u32 { } // CHECK-LABEL: @direct_calls( -// CHECK-NOT: ptrauth +// CHECK-NOT: "ptrauth" +// CHECK-NOT: ptrauth (ptr // CHECK: call {{.*}}i32 @{{.*}}rust_target{{.*}}() -// CHECK-NOT: ptrauth +// CHECK-NOT: "ptrauth" +// CHECK-NOT: ptrauth (ptr // CHECK: call {{.*}}i32 @c_target(i32 {{.*}}%{{.*}}) -// CHECK-NOT: ptrauth +// CHECK-NOT: "ptrauth" +// CHECK-NOT: ptrauth (ptr // CHECK: ret i32 #[unsafe(no_mangle)] pub fn direct_calls(x: u32) -> u32 { diff --git a/tests/codegen-llvm/arm64e-apple-ptrauth.rs b/tests/codegen-llvm/arm64e-apple-ptrauth.rs index c1aa5f78056bd..690a0682a8e85 100644 --- a/tests/codegen-llvm/arm64e-apple-ptrauth.rs +++ b/tests/codegen-llvm/arm64e-apple-ptrauth.rs @@ -26,7 +26,7 @@ extern crate minicore; -// CHECK: @test(){{.*}} [[ATTR:#[0-9]+]] { +// CHECK: @test(){{.*}} [[ATTR:#[0-9]+]] #[unsafe(no_mangle)] pub fn test() {} From 12272e256aa31d48211cf4e0529b9ad0731f82e2 Mon Sep 17 00:00:00 2001 From: cypherair <262752927+cypherair@users.noreply.github.com> Date: Tue, 10 Nov 2026 08:11:50 -0800 Subject: [PATCH 11/23] ci(arm64e): run every test suite even when one fails 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. --- .github/workflows/fork-arm64e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/fork-arm64e.yml b/.github/workflows/fork-arm64e.yml index ca145b4d332c9..dea9e02c4e39d 100644 --- a/.github/workflows/fork-arm64e.yml +++ b/.github/workflows/fork-arm64e.yml @@ -97,7 +97,7 @@ jobs: - name: Run arm64e and pauthtest tests id: targeted_tests run: | - python3 x.py test --stage 1 --force-rerun \ + python3 x.py test --stage 1 --force-rerun --no-fail-fast \ tests/codegen-llvm/arm64e-apple-ptrauth*.rs \ tests/assembly-llvm/arm64e-apple-ptrauth-calls.rs \ tests/assembly-llvm/targets/targets-macho.rs \ From a4e38bc9416b4d3e6a66becdf5c653b45612a0c5 Mon Sep 17 00:00:00 2001 From: cypherair <262752927+cypherair@users.noreply.github.com> Date: Tue, 10 Nov 2026 08:40:03 -0800 Subject: [PATCH 12/23] ci(arm64e): give the tidy step an explicit stage 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. --- .github/workflows/fork-arm64e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/fork-arm64e.yml b/.github/workflows/fork-arm64e.yml index dea9e02c4e39d..436847b17b3b9 100644 --- a/.github/workflows/fork-arm64e.yml +++ b/.github/workflows/fork-arm64e.yml @@ -115,7 +115,7 @@ jobs: - name: Run tidy id: tidy run: | - python3 x.py test tidy + python3 x.py test --stage 1 tidy - name: Build stage1 arm64e std toolchain id: build_stage1 From e43b12fee908103fa9ec92c5d6bb97a1b1136cd5 Mon Sep 17 00:00:00 2001 From: cypherair <262752927+cypherair@users.noreply.github.com> Date: Tue, 10 Nov 2026 08:41:41 -0800 Subject: [PATCH 13/23] ci(arm64e): keep later validation steps running after a failure 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. --- .github/workflows/fork-arm64e.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/fork-arm64e.yml b/.github/workflows/fork-arm64e.yml index 436847b17b3b9..43389a7237367 100644 --- a/.github/workflows/fork-arm64e.yml +++ b/.github/workflows/fork-arm64e.yml @@ -112,17 +112,22 @@ jobs: tests/ui/statics/crt-static-pauthtest.rs \ tests/ui/target_modifiers/incompatible_pauth.rs + # The steps below do not depend on each other's results: a failing test or tidy check must + # not hide the std build or the runtime results. Each failure still fails the job. - name: Run tidy id: tidy + if: ${{ !cancelled() }} run: | python3 x.py test --stage 1 tidy - name: Build stage1 arm64e std toolchain id: build_stage1 + if: ${{ !cancelled() && steps.check_codegen.outcome == 'success' }} run: | python3 x.py build compiler/rustc library/std --stage 1 --target arm64e-apple-darwin - name: Prepare arm64e smoke helpers + if: ${{ !cancelled() && steps.build_stage1.outcome == 'success' }} run: | mkdir -p /tmp/arm64e-diagnostics cat > /tmp/arm64e_collect_smoke.py <<'PY' @@ -261,6 +266,7 @@ jobs: - name: Diagnose arm64e hello runtime id: hello_diag + if: ${{ !cancelled() && steps.build_stage1.outcome == 'success' }} continue-on-error: true run: | STAGE1="build/${HOST_TRIPLE}/stage1/bin/rustc" @@ -283,6 +289,7 @@ jobs: - name: Diagnose arm64e thread id runtime id: thread_id_diag + if: ${{ !cancelled() && steps.build_stage1.outcome == 'success' }} continue-on-error: true run: | STAGE1="build/${HOST_TRIPLE}/stage1/bin/rustc" @@ -305,6 +312,7 @@ jobs: - name: Diagnose arm64e function pointer runtime id: fnptr_diag + if: ${{ !cancelled() && steps.build_stage1.outcome == 'success' }} continue-on-error: true run: | STAGE1="build/${HOST_TRIPLE}/stage1/bin/rustc" @@ -338,6 +346,7 @@ jobs: - name: Diagnose arm64e TLS runtime id: tls_diag + if: ${{ !cancelled() && steps.build_stage1.outcome == 'success' }} continue-on-error: true run: | STAGE1="build/${HOST_TRIPLE}/stage1/bin/rustc" @@ -374,6 +383,7 @@ jobs: - name: Diagnose arm64e pointer-authentication scope runtime id: scope_diag + if: ${{ !cancelled() && steps.build_stage1.outcome == 'success' }} continue-on-error: true run: | STAGE1="build/${HOST_TRIPLE}/stage1/bin/rustc" From 22cec466661085c7271ae1dc00efeab004df33f7 Mon Sep 17 00:00:00 2001 From: cypherair <262752927+cypherair@users.noreply.github.com> Date: Tue, 10 Nov 2026 09:15:14 -0800 Subject: [PATCH 14/23] ci(arm64e): fix tidy's format check, and smoke-test unwinding on its own Tidy's own checks pass. Its format check then needs a stage 0 rustfmt, which a stable release does not pin; point it at the runner's rustfmt. The scope smoke case trapped (SIGTRAP) right after its deliberate panic, inside catch_unwind, so it never showed whether the constructor, the dyn call and the closure worked. Unwinding now has its own case, and the scope case no longer panics. --- .github/workflows/fork-arm64e.yml | 35 ++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/.github/workflows/fork-arm64e.yml b/.github/workflows/fork-arm64e.yml index 43389a7237367..1b5cc7f2d6444 100644 --- a/.github/workflows/fork-arm64e.yml +++ b/.github/workflows/fork-arm64e.yml @@ -118,7 +118,8 @@ jobs: id: tidy if: ${{ !cancelled() }} run: | - python3 x.py test --stage 1 tidy + # A stable release pins no stage 0 rustfmt; tidy's format check uses the runner's. + python3 x.py test --stage 1 tidy --set build.rustfmt="$(rustup which rustfmt)" - name: Build stage1 arm64e std toolchain id: build_stage1 @@ -422,14 +423,12 @@ jobs: let shape = black_box(Box::new(Square(3)) as Box); let add_one = black_box((|x| x + 1) as fn(u32) -> u32); let signed = (add_one as usize) >> 47 != 0; - let caught = std::panic::catch_unwind(|| panic!("caught on purpose")).is_err(); let area = std::thread::spawn(move || shape.area()).join().unwrap(); println!( - "constructor={} area={} twice={} caught={} signed={}", + "constructor={} area={} twice={} signed={}", CONSTRUCTOR_RAN.load(Ordering::SeqCst), area, twice(add_one, 40), - caught, signed, ); } @@ -443,9 +442,33 @@ jobs: --json-out /tmp/arm64e-diagnostics/arm64e_scope.json \ --emit llvm-ir,asm,link \ --run-binary \ - --expected-stdout-regex '^constructor=true area=9 twice=42 caught=true signed=(true|false)$' \ + --expected-stdout-regex '^constructor=true area=9 twice=42 signed=(true|false)$' \ --asm-pattern 'braaz|blraaz' + - name: Diagnose arm64e unwind runtime + id: unwind_diag + if: ${{ !cancelled() && steps.build_stage1.outcome == 'success' }} + continue-on-error: true + run: | + STAGE1="build/${HOST_TRIPLE}/stage1/bin/rustc" + + cat > /tmp/arm64e-diagnostics/arm64e_unwind.rs <<'EOF' + fn main() { + let caught = std::panic::catch_unwind(|| panic!("caught on purpose")).is_err(); + println!("caught={caught}"); + } + EOF + + python3 /tmp/arm64e_collect_smoke.py \ + --name unwind \ + --rustc "$STAGE1" \ + --source /tmp/arm64e-diagnostics/arm64e_unwind.rs \ + --output-base /tmp/arm64e-diagnostics/arm64e_unwind \ + --json-out /tmp/arm64e-diagnostics/arm64e_unwind.json \ + --emit link \ + --run-binary \ + --expected-stdout 'caught=true' + - name: Upload arm64e diagnostics if: always() uses: actions/upload-artifact@v4 @@ -468,6 +491,7 @@ jobs: ("fnptr", "arm64e_fnptr.json"), ("tls", "arm64e_tls_smoke.json"), ("scope", "arm64e_scope.json"), + ("unwind", "arm64e_unwind.json"), ] def sanitize(text): @@ -489,6 +513,7 @@ jobs: print("- fnptr_diag (non-blocking): ${{ steps.fnptr_diag.outcome }}") print("- tls_diag (non-blocking): ${{ steps.tls_diag.outcome }}") print("- scope_diag (non-blocking): ${{ steps.scope_diag.outcome }}") + print("- unwind_diag (non-blocking): ${{ steps.unwind_diag.outcome }}") print() print("| case | compile | run | rc/signal | file | stdout | asm hits |") print("| --- | --- | --- | --- | --- | --- | --- |") From 2c3d2bec3d98adf1f845b1f6106c7d0fa3dd8bea Mon Sep 17 00:00:00 2001 From: cypherair <262752927+cypherair@users.noreply.github.com> Date: Tue, 10 Nov 2026 09:19:24 -0800 Subject: [PATCH 15/23] fix(arm64e): sign landing pads for the arm64e unwinder On arm64e, Apple's libunwind authenticates the address that a personality routine passes to _Unwind_SetIP: it must be signed with key IB and the stack pointer, like a return address, or libunwind traps. libc++abi signs its landing pads this way. std's personality passed them raw, so every catch_unwind on these targets ended in SIGTRAP. Upstream already signs landing pads for its Linux pauthtest target, in sign_lpad, behind cfg(target_abi = "pauthtest"). Use the same code on arm64e Apple targets. No cfg tells them from plain arm64, which must not sign, so std's build script sets cfg(arm64e_apple) from the target name. A new target_abi value would be visible to every build script: the cc crate, for one, appends target_abi to the clang triple it builds for visionOS, which would break C builds such as OpenSSL's. The other code addresses std gives the unwinder are plain C function pointers (exception_cleanup, the _Unwind_Backtrace callback), signed with key IA and discriminator 0 as libunwind expects, and _Unwind_GetIP already returns an authenticated address. --- library/std/build.rs | 8 ++++++++ library/std/src/sys/personality/gcc.rs | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/library/std/build.rs b/library/std/build.rs index 98856b58d890f..248e603151626 100644 --- a/library/std/build.rs +++ b/library/std/build.rs @@ -81,6 +81,14 @@ fn main() { println!("cargo:rustc-env=STD_ENV_ARCH={}", env::var("CARGO_CFG_TARGET_ARCH").unwrap()); + // The arm64e ABI on Apple platforms signs code pointers, and its unwinder expects signed + // landing pads. No `cfg` tells these targets from plain arm64, so recognise them by name. + // A cfg seen only by std keeps this out of `target_abi`, which every build script reads. + println!("cargo:rustc-check-cfg=cfg(arm64e_apple)"); + if target_vendor == "apple" && env::var("TARGET").is_ok_and(|t| t.starts_with("arm64e-")) { + println!("cargo:rustc-cfg=arm64e_apple"); + } + println!("cargo:rustc-check-cfg=cfg(vxworks_lt_25_09)"); if target_os == "vxworks" { diff --git a/library/std/src/sys/personality/gcc.rs b/library/std/src/sys/personality/gcc.rs index b2fa38293f4e5..d231a4276ea07 100644 --- a/library/std/src/sys/personality/gcc.rs +++ b/library/std/src/sys/personality/gcc.rs @@ -211,7 +211,9 @@ cfg_select! { #[rustc_force_inline] unsafe fn sign_lpad(context: *mut uw::_Unwind_Context, lpad: *const u8) -> *const u8 { cfg_select! { - all(target_abi = "pauthtest", target_arch = "aarch64") => { + // An unwinder that authenticates return addresses wants the landing pad signed + // with key IB and the stack pointer, as a return address would be. + any(all(target_abi = "pauthtest", target_arch = "aarch64"), arm64e_apple) => { // DWARF register number for SP on AArch64. const SP_REG: i32 = 31; From e40632fbf7b89be929c46e17253142ab71f250ed Mon Sep 17 00:00:00 2001 From: cypherair <262752927+cypherair@users.noreply.github.com> Date: Tue, 10 Nov 2026 09:19:25 -0800 Subject: [PATCH 16/23] ci(arm64e): check unwinding at runtime, and block on it The unwind case now catches a panic on the main thread, on another thread and through a dyn call, runs a cleanup landing pad, and joins a thread that panicked. The scope and unwind cases guard fixes, and the runner runs arm64e with pointer authentication on, so both are now blocking. The hello, thread id, fn pointer and thread-local cases stay non-blocking diagnostics. --- .github/workflows/fork-arm64e.yml | 49 ++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/.github/workflows/fork-arm64e.yml b/.github/workflows/fork-arm64e.yml index 1b5cc7f2d6444..de543b99cbe7d 100644 --- a/.github/workflows/fork-arm64e.yml +++ b/.github/workflows/fork-arm64e.yml @@ -385,7 +385,6 @@ jobs: - name: Diagnose arm64e pointer-authentication scope runtime id: scope_diag if: ${{ !cancelled() && steps.build_stage1.outcome == 'success' }} - continue-on-error: true run: | STAGE1="build/${HOST_TRIPLE}/stage1/bin/rustc" @@ -448,14 +447,48 @@ jobs: - name: Diagnose arm64e unwind runtime id: unwind_diag if: ${{ !cancelled() && steps.build_stage1.outcome == 'success' }} - continue-on-error: true run: | STAGE1="build/${HOST_TRIPLE}/stage1/bin/rustc" cat > /tmp/arm64e-diagnostics/arm64e_unwind.rs <<'EOF' + use std::hint::black_box; + use std::panic::{AssertUnwindSafe, catch_unwind}; + use std::sync::atomic::{AtomicU32, Ordering}; + + static DROPS: AtomicU32 = AtomicU32::new(0); + + struct CountsDrop; + + impl Drop for CountsDrop { + fn drop(&mut self) { + DROPS.fetch_add(1, Ordering::SeqCst); + } + } + + trait Task { + fn run(&self); + } + + struct Panics; + + impl Task for Panics { + fn run(&self) { + // Unwinding runs this frame's cleanup landing pad first. + let _guard = CountsDrop; + panic!("through dyn"); + } + } + fn main() { - let caught = std::panic::catch_unwind(|| panic!("caught on purpose")).is_err(); - println!("caught={caught}"); + let main = catch_unwind(|| panic!("on the main thread")).is_err(); + let thread = std::thread::spawn(|| catch_unwind(|| panic!("on a thread")).is_err()) + .join() + .unwrap(); + let task = black_box(Box::new(Panics) as Box); + let dyn_call = catch_unwind(AssertUnwindSafe(|| task.run())).is_err(); + let join = std::thread::spawn(|| panic!("out of a thread")).join().is_err(); + let drops = DROPS.load(Ordering::SeqCst); + println!("main={main} thread={thread} dyn={dyn_call} join={join} drops={drops}"); } EOF @@ -467,7 +500,7 @@ jobs: --json-out /tmp/arm64e-diagnostics/arm64e_unwind.json \ --emit link \ --run-binary \ - --expected-stdout 'caught=true' + --expected-stdout 'main=true thread=true dyn=true join=true drops=1' - name: Upload arm64e diagnostics if: always() @@ -512,8 +545,8 @@ jobs: print("- thread_id_diag (non-blocking): ${{ steps.thread_id_diag.outcome }}") print("- fnptr_diag (non-blocking): ${{ steps.fnptr_diag.outcome }}") print("- tls_diag (non-blocking): ${{ steps.tls_diag.outcome }}") - print("- scope_diag (non-blocking): ${{ steps.scope_diag.outcome }}") - print("- unwind_diag (non-blocking): ${{ steps.unwind_diag.outcome }}") + print("- scope_diag: ${{ steps.scope_diag.outcome }}") + print("- unwind_diag: ${{ steps.unwind_diag.outcome }}") print() print("| case | compile | run | rc/signal | file | stdout | asm hits |") print("| --- | --- | --- | --- | --- | --- | --- |") @@ -542,7 +575,7 @@ jobs: f"{file_output} | {stdout} | {asm_hits} |" ) print() - print("The host-binary smoke steps are non-blocking: a runner may not run arm64e binaries, or may run them with pointer authentication off. The tests, tidy and the stage1 build are blocking.") + print("The hello, thread_id, fnptr and tls cases are non-blocking diagnostics. The scope and unwind cases, the tests, tidy and the stage1 build are blocking.") PY { From 4c2dc2d24ddb463a5844857585cf7375ba176c6d Mon Sep 17 00:00:00 2001 From: cypherair <262752927+cypherair@users.noreply.github.com> Date: Tue, 10 Nov 2026 09:45:48 -0800 Subject: [PATCH 17/23] ci(arm64e): run tidy on the stable channel, as upstream does Tidy's own checks pass. Its format check then gave the runner's stable rustfmt nightly-only flags and failed. Upstream's CI for stable releases builds with rust.channel = stable, where bootstrap skips that check, since a stable release pins no stage 0 rustfmt. Do the same for the tidy step; it builds no compiler, so nothing else changes. --- .github/workflows/fork-arm64e.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/fork-arm64e.yml b/.github/workflows/fork-arm64e.yml index de543b99cbe7d..9abe3d5f51771 100644 --- a/.github/workflows/fork-arm64e.yml +++ b/.github/workflows/fork-arm64e.yml @@ -118,8 +118,10 @@ jobs: id: tidy if: ${{ !cancelled() }} run: | - # A stable release pins no stage 0 rustfmt; tidy's format check uses the runner's. - python3 x.py test --stage 1 tidy --set build.rustfmt="$(rustup which rustfmt)" + # On the stable channel, as in upstream's CI for stable releases, tidy skips its rustfmt + # check: a stable release pins no stage 0 rustfmt, and bootstrap passes nightly-only + # flags to it. This step builds no compiler, so the channel changes nothing else. + python3 x.py test --stage 1 tidy --set rust.channel=stable - name: Build stage1 arm64e std toolchain id: build_stage1 From 55ef406140bb60dc9eed8ec3e58485dd921d920c Mon Sep 17 00:00:00 2001 From: cypherair <262752927+cypherair@users.noreply.github.com> Date: Tue, 10 Nov 2026 09:59:38 -0800 Subject: [PATCH 18/23] fix(arm64e): make the scope checks a match, and other review notes Review notes on the codegen, none of which changes the output: - ptrauth_operand_bundle and maybe_sign_fn_ptr match on the scope instead of comparing it twice, so a new scope fails to compile, and upstream's comments sit in the ExternC arm, where they are true. - The personality is not a callee: it takes no schema at all. - The scalar_to_backend override replaces upstream's default for every target; a comment says that outside AllFunctions it must match it. - The Apple profile's doc comment says which fields nothing reads on Mach-O. --- compiler/rustc_codegen_llvm/src/builder.rs | 49 ++++++++++++---------- compiler/rustc_codegen_llvm/src/common.rs | 35 ++++++++++------ compiler/rustc_codegen_llvm/src/context.rs | 5 +-- compiler/rustc_session/src/session.rs | 4 +- 4 files changed, 55 insertions(+), 38 deletions(-) diff --git a/compiler/rustc_codegen_llvm/src/builder.rs b/compiler/rustc_codegen_llvm/src/builder.rs index 20633213e620e..b72ce45b4f6d9 100644 --- a/compiler/rustc_codegen_llvm/src/builder.rs +++ b/compiler/rustc_codegen_llvm/src/builder.rs @@ -2185,29 +2185,36 @@ impl<'a, 'll, 'tcx> Builder<'a, 'll, 'tcx> { if self.sess().pointer_authentication_functions().is_none() { return None; } - let scope = self.sess().pointer_authentication_scope(); - // When every function pointer is signed and no direct callee is, exactly the indirect - // calls authenticate their callee, whatever their ABI. - if scope == Some(PointerAuthScope::AllFunctions) && !llvm::is_indirect_callee(llfn) { - return None; - } - // Pointer authentication support is currently limited to extern "C" calls; filter out other - // ABIs. - if scope == Some(PointerAuthScope::ExternC) && fn_abi?.conv != CanonAbi::C { - return None; - } - // Filter out LLVM intrinsics. - if llvm::get_value_name(llfn).starts_with(b"llvm.") { - return None; + match self.sess().pointer_authentication_scope()? { + // Every function pointer is signed and no direct callee is, so exactly the indirect + // calls authenticate their callee, whatever their ABI. + PointerAuthScope::AllFunctions => { + if !llvm::is_indirect_callee(llfn) { + return None; + } + } + PointerAuthScope::ExternC => { + // Pointer authentication support is currently limited to extern "C" calls; filter + // out other ABIs. + if fn_abi?.conv != CanonAbi::C { + return None; + } + // Filter out LLVM intrinsics. + if llvm::get_value_name(llfn).starts_with(b"llvm.") { + return None; + } + + // FIXME(jchlanda) Operand bundles should only be attached to indirect function + // calls. However, function pointer signing is currently performed in + // `get_fn_addr`, which causes the logic to be applied too broadly, including to + // function values (not just pointers). As a result, direct calls using signed + // function values must also receive operand bundles. + // Once this is resolved, we should analyze each call and skip direct calls. See + // the discussion in the rust-lang issue: + // + } } - // FIXME(jchlanda) Operand bundles should only be attached to indirect function calls. - // However, function pointer signing is currently performed in `get_fn_addr`, which causes - // the logic to be applied too broadly, including to function values (not just pointers). - // As a result, direct calls using signed function values must also receive operand - // bundles. - // Once this is resolved, we should analyze each call and skip direct calls. See the - // discussion in the rust-lang issue: let key: u32 = 0; let discriminator: u64 = 0; Some(llvm::OperandBundleBox::new( diff --git a/compiler/rustc_codegen_llvm/src/common.rs b/compiler/rustc_codegen_llvm/src/common.rs index 8d71f96b9f157..a23ec74a35ef0 100644 --- a/compiler/rustc_codegen_llvm/src/common.rs +++ b/compiler/rustc_codegen_llvm/src/common.rs @@ -36,19 +36,26 @@ pub(crate) fn maybe_sign_fn_ptr<'ll, 'tcx>( return llfn; } - if cx.tcx.sess.pointer_authentication_scope() == Some(PointerAuthScope::ExternC) { - // Only free functions or methods - let def_id = instance.def_id(); - if !matches!(cx.tcx.def_kind(def_id), DefKind::Fn | DefKind::AssocFn) { - return llfn; - } - // Only C ABI - let abi = cx.tcx.fn_sig(def_id).skip_binder().abi(); - if !matches!(abi, ExternAbi::C { .. } | ExternAbi::System { .. }) { - return llfn; - } - if Some(def_id) == cx.tcx.lang_items().eh_personality() { - return llfn; + let Some(scope) = cx.tcx.sess.pointer_authentication_scope() else { + return llfn; + }; + match scope { + // Callers pass a schema only where the address becomes a value: sign every function. + PointerAuthScope::AllFunctions => {} + PointerAuthScope::ExternC => { + // Only free functions or methods + let def_id = instance.def_id(); + if !matches!(cx.tcx.def_kind(def_id), DefKind::Fn | DefKind::AssocFn) { + return llfn; + } + // Only C ABI + let abi = cx.tcx.fn_sig(def_id).skip_binder().abi(); + if !matches!(abi, ExternAbi::C { .. } | ExternAbi::System { .. }) { + return llfn; + } + if Some(def_id) == cx.tcx.lang_items().eh_personality() { + return llfn; + } } } // Ignore LLVM intrinsics @@ -402,6 +409,8 @@ impl<'ll, 'tcx> ConstCodegenMethods for CodegenCx<'ll, 'tcx> { }) } + // This replaces the trait default in `rustc_codegen_ssa/src/traits/consts.rs` for every + // target. Outside `AllFunctions` it must do what the default does. fn scalar_to_backend(&self, cv: Scalar, layout: abi::Scalar, llty: &'ll Type) -> &'ll Value { // A function address in a constant operand is a value: sign it if every function pointer // is signed. diff --git a/compiler/rustc_codegen_llvm/src/context.rs b/compiler/rustc_codegen_llvm/src/context.rs index f74bb9e857eec..92518f81df9b0 100644 --- a/compiler/rustc_codegen_llvm/src/context.rs +++ b/compiler/rustc_codegen_llvm/src/context.rs @@ -1046,8 +1046,7 @@ impl<'ll, 'tcx> MiscCodegenMethods<'tcx> for CodegenCx<'ll, 'tcx> { }; let tcx = self.tcx; - // Unwind tables refer to the personality by symbol. Rust code never calls it through a - // pointer, so it is treated like a direct callee. + // Unwind tables name the personality; it is never a signed value. let llfn = match tcx.lang_items().eh_personality() { Some(def_id) if name.is_none() => self.get_fn_addr( ty::Instance::expect_resolve( @@ -1057,7 +1056,7 @@ impl<'ll, 'tcx> MiscCodegenMethods<'tcx> for CodegenCx<'ll, 'tcx> { ty::List::empty(), DUMMY_SP, ), - tcx.sess.pointer_authentication_direct_callees(), + None, ), _ => { let name = name.unwrap_or("rust_eh_personality"); diff --git a/compiler/rustc_session/src/session.rs b/compiler/rustc_session/src/session.rs index e454dc128a5d3..55edfcf772dfc 100644 --- a/compiler/rustc_session/src/session.rs +++ b/compiler/rustc_session/src/session.rs @@ -200,7 +200,9 @@ impl PointerAuthConfig { /// The arm64e ABI of Apple's platforms, as clang's Darwin driver sets it up. C function /// pointers use key IA with no diversity. Rust function pointers and v-table entries use the /// same schema, so that Rust's indirect calls are as protected as those of the C and Swift - /// code in the same process. Initializer and terminator entries are not signed. + /// code in the same process. Initializer and terminator entries are not signed. `intrinsics` + /// and the three v-table fields feed only the ELF PAuth ABI version, so nothing reads them for + /// these Mach-O targets; they record what clang's Darwin driver turns on. fn arm64e_apple(target: &Target) -> Self { Self { scope: PointerAuthScope::AllFunctions, From 57169cff1ac1b6d1c8f919e4f634f8794143ebe5 Mon Sep 17 00:00:00 2001 From: cypherair <262752927+cypherair@users.noreply.github.com> Date: Tue, 10 Nov 2026 09:59:38 -0800 Subject: [PATCH 19/23] test(arm64e): cover the direct callees, a reified call and plain arm64 From the review of the tests: - A test without minicore checks that drop glue, a lang item that codegen calls and the personality stay unsigned, with no bundle. - The entry test no longer accepts a signed lang_start callee. - The scope test calls through a reified fn item at -O0, where the callee is the signed constant and needs a bundle. - The invoke test also checks the call through catch_fn. - The serialized-output test calls an external function, so the direct call it inspects cannot vanish, and asserts the call exists. - A control test checks that aarch64 Apple targets get nothing. - The assembly test covers every arm64e OS and checks pacibsp and retab. - The initializer test checks a terminator section found by type, and states its reason correctly: clang signs a static placed by hand in __mod_init_func; only its own constructors stay unsigned. Either form works on Apple's linker and dyld. --- .../arm64e-apple-ptrauth-calls.rs | 19 +++-- .../arm64e-apple-ptrauth-aarch64-control.rs | 44 ++++++++++ .../arm64e-apple-ptrauth-direct-callees.rs | 83 +++++++++++++++++++ .../arm64e-apple-ptrauth-entry.rs | 2 +- .../arm64e-apple-ptrauth-init-term.rs | 13 ++- .../arm64e-apple-ptrauth-invoke.rs | 3 +- .../arm64e-apple-ptrauth-scope.rs | 8 ++ .../ptrauth.rs | 11 ++- .../arm64e-ptrauth-serialized-output/rmake.rs | 27 ++++-- 9 files changed, 186 insertions(+), 24 deletions(-) create mode 100644 tests/codegen-llvm/arm64e-apple-ptrauth-aarch64-control.rs create mode 100644 tests/codegen-llvm/arm64e-apple-ptrauth-direct-callees.rs diff --git a/tests/assembly-llvm/arm64e-apple-ptrauth-calls.rs b/tests/assembly-llvm/arm64e-apple-ptrauth-calls.rs index d47bd83012cfb..4d24814e1ea2a 100644 --- a/tests/assembly-llvm/arm64e-apple-ptrauth-calls.rs +++ b/tests/assembly-llvm/arm64e-apple-ptrauth-calls.rs @@ -1,13 +1,20 @@ -// Indirect calls authenticate with `blraaz` or `braaz`; direct calls do not. +// Indirect calls authenticate with `blraaz` or `braaz`; direct calls do not. A function that saves +// its return address signs it with `pacibsp` and returns with `retab`. // Disabling `v8.3a` does not disable `paca` and `pacg`, which it implies: the backend keeps `pauth` -// and still emits these instructions. +// and still emits these instructions (without `pauth` it prints `pacibsp` as `hint #27`). //@ add-minicore //@ assembly-output: emit-asm -//@ revisions: DEFAULT NO_V83A -//@ [DEFAULT] compile-flags: --target arm64e-apple-darwin -Copt-level=2 -//@ [DEFAULT] needs-llvm-components: aarch64 +//@ revisions: DARWIN NO_V83A IOS TVOS VISIONOS +//@ [DARWIN] compile-flags: --target arm64e-apple-darwin -Copt-level=2 +//@ [DARWIN] needs-llvm-components: aarch64 //@ [NO_V83A] compile-flags: --target arm64e-apple-darwin -Copt-level=2 -Ctarget-feature=-v8.3a //@ [NO_V83A] needs-llvm-components: aarch64 +//@ [IOS] compile-flags: --target arm64e-apple-ios -Copt-level=2 +//@ [IOS] needs-llvm-components: aarch64 +//@ [TVOS] compile-flags: --target arm64e-apple-tvos -Copt-level=2 +//@ [TVOS] needs-llvm-components: aarch64 +//@ [VISIONOS] compile-flags: --target arm64e-apple-visionos -Copt-level=2 +//@ [VISIONOS] needs-llvm-components: aarch64 #![crate_type = "lib"] #![feature(no_core, lang_items)] @@ -47,4 +54,6 @@ pub extern "C" fn indirect_non_tail(f: extern "C" fn(i32) -> i32, x: i32) -> i32 // CHECK: braaz // CHECK-LABEL: _indirect_non_tail: +// CHECK: pacibsp // CHECK: blraaz +// CHECK: retab diff --git a/tests/codegen-llvm/arm64e-apple-ptrauth-aarch64-control.rs b/tests/codegen-llvm/arm64e-apple-ptrauth-aarch64-control.rs new file mode 100644 index 0000000000000..c43a3a8ffccfc --- /dev/null +++ b/tests/codegen-llvm/arm64e-apple-ptrauth-aarch64-control.rs @@ -0,0 +1,44 @@ +// The plain arm64 Apple targets keep upstream's behaviour: no pointer-authentication attributes, +// no signed function pointers and no `ptrauth` bundles. +//@ add-minicore +//@ revisions: AARCH64_DARWIN AARCH64_IOS AARCH64_VISIONOS +//@ [AARCH64_DARWIN] compile-flags: --target aarch64-apple-darwin -Copt-level=0 +//@ [AARCH64_DARWIN] needs-llvm-components: aarch64 +//@ [AARCH64_IOS] compile-flags: --target aarch64-apple-ios -Copt-level=0 +//@ [AARCH64_IOS] needs-llvm-components: aarch64 +//@ [AARCH64_VISIONOS] compile-flags: --target aarch64-apple-visionos -Copt-level=0 +//@ [AARCH64_VISIONOS] needs-llvm-components: aarch64 + +#![crate_type = "lib"] +#![feature(no_core, lang_items)] +#![no_core] + +extern crate minicore; + +// CHECK-NOT: "ptrauth +// CHECK-NOT: ptrauth (ptr +// CHECK-LABEL: @target( +#[unsafe(no_mangle)] +pub fn target() -> u32 { + 7 +} + +// CHECK-NOT: "ptrauth +// CHECK-NOT: ptrauth (ptr +// CHECK-LABEL: @reify( +// CHECK: ret ptr @target +#[unsafe(no_mangle)] +pub fn reify() -> fn() -> u32 { + target +} + +// CHECK-LABEL: @call( +// CHECK-NOT: "ptrauth +// CHECK: ret i32 +#[unsafe(no_mangle)] +pub fn call(f: fn() -> u32) -> u32 { + f() +} + +// CHECK: attributes # +// CHECK-NOT: "ptrauth diff --git a/tests/codegen-llvm/arm64e-apple-ptrauth-direct-callees.rs b/tests/codegen-llvm/arm64e-apple-ptrauth-direct-callees.rs new file mode 100644 index 0000000000000..610cb433e35df --- /dev/null +++ b/tests/codegen-llvm/arm64e-apple-ptrauth-direct-callees.rs @@ -0,0 +1,83 @@ +// ignore-tidy-linelength +// Callees that codegen calls directly stay unsigned and carry no `ptrauth` bundle, even +// unoptimised: drop glue, a lang item (the abort when a `nounwind` function would unwind), and +// the personality, which unwind tables name. minicore has no `Drop` impl and no personality, so +// this test declares its own lang items. +//@ revisions: DARWIN IOS TVOS VISIONOS +//@ [DARWIN] compile-flags: --target arm64e-apple-darwin -Copt-level=0 -Cpanic=unwind +//@ [DARWIN] needs-llvm-components: aarch64 +//@ [IOS] compile-flags: --target arm64e-apple-ios -Copt-level=0 -Cpanic=unwind +//@ [IOS] needs-llvm-components: aarch64 +//@ [TVOS] compile-flags: --target arm64e-apple-tvos -Copt-level=0 -Cpanic=unwind +//@ [TVOS] needs-llvm-components: aarch64 +//@ [VISIONOS] compile-flags: --target arm64e-apple-visionos -Copt-level=0 -Cpanic=unwind +//@ [VISIONOS] needs-llvm-components: aarch64 + +#![crate_type = "lib"] +#![feature(no_core, lang_items, rustc_attrs)] +#![no_core] + +#[lang = "pointee_sized"] +pub trait PointeeSized {} + +#[lang = "meta_sized"] +pub trait MetaSized: PointeeSized {} + +#[lang = "sized"] +pub trait Sized: MetaSized {} + +#[lang = "legacy_receiver"] +pub trait LegacyReceiver: PointeeSized {} + +impl LegacyReceiver for &mut T {} + +#[lang = "drop"] +pub trait Drop { + fn drop(&mut self); +} + +#[lang = "drop_glue"] +fn drop_glue(_: &mut T) {} + +#[lang = "panic_cannot_unwind"] +fn panic_cannot_unwind() -> ! { + loop {} +} + +#[lang = "eh_personality"] +#[unsafe(no_mangle)] +extern "C" fn rust_eh_personality() {} + +pub struct Guard; + +impl Drop for Guard { + fn drop(&mut self) {} +} + +unsafe extern "C-unwind" { + safe fn may_unwind(); +} + +// CHECK-LABEL: @drop_guard( +// CHECK-NOT: "ptrauth" +// CHECK-NOT: ptrauth (ptr +// CHECK: call void @{{.*}}drop_glue{{.*}}(ptr +// CHECK-NOT: "ptrauth" +// CHECK-NOT: ptrauth (ptr +// CHECK: ret void +#[unsafe(no_mangle)] +pub fn drop_guard(_guard: Guard) {} + +// CHECK-LABEL: @no_unwind_boundary( +// CHECK-SAME: personality ptr @rust_eh_personality +// CHECK-NOT: "ptrauth" +// CHECK-NOT: ptrauth (ptr +// CHECK: invoke void @may_unwind() +// CHECK-NOT: "ptrauth" +// CHECK-NOT: ptrauth (ptr +// CHECK: call void @{{.*}}panic_cannot_unwind{{.*}}() +#[unsafe(no_mangle)] +#[rustc_nounwind] +pub fn no_unwind_boundary() { + may_unwind(); +} diff --git a/tests/codegen-llvm/arm64e-apple-ptrauth-entry.rs b/tests/codegen-llvm/arm64e-apple-ptrauth-entry.rs index 4fc8f233de286..9303e5b6e57a4 100644 --- a/tests/codegen-llvm/arm64e-apple-ptrauth-entry.rs +++ b/tests/codegen-llvm/arm64e-apple-ptrauth-entry.rs @@ -29,6 +29,6 @@ fn lang_start( fn main() {} // CHECK: define i32 @main({{.*}}) unnamed_addr [[MAIN_ATTR:#[0-9]+]] -// CHECK: call {{.*}}@{{.*}}lang_start{{.*}}(ptr ptrauth (ptr @{{.*}}4main{{.*}}, i32 0), +// CHECK: call {{[^(]*}}@{{[^(]*}}lang_start{{[^(]*}}(ptr ptrauth (ptr @{{.*}}4main{{.*}}, i32 0), // CHECK: attributes [[MAIN_ATTR]] = {{.*}} "ptrauth-auth-traps" "ptrauth-calls" // CHECK-SAME: "ptrauth-indirect-gotos" "ptrauth-returns" diff --git a/tests/codegen-llvm/arm64e-apple-ptrauth-init-term.rs b/tests/codegen-llvm/arm64e-apple-ptrauth-init-term.rs index 9fd9a61133d31..9d185d714d9c8 100644 --- a/tests/codegen-llvm/arm64e-apple-ptrauth-init-term.rs +++ b/tests/codegen-llvm/arm64e-apple-ptrauth-init-term.rs @@ -1,7 +1,9 @@ // ignore-tidy-linelength -// Clang on Darwin does not sign initializer or terminator pointers, so on arm64e Apple targets the -// entries of `__mod_init_func` and `__mod_term_func` stay unsigned, whether the section is named -// or typed. The same function pointer in an ordinary static is signed. +// On arm64e Apple targets the entries of `__mod_init_func` and `__mod_term_func` stay unsigned, +// whether the section is named or typed, like the constructors clang emits itself. (Clang signs a +// static that C code places in these sections by hand.) Either form works: Apple's linker turns +// initializer entries into offsets and dyld re-signs them, and dyld ignores `__mod_term_func` on +// arm64e. The same function pointer in an ordinary static is signed. //@ add-minicore //@ revisions: DARWIN IOS TVOS VISIONOS //@ [DARWIN] compile-flags: --target arm64e-apple-darwin @@ -43,6 +45,11 @@ static BY_TYPE: extern "C" fn() = init_fn; #[unsafe(link_section = "__DATA,__mod_term_func,mod_term_funcs")] static TERM: extern "C" fn(i32) = term_fn; +// CHECK-DAG: @{{.*}}TERM_BY_TYPE = {{.*}}constant ptr @{{.*}}term_fn{{.*}}, section "__DATA,__terms,mod_term_funcs" +#[used] +#[unsafe(link_section = "__DATA,__terms,mod_term_funcs")] +static TERM_BY_TYPE: extern "C" fn(i32) = term_fn; + // CHECK-DAG: @{{.*}}ORDINARY = {{.*}}constant ptr ptrauth (ptr @{{.*}}init_fn{{.*}}, i32 0) #[used] static ORDINARY: extern "C" fn() = init_fn; diff --git a/tests/codegen-llvm/arm64e-apple-ptrauth-invoke.rs b/tests/codegen-llvm/arm64e-apple-ptrauth-invoke.rs index 6f55e87d94e9e..afb6ae1740f33 100644 --- a/tests/codegen-llvm/arm64e-apple-ptrauth-invoke.rs +++ b/tests/codegen-llvm/arm64e-apple-ptrauth-invoke.rs @@ -1,6 +1,6 @@ // ignore-tidy-linelength // `__rust_try`, which rustc generates for `catch_unwind`, carries the pointer-authentication -// attributes, and its call through the `try_fn` pointer authenticates the callee. +// attributes, and its calls through the `try_fn` and `catch_fn` pointers authenticate the callee. //@ add-minicore //@ revisions: DARWIN IOS TVOS VISIONOS //@ [DARWIN] compile-flags: --target arm64e-apple-darwin -Copt-level=0 -Cpanic=unwind -Zverify-llvm-ir @@ -35,4 +35,5 @@ pub unsafe fn invoke_catch() -> bool { // CHECK: define internal {{.*}}@__rust_try({{.*}}) unnamed_addr [[TRY_ATTR:#[0-9]+]] personality // CHECK: invoke void %{{.*}}(ptr %{{.*}}){{.*}} [ "ptrauth"(i32 0, i64 0) ] +// CHECK: call void %{{.*}}(ptr %{{.*}}, ptr %{{.*}}){{.*}} [ "ptrauth"(i32 0, i64 0) ] // CHECK: attributes [[TRY_ATTR]] = {{.*}} "ptrauth-auth-traps" "ptrauth-calls" "ptrauth-indirect-gotos" "ptrauth-returns" diff --git a/tests/codegen-llvm/arm64e-apple-ptrauth-scope.rs b/tests/codegen-llvm/arm64e-apple-ptrauth-scope.rs index 5847325250319..73f1d23042256 100644 --- a/tests/codegen-llvm/arm64e-apple-ptrauth-scope.rs +++ b/tests/codegen-llvm/arm64e-apple-ptrauth-scope.rs @@ -99,6 +99,14 @@ pub fn call_rust_fn_ptr(f: fn() -> u32) -> u32 { f() } +// CHECK-LABEL: @call_reified( +// CHECK: call {{.*}}i32 ptrauth (ptr @{{.*}}rust_target{{.*}}, i32 0)(){{.*}} [ "ptrauth"(i32 0, i64 0) ] +#[unsafe(no_mangle)] +pub fn call_reified() -> u32 { + let f: fn() -> u32 = rust_target; + f() +} + // CHECK-LABEL: @call_dyn( // CHECK: call {{.*}}i32 %{{.*}}(ptr {{.*}}%{{.*}}){{.*}} [ "ptrauth"(i32 0, i64 0) ] #[unsafe(no_mangle)] diff --git a/tests/run-make/arm64e-ptrauth-serialized-output/ptrauth.rs b/tests/run-make/arm64e-ptrauth-serialized-output/ptrauth.rs index f06a241238e4b..3ed331931bd6e 100644 --- a/tests/run-make/arm64e-ptrauth-serialized-output/ptrauth.rs +++ b/tests/run-make/arm64e-ptrauth-serialized-output/ptrauth.rs @@ -14,15 +14,14 @@ pub trait Sized: MetaSized {} #[lang = "copy"] pub trait Copy: Sized {} -#[inline(never)] -#[no_mangle] -pub extern "C" fn target(x: i32) -> i32 { - x +// External, so that LLVM cannot remove the call once it is direct. +extern "C" { + fn target(x: i32) -> i32; } #[inline(always)] -fn call_indirect(f: extern "C" fn(i32) -> i32, x: i32) -> i32 { - f(x) +fn call_indirect(f: unsafe extern "C" fn(i32) -> i32, x: i32) -> i32 { + unsafe { f(x) } } #[no_mangle] diff --git a/tests/run-make/arm64e-ptrauth-serialized-output/rmake.rs b/tests/run-make/arm64e-ptrauth-serialized-output/rmake.rs index dd69ec9e176fa..e50b97db14d68 100644 --- a/tests/run-make/arm64e-ptrauth-serialized-output/rmake.rs +++ b/tests/run-make/arm64e-ptrauth-serialized-output/rmake.rs @@ -1,19 +1,30 @@ +//! Inlining turns the call through a signed pointer in `devirtualized` into a direct call. In the +//! IR, the bitcode and the embedded bitcode that rustc writes, that call must carry no `ptrauth` +//! bundle. + //@ needs-llvm-components: aarch64 use std::path::{Path, PathBuf}; use run_make_support::{llvm_ar, llvm_dis, llvm_objcopy, rfs, rustc}; +/// Checks that `devirtualized` still calls `target` directly, without a `ptrauth` bundle. fn assert_no_illegal_direct_ptrauth(path: &str) { let ir = rfs::read_to_string(path); - for (line_idx, line) in ir.lines().enumerate() { - if line.contains("@target(") && line.contains("\"ptrauth\"") { - panic!( - "{path}:{} contains a direct call with a ptrauth operand bundle:\n{line}", - line_idx + 1 - ); - } - } + let body: Vec<&str> = ir + .lines() + .skip_while(|line| !(line.starts_with("define ") && line.contains("@devirtualized("))) + .take_while(|line| *line != "}") + .collect(); + assert!(!body.is_empty(), "{path} defines no `devirtualized`"); + let call = body + .iter() + .find(|line| line.contains("@target(")) + .unwrap_or_else(|| panic!("`devirtualized` in {path} no longer calls `target` directly")); + assert!( + !call.contains("\"ptrauth\""), + "{path}: the direct call carries a ptrauth operand bundle:\n{call}" + ); } fn main() { From 5605432fb667428ac7ddcf07fd17ec8938e55aca Mon Sep 17 00:00:00 2001 From: cypherair <262752927+cypherair@users.noreply.github.com> Date: Tue, 10 Nov 2026 09:59:38 -0800 Subject: [PATCH 20/23] ci(arm64e): require pointer authentication, check std, run at -O0 From the review of the validation: - A new blocking case calls the raw, unsigned address of a function through an authenticated call in a child process, and requires the child to die by a signal. With the keys off it would return. The scope case now requires signed=true. - The arm64e std is built with -Zverify-llvm-ir, through RUSTFLAGS_NOT_BOOTSTRAP so that the compiler is not rebuilt. - .github/arm64e/check_objects.py disassembles every object in the arm64e std rlibs and fails on a function that saves x30 without pacibsp, a plain blr, a plain br outside a jump table, or an unsigned data relocation to a function. - The scope and unwind cases run at -O and -O0; the fn pointer case calls through a black_box'ed pointer. - The checkout and upload-artifact actions are pinned by commit, with the versions upstream's CI uses. --- .github/arm64e/check_objects.py | 116 ++++++++++++++++++++++++++++ .github/workflows/fork-arm64e.yml | 122 ++++++++++++++++++++++++------ 2 files changed, 215 insertions(+), 23 deletions(-) create mode 100644 .github/arm64e/check_objects.py diff --git a/.github/arm64e/check_objects.py b/.github/arm64e/check_objects.py new file mode 100644 index 0000000000000..64dfde17f08fb --- /dev/null +++ b/.github/arm64e/check_objects.py @@ -0,0 +1,116 @@ +"""Checks every object in the given rlibs or archives for code that pointer authentication misses. + +Fails on: +- a function that saves its return address (x30) without signing it with `pacibsp`; +- an indirect call with a plain `blr`; +- an indirect branch with a plain `br`, unless it is a jump-table dispatch; +- a data relocation that stores a function's address unsigned. + +Usage: check_objects.py LLVM_BIN_DIR ARCHIVE... +""" + +import pathlib +import re +import subprocess +import sys +import tempfile + +# Sections whose relocations to code are not function pointers that code calls through. +UNSIGNED_OK_SECTIONS = { + "__text", + "__compact_unwind", + "__eh_frame", + "__gcc_except_tab", + "__mod_init_func", # left unsigned on purpose, like clang's own constructors + "__mod_term_func", + "__init_offsets", + "__thread_vars", # TLV descriptors name `_tlv_bootstrap`; dyld binds them +} + +FUNCTION_HEADER = re.compile(r"^[0-9a-f]+ <(.+)>:$") +INSTRUCTION = re.compile(r"^\s+[0-9a-f]+:\s+(\S+)\s*(.*)$") +SECTION_HEADER = re.compile(r"^RELOCATION RECORDS FOR \[(.+)\]:$") +RELOCATION = re.compile(r"^[0-9a-f]+\s+(\S+)\s+(\S+)$") +SYMBOL = re.compile(r"^[0-9a-f]+\s+[lg ]\s+F\s+__TEXT,__text\s+(\S+)$") + + +def run(tool, *args): + command = [str(tool), *map(str, args)] + return subprocess.run(command, check=True, capture_output=True, text=True).stdout + + +def function_symbols(objdump, obj): + lines = run(objdump, "-t", obj).splitlines() + return {m.group(1) for line in lines if (m := SYMBOL.match(line))} + + +def check_code(objdump, obj, problems): + functions = [] + for line in run(objdump, "-d", "--no-show-raw-insn", obj).splitlines(): + if m := FUNCTION_HEADER.match(line): + functions.append((m.group(1), [])) + elif (m := INSTRUCTION.match(line)) and functions: + functions[-1][1].append((m.group(1), m.group(2))) + for name, insns in functions: + mnemonics = [m for m, _ in insns] + # The registers stored, not the address: `stp x29, x30, [sp, #-0x10]!`. + stored = [ops.split("[")[0] for m, ops in insns if m in ("stp", "str", "stur")] + saves_lr = any("x30" in registers for registers in stored) + signs_lr = "pacibsp" in mnemonics + if saves_lr and not signs_lr: + problems.append(f"{obj.name}: {name} saves x30 without pacibsp") + for i, (m, ops) in enumerate(insns): + if m == "blr": + problems.append(f"{obj.name}: {name} calls through a plain blr {ops}") + elif m == "br" and "adr" not in mnemonics[max(0, i - 6) : i]: + problems.append(f"{obj.name}: {name} branches through a plain br {ops}") + return len(functions) + + +def check_data(objdump, obj, functions, problems): + authenticated = 0 + section = None + for line in run(objdump, "-r", obj).splitlines(): + if m := SECTION_HEADER.match(line): + section = m.group(1) + elif (m := RELOCATION.match(line)) and section is not None: + kind, target = m.groups() + if kind == "ARM64_RELOC_AUTHENTICATED_POINTER": + authenticated += 1 + elif ( + kind == "ARM64_RELOC_UNSIGNED" + and section not in UNSIGNED_OK_SECTIONS + and not section.startswith(("__debug", "__apple")) # DWARF names code addresses + ): + if target in functions or target == "__text": + problems.append(f"{obj.name}: [{section}] stores {target} unsigned") + return authenticated + + +def main(): + llvm_bin = pathlib.Path(sys.argv[1]) + archives = [pathlib.Path(a) for a in sys.argv[2:]] + objdump, ar = llvm_bin / "llvm-objdump", llvm_bin / "llvm-ar" + if not archives: + sys.exit("no archives given") + problems = [] + with tempfile.TemporaryDirectory() as tmp: + objects = [] + for archive in archives: + out = pathlib.Path(tmp) / archive.stem + out.mkdir() + run(ar, "x", f"--output={out}", archive) + objects += sorted(out.glob("*.o")) + functions = set().union(*(function_symbols(objdump, o) for o in objects)) + n_functions = sum(check_code(objdump, o, problems) for o in objects) + n_auth = sum(check_data(objdump, o, functions, problems) for o in objects) + print(f"{len(archives)} archives, {len(objects)} objects, {n_functions} functions, " + f"{n_auth} authenticated pointers in data, {len(problems)} problems") + for problem in problems[:200]: + print(problem) + if not objects or not n_functions or not n_auth or problems: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/fork-arm64e.yml b/.github/workflows/fork-arm64e.yml index 9abe3d5f51771..43f74fc659ca3 100644 --- a/.github/workflows/fork-arm64e.yml +++ b/.github/workflows/fork-arm64e.yml @@ -39,7 +39,7 @@ jobs: shell: bash steps: - name: Checkout source - uses: actions/checkout@v5 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 2 submodules: recursive @@ -126,8 +126,19 @@ jobs: - name: Build stage1 arm64e std toolchain id: build_stage1 if: ${{ !cancelled() && steps.check_codegen.outcome == 'success' }} + # Verify the LLVM IR of the arm64e std. This reaches only what the stage1 compiler builds; + # `rust.verify-llvm-ir` would be compiled into rustc and rebuild it. + env: + RUSTFLAGS_NOT_BOOTSTRAP: -Zverify-llvm-ir run: | - python3 x.py build compiler/rustc library/std --stage 1 --target arm64e-apple-darwin + python3 x.py build library/std --stage 1 --target arm64e-apple-darwin + + - name: Check the arm64e std objects + id: std_objects + if: ${{ !cancelled() && steps.build_stage1.outcome == 'success' }} + run: | + python3 -I .github/arm64e/check_objects.py "build/${HOST_TRIPLE}/ci-llvm/bin" \ + build/${HOST_TRIPLE}/stage1/lib/rustlib/arm64e-apple-darwin/lib/*.rlib - name: Prepare arm64e smoke helpers if: ${{ !cancelled() && steps.build_stage1.outcome == 'success' }} @@ -172,6 +183,7 @@ jobs: parser.add_argument("--expected-stdout") parser.add_argument("--expected-stdout-regex") parser.add_argument("--asm-pattern") + parser.add_argument("--opt-level", default="2") args = parser.parse_args() output_base = pathlib.Path(args.output_base) @@ -180,7 +192,7 @@ jobs: args.source, "--target", args.target, - "-O", + f"-Copt-level={args.opt_level}", "--emit", args.emit, "-o", @@ -325,7 +337,7 @@ jobs: pub extern "C" fn target(x: i32) -> i32 { x + 1 } fn main() { - let f: extern "C" fn(i32) -> i32 = target; + let f = std::hint::black_box(target as extern "C" fn(i32) -> i32); println!("{}", f(41)); } EOF @@ -435,16 +447,19 @@ jobs: } EOF - python3 /tmp/arm64e_collect_smoke.py \ - --name scope \ - --rustc "$STAGE1" \ - --source /tmp/arm64e-diagnostics/arm64e_scope.rs \ - --output-base /tmp/arm64e-diagnostics/arm64e_scope \ - --json-out /tmp/arm64e-diagnostics/arm64e_scope.json \ - --emit llvm-ir,asm,link \ - --run-binary \ - --expected-stdout-regex '^constructor=true area=9 twice=42 signed=(true|false)$' \ - --asm-pattern 'braaz|blraaz' + for opt in 2 0; do + python3 /tmp/arm64e_collect_smoke.py \ + --name "scope_O${opt}" \ + --rustc "$STAGE1" \ + --opt-level "$opt" \ + --source /tmp/arm64e-diagnostics/arm64e_scope.rs \ + --output-base "/tmp/arm64e-diagnostics/arm64e_scope_O${opt}" \ + --json-out "/tmp/arm64e-diagnostics/arm64e_scope_O${opt}.json" \ + --emit llvm-ir,asm,link \ + --run-binary \ + --expected-stdout 'constructor=true area=9 twice=42 signed=true' \ + --asm-pattern 'braaz|blraaz' + done - name: Diagnose arm64e unwind runtime id: unwind_diag @@ -494,19 +509,75 @@ jobs: } EOF + for opt in 2 0; do + python3 /tmp/arm64e_collect_smoke.py \ + --name "unwind_O${opt}" \ + --rustc "$STAGE1" \ + --opt-level "$opt" \ + --source /tmp/arm64e-diagnostics/arm64e_unwind.rs \ + --output-base "/tmp/arm64e-diagnostics/arm64e_unwind_O${opt}" \ + --json-out "/tmp/arm64e-diagnostics/arm64e_unwind_O${opt}.json" \ + --emit link \ + --run-binary \ + --expected-stdout 'main=true thread=true dyn=true join=true drops=1' + done + + # The cases above pass on a machine that runs arm64e with pointer authentication off. This + # one does not: a call through the raw, unsigned address of a function must kill the process. + - name: Check that the runner enforces pointer authentication + id: enforce_diag + if: ${{ !cancelled() && steps.build_stage1.outcome == 'success' }} + run: | + STAGE1="build/${HOST_TRIPLE}/stage1/bin/rustc" + + cat > /tmp/arm64e-diagnostics/arm64e_enforce.rs <<'EOF' + use std::hint::black_box; + use std::os::unix::process::ExitStatusExt; + use std::process::Command; + + #[inline(never)] + fn target() -> u32 { + 7 + } + + /// Removes the signature from a code pointer. + fn strip(signed: usize) -> usize { + let mut addr = signed; + unsafe { std::arch::asm!("xpaci {0}", inout(reg) addr, options(nomem, nostack)) }; + addr + } + + fn main() { + let f = black_box(target as fn() -> u32); + let raw = strip(f as usize); + if std::env::args().nth(1).as_deref() == Some("raw") { + // A valid address with no signature, called through an authenticated call. + let unsigned: fn() -> u32 = unsafe { std::mem::transmute(raw) }; + println!("returned {}", black_box(unsigned)()); + return; + } + let status = Command::new(std::env::current_exe().unwrap()).arg("raw").status(); + let raw_call = match status.unwrap() { + s if s.signal().is_some() => format!("signal {}", s.signal().unwrap()), + s => format!("exit {s}"), + }; + println!("signed={} valid_call={} raw_call={raw_call}", f as usize != raw, f()); + } + EOF + python3 /tmp/arm64e_collect_smoke.py \ - --name unwind \ + --name enforce \ --rustc "$STAGE1" \ - --source /tmp/arm64e-diagnostics/arm64e_unwind.rs \ - --output-base /tmp/arm64e-diagnostics/arm64e_unwind \ - --json-out /tmp/arm64e-diagnostics/arm64e_unwind.json \ + --source /tmp/arm64e-diagnostics/arm64e_enforce.rs \ + --output-base /tmp/arm64e-diagnostics/arm64e_enforce \ + --json-out /tmp/arm64e-diagnostics/arm64e_enforce.json \ --emit link \ --run-binary \ - --expected-stdout 'main=true thread=true dyn=true join=true drops=1' + --expected-stdout-regex '^signed=true valid_call=7 raw_call=signal [0-9]+$' - name: Upload arm64e diagnostics if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: arm64e-diagnostics-${{ matrix.runner }} if-no-files-found: ignore @@ -525,8 +596,11 @@ jobs: ("thread_id", "arm64e_thread_id.json"), ("fnptr", "arm64e_fnptr.json"), ("tls", "arm64e_tls_smoke.json"), - ("scope", "arm64e_scope.json"), - ("unwind", "arm64e_unwind.json"), + ("scope -O", "arm64e_scope_O2.json"), + ("scope -O0", "arm64e_scope_O0.json"), + ("unwind -O", "arm64e_unwind_O2.json"), + ("unwind -O0", "arm64e_unwind_O0.json"), + ("enforce", "arm64e_enforce.json"), ] def sanitize(text): @@ -543,12 +617,14 @@ jobs: print("- targeted_tests: ${{ steps.targeted_tests.outcome }}") print("- tidy: ${{ steps.tidy.outcome }}") print("- build_stage1: ${{ steps.build_stage1.outcome }}") + print("- std_objects: ${{ steps.std_objects.outcome }}") print("- hello_diag (non-blocking): ${{ steps.hello_diag.outcome }}") print("- thread_id_diag (non-blocking): ${{ steps.thread_id_diag.outcome }}") print("- fnptr_diag (non-blocking): ${{ steps.fnptr_diag.outcome }}") print("- tls_diag (non-blocking): ${{ steps.tls_diag.outcome }}") print("- scope_diag: ${{ steps.scope_diag.outcome }}") print("- unwind_diag: ${{ steps.unwind_diag.outcome }}") + print("- enforce_diag: ${{ steps.enforce_diag.outcome }}") print() print("| case | compile | run | rc/signal | file | stdout | asm hits |") print("| --- | --- | --- | --- | --- | --- | --- |") @@ -577,7 +653,7 @@ jobs: f"{file_output} | {stdout} | {asm_hits} |" ) print() - print("The hello, thread_id, fnptr and tls cases are non-blocking diagnostics. The scope and unwind cases, the tests, tidy and the stage1 build are blocking.") + print("The hello, thread_id, fnptr and tls cases are non-blocking diagnostics. Everything else is blocking.") PY { From e31c9c92c23dc9966f64a8bbd1541aa34802bee4 Mon Sep 17 00:00:00 2001 From: cypherair <262752927+cypherair@users.noreply.github.com> Date: Tue, 10 Nov 2026 10:37:47 -0800 Subject: [PATCH 21/23] test(arm64e): fix the direct-callees test and the tidy line-length opt-out The direct-callees test did not compile: an internal lang item cannot be #[no_mangle], and drop glue needs the copy lang item. Name the personality by pattern, and declare destruct and copy as minicore does. Tidy's own checks ran for the first time and rejected lines over 100 columns. In 1.100's tidy, `// ignore-tidy-linelength` applies to its own line only; a whole file needs `// ignore-tidy-file-linelength`. --- .../arm64e-apple-ptrauth-direct-callees.rs | 11 ++++++++--- tests/codegen-llvm/arm64e-apple-ptrauth-init-term.rs | 2 +- tests/codegen-llvm/arm64e-apple-ptrauth-invoke.rs | 2 +- tests/codegen-llvm/arm64e-apple-ptrauth-scope.rs | 2 +- tests/codegen-llvm/arm64e-apple-ptrauth.rs | 2 +- .../invalid/branch-protection-arm64e-apple-pac-ret.rs | 2 +- .../target-feature-arm64e-apple-ptrauth-disable.rs | 2 +- .../arm64e_apple_fixed_pointer_authentication.rs | 2 +- 8 files changed, 15 insertions(+), 10 deletions(-) diff --git a/tests/codegen-llvm/arm64e-apple-ptrauth-direct-callees.rs b/tests/codegen-llvm/arm64e-apple-ptrauth-direct-callees.rs index 610cb433e35df..f70a84d3b875e 100644 --- a/tests/codegen-llvm/arm64e-apple-ptrauth-direct-callees.rs +++ b/tests/codegen-llvm/arm64e-apple-ptrauth-direct-callees.rs @@ -1,4 +1,4 @@ -// ignore-tidy-linelength +// ignore-tidy-file-linelength // Callees that codegen calls directly stay unsigned and carry no `ptrauth` bundle, even // unoptimised: drop glue, a lang item (the abort when a `nounwind` function would unwind), and // the personality, which unwind tables name. minicore has no `Drop` impl and no personality, so @@ -26,6 +26,12 @@ pub trait MetaSized: PointeeSized {} #[lang = "sized"] pub trait Sized: MetaSized {} +#[lang = "destruct"] +pub trait Destruct: PointeeSized {} + +#[lang = "copy"] +pub trait Copy: Sized {} + #[lang = "legacy_receiver"] pub trait LegacyReceiver: PointeeSized {} @@ -45,7 +51,6 @@ fn panic_cannot_unwind() -> ! { } #[lang = "eh_personality"] -#[unsafe(no_mangle)] extern "C" fn rust_eh_personality() {} pub struct Guard; @@ -69,7 +74,7 @@ unsafe extern "C-unwind" { pub fn drop_guard(_guard: Guard) {} // CHECK-LABEL: @no_unwind_boundary( -// CHECK-SAME: personality ptr @rust_eh_personality +// CHECK-SAME: personality ptr @{{[^ ]*}}rust_eh_personality // CHECK-NOT: "ptrauth" // CHECK-NOT: ptrauth (ptr // CHECK: invoke void @may_unwind() diff --git a/tests/codegen-llvm/arm64e-apple-ptrauth-init-term.rs b/tests/codegen-llvm/arm64e-apple-ptrauth-init-term.rs index 9d185d714d9c8..beb83a7654f23 100644 --- a/tests/codegen-llvm/arm64e-apple-ptrauth-init-term.rs +++ b/tests/codegen-llvm/arm64e-apple-ptrauth-init-term.rs @@ -1,4 +1,4 @@ -// ignore-tidy-linelength +// ignore-tidy-file-linelength // On arm64e Apple targets the entries of `__mod_init_func` and `__mod_term_func` stay unsigned, // whether the section is named or typed, like the constructors clang emits itself. (Clang signs a // static that C code places in these sections by hand.) Either form works: Apple's linker turns diff --git a/tests/codegen-llvm/arm64e-apple-ptrauth-invoke.rs b/tests/codegen-llvm/arm64e-apple-ptrauth-invoke.rs index afb6ae1740f33..99d5340beda78 100644 --- a/tests/codegen-llvm/arm64e-apple-ptrauth-invoke.rs +++ b/tests/codegen-llvm/arm64e-apple-ptrauth-invoke.rs @@ -1,4 +1,4 @@ -// ignore-tidy-linelength +// ignore-tidy-file-linelength // `__rust_try`, which rustc generates for `catch_unwind`, carries the pointer-authentication // attributes, and its calls through the `try_fn` and `catch_fn` pointers authenticate the callee. //@ add-minicore diff --git a/tests/codegen-llvm/arm64e-apple-ptrauth-scope.rs b/tests/codegen-llvm/arm64e-apple-ptrauth-scope.rs index 73f1d23042256..6923de3f58766 100644 --- a/tests/codegen-llvm/arm64e-apple-ptrauth-scope.rs +++ b/tests/codegen-llvm/arm64e-apple-ptrauth-scope.rs @@ -1,4 +1,4 @@ -// ignore-tidy-linelength +// ignore-tidy-file-linelength // On arm64e Apple targets every function address that becomes a value is signed with key IA and // discriminator 0, whatever the function's ABI: function pointers, closures, constants, statics, // `extern_weak` statics and v-table entries. Every indirect call authenticates its callee, and diff --git a/tests/codegen-llvm/arm64e-apple-ptrauth.rs b/tests/codegen-llvm/arm64e-apple-ptrauth.rs index 690a0682a8e85..fee3859adf892 100644 --- a/tests/codegen-llvm/arm64e-apple-ptrauth.rs +++ b/tests/codegen-llvm/arm64e-apple-ptrauth.rs @@ -1,4 +1,4 @@ -// ignore-tidy-linelength +// ignore-tidy-file-linelength // Every function on an arm64e Apple target carries the four attributes that clang's Darwin driver // turns on, and none of the ELF PAuth ABI's attributes or module flags. //@ add-minicore diff --git a/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.rs b/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.rs index c91e13fd7f650..b02edc5976790 100644 --- a/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.rs +++ b/tests/ui/compile-flags/invalid/branch-protection-arm64e-apple-pac-ret.rs @@ -1,4 +1,4 @@ -// ignore-tidy-linelength +// ignore-tidy-file-linelength // arm64e Apple targets sign return addresses through their ABI, so they reject `pac-ret`. //@ ignore-backends: gcc //@ revisions: DARWIN_PACRET DARWIN_PAUTHLR_LEAF IOS_PACRET IOS_PAUTHLR_LEAF TVOS_PACRET TVOS_PAUTHLR_LEAF VISIONOS_PACRET VISIONOS_PAUTHLR_LEAF diff --git a/tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.rs b/tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.rs index 3bef9f9ed0186..035145284fc6e 100644 --- a/tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.rs +++ b/tests/ui/compile-flags/invalid/target-feature-arm64e-apple-ptrauth-disable.rs @@ -1,4 +1,4 @@ -// ignore-tidy-linelength +// ignore-tidy-file-linelength // `-Ctarget-feature` cannot turn pointer authentication off on arm64e Apple targets, under // Rust's feature names or LLVM's. //@ ignore-backends: gcc diff --git a/tests/ui/pointer_authentication/arm64e_apple_fixed_pointer_authentication.rs b/tests/ui/pointer_authentication/arm64e_apple_fixed_pointer_authentication.rs index 04b6b4439e768..ab273adb367a3 100644 --- a/tests/ui/pointer_authentication/arm64e_apple_fixed_pointer_authentication.rs +++ b/tests/ui/pointer_authentication/arm64e_apple_fixed_pointer_authentication.rs @@ -1,4 +1,4 @@ -// ignore-tidy-linelength +// ignore-tidy-file-linelength // The arm64e ABI fixes pointer authentication on Apple targets, so `-Z pointer-authentication` is // an error there rather than an option that is quietly ignored. //@ ignore-backends: gcc From 3351bf7ca65654f747b30a5d8a56a8904cff259e Mon Sep 17 00:00:00 2001 From: cypherair <262752927+cypherair@users.noreply.github.com> Date: Tue, 10 Nov 2026 10:37:47 -0800 Subject: [PATCH 22/23] ci(arm64e): make every runtime case blocking hello, thread id, fn pointer and thread-local were non-blocking in case a runner could not run arm64e, or ran it with the keys off. Neither holds on this image, the enforcement case now catches the second, and a non-blocking step is how the landing-pad trap once showed green. The object check also fails when it finds no function symbols, which its data check needs. --- .github/arm64e/check_objects.py | 3 ++- .github/workflows/fork-arm64e.yml | 14 +++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/.github/arm64e/check_objects.py b/.github/arm64e/check_objects.py index 64dfde17f08fb..ca54058322e88 100644 --- a/.github/arm64e/check_objects.py +++ b/.github/arm64e/check_objects.py @@ -108,7 +108,8 @@ def main(): f"{n_auth} authenticated pointers in data, {len(problems)} problems") for problem in problems[:200]: print(problem) - if not objects or not n_functions or not n_auth or problems: + # The data check needs the function symbols to recognise a relocation to a function. + if not objects or not functions or not n_functions or not n_auth or problems: sys.exit(1) diff --git a/.github/workflows/fork-arm64e.yml b/.github/workflows/fork-arm64e.yml index 43f74fc659ca3..e7e68fcff82fc 100644 --- a/.github/workflows/fork-arm64e.yml +++ b/.github/workflows/fork-arm64e.yml @@ -282,7 +282,6 @@ jobs: - name: Diagnose arm64e hello runtime id: hello_diag if: ${{ !cancelled() && steps.build_stage1.outcome == 'success' }} - continue-on-error: true run: | STAGE1="build/${HOST_TRIPLE}/stage1/bin/rustc" @@ -305,7 +304,6 @@ jobs: - name: Diagnose arm64e thread id runtime id: thread_id_diag if: ${{ !cancelled() && steps.build_stage1.outcome == 'success' }} - continue-on-error: true run: | STAGE1="build/${HOST_TRIPLE}/stage1/bin/rustc" @@ -328,7 +326,6 @@ jobs: - name: Diagnose arm64e function pointer runtime id: fnptr_diag if: ${{ !cancelled() && steps.build_stage1.outcome == 'success' }} - continue-on-error: true run: | STAGE1="build/${HOST_TRIPLE}/stage1/bin/rustc" @@ -362,7 +359,6 @@ jobs: - name: Diagnose arm64e TLS runtime id: tls_diag if: ${{ !cancelled() && steps.build_stage1.outcome == 'success' }} - continue-on-error: true run: | STAGE1="build/${HOST_TRIPLE}/stage1/bin/rustc" @@ -618,10 +614,10 @@ jobs: print("- tidy: ${{ steps.tidy.outcome }}") print("- build_stage1: ${{ steps.build_stage1.outcome }}") print("- std_objects: ${{ steps.std_objects.outcome }}") - print("- hello_diag (non-blocking): ${{ steps.hello_diag.outcome }}") - print("- thread_id_diag (non-blocking): ${{ steps.thread_id_diag.outcome }}") - print("- fnptr_diag (non-blocking): ${{ steps.fnptr_diag.outcome }}") - print("- tls_diag (non-blocking): ${{ steps.tls_diag.outcome }}") + print("- hello_diag: ${{ steps.hello_diag.outcome }}") + print("- thread_id_diag: ${{ steps.thread_id_diag.outcome }}") + print("- fnptr_diag: ${{ steps.fnptr_diag.outcome }}") + print("- tls_diag: ${{ steps.tls_diag.outcome }}") print("- scope_diag: ${{ steps.scope_diag.outcome }}") print("- unwind_diag: ${{ steps.unwind_diag.outcome }}") print("- enforce_diag: ${{ steps.enforce_diag.outcome }}") @@ -653,7 +649,7 @@ jobs: f"{file_output} | {stdout} | {asm_hits} |" ) print() - print("The hello, thread_id, fnptr and tls cases are non-blocking diagnostics. Everything else is blocking.") + print("Every step is blocking. A runner that cannot run arm64e fails the runtime cases.") PY { From 76fbeb9f04d552038abc0349a353725b9cbb05cd Mon Sep 17 00:00:00 2001 From: cypherair <262752927+cypherair@users.noreply.github.com> Date: Tue, 10 Nov 2026 11:05:41 -0800 Subject: [PATCH 23/23] test(arm64e): declare the marker lang items the direct-callees test needs With the earlier errors fixed, rustc asked for the freeze lang item: the &mut arguments of drop glue and Drop::drop query Freeze, UnsafeUnpin and Unpin. Declare all three as minicore does. --- .../arm64e-apple-ptrauth-direct-callees.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/codegen-llvm/arm64e-apple-ptrauth-direct-callees.rs b/tests/codegen-llvm/arm64e-apple-ptrauth-direct-callees.rs index f70a84d3b875e..13baaa1af720e 100644 --- a/tests/codegen-llvm/arm64e-apple-ptrauth-direct-callees.rs +++ b/tests/codegen-llvm/arm64e-apple-ptrauth-direct-callees.rs @@ -14,7 +14,7 @@ //@ [VISIONOS] needs-llvm-components: aarch64 #![crate_type = "lib"] -#![feature(no_core, lang_items, rustc_attrs)] +#![feature(auto_traits, no_core, lang_items, rustc_attrs)] #![no_core] #[lang = "pointee_sized"] @@ -32,6 +32,15 @@ pub trait Destruct: PointeeSized {} #[lang = "copy"] pub trait Copy: Sized {} +#[lang = "freeze"] +pub unsafe auto trait Freeze {} + +#[lang = "unsafe_unpin"] +pub unsafe auto trait UnsafeUnpin {} + +#[lang = "unpin"] +pub auto trait Unpin {} + #[lang = "legacy_receiver"] pub trait LegacyReceiver: PointeeSized {}