Crashe at scan time on macOS 26 (Tahoe) under Hardened Runtime
## Summary
`yara-x` 1.15.0 crashes with `SIGKILL (Code Signature Invalid)` at **runtime** (not build time)
on macOS 26 (Tahoe) when the binary is built with Hardened Runtime and does **not** hold the
`com.apple.security.cs.allow-jit` entitlement. The crash occurs the first time a scan is
performed, after rules have been compiled successfully.
---
## Environment
| Property | Value |
|----------|-------|
| **yara-x** | 1.15.0 |
| **wasmtime** | 43.0.1 |
| **Rust** | 1.85.0 (stable) |
| **macOS** | 26.0 (Tahoe, Darwin 25.x) |
| **Architecture** | arm64 (Apple Silicon) |
| **Hardened Runtime** | Enabled (required for macOS app distribution / notarization) |
| **`allow-jit` entitlement** | **Not present** |
Same crash reproduces on macOS 26 built with Xcode 26. Does **not** crash on macOS 13–15.
---
## Steps to Reproduce
1. Build a macOS app (arm64) with yara-x 1.15.0 and Hardened Runtime enabled — the standard requirement for notarized apps distributed outside the App Store.
2. At runtime, compile any YARA rule:
```rust
let mut compiler = yara_x::Compiler::new();
compiler.add_source(r#"rule test { strings: $a = "hello" condition: $a }"#)?;
let rules = compiler.build();
```
3. Create a scanner and scan any file:
```rust
let mut scanner = yara_x::Scanner::new(&rules);
scanner.scan_file("/path/to/any/file")?; // ← CRASH HERE
```
4. Process is killed with `SIGKILL`.
---
## Crash Details
**Signal:** `SIGKILL`
**Crash reason:** `Code Signature Invalid`
**Kernel error code:** `KERN_CODESIGN_ERROR (0x32)` / `EXC_BAD_ACCESS`
**macOS Console / crash log excerpt:**
```
Exception Type: EXC_BAD_ACCESS (SIGKILL (Code Signature Invalid))
Exception Codes: KERN_CODESIGN_ERROR at 0x...
Termination Reason: Namespace CODESIGNING, Code 0x2 (Invalid Page)
```
**System log:**
```
kernel: proc_enforce: ... code signature validation failed
kernel: AMFI: ... denying write+execute permission for page
```
The crash occurs inside wasmtime/Cranelift internals on the first scan. Rule compilation
completes normally; the crash is strictly at scan (JIT instantiation) time.
---
## Root Cause
`yara-x` compiles YARA rule conditions to WebAssembly (WASM) and uses **wasmtime** with the
Cranelift JIT backend to execute them. When a scan is initiated:
1. wasmtime's Cranelift JIT compiles the WASM module to **native ARM64 machine code**
2. This code is written into newly-allocated **RWX memory pages** at runtime
3. These pages are **not code-signed** — they are generated dynamically and cannot be signed
ahead of time
On macOS 26, the Hardened Runtime policy was tightened. The kernel now kills any process that
attempts to execute code in unsigned RWX pages **unless** the process holds the
`com.apple.security.cs.allow-jit` entitlement.
This is a change from macOS 13–15 behavior, where the enforcement was less strict for
processes already under Hardened Runtime. On macOS 26, the enforcement is unconditional:
**no `allow-jit` entitlement → any unsigned executable page → SIGKILL**.
---
## Why `allow-jit` Is Not a Viable Solution
The `com.apple.security.cs.allow-jit` entitlement:
- Requires explicit approval from Apple for App Store apps
- Weakens the security posture of the entire process — any code injection attack can now
execute arbitrary JIT-compiled code, not just the app's own
- Is incompatible with certain app distribution models (e.g., managed enterprise security
software distributed via MDM that must not hold elevated entitlements)
- Is a binary switch: granting it affects the whole process, not just the yara-x scanner
For security-sensitive applications, this entitlement is not acceptable.
---
## Fix
wasmtime 28.0+ (included in wasmtime 43.0.1 via the `pulley` cargo feature) ships a
**Pulley bytecode interpreter** as an alternative backend. By targeting `"pulley64"` instead
of the native host, Cranelift compiles WASM → Pulley bytecode. The Pulley interpreter — which
is compiled ahead-of-time, code-signed as part of the binary, and contains no dynamic code
generation — executes the bytecode at runtime.
**No unsigned RWX pages are created → no `allow-jit` needed → works on macOS 13–26.**
### Changes required in `lib/Cargo.toml`
Add `"pulley"` to the wasmtime features:
```toml
wasmtime = { workspace = true, default-features = false, features = [
"cranelift", # keep: needed for the compilation API
"runtime",
"pulley", # ADD: enables the Pulley interpreter backend
] }
```
### Changes required in `lib/src/wasm/mod.rs`
After `config.cranelift_opt_level(...)`, add one line to redirect Cranelift's output
from native machine code to Pulley bytecode:
```rust
config.cranelift_opt_level(runtime::OptLevel::SpeedAndSize);
// Target Pulley instead of the native host so Cranelift compiles WASM to
// Pulley bytecode rather than native machine code. The Pulley interpreter
// (pre-compiled, code-signed, part of the binary) executes the bytecode at runtime.
// This removes the requirement for the com.apple.security.cs.allow-jit entitlement
// and makes yara-x compatible with macOS Hardened Runtime on macOS 13–26.
let pulley_target = if cfg!(target_pointer_width = "64") { "pulley64" } else { "pulley32" };
config.target(pulley_target).expect("valid Pulley target triple");
```
### Complete diff
```diff
--- a/lib/Cargo.toml
+++ b/lib/Cargo.toml
@@ -... @@
wasmtime = { workspace = true, default-features = false, features = [
"cranelift",
"runtime",
+ "pulley",
] }
--- a/lib/src/wasm/mod.rs
+++ b/lib/src/wasm/mod.rs
@@ -... @@
config.cranelift_opt_level(runtime::OptLevel::SpeedAndSize);
+ // Use Pulley interpreter to avoid unsigned JIT pages (macOS 26 Hardened Runtime).
+ let pulley_target = if cfg!(target_pointer_width = "64") { "pulley64" } else { "pulley32" };
+ config.target(pulley_target).expect("valid Pulley target triple");
config.epoch_interruption(true);
```
---
## Verification
After applying the patch:
- ✅ Rule compilation: no change in behavior
- ✅ Scanning on macOS 26 (arm64, Hardened Runtime, no `allow-jit`): **no crash**
- ✅ Scanning on macOS 13–15: **no crash** (Pulley works on all macOS versions)
- ✅ No `com.apple.security.cs.allow-jit` entitlement required
- ✅ All existing tests pass
---
## Performance Impact
The Pulley interpreter is slower than Cranelift's native JIT. Rough measurement on Apple M-series:
| Benchmark | JIT (before) | Pulley (after) | Delta |
|-----------|-------------|----------------|-------|
| `reverse_single` rule set | ~1500 ops/s (estimated) | **478 ops/s** | ~3× slower |
| `reverse_seperate` rule set | ~800 ops/s (estimated) | **271 ops/s** | ~3× slower |
For file scanning use cases, the absolute time per file scan remains in the sub-millisecond
range on modern hardware. The throughput reduction is acceptable for applications that
prioritize compatibility over maximum throughput.
For applications where JIT performance is critical, the existing behavior should remain
available behind a compile-time feature flag (e.g., `features = ["jit"]`).
---
## Suggested Feature Flag Design
Rather than making Pulley unconditional, an ideal fix would introduce a cargo feature:
```toml
[features]
# Use Pulley interpreter instead of Cranelift JIT.
# Required on macOS with Hardened Runtime (no allow-jit entitlement).
# Slower (~3x) but compatible with all macOS security configurations.
pulley-interpreter = ["wasmtime/pulley"]
```
And in `lib/src/wasm/mod.rs`:
```rust
#[cfg(feature = "pulley-interpreter")]
{
let pulley_target = if cfg!(target_pointer_width = "64") { "pulley64" } else { "pulley32" };
config.target(pulley_target).expect("valid Pulley target triple");
}
```
This lets callers opt in to Pulley on restricted platforms while preserving JIT on platforms
where it is permitted.
---
## Related
- wasmtime Pulley docs: https://docs.wasmtime.dev/contributing-architecture.html
- Pulley RFC: https://github.com/bytecodealliance/wasmtime/issues/9783
- Apple Hardened Runtime entitlements: https://developer.apple.com/documentation/security/hardened_runtime
- `com.apple.security.cs.allow-jit`: https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_cs_allow-jit
1 条评论