Fix native stack-walking on macOS arm64 - #14303
Conversation
| 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 | ||
| } |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
2232fa6 to
b68942b
Compare
| @@ -117,6 +119,10 @@ harness = false | |||
| name = "pooling_alloc_near_oom" | |||
| harness = false | |||
|
|
|||
| [[test]] | |||
| name = "native_backtrace" | |||
| required-features = ["cranelift", "runtime", "std", "wat"] | |||
There was a problem hiding this comment.
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)))] | |||
There was a problem hiding this comment.
It's ok to trim this down to just not(miri), everything else should naturally fall out of the testing matrix
There was a problem hiding this comment.
Ah I forgot to ask from prior, but could this be moved to the tests/all/*.rs suite?
| 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 | ||
| } |
There was a problem hiding this comment.
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?
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.