From d8f830597d19abd7d65259e9cf73a53624620ab7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 05:16:46 +0000 Subject: [PATCH 1/3] cpu_guard: warn at build time when a target feature has no guard row build.rs compares CARGO_CFG_TARGET_FEATURE with the feature names in cpu_guard's x86_64 table and emits cargo:warning for any enabled feature the guard cannot check (baseline sse/sse2/x87 excluded). Such a feature is compiled in but never compared with the running CPU, so the guard passes a binary that can still die with SIGILL. The parser and filter live in src/cpu_guard_drift.rs, included by build.rs and by three tests: the parser reads exactly the 54 FEATURES rows, an avx10.1 input is reported, and table plus baseline reports nothing. Limits: stable rustc omits unstable features (avx10.x, amx-*) from the cfg, for the guard and the build script alike; cargo hides build-script warnings from git and registry dependencies. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- Cargo.toml | 1 + build.rs | 37 +++++++++++++++++++++++++++++++++++++ src/cpu_guard.rs | 33 +++++++++++++++++++++++++++++++++ src/cpu_guard_drift.rs | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+) create mode 100644 build.rs create mode 100644 src/cpu_guard_drift.rs diff --git a/Cargo.toml b/Cargo.toml index 210d361d..8c8fa2e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ categories = ["data-structures", "science"] include = [ "/src/**/*.rs", + "/build.rs", "LICENSE-MIT", "LICENSE-APACHE", "RELEASES.md", diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..fb257b41 --- /dev/null +++ b/build.rs @@ -0,0 +1,37 @@ +// Warns when a build enables x86_64 target features that `cpu_guard` has no +// row for. Those features are compiled in but never compared with the running +// CPU, so the guard would pass a binary that can still die with SIGILL. +// +// Limits: on stable rustc, `CARGO_CFG_TARGET_FEATURE` omits unstable features +// (avx10.x, amx-*), exactly as `cfg!` in the guard does, so they cannot be +// reported here. Cargo also hides warnings from git and registry dependencies, +// so this fires when building ndarray itself, not inside a consumer. + +include!("src/cpu_guard_drift.rs"); + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=src/cpu_guard.rs"); + println!("cargo:rerun-if-changed=src/cpu_guard_drift.rs"); + + if std::env::var("CARGO_CFG_TARGET_ARCH").as_deref() != Ok("x86_64") { + return; + } + let enabled = std::env::var("CARGO_CFG_TARGET_FEATURE").unwrap_or_default(); + let guard_src = match std::fs::read_to_string("src/cpu_guard.rs") { + Ok(src) => src, + Err(e) => { + println!("cargo:warning=cpu_guard drift check skipped: cannot read src/cpu_guard.rs: {e}"); + return; + } + }; + let table = guard_table_names(&guard_src); + let unchecked = unchecked_features(&enabled, &table); + if !unchecked.is_empty() { + println!( + "cargo:warning=cpu_guard has no row for enabled target features: {} \ + (the guard cannot detect a CPU that lacks them; add rows in src/cpu_guard.rs)", + unchecked.join(", ") + ); + } +} diff --git a/src/cpu_guard.rs b/src/cpu_guard.rs index eb6ae537..2ce4ce1d 100644 --- a/src/cpu_guard.rs +++ b/src/cpu_guard.rs @@ -640,6 +640,39 @@ static GUARD_BEFORE_MAIN: extern "C" fn() = guard_before_main; mod tests { use super::*; + mod drift { + include!("cpu_guard_drift.rs"); + + const GUARD_SRC: &str = include_str!("cpu_guard.rs"); + + /// The build script's parser must read exactly the names the guard + /// checks, or its warning reports the wrong set. + #[cfg(target_arch = "x86_64")] + #[test] + fn parser_reads_exactly_the_guard_table() { + let parsed = guard_table_names(GUARD_SRC); + let rows: Vec<&str> = super::FEATURES.iter().map(|r| r.0).collect(); + assert_eq!(parsed.len(), 54); + assert_eq!(parsed, rows); + } + + /// Can fire: a feature the table lacks is reported, and only that one. + #[test] + fn a_feature_without_a_row_is_reported() { + let table = guard_table_names(GUARD_SRC); + let enabled = "sse,sse2,x87,avx2,avx512f,avx10.1,fma"; + assert_eq!(unchecked_features(enabled, &table), ["avx10.1"]); + } + + /// Can stay silent: every row plus the x86_64 baseline reports nothing. + #[test] + fn the_table_and_the_baseline_report_nothing() { + let table = guard_table_names(GUARD_SRC); + let enabled = format!("sse,sse2,x87,{}", table.join(",")); + assert!(unchecked_features(&enabled, &table).is_empty()); + } + } + /// The exit-or-warn decision must see a test binary as an executable; /// if this were false, every mismatching executable would only warn. #[cfg(not(miri))] diff --git a/src/cpu_guard_drift.rs b/src/cpu_guard_drift.rs new file mode 100644 index 00000000..5a8b4f19 --- /dev/null +++ b/src/cpu_guard_drift.rs @@ -0,0 +1,32 @@ +// Shared by `build.rs` and the tests in `cpu_guard.rs` (both `include!` it); +// not a module of the crate. +// +// Which target features a build enables that `cpu_guard` cannot check. The +// guard asks `cfg!(target_feature = ...)` row by row, so a feature that is +// compiled in but has no row is never compared with the running CPU: the +// binary passes the guard and can still die with SIGILL. That happens when a +// toolchain bump lets LLVM enable a feature the table predates. + +/// Features every x86_64 CPU has; the guard does not need rows for them. +const X86_64_BASELINE: &[&str] = &["sse", "sse2", "x87"]; + +/// The feature names in the x86_64 table of `cpu_guard.rs` (`"name" =>` rows). +fn guard_table_names(cpu_guard_src: &str) -> Vec<&str> { + cpu_guard_src + .lines() + .filter_map(|line| { + let rest = line.trim_start().strip_prefix('"')?; + let (name, tail) = rest.split_once('"')?; + tail.trim_start().starts_with("=>").then_some(name) + }) + .collect() +} + +/// The enabled features (`CARGO_CFG_TARGET_FEATURE`, comma-separated) that +/// are neither in the guard's table nor x86_64 baseline, in input order. +fn unchecked_features<'a>(enabled: &'a str, table: &[&str]) -> Vec<&'a str> { + enabled + .split(',') + .filter(|f| !f.is_empty() && !table.contains(f) && !X86_64_BASELINE.contains(f)) + .collect() +} From 6ace10630a098590d3e490a915a6401481be3052 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 06:15:00 +0000 Subject: [PATCH 2/3] cpu_guard: test that crt-static is not reported (red) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- src/cpu_guard.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/cpu_guard.rs b/src/cpu_guard.rs index 2ce4ce1d..e15e4549 100644 --- a/src/cpu_guard.rs +++ b/src/cpu_guard.rs @@ -671,6 +671,17 @@ mod tests { let enabled = format!("sse,sse2,x87,{}", table.join(",")); assert!(unchecked_features(&enabled, &table).is_empty()); } + + /// The default x86_64 musl target enables `crt-static`, which is C + /// runtime linkage rather than a CPU feature, so it must not be + /// reported; a feature without a row beside it still is. + #[test] + fn crt_static_is_not_a_cpu_feature() { + let table = guard_table_names(GUARD_SRC); + let musl_default = "crt-static,fxsr,sse,sse2"; + assert!(unchecked_features(musl_default, &table).is_empty()); + assert_eq!(unchecked_features("crt-static,avx10.1", &table), ["avx10.1"]); + } } /// The exit-or-warn decision must see a test binary as an executable; From 9869e02a5c080fdff9086627e5c97ebef47883fa Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 06:21:01 +0000 Subject: [PATCH 3/3] cpu_guard: crt-static is not a CPU feature; blackboard entry for the drift warning On musl targets crt-static is a default target feature (C-runtime linkage), so every musl build warned about it. It is now excluded; a feature without a row beside it is still reported. (codex P2) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- .claude/blackboard.md | 7 +++++++ src/cpu_guard_drift.rs | 11 +++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.claude/blackboard.md b/.claude/blackboard.md index 28b41658..68bbc646 100644 --- a/.claude/blackboard.md +++ b/.claude/blackboard.md @@ -1,3 +1,10 @@ +## 2026-10-13 — build-time warning for target features `cpu_guard` cannot check + +- **Change:** new `build.rs` + `src/cpu_guard_drift.rs` (shared by `build.rs` and the tests via `include!`). On x86_64 the build script reads `CARGO_CFG_TARGET_FEATURE` and the guard's `"name" =>` rows and prints `cargo:warning=` for every enabled feature without a row. Without a row the guard never compares that feature with the running CPU, so the binary passes the guard and can still SIGILL; a toolchain bump that lets LLVM enable a newer feature is the usual cause. +- **Exclusions:** the x86_64 baseline (`sse`, `sse2`, `x87`) and non-CPU target features. `crt-static` (static C runtime, on by default for musl) was the one non-CPU feature in the default x86_64 sets; measured `rustc --print cfg` for gnu, musl, msvc and darwin (codex P2). +- **Tests:** the parser reads exactly the 54 guard rows; a missing row is reported (`avx10.1`); table + baseline report nothing; musl's default set reports nothing while a real missing row beside `crt-static` still does (red before the exclusion). +- **Limits:** cargo hides build-script warnings from git dependencies, so a consumer pulling ndarray by git URL sees nothing; path dependencies (the sibling layout) do show it. Stable rustc hides unstable features (amx, avx10), which are therefore never enabled or reported. + ## 2026-10-12 — `mask_gather_chain_u32` withdrawn: rejected design, moved to backup - **Removed:** `mask_gather_chain_u32`, `mask_gather_chain_u32_under`, `ChainHop`, `ChainTest`, their tests and the parity case 0xD02/0xD03 (shipped in #358). The exact state is kept on branch `backup/session-011BMJ-2026-10-12`. diff --git a/src/cpu_guard_drift.rs b/src/cpu_guard_drift.rs index 5a8b4f19..5fe5b702 100644 --- a/src/cpu_guard_drift.rs +++ b/src/cpu_guard_drift.rs @@ -10,6 +10,10 @@ /// Features every x86_64 CPU has; the guard does not need rows for them. const X86_64_BASELINE: &[&str] = &["sse", "sse2", "x87"]; +/// Target features that do not describe the CPU: `crt-static` selects static +/// C-runtime linkage and is on by default for musl targets. +const NOT_CPU_FEATURES: &[&str] = &["crt-static"]; + /// The feature names in the x86_64 table of `cpu_guard.rs` (`"name" =>` rows). fn guard_table_names(cpu_guard_src: &str) -> Vec<&str> { cpu_guard_src @@ -23,10 +27,13 @@ fn guard_table_names(cpu_guard_src: &str) -> Vec<&str> { } /// The enabled features (`CARGO_CFG_TARGET_FEATURE`, comma-separated) that -/// are neither in the guard's table nor x86_64 baseline, in input order. +/// are neither in the guard's table, nor x86_64 baseline, nor a non-CPU +/// feature, in input order. fn unchecked_features<'a>(enabled: &'a str, table: &[&str]) -> Vec<&'a str> { enabled .split(',') - .filter(|f| !f.is_empty() && !table.contains(f) && !X86_64_BASELINE.contains(f)) + .filter(|f| { + !f.is_empty() && !table.contains(f) && !X86_64_BASELINE.contains(f) && !NOT_CPU_FEATURES.contains(f) + }) .collect() }