diff --git a/crates/headroom-core/src/transforms/detection.rs b/crates/headroom-core/src/transforms/detection.rs
index 386c28b57..3fabe96af 100644
--- a/crates/headroom-core/src/transforms/detection.rs
+++ b/crates/headroom-core/src/transforms/detection.rs
@@ -28,6 +28,14 @@
//! issue. Loud-on-error stays at the [`magika_detect`] entry point for
//! callers who care; the chain swallows the err with a log line.
//!
+//! # CPU compatibility
+//!
+//! Precompiled ONNX Runtime binaries from `ort-sys` may contain AVX2-family
+//! instructions. On x86/x86_64 CPUs where AVX2 is unavailable, the magika
+//! session init returns an init error before touching ONNX. The chain
+//! handles this identically to any other tier-1 error — logs it and falls
+//! through to Tier 2 / Tier 3.
+//!
//! # SearchResults / BuildOutput
//!
//! The retired regex detector recognized grep-style search output
@@ -84,6 +92,7 @@ pub fn detect(content: &str) -> ContentType {
#[cfg(test)]
mod tests {
use super::*;
+ use crate::transforms::magika_detector::magika_onnx_runtime_supported_by_cpu;
#[test]
fn empty_input_short_circuits_to_plain_text() {
@@ -93,19 +102,38 @@ mod tests {
#[test]
fn json_array_routes_via_tier_1() {
let payload = r#"[{"id": 1}, {"id": 2}, {"id": 3}]"#;
- assert_eq!(detect(payload), ContentType::JsonArray);
+ if !magika_onnx_runtime_supported_by_cpu() {
+ // On no-AVX2 hosts, magika returns Err and the chain
+ // falls through to Tier 2 (unidiff — no match for JSON)
+ // then Tier 3 (PlainText).
+ assert_eq!(detect(payload), ContentType::PlainText);
+ } else {
+ assert_eq!(detect(payload), ContentType::JsonArray);
+ }
}
#[test]
fn source_code_routes_via_tier_1() {
let py = "def hello():\n print('world')\n\nclass Foo:\n pass\n";
- assert_eq!(detect(py), ContentType::SourceCode);
+ if !magika_onnx_runtime_supported_by_cpu() {
+ // Magika fallthrough — unidiff won't catch Python source,
+ // so the chain lands on PlainText.
+ assert_eq!(detect(py), ContentType::PlainText);
+ } else {
+ assert_eq!(detect(py), ContentType::SourceCode);
+ }
}
#[test]
fn html_routes_via_tier_1() {
let html = "
x
";
- assert_eq!(detect(html), ContentType::Html);
+ if !magika_onnx_runtime_supported_by_cpu() {
+ // Magika fallthrough — unidiff won't catch HTML,
+ // so the chain lands on PlainText.
+ assert_eq!(detect(html), ContentType::PlainText);
+ } else {
+ assert_eq!(detect(html), ContentType::Html);
+ }
}
#[test]
@@ -118,7 +146,8 @@ mod tests {
+ print(\"new\")\n";
// Either magika tags it `diff` (Tier 1 hit) or magika
// mis-classifies as text and unidiff catches it (Tier 2).
- // Both paths produce GitDiff.
+ // On no-AVX2 hosts, magika is unavailable so Tier 2 still
+ // catches the diff. Both paths produce GitDiff.
assert_eq!(detect(diff), ContentType::GitDiff);
}
@@ -127,7 +156,7 @@ mod tests {
// Magika often mis-classifies naked hunks (no `diff --git`
// wrapper) because the visible bytes look like ordinary
// patch lines mixed with code. Tier 2 (unidiff parser)
- // catches these.
+ // catches these — even when magika is unavailable.
let diff = "--- a/foo.py\n\
+++ b/foo.py\n\
@@ -1,2 +1,2 @@\n\
@@ -195,7 +224,13 @@ mod tests {
// YAML lives in magika's `code` group; the chain returns it
// as SourceCode so the router picks the code-aware compressor.
let yaml = "name: my-app\nversion: 1.0\ndependencies:\n - foo\n";
- assert_eq!(detect(yaml), ContentType::SourceCode);
+ if !magika_onnx_runtime_supported_by_cpu() {
+ // Magika fallthrough — unidiff won't catch YAML,
+ // so the chain lands on PlainText.
+ assert_eq!(detect(yaml), ContentType::PlainText);
+ } else {
+ assert_eq!(detect(yaml), ContentType::SourceCode);
+ }
}
#[test]
@@ -205,13 +240,21 @@ mod tests {
impl Counter {\n \
pub fn new() -> Self { Self { counts: HashMap::new() } }\n\
}\n";
- assert_eq!(detect(rs), ContentType::SourceCode);
+ if !magika_onnx_runtime_supported_by_cpu() {
+ // Magika fallthrough — unidiff won't catch Rust source,
+ // so the chain lands on PlainText.
+ assert_eq!(detect(rs), ContentType::PlainText);
+ } else {
+ assert_eq!(detect(rs), ContentType::SourceCode);
+ }
}
#[test]
fn chain_is_deterministic_across_repeated_calls() {
// Magika returns the same label for identical input on
// repeated calls; the chain wraps that determinism.
+ // On no-AVX2 hosts, the chain always falls through to
+ // PlainText — which is equally deterministic.
let payload = r#"{"users": [{"id": 1}, {"id": 2}]}"#;
let a = detect(payload);
let b = detect(payload);
diff --git a/crates/headroom-core/src/transforms/magika_detector.rs b/crates/headroom-core/src/transforms/magika_detector.rs
index 7e0cea970..90c71d9c7 100644
--- a/crates/headroom-core/src/transforms/magika_detector.rs
+++ b/crates/headroom-core/src/transforms/magika_detector.rs
@@ -28,6 +28,12 @@
//! - **No router rewiring here.** PR3 lands the detector + tests
//! only. PR5 flips the ContentRouter to call us instead of the
//! regex-based [`crate::transforms::content_detector`].
+//!
+//! - **CPU compatibility.** The precompiled ONNX Runtime binary shipped by
+//! `ort-sys` may contain AVX2-family instructions on x86/x86_64. Where
+//! AVX2 is unavailable on those targets, the session init returns an
+//! error early instead of crashing with SIGILL; the detection chain then
+//! falls through to Tier 2 and Tier 3 normally.
use std::sync::mpsc;
use std::sync::{Mutex, OnceLock};
@@ -39,6 +45,25 @@ use tracing;
use crate::transforms::content_detector::ContentType;
+/// Check whether the CPU can run the precompiled ONNX Runtime binary
+/// that magika depends on.
+///
+/// On x86/x86_64 without AVX2, the `onnxruntime` shared library shipped
+/// by `ort-sys` can contain AVX2-family instructions that will SIGILL.
+/// We detect this up front so the magika session init can fail gracefully
+/// instead of crashing.
+///
+/// On non-x86 targets, this x86-specific AVX2 gate is not applied.
+#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
+pub(crate) fn magika_onnx_runtime_supported_by_cpu() -> bool {
+ std::is_x86_feature_detected!("avx2")
+}
+
+#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
+pub(crate) fn magika_onnx_runtime_supported_by_cpu() -> bool {
+ true
+}
+
/// Errors from the magika detector. Wraps the underlying `magika::Error`
/// so callers can match on whether init or inference broke without
/// pulling magika types into their imports.
@@ -102,6 +127,17 @@ fn magika_init_timeout() -> Duration {
fn session() -> &'static Mutex> {
MAGIKA_SESSION.get_or_init(|| {
+ // Early-out if the CPU can't run the precompiled ONNX Runtime.
+ // Without this check the `onnxruntime` shared library will crash
+ // with SIGILL on x86 CPUs lacking AVX2.
+ if !magika_onnx_runtime_supported_by_cpu() {
+ return Mutex::new(Err(
+ "Magika ONNX Runtime backend requires AVX2 on this platform; \
+ falling back to non-Magika detection"
+ .to_string(),
+ ));
+ }
+
let timeout = magika_init_timeout();
let (tx, rx) = mpsc::channel();
// Run the (potentially hanging) ONNX init on a side thread so we
@@ -234,9 +270,26 @@ mod tests {
use super::*;
fn assert_detect(content: &str, expected: ContentType, hint: &str) {
- match magika_detect(content) {
- Ok(got) => assert_eq!(got, expected, "{hint}: expected {expected:?}, got {got:?}"),
- Err(e) => panic!("{hint}: detection failed: {e}"),
+ if !magika_onnx_runtime_supported_by_cpu() {
+ // On x86 hosts without AVX2 the magika session returns Err
+ // before any ONNX init — assert graceful degradation
+ // rather than panicking.
+ match magika_detect(content) {
+ Err(MagikaDetectorError::Init(msg)) => {
+ assert!(
+ msg.contains("AVX2"),
+ "{hint}: expected AVX2 error, got: {msg}"
+ );
+ }
+ other => panic!("{hint}: on no-AVX2 host expected Init(AVX2) error, got {other:?}"),
+ }
+ } else {
+ match magika_detect(content) {
+ Ok(got) => {
+ assert_eq!(got, expected, "{hint}: expected {expected:?}, got {got:?}")
+ }
+ Err(e) => panic!("{hint}: detection failed: {e}"),
+ }
}
}
@@ -368,15 +421,32 @@ index abc123..def456 100644
#[test]
fn singleton_session_is_reused_across_calls() {
- // Two back-to-back calls should both succeed without re-initing
- // the model (no panic, no error). We can't directly observe
- // the singleton hit-rate without instrumenting the test, but
- // wall-clock asymmetry between the first and second call is
- // strong evidence (cold ~50 ms, warm <1 ms). For the unit
- // suite, just prove neither call errors.
- magika_detect("hello world").unwrap();
- magika_detect("def f(): pass").unwrap();
- magika_detect(r#"{"a":1}"#).unwrap();
+ // Two back-to-back calls should reuse the same session
+ // (or same cached error). On AVX2 hosts the session is
+ // Ok and repeated calls succeed; on no-AVX2 hosts the
+ // session is Err and repeated calls return the same Err.
+ if !magika_onnx_runtime_supported_by_cpu() {
+ // On no-AVX2 the singleton caches the init error;
+ // repeated calls all return the same Init error.
+ let r1 = magika_detect("hello world");
+ let r2 = magika_detect("def f(): pass");
+ let r3 = magika_detect(r#"{"a":1}"#);
+ for r in [&r1, &r2, &r3] {
+ match r {
+ Err(MagikaDetectorError::Init(msg)) => {
+ assert!(msg.contains("AVX2"), "expected AVX2 error, got: {msg}");
+ }
+ other => panic!("on no-AVX2 host expected Init(AVX2) error, got {other:?}"),
+ }
+ }
+ } else {
+ // On AVX2 hosts the session loads once and all calls
+ // succeed. Wall-clock asymmetry (cold ~50 ms, warm
+ // <1 ms) confirms reuse.
+ magika_detect("hello world").unwrap();
+ magika_detect("def f(): pass").unwrap();
+ magika_detect(r#"{"a":1}"#).unwrap();
+ }
}
#[test]
diff --git a/wiki/troubleshooting.md b/wiki/troubleshooting.md
index c91583aff..35907f057 100644
--- a/wiki/troubleshooting.md
+++ b/wiki/troubleshooting.md
@@ -441,6 +441,21 @@ print(f"Compressed content: {result.messages[0]['content'][:200]}...")
---
+### "Native detector crashes with illegal instruction"
+
+On some older or virtualized x86_64 CPUs, AVX2 may be unavailable. The
+Magika/ONNX Runtime detector can require AVX2 through its precompiled runtime
+binary. Headroom skips that detector tier on x86/x86_64 hosts without AVX2 and
+falls back to non-Magika detection tiers instead of crashing.
+
+If native startup still fails on an older CPU, set:
+
+```bash
+export HEADROOM_REQUIRE_RUST_CORE=false
+```
+
+---
+
## Error Reference
| Exception | Meaning | Solution |