Skip to content

Fix native stack-walking on macOS arm64 - #14303

Open
macovedj wants to merge 1 commit into
bytecodealliance:mainfrom
macovedj:arm64-native-unwind
Open

Fix native stack-walking on macOS arm64#14303
macovedj wants to merge 1 commit into
bytecodealliance:mainfrom
macovedj:arm64-native-unwind

Conversation

@macovedj

@macovedj macovedj commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

macOS disables pointer authentication for ordinary arm64 executables. This PR checks the main executable’s Mach-O header and enables return-address signing by default only for arm64e processes.

Previously, Cranelift generated unwind metadata that incorrectly described unsigned return addresses as signed. This caused native backtraces captured inside host imports to stop before recovering the Wasm callers.

The issue was discovered while developing native stack-walking tests in #14304.

@macovedj
macovedj requested review from a team as code owners September 9, 2026 12:54
@macovedj
macovedj requested review from alexcrichton and removed request for a team September 9, 2026 12:54
@github-actions github-actions Bot added cranelift Issues related to the Cranelift code generator wasmtime:api Related to the API of the `wasmtime` crate itself labels Sep 9, 2026
Comment thread cranelift/native/src/macos.rs Outdated
Comment on lines +3 to +19
pub(super) fn is_arm64e() -> bool {
// The main executable, rather than the architecture of this library,
// determines whether macOS enables pointer authentication for the process.
// Image zero is the main executable and its header remains mapped while
// the process is running.
// SAFETY: dyld supplies a pointer to the loaded Mach-O header. Only the
// CPU subtype field in the common 32-/64-bit header prefix is read.
let Some(header) = (unsafe { mach2::dyld::_dyld_get_image_header(0).as_ref() }) else {
// Preserve signing if the process ABI cannot be determined.
return true;
};

// mach/machine.h: the high byte contains capability and ABI-version bits.
const CPU_SUBTYPE_MASK: u32 = 0xff00_0000;
const CPU_SUBTYPE_ARM64E: u32 = 2;
(header.cpusubtype as u32 & !CPU_SUBTYPE_MASK) == CPU_SUBTYPE_ARM64E
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Question on this, as I'm pretty unfamiliar with this part of macos -- Rust has a arm64e-apple-darwin in contrast to the normal aarch64-apple-darwin target. I naively thought that the normal target used pointer authentication everywhere, but I suppose it doesn't and the arm64e target does? Is it possible to use aarch64-apple-darwin libraries within an arm64e executable? If so, then this check seems required, but if not then another option might be to detect when the target arch is arm64e which would probably require a build script (I don't see a #[cfg] in Rust for this)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I’m also getting familiar with this part of macos. From what I can tell, aarch64-apple-darwin uses the ordinary arm64 ABI. It’s also possible to run arm64e binaries with pointer authentication disabled, and when disabled, these targets can mix through dynamically loaded libraries.

This means my original check isn’t sufficient, since it checks the main executable’s architecture rather than whether authentication is actually enabled, so I replaced it with thread_get_state query that checks the runtime authentication state.

I’m not sure how prevalent these mixed configurations are, although it seems LLVM explicitly documents them, and I was able to reproduce locally. So a build script check for compilation target would definitely miss this case, but if you think that's acceptable then we could still consider it

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Man every time I think I know how pointer authentication works in the "real world" new things come up and turns out I have no idea what's happening...

I can't exactly claim to know whether what you're adding in this PR is correct myself -- would you be able to link to documentation of these constants and/or the APIs here to learn about whether it's dynamically enabled?

Comment thread crates/wasmtime/tests/native_unwind_pauth.rs Outdated
Comment thread crates/wasmtime/tests/native_unwind_pauth.rs Outdated
@macovedj
macovedj force-pushed the arm64-native-unwind branch from 2232fa6 to b68942b Compare September 9, 2026 19:38
@@ -117,6 +119,10 @@ harness = false
name = "pooling_alloc_near_oom"
harness = false

[[test]]
name = "native_backtrace"
required-features = ["cranelift", "runtime", "std", "wat"]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's to not bother with this, most tests in Wasmtime break when features are disabled and we don't try to keep that working. It's assumed the default features are enabled when tests are running

@@ -0,0 +1,112 @@
#![cfg(all(any(unix, windows), has_host_compiler_backend, not(miri)))]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's ok to trim this down to just not(miri), everything else should naturally fall out of the testing matrix

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ah I forgot to ask from prior, but could this be moved to the tests/all/*.rs suite?

Comment thread cranelift/native/src/macos.rs Outdated
Comment on lines +3 to +19
pub(super) fn is_arm64e() -> bool {
// The main executable, rather than the architecture of this library,
// determines whether macOS enables pointer authentication for the process.
// Image zero is the main executable and its header remains mapped while
// the process is running.
// SAFETY: dyld supplies a pointer to the loaded Mach-O header. Only the
// CPU subtype field in the common 32-/64-bit header prefix is read.
let Some(header) = (unsafe { mach2::dyld::_dyld_get_image_header(0).as_ref() }) else {
// Preserve signing if the process ABI cannot be determined.
return true;
};

// mach/machine.h: the high byte contains capability and ABI-version bits.
const CPU_SUBTYPE_MASK: u32 = 0xff00_0000;
const CPU_SUBTYPE_ARM64E: u32 = 2;
(header.cpusubtype as u32 & !CPU_SUBTYPE_MASK) == CPU_SUBTYPE_ARM64E
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Man every time I think I know how pointer authentication works in the "real world" new things come up and turns out I have no idea what's happening...

I can't exactly claim to know whether what you're adding in this PR is correct myself -- would you be able to link to documentation of these constants and/or the APIs here to learn about whether it's dynamically enabled?

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

Labels

cranelift Issues related to the Cranelift code generator wasmtime:api Related to the API of the `wasmtime` crate itself

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants