From f645e19fd8de6a32eabd83079ca5af706c992d6b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 06:46:45 +0000 Subject: [PATCH 1/9] cpu_guard: inside a loaded library, warn instead of exiting the host The pre-main hook runs in every artifact that links ndarray, a cdylib included. Measured: an x86-64-v4 cdylib linking ndarray, dlopen'ed by a C program under qemu-x86_64 -cpu Haswell, ended the host process with status 132. A JVM loading lance-graph-java's .so, or Python loading a wheel, would be killed the same way, even if it never calls the AVX-512 code. The hook now asks the loader whether its own code lies in the main executable (dl_iterate_phdr on ELF, dladdr + _dyld_get_image_header(0) on macOS, GetModuleHandleExW on Windows). In the executable it still prints and exits 132; in a library it prints a warning and returns, and the host can call check_build_cpu() itself. Each wrong answer degrades to the cheaper failure: an executable seen as a library warns, then SIGILLs as it would have without the guard. Measured after the change: cdylib under Haswell warns and the host survives; a v4 executable under Haswell still exits 132; the cdylib on an AVX-512 host is silent. Two unit tests pin both sides of the lookup. Clippy -D warnings clean for x86_64 linux/macos/windows/freebsd/android and aarch64 linux. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- src/cpu_guard.rs | 218 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 216 insertions(+), 2 deletions(-) diff --git a/src/cpu_guard.rs b/src/cpu_guard.rs index d96e7e88..f353ffab 100644 --- a/src/cpu_guard.rs +++ b/src/cpu_guard.rs @@ -38,6 +38,24 @@ //! that links this crate. On failure it prints the missing features to stderr //! and exits with status 132 (`128 + SIGILL`), the status the crash would //! have produced. +//! * **Inside a library another program loads** (a `cdylib` opened by a JVM +//! through Panama, by Python through `ctypes` or a wheel, by a C host through +//! `dlopen`), the same hook runs when the library is loaded, but it does NOT +//! exit: the process belongs to the host, which may never call the code that +//! needs the missing features, or may load a different build instead. It +//! prints a `warning:` with the same message and returns. The host decides, +//! by calling [`check_build_cpu`] from its own initialisation. +//! Measured before this rule existed (2026-10-11): an `x86-64-v4` `cdylib` +//! linking this crate, `dlopen`ed by a C program under +//! `qemu-x86_64 -cpu Haswell`, ended the host process with status 132. +//! +//! Which of the two applies is decided by asking the loader whether the +//! guard's own code lies in the main executable's image (`dl_iterate_phdr` +//! on ELF, `dladdr` + `_dyld_get_image_header(0)` on macOS, +//! `GetModuleHandleExW` on Windows). Every way that lookup can fail falls +//! back to the behaviour that costs least: an unrecognised executable only +//! warns and then crashes with SIGILL as it would have without the guard; +//! on a platform with no lookup the guard keeps exiting. //! * On demand, through [`check_build_cpu`] (returns the mismatch) or //! [`assert_build_cpu`] (panics with the same message), for targets without a //! pre-`main` hook or for callers that want to report it themselves. @@ -358,8 +376,184 @@ pub fn assert_build_cpu() { extern "C" fn guard_before_main() { if let Err(mismatch) = check_build_cpu() { use std::io::Write; - let _ = writeln!(std::io::stderr(), "error: {mismatch}"); - std::process::exit(132); + if guard_is_in_main_executable() { + let _ = writeln!(std::io::stderr(), "error: {mismatch}"); + std::process::exit(132); + } + // A library loaded by another program: the process is the host's. + let _ = writeln!( + std::io::stderr(), + "warning: {mismatch} This copy of ndarray is inside a library loaded by \ + another program, so it does not exit; calling into it on this CPU may \ + crash. The host can check `ndarray::cpu_guard::check_build_cpu()` first." + ); + } +} + +/// Whether the running copy of the guard belongs to the process's main +/// executable rather than to a library the executable loaded. +/// +/// Only consulted on a mismatch, and mismatches only exist on x86_64 (the +/// feature tables are empty elsewhere), so only x86_64 asks the loader. A +/// `false` answer turns "exit 132" into "warn and return"; see the module +/// docs for why each wrong answer is the cheaper one. +#[cfg(not(miri))] +fn guard_is_in_main_executable() -> bool { + main_image::contains(guard_before_main as extern "C" fn() as usize) +} + +/// ELF: the first object `dl_iterate_phdr` reports is the main program +/// (glibc, bionic and FreeBSD all document this order). +#[cfg(all( + not(miri), + target_arch = "x86_64", + any(target_os = "linux", target_os = "android", target_os = "freebsd") +))] +mod main_image { + use core::ffi::{c_char, c_int, c_void}; + + /// The leading fields of `struct dl_phdr_info`; the loader passes its + /// real size, and only these four are read. + #[repr(C)] + struct DlPhdrInfo { + addr: usize, + name: *const c_char, + phdr: *const Elf64Phdr, + phnum: u16, + } + + #[repr(C)] + struct Elf64Phdr { + p_type: u32, + p_flags: u32, + p_offset: u64, + p_vaddr: u64, + p_paddr: u64, + p_filesz: u64, + p_memsz: u64, + p_align: u64, + } + + const PT_LOAD: u32 = 1; + + extern "C" { + fn dl_iterate_phdr( + callback: unsafe extern "C" fn(*mut DlPhdrInfo, usize, *mut c_void) -> c_int, data: *mut c_void, + ) -> c_int; + } + + /// `data` is the `(address, found)` pair passed by [`contains`]. + unsafe extern "C" fn first_object(info: *mut DlPhdrInfo, _size: usize, data: *mut c_void) -> c_int { + // SAFETY: `data` is the `&mut (usize, bool)` that `contains` passed to + // `dl_iterate_phdr`, which hands it back unchanged on this thread for + // the duration of the call; `info` is non-null and valid for the + // callback, and `phdr` points at `phnum` program headers (ELF gABI). + unsafe { + let (addr, found) = &mut *(data as *mut (usize, bool)); + let info = &*info; + for i in 0..usize::from(info.phnum) { + let ph = &*info.phdr.add(i); + if ph.p_type == PT_LOAD { + let start = info.addr.wrapping_add(ph.p_vaddr as usize); + if *addr >= start && *addr - start < ph.p_memsz as usize { + *found = true; + } + } + } + } + 1 // stop after the first object: the main program + } + + pub(super) fn contains(addr: usize) -> bool { + let mut state = (addr, false); + // SAFETY: `first_object` matches the callback ABI `dl_iterate_phdr` + // expects and reads `data` only as the `(usize, bool)` passed here, + // which outlives the call. + unsafe { dl_iterate_phdr(first_object, (&mut state as *mut (usize, bool)).cast()) }; + state.1 + } +} + +/// macOS: image 0 is the main executable; `dladdr` names the image that +/// contains an address. +#[cfg(all(not(miri), target_arch = "x86_64", target_os = "macos"))] +mod main_image { + use core::ffi::{c_char, c_int, c_void}; + + #[repr(C)] + struct DlInfo { + fname: *const c_char, + fbase: *mut c_void, + sname: *const c_char, + saddr: *mut c_void, + } + + extern "C" { + fn dladdr(addr: *const c_void, info: *mut DlInfo) -> c_int; + fn _dyld_get_image_header(index: u32) -> *const c_void; + } + + pub(super) fn contains(addr: usize) -> bool { + let mut info = DlInfo { + fname: core::ptr::null(), + fbase: core::ptr::null_mut(), + sname: core::ptr::null(), + saddr: core::ptr::null_mut(), + }; + // SAFETY: both are libSystem functions; `dladdr` writes only into the + // `DlInfo` it is given and returns 0 when the address is in no image; + // `_dyld_get_image_header(0)` returns the main executable's header. + unsafe { + dladdr(addr as *const c_void, &mut info) != 0 + && core::ptr::eq(info.fbase as *const c_void, _dyld_get_image_header(0)) + } + } +} + +/// Windows: `GetModuleHandleW(NULL)` is the executable; the module that holds +/// an address comes from `GetModuleHandleExW(FROM_ADDRESS)`. +#[cfg(all(not(miri), target_arch = "x86_64", target_os = "windows"))] +mod main_image { + use core::ffi::c_void; + + const FROM_ADDRESS: u32 = 0x4; + const UNCHANGED_REFCOUNT: u32 = 0x2; + + #[link(name = "kernel32")] + extern "system" { + fn GetModuleHandleW(name: *const u16) -> *mut c_void; + fn GetModuleHandleExW(flags: u32, name: *const u16, module: *mut *mut c_void) -> i32; + } + + pub(super) fn contains(addr: usize) -> bool { + let mut module = core::ptr::null_mut(); + // SAFETY: with FROM_ADDRESS the `name` argument is read as an address + // inside a module, not as a string; UNCHANGED_REFCOUNT means the + // handle needs no release. Both calls write only their out-parameter. + unsafe { + GetModuleHandleExW(FROM_ADDRESS | UNCHANGED_REFCOUNT, addr as *const u16, &mut module) != 0 + && module == GetModuleHandleW(core::ptr::null()) + } + } +} + +/// No loader lookup here (and no mismatch is possible off x86_64): keep exiting. +#[cfg(all( + not(miri), + not(all( + target_arch = "x86_64", + any( + target_os = "linux", + target_os = "android", + target_os = "freebsd", + target_os = "macos", + target_os = "windows" + ) + )) +))] +mod main_image { + pub(super) fn contains(_addr: usize) -> bool { + true } } @@ -384,6 +578,26 @@ static GUARD_BEFORE_MAIN: extern "C" fn() = guard_before_main; mod tests { use super::*; + /// 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))] + #[test] + fn a_test_binary_is_its_own_main_executable() { + assert!(guard_is_in_main_executable()); + } + + /// Can-stay-silent half: an address in a loaded system library (libc's + /// `malloc`) is not in the main executable. Without this the lookup + /// could be `true` everywhere and the library case would never happen. + #[cfg(all(not(miri), target_arch = "x86_64", target_os = "linux", target_env = "gnu"))] + #[test] + fn an_address_in_libc_is_not_in_the_main_executable() { + extern "C" { + fn malloc(size: usize) -> *mut core::ffi::c_void; + } + assert!(!main_image::contains(malloc as unsafe extern "C" fn(usize) -> *mut core::ffi::c_void as usize)); + } + #[test] fn the_host_runs_its_own_build() { // Built here, run here: nothing may be missing. From 9203d0c97564b1e0eba8b5cea623be8c0506868d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 06:50:36 +0000 Subject: [PATCH 2/9] scripts: two-sided probe for cpu_guard in a library host Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- scripts/cpu-guard-library-probe.sh | 81 ++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100755 scripts/cpu-guard-library-probe.sh diff --git a/scripts/cpu-guard-library-probe.sh b/scripts/cpu-guard-library-probe.sh new file mode 100755 index 00000000..3c78d85c --- /dev/null +++ b/scripts/cpu-guard-library-probe.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# cpu_guard in a library host, two-sided (coresearch 2026-10-11, X1). +# +# Builds, for x86-64-v4, (a) a cdylib that links ndarray and is dlopen'ed by +# a small C program, and (b) ndarray's own `cpu_guard_probe` executable. +# Both run under `qemu-x86_64-static -cpu Haswell` (AVX2, no AVX-512): +# +# * the library must NOT end its host: the C program prints `survived` +# and exits 0, and stderr carries the guard's `warning:` line; +# * the executable must still exit 132 with the guard's `error:` line. +# +# Before the library rule existed, (a) ended the host with status 132. +# Requires: cc, qemu-x86_64-static. Every compile uses debug 0 and ignores +# any ambient RUSTFLAGS (which would REPLACE the pinned v4 config). +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +QEMU="${QEMU:-qemu-x86_64-static}" +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT +export CARGO_PROFILE_DEV_DEBUG=0 CARGO_PROFILE_TEST_DEBUG=0 CARGO_INCREMENTAL=0 +CFG="$ROOT/.cargo/config-v4.toml" + +mkdir -p "$WORK/lib/src" +cp "$ROOT/rust-toolchain.toml" "$WORK/lib/" +cat > "$WORK/lib/Cargo.toml" < "$WORK/lib/src/lib.rs" <<'RS' +#[no_mangle] +pub extern "C" fn probe_xor(a: u64, b: u64) -> u64 { + let x = ndarray::simd::U64x8::splat(a) ^ ndarray::simd::U64x8::splat(b); + x.to_array()[0] +} +RS +cat > "$WORK/host.c" <<'C' +#include +#include +int main(int argc, char **argv) { + void *h = dlopen(argv[1], RTLD_NOW); + if (!h) { printf("dlopen failed: %s\n", dlerror()); return 2; } + printf("survived\n"); + return 0; +} +C +cc -O1 -o "$WORK/host" "$WORK/host.c" -ldl + +(cd "$WORK/lib" && CARGO_TARGET_DIR="$WORK/target" env -u RUSTFLAGS -u CARGO_BUILD_RUSTFLAGS \ + cargo --config "$CFG" build --release --quiet) +(cd "$ROOT" && CARGO_TARGET_DIR="$WORK/target-exe" env -u RUSTFLAGS -u CARGO_BUILD_RUSTFLAGS \ + cargo --config "$CFG" build --release --quiet --example cpu_guard_probe) + +fail=0 + +set +e +lib_out=$("$QEMU" -cpu Haswell "$WORK/host" "$WORK/target/release/libcpu_guard_library_probe.so" 2>&1) +lib_rc=$? +exe_out=$("$QEMU" -cpu Haswell "$WORK/target-exe/release/examples/cpu_guard_probe" 2>&1) +exe_rc=$? +set -e + +echo "--- library host: exit $lib_rc"; echo "$lib_out" | grep -v 'TCG doesn' || true +echo "--- executable: exit $exe_rc"; echo "$exe_out" | grep -v 'TCG doesn' || true + +[ "$lib_rc" -eq 0 ] || { echo "FAIL: the library ended its host (exit $lib_rc)"; fail=1; } +echo "$lib_out" | grep -q '^survived$' || { echo "FAIL: host did not reach 'survived'"; fail=1; } +echo "$lib_out" | grep -q '^warning: this binary needs CPU features' || { echo "FAIL: no guard warning from the library"; fail=1; } +[ "$exe_rc" -eq 132 ] || { echo "FAIL: the v4 executable exited $exe_rc, expected 132"; fail=1; } +echo "$exe_out" | grep -q '^error: this binary needs CPU features' || { echo "FAIL: no guard error from the executable"; fail=1; } + +[ "$fail" -eq 0 ] && echo "cpu_guard library probe: PASS" +exit "$fail" From 1d1e68bfeb5d6e2bf6899e3125e302b03a4d48c1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 06:52:07 +0000 Subject: [PATCH 3/9] ci: cpu_guard library-host job (qemu -cpu Haswell) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- .github/workflows/simd-matrix.yaml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/simd-matrix.yaml b/.github/workflows/simd-matrix.yaml index 71d918a5..7acea754 100644 --- a/.github/workflows/simd-matrix.yaml +++ b/.github/workflows/simd-matrix.yaml @@ -218,6 +218,28 @@ jobs: echo "::warning::runner lacks avx512f — v4 parity run and probe self-check SKIPPED; asserting the emitted assembly only" env -u RUSTFLAGS WITNESS_NO_RUN=1 CARGO_ARGS='--config .cargo/config-v4.toml' bash scripts/codegen-witness.sh avx512 + cpu-guard-library: + # `cpu_guard` must never end a process it does not own. A v4 cdylib that + # links ndarray, dlopen'ed by a C host under `qemu -cpu Haswell`, must + # warn and return; a v4 executable on the same emulated CPU must still + # exit 132. Before the library rule (2026-10-11) the cdylib ended its + # host with 132, which is what a JVM or Python process loading an + # ndarray-linked library would have suffered. + runs-on: ubuntu-latest + name: cpu_guard × library host + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + key: cpu-guard-library + - name: install qemu-user + run: sudo apt-get update && sudo apt-get install -y qemu-user-static + - name: library warns, executable exits 132 (qemu -cpu Haswell) + run: bash scripts/cpu-guard-library-probe.sh + avx: # AVX-without-AVX2 realization (`src/simd_avx.rs`, Sandy Bridge class). # Built for `sandybridge` (`.cargo/config-avx.toml`) and RUN under From bd58f5c9ff780ea3be13486b4b53c889c7d62f0f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 06:58:35 +0000 Subject: [PATCH 4/9] Close three safe-code out-of-bounds paths from the unsafe inventory All three were safe pub fns whose only guard was skipped in release builds or checked just the start of a range: - GridBlockMut::row_mut: both bounds checks were debug_assert!, then a raw slice. Now assert!. - sgemm_blocked / dgemm_blocked: the microkernel stores whole rows of C through &mut c[row*ldc+col..], which checks only the start. New assert_c_extent requires ldc >= n and c.len() >= (m-1)*ldc + n (overflow-checked). - int8_gemm_amx_tiled: the 16/16/64 shape and amx_available() were debug_assert!. Now assert!, shape first so a misaligned call on a machine without AMX names the shape. Tests read the panic message so none passes for the wrong reason, and all six pass under both cargo test and cargo test --release on v4 (release is where the old checks vanished). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- src/backend/kernels_avx512.rs | 85 +++++++++++++++++++++++++++++++++++ src/hpc/blocked_grid/iter.rs | 18 +++++++- src/hpc/int8_tile_gemm.rs | 61 ++++++++++++++++++++----- 3 files changed, 152 insertions(+), 12 deletions(-) diff --git a/src/backend/kernels_avx512.rs b/src/backend/kernels_avx512.rs index d0f27333..582b7b0b 100644 --- a/src/backend/kernels_avx512.rs +++ b/src/backend/kernels_avx512.rs @@ -657,6 +657,21 @@ unsafe fn sgemm_ukernel_6x16( } } +/// Panics unless a row-major `m × n` output with leading dimension `ldc` fits +/// in `c_len` elements. Shared by [`sgemm_blocked`] and [`dgemm_blocked`]. +#[cfg(target_arch = "x86_64")] +fn assert_c_extent(m: usize, n: usize, c_len: usize, ldc: usize) { + if m == 0 || n == 0 { + return; + } + assert!(ldc >= n, "blocked gemm: ldc={ldc} < n={n}"); + let needed = (m - 1).checked_mul(ldc).and_then(|x| x.checked_add(n)); + assert!( + needed.is_some_and(|need| c_len >= need), + "blocked gemm: c.len()={c_len} < (m-1)*ldc+n for m={m} n={n} ldc={ldc}" + ); +} + /// Goto BLAS style blocked SGEMM with packing and AVX-512 microkernel. /// # Safety /// Caller must ensure AVX-512F is available (`simd_caps().avx512f`). @@ -665,6 +680,10 @@ unsafe fn sgemm_ukernel_6x16( pub fn sgemm_blocked( m: usize, n: usize, k: usize, alpha: f32, a: &[f32], lda: usize, b: &[f32], ldb: usize, c: &mut [f32], ldc: usize, ) { + // The microkernel stores whole rows of C through raw pointers from + // `&mut c[row * ldc + col..]`, which checks only the START of each row. + // These are the bounds that make every row it writes lie inside `c`. + assert_c_extent(m, n, c.len(), ldc); let mut a_packed = vec![0.0f32; SGEMM_MC * SGEMM_KC]; let mut b_packed = vec![0.0f32; SGEMM_KC * SGEMM_NC]; @@ -833,6 +852,10 @@ unsafe fn dgemm_ukernel_6x8( pub fn dgemm_blocked( m: usize, n: usize, k: usize, alpha: f64, a: &[f64], lda: usize, b: &[f64], ldb: usize, c: &mut [f64], ldc: usize, ) { + // The microkernel stores whole rows of C through raw pointers from + // `&mut c[row * ldc + col..]`, which checks only the START of each row. + // These are the bounds that make every row it writes lie inside `c`. + assert_c_extent(m, n, c.len(), ldc); let mut a_packed = vec![0.0f64; DGEMM_MC * DGEMM_KC]; let mut b_packed = vec![0.0f64; DGEMM_KC * DGEMM_NC]; @@ -1173,3 +1196,65 @@ mod block_stop_probe { } } } + +/// The blocked GEMMs are safe `pub fn`s that store whole rows of C through +/// raw pointers; these pin the extent check in every profile. +#[cfg(all(test, target_arch = "x86_64"))] +mod blocked_extent_tests { + use super::*; + + fn panic_message(r: std::thread::Result<()>) -> String { + match r { + Ok(()) => String::from(""), + Err(e) => e + .downcast_ref::() + .cloned() + .or_else(|| e.downcast_ref::<&str>().map(|s| s.to_string())) + .unwrap_or_default(), + } + } + + #[test] + fn assert_c_extent_accepts_the_exact_size_and_rejects_one_less() { + assert_c_extent(3, 16, 2 * 20 + 16, 20); + let r = std::panic::catch_unwind(|| assert_c_extent(3, 16, 2 * 20 + 15, 20)); + assert!(panic_message(r).contains("c.len()=55")); + let r = std::panic::catch_unwind(|| assert_c_extent(3, 16, 1000, 15)); + assert!(panic_message(r).contains("ldc=15 < n=16")); + assert_c_extent(0, 16, 0, 0); + assert_c_extent(16, 0, 0, 0); + } + + /// End to end on an AVX-512 host: one element short of the last row must + /// panic, never store past the slice. The exact size must still compute + /// the right product. + #[test] + fn sgemm_and_dgemm_blocked_reject_a_short_c() { + if !std::is_x86_feature_detected!("avx512f") { + return; + } + let (m, n, k, ldc) = (7, 16, 3, 16); + let a = vec![1.0f32; m * k]; + let b = vec![2.0f32; k * n]; + let mut short = vec![0.0f32; (m - 1) * ldc + n - 1]; + // SAFETY: avx512f detected above. + let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| unsafe { + sgemm_blocked(m, n, k, 1.0, &a, k, &b, n, &mut short, ldc) + })); + assert!(panic_message(r).contains("blocked gemm: c.len()")); + + let mut exact = vec![0.0f32; (m - 1) * ldc + n]; + // SAFETY: avx512f detected above. + unsafe { sgemm_blocked(m, n, k, 1.0, &a, k, &b, n, &mut exact, ldc) }; + assert!(exact.iter().all(|&x| x == 6.0)); + + let ad = vec![1.0f64; m * k]; + let bd = vec![2.0f64; k * n]; + let mut shortd = vec![0.0f64; (m - 1) * ldc + n - 1]; + // SAFETY: avx512f detected above. + let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| unsafe { + dgemm_blocked(m, n, k, 1.0, &ad, k, &bd, n, &mut shortd, ldc) + })); + assert!(panic_message(r).contains("blocked gemm: c.len()")); + } +} diff --git a/src/hpc/blocked_grid/iter.rs b/src/hpc/blocked_grid/iter.rs index 2728a78f..f9272a2d 100644 --- a/src/hpc/blocked_grid/iter.rs +++ b/src/hpc/blocked_grid/iter.rs @@ -304,10 +304,13 @@ impl<'a, T, const BR: usize, const BC: usize> GridBlockMut<'a, T, BR, BC> { /// assert_eq!(g.get(0, 0), 42); /// ``` pub fn row_mut(&mut self, r: usize) -> &mut [T] { - debug_assert!(r < BR, "row {} out of block range {}", r, BR); + // Real asserts, not `debug_assert!`: this is a safe `pub fn` and the + // second check is the only thing between `r` and the raw slice below + // (unsafe inventory, 2026-10-10: release builds skipped both). + assert!(r < BR, "row {} out of block range {}", r, BR); let stride = self.padded_cols_stride(); let start = r * stride; - debug_assert!( + assert!( start + BC <= self.data_len(), "row materialization {} extends past data_len {}", start + BC, @@ -478,4 +481,15 @@ mod tests { assert_eq!(g.get(4, 4), 0xABCD); assert_eq!(g.get(4, 7), 0x1234); } + + /// A row past the block must panic in EVERY profile. Until 2026-10-11 the + /// check was a `debug_assert!`, so a release build handed out a slice + /// past the grid (run `cargo test --release` to see the difference). + #[test] + #[should_panic(expected = "out of block range")] + fn row_mut_past_the_block_panics() { + let mut g = BlockedGrid::::new(8, 8); + let mut blk = GridBlockMut::from_grid(&mut g, 1, 1); + let _ = blk.row_mut(4); + } } diff --git a/src/hpc/int8_tile_gemm.rs b/src/hpc/int8_tile_gemm.rs index 9a732469..b5f93fbd 100644 --- a/src/hpc/int8_tile_gemm.rs +++ b/src/hpc/int8_tile_gemm.rs @@ -349,12 +349,11 @@ fn fallback_path(a_u8: &[u8], b_i8: &[i8], c: &mut [i32], k: usize) { /// /// # Panics /// Panics if `a_u8`, `b_i8`, or `c` are too small for the requested -/// `(m, n, k)`, mirroring the boundary contract from `gemm_u8_i8`. Also -/// panics in debug builds when AMX isn't OS-enabled or when the shape -/// alignment constraints aren't met (production builds skip those for -/// performance — callers must runtime-check -/// `crate::hpc::amx_matmul::amx_available()` and the 16/16/64 -/// alignment themselves). +/// `(m, n, k)`, mirroring the boundary contract from `gemm_u8_i8`; if `m` +/// or `n` is not a multiple of 16 or `k` not a multiple of 64; or if AMX +/// is not available (`crate::hpc::amx_matmul::amx_available()`). These +/// were debug-only until 2026-10-11, which let a release build index +/// past the slices. pub fn int8_gemm_amx_tiled(a_u8: &[u8], b_i8: &[i8], c: &mut [i32], m: usize, n: usize, k: usize) { // Length assertions (codex P1 from PR #185 — the function reads // `b_i8` via a 16-wide window per (kk, j_tile) iteration and a_u8 @@ -364,10 +363,15 @@ pub fn int8_gemm_amx_tiled(a_u8: &[u8], b_i8: &[i8], c: &mut [i32], m: usize, n: assert!(b_i8.len() >= k * n, "int8_gemm_amx_tiled: b_i8.len()={} < k*n={}", b_i8.len(), k * n); assert!(c.len() >= m * n, "int8_gemm_amx_tiled: c.len()={} < m*n={}", c.len(), m * n); - debug_assert!(crate::hpc::amx_matmul::amx_available()); - debug_assert_eq!(m % 16, 0, "int8_gemm_amx_tiled: M must be multiple of 16"); - debug_assert_eq!(n % 16, 0, "int8_gemm_amx_tiled: N must be multiple of 16"); - debug_assert_eq!(k % 64, 0, "int8_gemm_amx_tiled: K must be multiple of 64"); + // Real asserts, not `debug_assert!`: this is a safe `pub fn`, and the + // tile loops index whole 16×64 tiles, so a misaligned shape reads and + // writes past the slices in a release build (unsafe inventory, + // 2026-10-10). Shape first, so a misaligned call on a machine without + // AMX names the shape. `amx_available()` is a cached static. + assert_eq!(m % 16, 0, "int8_gemm_amx_tiled: M must be multiple of 16"); + assert_eq!(n % 16, 0, "int8_gemm_amx_tiled: N must be multiple of 16"); + assert_eq!(k % 64, 0, "int8_gemm_amx_tiled: K must be multiple of 64"); + assert!(crate::hpc::amx_matmul::amx_available(), "int8_gemm_amx_tiled: AMX is not available on this CPU"); // With `rayon`, fan the M/16 row-tiles across the pool — but only for LARGE // GEMMs. This AMX kernel is memory-bandwidth-bound, so on a few-core host the @@ -837,4 +841,41 @@ mod tests { } } } + + /// A misaligned shape must panic in every profile and name the shape, + /// even on a CPU without AMX (shape is checked first). Until 2026-10-11 + /// these were `debug_assert!`s and a release build indexed past the + /// slices; run `cargo test --release` to see the difference. + #[test] + #[should_panic(expected = "M must be multiple of 16")] + fn int8_gemm_amx_tiled_rejects_a_misaligned_m() { + let (m, n, k) = (17, 16, 64); + let a = vec![0u8; m * k]; + let b = vec![0i8; k * n]; + let mut c = vec![0i32; m * n]; + int8_gemm_amx_tiled(&a, &b, &mut c, m, n, k); + } + + /// On a CPU without AMX an aligned call must panic with the AMX message + /// instead of executing tile instructions. + #[test] + fn int8_gemm_amx_tiled_without_amx_panics_by_name() { + if crate::hpc::amx_matmul::amx_available() { + return; + } + let (m, n, k) = (16, 16, 64); + let a = vec![0u8; m * k]; + let b = vec![0i8; k * n]; + let mut c = vec![0i32; m * n]; + let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| int8_gemm_amx_tiled(&a, &b, &mut c, m, n, k))); + let msg = match r { + Ok(()) => String::new(), + Err(e) => e + .downcast_ref::() + .cloned() + .or_else(|| e.downcast_ref::<&str>().map(|s| s.to_string())) + .unwrap_or_default(), + }; + assert!(msg.contains("AMX is not available"), "got: {msg:?}"); + } } From eccf8825e2044b83b4df3251b786604251d047b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 07:13:29 +0000 Subject: [PATCH 5/9] cpu_guard: find the main program by AT_PHDR and _NSGetMachExecuteHeader sentinel-qa read the loaders' sources and found two lookups that could answer 'executable' for a library, the expensive direction (the host exits 132): - ELF: 'the first object dl_iterate_phdr reports is the main program' is false on bionic (the linker comes first) and inside a glibc dlmopen(LM_ID_NEWLM) namespace (only the caller's namespace is walked, so the first object is the library). The walk now matches the object whose program headers are at getauxval(AT_PHDR) (elf_aux_info on FreeBSD); in a dlmopen namespace the main program is not found, which answers 'library'. - macOS: _dyld_get_image_header(0) can be an inserted dylib (dyld normalizeImageIndex). Now compared against _NSGetMachExecuteHeader(). Every failure of a lookup now answers 'library', which only warns. The libc test is gated off crt-static builds, where malloc is in the executable. No deadlock: dl_iterate_phdr takes dl_load_write_lock, not the dl_load_lock dlopen holds while running constructors (both recursive); bionic, FreeBSD, dyld and the Windows loader lock were checked the same way. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- src/cpu_guard.rs | 136 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 103 insertions(+), 33 deletions(-) diff --git a/src/cpu_guard.rs b/src/cpu_guard.rs index f353ffab..eb6ae537 100644 --- a/src/cpu_guard.rs +++ b/src/cpu_guard.rs @@ -50,12 +50,17 @@ //! `qemu-x86_64 -cpu Haswell`, ended the host process with status 132. //! //! Which of the two applies is decided by asking the loader whether the -//! guard's own code lies in the main executable's image (`dl_iterate_phdr` -//! on ELF, `dladdr` + `_dyld_get_image_header(0)` on macOS, -//! `GetModuleHandleExW` on Windows). Every way that lookup can fail falls -//! back to the behaviour that costs least: an unrecognised executable only -//! warns and then crashes with SIGILL as it would have without the guard; -//! on a platform with no lookup the guard keeps exiting. +//! guard's own code lies in the main executable's image: on ELF the object +//! whose program headers sit at `AT_PHDR` (`getauxval` / `elf_aux_info` + +//! `dl_iterate_phdr`), on macOS `dladdr` against +//! `_NSGetMachExecuteHeader()`, on Windows `GetModuleHandleExW` against +//! `GetModuleHandleW(NULL)`. Each lookup only answers "executable" when it +//! positively finds the guard there; every failure (no `AT_PHDR`, a +//! `dlmopen` namespace without the main program, an address no image +//! claims) answers "library", which only warns. The cost of that wrong +//! answer is the behaviour before the guard existed: a SIGILL later. On an +//! x86_64 platform with no lookup (for example the iOS simulator) the guard +//! keeps exiting. //! * On demand, through [`check_build_cpu`] (returns the mismatch) or //! [`assert_build_cpu`] (panics with the same message), for targets without a //! pre-`main` hook or for callers that want to report it themselves. @@ -402,8 +407,13 @@ fn guard_is_in_main_executable() -> bool { main_image::contains(guard_before_main as extern "C" fn() as usize) } -/// ELF: the first object `dl_iterate_phdr` reports is the main program -/// (glibc, bionic and FreeBSD all document this order). +/// ELF: the main program is the object whose program headers are at the +/// address the kernel passed in the auxiliary vector (`AT_PHDR`). Matching +/// that, rather than trusting "the first object reported", matters twice +/// (sentinel-qa, 2026-10-11, from loader source): bionic's list starts with +/// the linker itself, and glibc reports only the CALLER's namespace, so +/// inside a `dlmopen(LM_ID_NEWLM)` namespace the first object is the library. +/// There the main program is simply not found, which answers "library". #[cfg(all( not(miri), target_arch = "x86_64", @@ -412,8 +422,9 @@ fn guard_is_in_main_executable() -> bool { mod main_image { use core::ffi::{c_char, c_int, c_void}; - /// The leading fields of `struct dl_phdr_info`; the loader passes its - /// real size, and only these four are read. + /// The leading fields of `struct dl_phdr_info` (identical in glibc, + /// bionic and FreeBSD); the loader fills all four for every object and + /// passes the real, larger size. Only these are read. #[repr(C)] struct DlPhdrInfo { addr: usize, @@ -435,6 +446,8 @@ mod main_image { } const PT_LOAD: u32 = 1; + /// `AT_PHDR`: 3 on Linux, Android and FreeBSD. + const AT_PHDR: c_int = 3; extern "C" { fn dl_iterate_phdr( @@ -442,40 +455,88 @@ mod main_image { ) -> c_int; } - /// `data` is the `(address, found)` pair passed by [`contains`]. - unsafe extern "C" fn first_object(info: *mut DlPhdrInfo, _size: usize, data: *mut c_void) -> c_int { - // SAFETY: `data` is the `&mut (usize, bool)` that `contains` passed to + #[cfg(any(target_os = "linux", target_os = "android"))] + fn main_program_headers() -> usize { + extern "C" { + fn getauxval(kind: core::ffi::c_ulong) -> core::ffi::c_ulong; + } + // SAFETY: `getauxval` only reads the auxiliary vector the kernel + // passed at exec time; it returns 0 for an absent entry. + unsafe { getauxval(AT_PHDR as core::ffi::c_ulong) as usize } + } + + #[cfg(target_os = "freebsd")] + fn main_program_headers() -> usize { + extern "C" { + fn elf_aux_info(aux: c_int, buf: *mut c_void, buflen: c_int) -> c_int; + } + let mut phdr: usize = 0; + // SAFETY: `elf_aux_info` writes at most `buflen` bytes into `buf`, + // here a `usize` of exactly that size, and returns 0 on success. + let ok = + unsafe { elf_aux_info(AT_PHDR, (&mut phdr as *mut usize).cast(), core::mem::size_of::() as c_int) }; + if ok == 0 { + phdr + } else { + 0 + } + } + + /// The `(address, main program headers, found)` triple passed through + /// `dl_iterate_phdr`'s `data` argument. + struct Search { + addr: usize, + main_phdr: usize, + found: bool, + } + + unsafe extern "C" fn visit(info: *mut DlPhdrInfo, _size: usize, data: *mut c_void) -> c_int { + // SAFETY: `data` is the `&mut Search` that `contains` passed to // `dl_iterate_phdr`, which hands it back unchanged on this thread for - // the duration of the call; `info` is non-null and valid for the - // callback, and `phdr` points at `phnum` program headers (ELF gABI). + // the duration of the call; `info` is valid for the callback, its + // four-field prefix is always populated, and `phdr` points at + // `phnum` program headers of `e_phentsize == 56` bytes (ELF gABI). unsafe { - let (addr, found) = &mut *(data as *mut (usize, bool)); + let search = &mut *(data as *mut Search); let info = &*info; + if info.phdr as usize != search.main_phdr { + return 0; // not the main program: keep walking + } for i in 0..usize::from(info.phnum) { let ph = &*info.phdr.add(i); if ph.p_type == PT_LOAD { let start = info.addr.wrapping_add(ph.p_vaddr as usize); - if *addr >= start && *addr - start < ph.p_memsz as usize { - *found = true; + if search.addr >= start && search.addr - start < ph.p_memsz as usize { + search.found = true; } } } } - 1 // stop after the first object: the main program + 1 // the main program was visited: stop } pub(super) fn contains(addr: usize) -> bool { - let mut state = (addr, false); - // SAFETY: `first_object` matches the callback ABI `dl_iterate_phdr` - // expects and reads `data` only as the `(usize, bool)` passed here, - // which outlives the call. - unsafe { dl_iterate_phdr(first_object, (&mut state as *mut (usize, bool)).cast()) }; - state.1 + let main_phdr = main_program_headers(); + if main_phdr == 0 { + return false; // no AT_PHDR: answer "library", the cheap side + } + let mut search = Search { + addr, + main_phdr, + found: false, + }; + // SAFETY: `visit` matches the callback ABI `dl_iterate_phdr` expects + // and reads `data` only as the `Search` passed here, which outlives + // the call. + unsafe { dl_iterate_phdr(visit, (&mut search as *mut Search).cast()) }; + search.found } } -/// macOS: image 0 is the main executable; `dladdr` names the image that -/// contains an address. +/// macOS: `_NSGetMachExecuteHeader()` is the main executable's Mach-O +/// header; `dladdr` names the image that contains an address. Image index 0 +/// is NOT used: dyld can report an inserted library there (sentinel-qa, +/// 2026-10-11, dyld `normalizeImageIndex`). #[cfg(all(not(miri), target_arch = "x86_64", target_os = "macos"))] mod main_image { use core::ffi::{c_char, c_int, c_void}; @@ -490,7 +551,7 @@ mod main_image { extern "C" { fn dladdr(addr: *const c_void, info: *mut DlInfo) -> c_int; - fn _dyld_get_image_header(index: u32) -> *const c_void; + fn _NSGetMachExecuteHeader() -> *const c_void; } pub(super) fn contains(addr: usize) -> bool { @@ -500,12 +561,13 @@ mod main_image { sname: core::ptr::null(), saddr: core::ptr::null_mut(), }; - // SAFETY: both are libSystem functions; `dladdr` writes only into the - // `DlInfo` it is given and returns 0 when the address is in no image; - // `_dyld_get_image_header(0)` returns the main executable's header. + // SAFETY: both are libSystem functions. `dladdr` writes only into the + // `DlInfo` it is given and returns 0 when no image contains the + // address; `_NSGetMachExecuteHeader` returns the main executable's + // header and takes no arguments. unsafe { dladdr(addr as *const c_void, &mut info) != 0 - && core::ptr::eq(info.fbase as *const c_void, _dyld_get_image_header(0)) + && core::ptr::eq(info.fbase as *const c_void, _NSGetMachExecuteHeader()) } } } @@ -589,7 +651,15 @@ mod tests { /// Can-stay-silent half: an address in a loaded system library (libc's /// `malloc`) is not in the main executable. Without this the lookup /// could be `true` everywhere and the library case would never happen. - #[cfg(all(not(miri), target_arch = "x86_64", target_os = "linux", target_env = "gnu"))] + /// Assumes libc is a shared object (the default dynamic linux-gnu + /// build): with `+crt-static`, `malloc` is inside the executable. + #[cfg(all( + not(miri), + target_arch = "x86_64", + target_os = "linux", + target_env = "gnu", + not(target_feature = "crt-static") + ))] #[test] fn an_address_in_libc_is_not_in_the_main_executable() { extern "C" { From 9f418689fd4eefbfd15411176f7675b99f8552b1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 07:14:46 +0000 Subject: [PATCH 6/9] probe: dlmopen(LM_ID_NEWLM) leg for the cpu_guard library check Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- scripts/cpu-guard-library-probe.sh | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/scripts/cpu-guard-library-probe.sh b/scripts/cpu-guard-library-probe.sh index 3c78d85c..85cd9e26 100755 --- a/scripts/cpu-guard-library-probe.sh +++ b/scripts/cpu-guard-library-probe.sh @@ -7,6 +7,8 @@ # # * the library must NOT end its host: the C program prints `survived` # and exits 0, and stderr carries the guard's `warning:` line; +# * the same library opened with dlmopen(LM_ID_NEWLM) must not end its host +# either (a lookup trusting "first object = main program" would); # * the executable must still exit 132 with the guard's `error:` line. # # Before the library rule existed, (a) ended the host with status 132. @@ -42,6 +44,20 @@ pub extern "C" fn probe_xor(a: u64, b: u64) -> u64 { x.to_array()[0] } RS +# The same host, but opening the library in a NEW link-map namespace. glibc +# then walks only that namespace, so a lookup that trusted "the first object +# is the main program" would see the library there and exit 132. +cat > "$WORK/host_ns.c" <<'C' +#define _GNU_SOURCE +#include +#include +int main(int argc, char **argv) { + void *h = dlmopen(LM_ID_NEWLM, argv[1], RTLD_NOW); + if (!h) { printf("dlmopen failed: %s\n", dlerror()); return 2; } + printf("survived\n"); + return 0; +} +C cat > "$WORK/host.c" <<'C' #include #include @@ -53,6 +69,7 @@ int main(int argc, char **argv) { } C cc -O1 -o "$WORK/host" "$WORK/host.c" -ldl +cc -O1 -o "$WORK/host_ns" "$WORK/host_ns.c" -ldl (cd "$WORK/lib" && CARGO_TARGET_DIR="$WORK/target" env -u RUSTFLAGS -u CARGO_BUILD_RUSTFLAGS \ cargo --config "$CFG" build --release --quiet) @@ -64,16 +81,21 @@ fail=0 set +e lib_out=$("$QEMU" -cpu Haswell "$WORK/host" "$WORK/target/release/libcpu_guard_library_probe.so" 2>&1) lib_rc=$? +ns_out=$("$QEMU" -cpu Haswell "$WORK/host_ns" "$WORK/target/release/libcpu_guard_library_probe.so" 2>&1) +ns_rc=$? exe_out=$("$QEMU" -cpu Haswell "$WORK/target-exe/release/examples/cpu_guard_probe" 2>&1) exe_rc=$? set -e echo "--- library host: exit $lib_rc"; echo "$lib_out" | grep -v 'TCG doesn' || true +echo "--- dlmopen host: exit $ns_rc"; echo "$ns_out" | grep -v 'TCG doesn' || true echo "--- executable: exit $exe_rc"; echo "$exe_out" | grep -v 'TCG doesn' || true [ "$lib_rc" -eq 0 ] || { echo "FAIL: the library ended its host (exit $lib_rc)"; fail=1; } echo "$lib_out" | grep -q '^survived$' || { echo "FAIL: host did not reach 'survived'"; fail=1; } echo "$lib_out" | grep -q '^warning: this binary needs CPU features' || { echo "FAIL: no guard warning from the library"; fail=1; } +[ "$ns_rc" -eq 0 ] || { echo "FAIL: the library ended its dlmopen host (exit $ns_rc)"; fail=1; } +echo "$ns_out" | grep -q '^survived$' || { echo "FAIL: dlmopen host did not reach 'survived'"; fail=1; } [ "$exe_rc" -eq 132 ] || { echo "FAIL: the v4 executable exited $exe_rc, expected 132"; fail=1; } echo "$exe_out" | grep -q '^error: this binary needs CPU features' || { echo "FAIL: no guard error from the executable"; fail=1; } From 96d4df04bc8edce0bcbe13abce9e8895d766379d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 07:17:21 +0000 Subject: [PATCH 7/9] simd_runtime: the AMX rung requires the AVX-512 VNNI its kernels run CPU_OPS_AMX_INT8's slice kernels are AVX-512 VNNI and AVX-512F, but the rung checked only amx_int8 and the OS tile grant. A hypervisor that exposes AMX while masking AVX-512 would get kernels it cannot run. The rung now also requires avx512f && avx512vnni; tier_ladder_gates_on_the_kernel_feature pins both masked cases. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- src/simd_runtime/cpu_ops.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/simd_runtime/cpu_ops.rs b/src/simd_runtime/cpu_ops.rs index 725ac296..b5894cbe 100644 --- a/src/simd_runtime/cpu_ops.rs +++ b/src/simd_runtime/cpu_ops.rs @@ -209,7 +209,12 @@ fn select_cpu_ops(_caps: crate::hpc::simd_caps::SimdCaps, amx_os_ok: bool) -> &' // being set. `simd_amx::amx_available()` runs the full // four-step gate (CPUID + OSXSAVE + XCR0 + arch_prctl); // demote to the AVX-512 path when the OS-check fails. - if _caps.amx_int8 && amx_os_ok { + // + // The AMX table's slice kernels are AVX-512 VNNI and AVX-512F + // (see `CPU_OPS_AMX_INT8`), so the rung also requires those: a + // hypervisor can expose the AMX bits while masking AVX-512, and + // the rung would then install kernels the CPU cannot run. + if _caps.amx_int8 && amx_os_ok && _caps.avx512f && _caps.avx512vnni { return &CPU_OPS_AMX_INT8; } if _caps.avx512f && _caps.avx512vnni { @@ -505,5 +510,18 @@ mod tests { }; assert_eq!(select_cpu_ops(spr, true).tier, "amx_int8"); assert_eq!(select_cpu_ops(spr, false).tier, "avx512vnni"); + // AMX bits exposed but AVX-512 masked (a hypervisor can do this): + // the AMX table's kernels are AVX-512, so the rung must not fire. + let amx_without_avx512 = SimdCaps { + avx512f: false, + avx512vnni: false, + ..spr + }; + assert_ne!(select_cpu_ops(amx_without_avx512, true).tier, "amx_int8"); + let amx_without_vnni = SimdCaps { + avx512vnni: false, + ..spr + }; + assert_eq!(select_cpu_ops(amx_without_vnni, true).tier, "avx512f"); } } From c26c671c79c243652dafda9c48b90dce428fb446 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 07:19:53 +0000 Subject: [PATCH 8/9] blackboard, unsafe inventory, co-research map (2026-10-11) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- .claude/blackboard.md | 10 ++ .../coresearch-2026-10-11-simd-fleet.md | 163 ++++++++++++++++++ .claude/knowledge/unsafe-inventory/README.md | 44 +++++ 3 files changed, 217 insertions(+) create mode 100644 .claude/knowledge/coresearch-2026-10-11-simd-fleet.md diff --git a/.claude/blackboard.md b/.claude/blackboard.md index e798ac4c..1d98294b 100644 --- a/.claude/blackboard.md +++ b/.claude/blackboard.md @@ -1,3 +1,13 @@ +## 2026-10-11 — `cpu_guard` no longer kills library hosts; three OOB paths and the AMX rung closed; fleet co-research map + +- **Defect found and fixed (X1):** the pre-`main` hook from #348 runs in every artifact that links ndarray, a `cdylib` included. Measured: an `x86-64-v4` cdylib `dlopen`ed by a C host under `qemu -cpu Haswell` ended the host with 132. A JVM loading lance-graph-java's `.so`, or Python loading tesseract-ocr-python, would die the same way. The guard now exits only when its code lies in the main executable (ELF: object at `getauxval(AT_PHDR)`, FreeBSD `elf_aux_info`; macOS `dladdr` vs `_NSGetMachExecuteHeader()`; Windows `GetModuleHandleExW`); in a library it prints a `warning:` and returns. Every lookup failure answers "library". +- **sentinel-qa:** no BLOCK, no loader-lock deadlock (glibc `dl_iterate_phdr` takes `dl_load_write_lock`, not the `dl_load_lock` `dlopen` holds; both recursive). Its two CONDITIONAL findings were real and are fixed: the first version trusted "first object = main program" (false on bionic; glibc `dlmopen` walks only the caller's namespace) and `_dyld_get_image_header(0)` (dyld can put an inserted dylib there). Measured: the first-object rule kills a `dlmopen(LM_ID_NEWLM)` host with 132; the `AT_PHDR` rule survives. +- **Probe in CI:** `scripts/cpu-guard-library-probe.sh`, job `cpu-guard-library`: `dlopen` host survives with the warning, `dlmopen` host survives, v4 executable exits 132, all under `qemu -cpu Haswell`. Disable runs: the pre-fix guard fails leg 1; the first-object rule fails leg 2. Unit tests pin both sides of the lookup (a test binary is its own executable; libc's `malloc` is not). +- **Unsafe inventory, all four confirmed safe-code OOB paths now closed:** `row_mut` (asserts), `sgemm/dgemm_blocked` (`assert_c_extent`), `int8_gemm_amx_tiled` (shape + AMX asserts), plus `dot_i8` from #349. Tests read panic messages and pass in debug and `--release`; each fix reverted turns its test red in `--release`. +- **AMX rung** in `simd_runtime/cpu_ops.rs` now requires `avx512f && avx512vnni`, the features of the kernels it installs. +- **Co-research map:** `.claude/knowledge/coresearch-2026-10-11-simd-fleet.md` — every point of the earlier fearless_simd / Accelerate / unsafe listings marked CLOSED / LOOSE / EXCLUDED, 27 crosswalk ideas, low/high/heavy fruit with probes. Two earlier "loose" rows are actually closed by measurement (array-backed AVX2 integer types: blackboard (15)/(18)). Other-repo items are recorded there, not pushed. +- **Open:** 18 facade types in no parity crate; `mul_add` not bit-identical across realizations (wasm); aarch64 guard; 95 runtime-detect sites unclassified; ladybug "generic" slice floors at AVX2; MedCare/q2 v4 Railway images unverified against the host CPU. + ## 2026-10-10 — the three `PackedDatabase`s (stroke split pinned) - **`PackedDatabase` ×3 is not a duplicate.** `packed` (fixed 128/384/1536 + an index) and `cascade` (width-derived split) lay out the same strokes at 2048 bytes, now pinned by `stroke_split_equals_the_cascade_split_at_2048` (red if cascade's split point moves); they keep separate query paths. `cam_pq::PackedDatabase` holds 6-byte CAM codes and shares only the name. Doc cross-links added; not merged. diff --git a/.claude/knowledge/coresearch-2026-10-11-simd-fleet.md b/.claude/knowledge/coresearch-2026-10-11-simd-fleet.md new file mode 100644 index 00000000..d6dea0e9 --- /dev/null +++ b/.claude/knowledge/coresearch-2026-10-11-simd-fleet.md @@ -0,0 +1,163 @@ +# Co-research map — `ndarray::simd` × fleet, 2026-10-11 + +READ BY: anyone deciding the next SIMD-tier, `cpu_guard`, consumer-tier or +unsafe-inventory work; `cargo-substrate-architect`, `simd-savant`, +`sentinel-qa`. + +Council: `/coresearch` (lance-graph `.claude/agents/coresearch-council.md`). +Five scouts (code cartographer, internal prior art, literature, systems, +concepts), a crosswalk of 27 ideas, three co-architects (bridge, +firewall/fit, falsifier). The map ratifies nothing. Items marked SHIPPED were +then implemented in this repo, in the same branch, because they were +ndarray-local and the session runs autonomously. Everything in another repo is +recorded, not pushed (this session pushes only to ndarray). + +## The question + +After #348–#353 (startup CPU guard, AVX-without-AVX2 arm, x86-64-v2 arm, +unsafe inventory), which points of the earlier "fearless_simd vs +`ndarray::simd`" and "Apple Accelerate: two halves" listings are closed, +which are loose, and which loose ends connect across the fleet +(lance-graph, OGAR, lance-graph-java, tesseract-rs, stockfish-rs, MedCare-rs, +ladybug-rs, q2, HubSPO-rs, spear, a2ui-rs, surrealdb fork, lichess-rs)? + +Premise split (each answer names one): **Q-TIER** which realization a build +selects; **Q-HOST** whether a built binary runs on its deploy host; +**Q-SWEEP** consumer raw intrinsics; **Q-SOUND** unsafe soundness; +**Q-MSRV** one canonical coordinate couples the fleet floor; **Q-PRIM** +missing facade words. + +## 1. The earlier listing, point by point + +### A. `fearless_simd` vs `ndarray::simd` + +| point | now | evidence | +|---|---|---| +| compile-time dispatch, one realization per build | CLOSED (design) | 8 realizations: avx512, avx2, avx (#349), v2 → scalar (#353), neon, wasm-simd128, wasm-scalar, scalar; nightly validation-only | +| runtime option only per kernel (`simd_runtime`) | CLOSED (design) | runtime dispatch of the core types was proposed (OQ-CSV-13, simd-dispatch-architecture Phase 5) and never built; consumer contract forbids ISA dispatch above `simd.rs`. Highway / `multiversion` / ifunc per call = second dispatch layer → SKIP | +| safety rests on build-time features; v4-on-AVX2 SIGILL | CLOSED for executables (#348) and **for library hosts (this branch)**; aarch64 not covered | see §3 | +| fixed 512-shaped names | CLOSED (design): the type is the layout. Highway's `FixedTag` is the same contract ("may cost extra masking"), which is what lets the parity matrix promise bit-identity | +| no SSE tiers | CLOSED: avx arm + v2 arm. A build with no target-cpu still gets the AVX2 arm + floor, by operator choice | +| AVX2 integer types are scalar polyfills | **CLOSED by measurement**, not loose: blackboard (15) found array `U64x4::and` and a hand loop compile to the same `vandps ymm`; (18) closed the `U64x4/U64x2+avx512vl` build "measured-not-worth-doing". Today 11 of the 13 array-backed types have 0–8 intrinsic methods; `U16x16` 26/29 and `U8x32` 28/34 are real `__m256i`. CERFACS (arXiv 2601.04922, READ-IN-FULL) agrees: intrinsics win mainly on data-dependent compare/select. Reopen only with a named consumer workload on byte compares | +| hand-written vs generated backends | PARTIAL: only the ternlog bodies are generated (`tools/gen_ternlog_bodies.py`, 256 tables asserted vs bit-serial). `crates/ndarray-gen` is test-array generation, unrelated. TSLGen (arXiv 2407.18728) is the published form of the idea | +| per-level tests vs scalar | PARTIAL: 15 parity groups, bit-identical across 8 realizations, but **18 facade types are in no parity crate**: F32x8, F64x4, I16x16, I16x32, I32x8, I64x4, I64x8, I8x32, I8x64, U16x16, U16x32, U16x8, U32x8, U64x4, U8x32, U8x8, F16x16, BF16x16 | +| MSRV | PARTIAL, resolved by fact: `rust-version = "1.99.0"` (2026-10-10) and the fleet's default branches moved to 1.99.0 the same day, which executed the "fleet-wide bump" option of lance-graph `ISS-NDARRAY-CANONICAL-COORDINATE-COUPLES-FLEET-MSRV` | +| one binary everywhere, no SIGILL | EXCLUDED as API (operator); the A1-compatible form is a choice ONCE before `main` (glibc-hwcaps, Docker multi-slice) — see §4 | + +### B. "Three places the CPU leaks back" + +| point | now | +|---|---| +| the build cares (no startup guard) | CLOSED (#348) | +| width fixed, speed not | CLOSED by measurement (above), plus a NEW instance: consumers do not inherit ndarray's `.cargo/config.toml` (Cargo reads config only from the invoking dir and parents; `RUSTFLAGS` replaces every config flag). tesseract-rs Docker, stockfish-rs, HubSPO, spear, OGAR, lichess build baseline x86-64 outside the intrinsic bodies; lance-graph pins v3 and measured baseline as 4.5× slower on its Goursat probe | +| runtime dispatch covers some kernels | CLOSED (design, anchor: startup-only checks) | + +### C. Accelerate mapping + +The mapping stands. Still LOOSE and unowned: about half of `src/hpc/` is +domain logic (nars, qualia, deepnsm, cam_pq, clam, gpt2, stable_diffusion), +`hpc-extras` gates six modules, and consumers import domain modules +(`hpc::splat3d`, `hpc::clam`, `hpc::bf16_truth`) across the fleet. PARK +until someone owns a cross-repo plan. + +### D. Unsafe inventory + +| point | now | +|---|---| +| 4 confirmed safe-code OOB paths | **CLOSED** (dot_i8 #349; row_mut, sgemm/dgemm_blocked, int8_gemm_amx_tiled this branch) | +| 212 NEEDS-TF | CLOSED as fact: E0133 for a plain fn calling even a safe `#[target_feature]` fn on 1.99 (RFC 2396 adds no reduction for facade methods) | +| AMX rung missing avx512f/vnni | **CLOSED** this branch | +| 856 missing `// SAFETY:` | LOOSE (documentation debt, lower than soundness) | +| 186 CONSOLIDATE, 139 SAFE-API, 42 REMOVABLE | LOOSE, mechanical | +| 28 SAFE-CRATE (`bytemuck`/`zerocopy`) | LOOSE, operator question; no prior ruling either way (searched: ndarray Cargo.toml + blackboard, lance-graph board + knowledge) | +| aarch64 guard | LOOSE (needs `getauxval(AT_HWCAP)`) | +| 95 `is_x86_feature_detected!` in 17 files | LOOSE: classify each as startup/once-only (sanctioned) vs per-call inside SIMD code (forbidden) | + +### E/F. Review issues and SSE follow-ups + +All three Codex issues on #348 CLOSED. SSE follow-ups: `U8x64::cmpeq_mask` +146 instructions on AVX1 and `F32x16::mul_add` → 16 software `fmaf` on +non-FMA tiers stay LOOSE (correct, slow; no consumer on those CPUs named). + +## 2. New findings (not in any earlier listing) + +| id | finding | sub-q | +|---|---|---| +| X1 | **`cpu_guard` killed library hosts.** Measured: an `x86-64-v4` cdylib linking ndarray, `dlopen`ed by a C host under `qemu -cpu Haswell`, ended the host with 132. Affects lance-graph-java's `.so` in a customer JVM and tesseract-ocr-python's wheel. **SHIPPED fix** (below) | Q-HOST | +| X2 | A slice built with no target-cpu gets the AVX2 floor, so ladybug's "generic" binary exits 132 on any non-AVX2 host: its fallback floor is v3, not generic | Q-HOST | +| X3/X4 | ladybug's multi-slice image picks by `grep avx512f /proc/cpuinfo`; v4 also needs bw/cd/dq/vl. The exact selector already exists: the guard's own predicate | Q-HOST | +| X6 | MedCare-rs and q2 ship `x86-64-v4` images to Railway; Railway publishes no CPU guarantee; GCP E2 mixes Broadwell (no AVX-512); MedCare's own handover says "v4 build never run"; MedCare measured the v4 gain as µs beside a 43 ms KDF | Q-HOST | +| X7 | psABI: v4 = AVX512F/BW/CD/DQ/VL only; VNNI/BF16/AMX/VPOPCNTDQ are outside every level, so `config-avx512.toml` and holograph's `avx512vpopcntdq` are "beyond v4" | Q-TIER | +| X11 | `F32x16::mul_add` is not bit-identical across realizations: wasm without `relaxed-simd` is mul-then-add, with it the host chooses; x86 fuses. lance-graph D-WFL-DET must exclude it | Q-TIER | +| X16 | lance-graph `ndarray_bridge.rs` intrinsic blocks are all under `cfg(not(feature = "ndarray-hpc"))` and `ndarray-hpc` is default → dead in default builds; holograph `hamming.rs` is live (feature `simd` default, no ndarray dep); ladybug `collapse_gate.rs` is a per-call `is_x86_feature_detected!` + raw `_mm256_*` kernel in no sweep ledger | Q-SWEEP | +| X17 | the MSRV issue still lists a `rev`-pin option forbidden by `E-CONSUMER-PINS-ON-INTERNAL-SIBLINGS-PROHIBITED-1`; `CARGO-COMPUTE-SUBSTRATE.md` §3 still shows `rev = ""`; lichess-rs says `cargo +1.95`; tesseract `rust.yml:59` says "1.95 floor" | Q-MSRV | +| X18 | OGAR pulls `encryption` (+ argon2 `ndarray-simd`) from ndarray **git master** while lance-graph uses a path: two package ids in a binary linking both. surrealdb's `ndarray = "0.17.2"` is crates.io, a different package from the fork at the same version | Q-MSRV | +| X19 | lance-graph-java `native/lgj-abi/.cargo/config.toml` says ndarray defaults to v3 (now native) and "one known host" (now BYOS); its test `abi.rs:719-729` asserts backend ∈ {AVX2, AVX-512}, so a v2 or AVX-arm build would fail it | Q-HOST | + +## 3. Shipped in this branch (ndarray only) + +1. **`cpu_guard` in a library host.** The pre-`main` hook exits 132 only + when the guard's code lies in the main executable; in a loaded library it + prints a `warning:` and returns, and the host can call + `check_build_cpu()`. Lookup: ELF matches the object whose program headers + are at `AT_PHDR` (`getauxval` / `elf_aux_info` + `dl_iterate_phdr`); + macOS `dladdr` vs `_NSGetMachExecuteHeader()`; Windows + `GetModuleHandleExW(FROM_ADDRESS)` vs `GetModuleHandleW(NULL)`. Every + lookup failure answers "library". sentinel-qa: no BLOCK, no loader-lock + deadlock; its two CONDITIONAL findings (bionic order, glibc `dlmopen`, + dyld image 0) are fixed. `scripts/cpu-guard-library-probe.sh` (CI job + `cpu-guard-library`) runs three legs under `qemu -cpu Haswell`: + `dlopen` host survives, `dlmopen(LM_ID_NEWLM)` host survives, v4 + executable exits 132. Disable runs: the pre-fix guard fails the first + leg; the first-object rule fails the `dlmopen` leg. +2. **Three safe-code OOB paths** closed (`row_mut`, `sgemm/dgemm_blocked`, + `int8_gemm_amx_tiled`), tests pass in debug and release, each red in + release when its fix is reverted. +3. **AMX rung** requires `avx512f && avx512vnni`. + +## 4. Exploration map — verdicts and fruit + +Verdicts: ADOPT-NOW (shipped or ready), PROBE (measure first), PARK, SKIP. + +### Low-hanging (≤1 hour each) + +| item | verdict | repo | why / probe | +|---|---|---|---| +| strike forbidden remedies from records: MSRV issue `rev` option; TD-NDARRAY-SIMD-POPCNT-NATIVE item (b) "runtime-detect VPOPCNTDQ"; regrade that TD (now the case `cpu_guard` reports) | ADOPT-NOW | lance-graph board | both contradict standing rulings | +| stale floors: lichess-rs `+1.95`, tesseract `rust.yml:59` | ADOPT-NOW | lichess-rs, tesseract-rs | one-line doc fixes | +| stale tier comments: lance-graph `.cargo/config.toml` "anything below v3 SIGILLs" (avx/v2 arms exist, guard reports), arm-discovery `Cargo.toml:30` recommends native/v4, lgj config "ndarray defaults to v3" | ADOPT-NOW | lance-graph, lgj | doc drift | +| consumer contract paragraph: a consumer build does not inherit ndarray's `.cargo/config.toml`; built-elsewhere artifacts name a psABI level | ADOPT-NOW | ndarray `.claude/knowledge/vertical-simd-consumer-contract.md` | X5/X6 | +| `mul_add` caveat in the facade docs; D-WFL-DET excludes it | ADOPT-NOW (doc) + PROBE | ndarray, lance-graph | probe: `a=1+2⁻²³, b=1−2⁻²³, c=−1` gives −2⁻⁴⁶ fused, 0 unfused; run on all arms | +| lgj: call `check_build_cpu()` in the `Abi` handshake and raise a Java exception | ADOPT-NOW | lance-graph-java | the warning path now exists | + +### High-hanging (a session each) + +| item | verdict | repo | probe | +|---|---|---|---| +| parity groups for the 18 uncovered facade types, table-generated, seeded random inputs + edge values (shifts and saturating ops are where stdarch bugs were found, arXiv 2606.17374) | PROBE then ADOPT | ndarray | disable: flip one scalar `cmpgt` in a new group; it must go red | +| fleet tier policy: deploys default to v3 unless they ship slices; MedCare/q2 v4 Railway images → v3 (measured gain negligible) or v4/v3/v2 slices | PROBE | MedCare-rs, q2 | read Railway `/proc/cpuinfo` at deploy; run the v4 image under `qemu -cpu Haswell` (must exit 132 at start, not mid-request) | +| slice selector from the guard: `--cpu-check`-style exit-code probe per slice, pick the first slice whose guard passes; ladybug's "generic" slice built with `config-v2.toml` | PROBE | ndarray (probe API) + ladybug | KnightsMill (avx512f without bw/vl) separates the grep selector from the guard selector under qemu | +| classify the 95 `is_x86_feature_detected!` sites (startup / once / per-call) as an inventory column | ADOPT | ndarray | ladybug `collapse_gate.rs` must classify per-call and `simd_runtime` once, or the classifier is wrong | +| consumer sweep: delete lance-graph `ndarray_bridge.rs` dead blocks; add holograph and ladybug `collapse_gate.rs` to the sweep ledger; TD-SIMD-SWEEP-W1..W5 | ADOPT (other repos) | lance-graph, ladybug | grep is navigation; a CI membrane test (like lgj's `ApiSurfaceTest`) is evidence | +| aarch64 guard via `getauxval(AT_HWCAP/HWCAP2)` | ADOPT | ndarray | dotprod build under `qemu-aarch64 -cpu cortex-a53` must report, not SIGILL | +| OGAR git-master vs lance-graph path: two ndarray package ids | PROBE | OGAR | `cargo metadata` on a binary linking both counts distinct ndarray ids | + +### Heavy-hanging (multi-session, needs an owner or a ruling) + +| item | verdict | why | +|---|---|---| +| Accelerate-core vs domain split of `src/hpc/` | PARK | cross-repo plan, consumers import domain modules directly | +| one-spec generated backends (TSLGen / fearless_simd_gen style) beyond ternlog | PARK | large; parity matrix already catches drift; revisit if a ninth realization appears | +| `bytemuck`/`zerocopy` for the 28 SAFE-CRATE rows | operator question | licences fit (Zlib/MIT/Apache, BSD-2/Apache/MIT); fork policy applies | +| Kani function contracts for index paths | PARK | needs a nightly CI-only job; 721 LLVM SIMD intrinsics unmodeled | +| glibc-hwcaps layout for the lgj `.so` | PROBE | does Panama's absolute-path `dlopen` search `glibc-hwcaps/`? Probably not; lgj's own picker would use `check_build_cpu()` | +| runtime dispatch of core types | SKIP | conflicts with the startup-only / compile-time anchors | + +## 5. Not searched (so "nothing found" is not "nothing exists") + +SIMDe and xsimd test methodology; Apple universal-binary practice in detail; +AWS and Hetzner CPU pages; tokio/rustls MSRV policies and `cargo-msrv`; +GCC's function-multiversioning page (HTTP 403, all ifunc claims SECONDHAND); +Windows loader documentation (sentinel-qa cited from memory); the argon2 +fork's own ndarray dependency line; whether any fleet binary is built with +`+crt-static` or non-PIE (the libc test assumes neither). diff --git a/.claude/knowledge/unsafe-inventory/README.md b/.claude/knowledge/unsafe-inventory/README.md index 25fd3334..ff617704 100644 --- a/.claude/knowledge/unsafe-inventory/README.md +++ b/.claude/knowledge/unsafe-inventory/README.md @@ -173,3 +173,47 @@ bug the flag did not name: the `vpmaddubsw` form saturated i16 at flag ("safe pub fn uses AVX2 with no cfg") is now covered: the AVX2 bodies are cfg-gated off the AVX-without-AVX2 arm, and a baseline (no `avx` cfg) build keeps `cpu_guard`'s AVX2 startup floor. + +## Addendum 2026-10-11 — confirmed OOB paths closed; `cpu_guard` loader lookups + +**Status of the four "Confirmed: out-of-bounds reachable from safe code" rows +above — all four closed:** + +- `simd_avx2.rs` `dot_i8`: fixed in #349 (see the 2026-10-10 addendum). +- `hpc/blocked_grid/iter.rs` `GridBlockMut::row_mut`: both checks are now + `assert!`. Test `row_mut_past_the_block_panics`. +- `backend/kernels_avx512.rs` `sgemm_blocked` / `dgemm_blocked`: new + `assert_c_extent` (`ldc >= n`, `c.len() >= (m-1)*ldc + n`, overflow-checked) + before any store. Tests `assert_c_extent_accepts_the_exact_size_and_rejects_one_less` + and `sgemm_and_dgemm_blocked_reject_a_short_c` (runs on an AVX-512 host). +- `hpc/int8_tile_gemm.rs` `int8_gemm_amx_tiled`: the 16/16/64 shape and + `amx_available()` are now `assert!`, shape first. Tests + `int8_gemm_amx_tiled_rejects_a_misaligned_m`, + `int8_gemm_amx_tiled_without_amx_panics_by_name`. + +All six tests pass under `cargo test` and `cargo test --release` on v4; each +fix reverted one at a time turns its test red in `--release` (where the old +`debug_assert!` vanished). + +**ISA-gating lead closed:** `simd_runtime/cpu_ops.rs` AMX rung now also +requires `avx512f && avx512vnni`, the features of the kernels it installs. +`simd_int_ops.rs:315/320` and AMX-BF16 remain open as listed above. + +**New FFI sites in `src/cpu_guard.rs` (sentinel-qa audited, 2026-10-11):** + +| block | verdict | +|---|---| +| ELF `main_image::visit` (`dl_iterate_phdr` callback; reads the 4-field `dl_phdr_info` prefix and `Elf64_Phdr`) | IRREDUCIBLE (FFI) — layout checked against glibc, bionic, FreeBSD headers | +| ELF `main_image::contains` (`dl_iterate_phdr`) | IRREDUCIBLE (FFI) | +| `main_program_headers` (`getauxval(AT_PHDR)` / FreeBSD `elf_aux_info`) | IRREDUCIBLE (FFI) | +| macOS `main_image::contains` (`dladdr`, `_NSGetMachExecuteHeader`) | IRREDUCIBLE (FFI) | +| Windows `main_image::contains` (`GetModuleHandleExW(FROM_ADDRESS)`, `GetModuleHandleW(NULL)`) | IRREDUCIBLE (FFI) | + +The first version matched "first object = main program" and `_dyld_get_image_header(0)`; +sentinel-qa showed both can answer "executable" for a library (bionic lists the +linker first; glibc `dlmopen` walks only the caller's namespace; dyld can put an +inserted dylib at index 0). Measured: under the first-object rule a +`dlmopen(LM_ID_NEWLM)` host is killed with 132; under `AT_PHDR` it survives +(`scripts/cpu-guard-library-probe.sh`, CI job `cpu-guard-library`). +No loader-lock deadlock: glibc `dl_iterate_phdr` takes `dl_load_write_lock`, +not the `dl_load_lock` `dlopen` holds while running constructors (both recursive). From ab9bc3aedcb292cfcb7669a62934bb5b458a45b2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 07:35:07 +0000 Subject: [PATCH 9/9] cpu-guard-library-probe: build only target artifacts for v4 The v4 config sets rustflags under target.cfg(x86_64). Without an explicit --target, cargo applies those to host artifacts too, so the build scripts of matrixmultiply and num-traits were compiled for AVX-512 and died with SIGILL on the CI runner, which has no AVX-512. Passing --target x86_64-unknown-linux-gnu restricts the flags to the target artifacts. Measured with cargo -v: 17 build-script compiles carried x86-64-v4 before, 0 after. The probe still passes locally. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- scripts/cpu-guard-library-probe.sh | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/scripts/cpu-guard-library-probe.sh b/scripts/cpu-guard-library-probe.sh index 85cd9e26..73035721 100755 --- a/scripts/cpu-guard-library-probe.sh +++ b/scripts/cpu-guard-library-probe.sh @@ -22,6 +22,12 @@ WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT export CARGO_PROFILE_DEV_DEBUG=0 CARGO_PROFILE_TEST_DEBUG=0 CARGO_INCREMENTAL=0 CFG="$ROOT/.cargo/config-v4.toml" +# An explicit --target is load-bearing. Without it cargo applies the config's +# target.cfg rustflags to HOST artifacts too, so build scripts are compiled for +# v4 and SIGILL on a runner that lacks AVX-512 (matrixmultiply's and +# num-traits' build scripts did exactly that in CI). With --target, only the +# target artifacts get v4; build scripts are built for the machine running them. +TRIPLE=x86_64-unknown-linux-gnu mkdir -p "$WORK/lib/src" cp "$ROOT/rust-toolchain.toml" "$WORK/lib/" @@ -72,18 +78,18 @@ cc -O1 -o "$WORK/host" "$WORK/host.c" -ldl cc -O1 -o "$WORK/host_ns" "$WORK/host_ns.c" -ldl (cd "$WORK/lib" && CARGO_TARGET_DIR="$WORK/target" env -u RUSTFLAGS -u CARGO_BUILD_RUSTFLAGS \ - cargo --config "$CFG" build --release --quiet) + cargo --config "$CFG" build --release --quiet --target "$TRIPLE") (cd "$ROOT" && CARGO_TARGET_DIR="$WORK/target-exe" env -u RUSTFLAGS -u CARGO_BUILD_RUSTFLAGS \ - cargo --config "$CFG" build --release --quiet --example cpu_guard_probe) + cargo --config "$CFG" build --release --quiet --target "$TRIPLE" --example cpu_guard_probe) fail=0 set +e -lib_out=$("$QEMU" -cpu Haswell "$WORK/host" "$WORK/target/release/libcpu_guard_library_probe.so" 2>&1) +lib_out=$("$QEMU" -cpu Haswell "$WORK/host" "$WORK/target/$TRIPLE/release/libcpu_guard_library_probe.so" 2>&1) lib_rc=$? -ns_out=$("$QEMU" -cpu Haswell "$WORK/host_ns" "$WORK/target/release/libcpu_guard_library_probe.so" 2>&1) +ns_out=$("$QEMU" -cpu Haswell "$WORK/host_ns" "$WORK/target/$TRIPLE/release/libcpu_guard_library_probe.so" 2>&1) ns_rc=$? -exe_out=$("$QEMU" -cpu Haswell "$WORK/target-exe/release/examples/cpu_guard_probe" 2>&1) +exe_out=$("$QEMU" -cpu Haswell "$WORK/target-exe/$TRIPLE/release/examples/cpu_guard_probe" 2>&1) exe_rc=$? set -e