Notebook/Essay

The Illusion of Safe Tensors: Why Valid Model Files Still Need Numerical Checks

SafeTensors guarantees that deserializing a model file cannot execute pickle code. It does not guarantee that the numbers inside are safe for the C++ and CUDA kernels that consume them.

on this page

When the machine learning ecosystem moved from Python pickle checkpoints to .safetensors, it solved a very specific and very real problem. Loading a .pt file used to mean handing untrusted bytes to a stack-based virtual machine that could call os.system before a single weight ever reached the GPU. SafeTensors replaced that with a flat JSON header and raw byte buffers. Opening a .safetensors file cannot execute arbitrary Python code during deserialization.

Somewhere along the way, that narrow parser guarantee turned into a much broader assumption. Teams started treating .safetensors and schema-validated .gguf files as inert data. If the JSON header parses cleanly, the tensor offsets stay inside the file bounds, and the shapes match the model architecture, the checkpoint is assumed to be memory-safe to load and run.

Those checks alone do not establish memory safety during inference. Once the file loader maps the byte buffers into memory, C++ and CUDA kernels take over. Besides matrix multiplication, they use stored values for expert routing, quantization scales, sampling, and array indexing.

If a checkpoint supplies an out-of-range integer in an expert routing table, or an IEEE-754 NaN in a router bias tensor where a C++ kernel assumes at least one comparison will succeed, the file format parser has done nothing wrong. Every offset and shape in the header is completely valid. Yet a native kernel with this unchecked invariant can corrupt its own stack or heap on the first forward pass that reads those values.

When MIT CSAIL’s Sparks of Weight-Space Programming (November 2, 2026) called for “auditing without evaluation” and turning direct weight inspection into “a rigorous, reproducible engineering discipline,” their focus was on behavioral circuits and backdoors in weight space, which we explore in Part II and Part III. Before you even get to behavioral circuits, however, auditing weights directly on disk raises a separate systems question: stored weight values control native C++ memory indexing and pointer arithmetic.

This first post in the series breaks that gap between container parsing and native kernel execution into four C++ failure classes. Each comes with a self-contained C++ kernel under 30 lines that you can compile and run on your own machine, plus our open-source Secant static weight scanner (secant sanitize and secant probe).

Where the trust boundary actually sits

SafeTensors documents this distinction: tensor values are not inspected, and NaN or ±Inf can appear inside a valid file.

A model checkpoint passes through three distinct trust boundaries between disk and execution. Checking the first two does not cover the third:

  1. Boundary 1 (Container / Deserializer - file format parsing): Can the file parser (SafeTensors::deserialize, gguf_init_from_file, or a pickle/protobuf reader) be tricked into executing code or corrupting memory while reading header lengths and byte spans? Eliminating pickle in favor of .safetensors closed Boundary 1.
  2. Boundary 2 (Activation / Prompt Runtime - generation & schema loops): Once a valid model is loaded, does its architectural schema match config.json, and can an inference-time prompt trap the generation loop in a degenerate state (such as the <think> reasoning-loop exhaustion we dissect in Part II)?
  3. Boundary 3 (Weight-to-Kernel / Config-to-Runtime Seam): What happens after a .safetensors or .gguf file passes 100% of Boundary-1 format validation, when the inference engine trusts a number inside a tensor (g_idx, ffn_gate_tid2eid.weight, Q8_0/Q4_K scale headers, or router biases) or companion config (eos_token_id in generation_config.json, leading_dense_block) and uses it directly as a C++/CUDA array index, loop bound, or divisor?
BoundaryPlain-English LayerWhat It ChecksOwner
1. FramingFile format parsingMagic bytes, offsets, lengths, no pickle opcodesFormat parser (safetensors, gguf)
2. Schema & RuntimeModel graph & generation loopsTensor names, dtypes, shapes, token-budget loopsModel loader & sampler
3. Values & SeamsWeight/config numbers used as kernel indices or divisorsFinite weights/scales, divisor floors, routing & vocab index boundssecant sanitize & C++/CUDA kernel guards

The sentence “SafeTensors is safe because it has no pickle” is a statement about Boundary 1 only. Removing pickle did nothing for Boundary 3, where a kernel reads a stored number and uses it as an index or divisor. To get a sense of how many such sites exist, secant scan-sinks found 14,512 places across 7 inference codebases where a weight value flows into a cast, index, division, or release assert. Those are review leads, not bugs; none has been triaged.

Click through the interactive explorer below to see, for each failure class, the stored value that framing and schema checks never inspect, and what the synthetic C++ kernel does with it at Boundary 3 on both x86-64 and ARM64 hardware. The explorer’s Boundary 1 and 2 results are those of the synthetic fixture, not of a named runtime.

Interactive trust-boundary explorer

SafeTensors secures the file parser. It does not secure the C++ kernels that consume the weights.

Select each of the four Boundary-3 failure classes below to trace how a structurally valid .safetensors file can pass container and schema checks and still carry a value that a native kernel with an unchecked invariant mishandles: it can corrupt memory, fail open, or diverge across CPU architectures. The kernels are the synthetic listings from this post, not a named production runtime.

Boundary 1 · File Parser Container Framing

JSON header ≤ 100 MB; byte offsets fit file length.

PASS in the synthetic fixture
Boundary 2 · Model Loader Architecture Schema

moe.gate.bias shape [8], dtype F32 matches graph.

PASS in the synthetic fixture
Boundary 3 · Synthetic C++ kernel Argmax Sentinel Under-Write (counts[-1])

IEEE-754 NaN comparisons always return false, so idx stays at the -1 sentinel and counts[-1] is incremented.

HEAP UNDER-WRITE (4 BYTES)
Stored weight value in valid .safetensors
tensor "moe.gate.bias" (F32, [8]):
[ NaN, NaN, NaN, NaN, ... ]
What a native kernel with this unchecked invariant does (synthetic listing)
float best = -INFINITY;
int idx = -1; // sentinel
for (int e = 0; e < n; ++e) {
  float s = w[e] * x + bias[e];
  if (s > best) {
    best = s; idx = e;
  }
}
counts[idx]++; // OOB at idx == -1
Measured outcome (class1_sentinel.cpp, no sanitizer): on x86-64 Linux the 4-byte under-write corrupts the heap chunk header and glibc aborts in free() with double free or corruption (out) (SIGABRT, all 16 runs, GCC 16 and Clang 22 at -O0 to -O3). On ARM64 macOS the under-write executes silently and the program exits 0, printing selected=-1 count=0. secant sanitize --clamp-nonfinite clamps non-finite float weights before load.
Kernels are condensed from the four listings in this post. Outcomes are from 212 sanitizer-free runs: 176 on x86-64 Linux (GCC 16 and Clang 22, -O0 to -O3) and 36 on ARM64 macOS (Apple clang 21, -O1 and -O2). secant sanitize clamps non-finite float weights and rejects out-of-range routing indices before any kernel runs.

Looking directly at the huggingface/safetensors parser implementation (safetensors/src/tensor.rs) makes the boundary explicit. Calling SafeTensors::deserialize(buffer) invokes SafeTensors::read_metadata(buffer) followed by Metadata::validate(&self), which checks only five structural invariants:

  1. Offset contiguity: Each tensor’s data_offsets (s, e) must start at the previous tensor’s end (s == start) and satisfy e >= s (SafeTensorError::InvalidOffset).
  2. Shape-product overflow: Multiplying non-zero dimensions in info.shape by info.dtype.bitsize() via try_fold must not overflow usize (SafeTensorError::ValidationOverflow).
  3. Bit-count byte alignment: The total bit count nbits must be byte-aligned (nbits % 8 == 0, raising SafeTensorError::MisalignedSlice otherwise).
  4. Byte-span equality: The byte count nbits / 8 must equal the offset span e - s (SafeTensorError::TensorInvalidInfo).
  5. Final buffer length: The end of the last tensor (buffer_end + 8 + n) must equal buffer.len().

Once validate() returns Ok, deserialize() returns a borrowed slice &buffer[8 + n..] without inspecting a single byte of tensor payload data. A .safetensors file holding 0x7FC00000 (qNaN) router weights, -100.0 scales, or g_idx = 0x7FFFFFFF passes SafeTensors::deserialize() without error.

The scanner descriptions below concern code deserialization and container structure. Those checks address different questions from the numerical-value checks in this post:

  • ModelScan 0.8.6 (Protect AI): Scans Pickle (.pkl, .pt, .bin), H5 (.h5), Keras (.keras), and TensorFlow SavedModel graphs for unsafe Python opcodes (REDUCE, GLOBAL, INST, OBJ), Keras Lambda layers, and file I/O operators. It skips .gguf files entirely and excludes .safetensors, .gguf, and .onnx numerical payloads from its inspection domain (arXiv:2608.27424).
  • Fickling 0.1.6+ (Trail of Bits) and ModelAudit 0.2: Decompile Python pickle streams into abstract syntax trees to enforce import allowlists, and explicitly recommend .safetensors as a safe alternative without parsing .safetensors or .gguf tensor values.
  • Hugging Face Hub picklescan, ClamAV, and the model-viewer metadata parser: picklescan scans Python pickle opcodes, ClamAV scans for known malware signatures, and the Hub’s model-viewer metadata parser reads .safetensors and .gguf headers to render tensor shapes and chat templates in the web UI, while none of the three scans binary .safetensors or .gguf tensor payloads for non-finite floats, divisor scales, or out-of-range routing indices.

Many of the best-known public model-file CVEs sit at Boundary 1 or Boundary 2 as classic container-framing bugs, such as CVE-2025-53630, CVE-2026-27940, and CVE-2026-33298 (GGUF parser allocation and stride overflows), CVE-2025-49847 (GGUF vocabulary buffer overflow), and GHSA-4gw4-j89j-4c8r (Colibri .safetensors offset vs. config.json allocation mismatch).

Value-driven faults have also surfaced in both request-side inputs and persistent model checkpoints. On the request side, CVE-2020-15207 (TFLite axis bounds), CVE-2022-23560 (TensorFlow sparse-to-dense conversion), and CVE-2025-62164 / CVE-2026-56340 / CVE-2026-73557 (vLLM prompt_embeds sparse coordinate indices, where torch.sparse.check_sparse_tensor_invariants() mutated a process-global flag that raced under concurrent asyncio.gather requests) all allowed caller-supplied tensor values to corrupt native memory. On the persistent checkpoint side, disclosures through late 2026 include GHSA-5xpg-vw35-2687 (Colibri NaN MoE router weights), CVE-2026-86288 (GPTQModel out-of-bounds g_idx group indices in .safetensors), and CVE-2026-84286 / GHSA-mw4q-pwhm-35f7 (ExLlamaV3 checkpoint parameter indexing an out-of-range host function pointer). What we examine below is the core systems mechanism across all four failure classes when persistent model weights inside structurally valid .safetensors and .gguf checkpoints violate the numerical assumptions of native C++ and CUDA inference kernels.

Four failure classes in under 30 lines of C++

Every kernel below is under 30 lines of standard C++, compiles with stock clang++, and takes either a control input or a crafted weight value.

Class I: Comparison-sentinel exhaustion (NaN router collapse)

This failure class is prior art. Vincenzo Fornaro’s GHSA-5xpg-vw35-2687, published August 5, 2026, documented NaN router logits leaving best = -1 and causing negative-index heap out-of-bounds reads and writes in Colibri. This post places that mechanism alongside three other numerical failure classes.

When an MoE router or top-k kernel picks a winning expert, it usually initializes the best score to -INFINITY and the winning index to -1, then loops over the candidates with if (score > best).

Under IEEE-754 rules, every ordered comparison (>, <, >=, <=) involving NaN evaluates to false. If a checkpoint stores NaN in a router weight or bias tensor, s > best never succeeds. The loop finishes with idx still set to -1, and the very next line writes to counts[-1], four bytes before the start of the buffer.

// class1_sentinel.cpp: comparison-sentinel exhaustion (Class I)
// Build: clang++ -O1 -g -fsanitize=address class1_sentinel.cpp -o class1
// Run:   ./class1 control | nan
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <vector>
int pick_expert(const float* w, const float* bias, float x, int n) {
    float best = -INFINITY;
    int idx = -1;                        // sentinel: nothing selected yet
    for (int e = 0; e < n; ++e) {
        float s = w[e] * x + bias[e];
        if (s > best) { best = s; idx = e; }
    }
    return idx;                          // returned without validation
}
int main(int argc, char** argv) {
    const int n = 8;
    std::vector<float> w(n, 0.5f), b(n, 0.1f);
    if (argc > 1 && !strcmp(argv[1], "nan")) for (auto& v : b) v = NAN;
    int* counts = (int*)calloc(n, sizeof(int));   // per-expert token counters
    int idx = pick_expert(w.data(), b.data(), 1.0f, n);
    counts[idx]++;                       // idx == -1 under NaN: out of bounds
    printf("selected=%d count=%d\n", idx, counts[idx < 0 ? 0 : idx]);
    free(counts);
    return 0;
}

On the control run, this prints selected=0 count=1 and exits cleanly. With nan, counts[idx]++ overwrites the 4 bytes immediately preceding the heap allocation. Even without AddressSanitizer on x86-64 Linux, that under-write corrupts glibc’s heap chunk metadata and triggers a SIGABRT (double free or corruption (out)) as soon as free(counts) runs.

Class II: Float-to-integer cast overflow on quantization scales

Some quantization paths use a stored scale as a divisor before converting to an integer. The synthetic kernel below models that operation. Multiplicative weight-dequantization scales have different requirements, discussed later in this post.

In C++, casting NaN, INFINITY, or any float outside the range of int into a signed 32-bit integer is undefined behavior. On x86-64, the scalar cvttss2si instruction returns the hardware indefinite value 0x80000000 (INT_MIN, or -2,147,483,648). If the developer only wrote an upper clamp (if (q > 255) q = 255), INT_MIN bypasses the guard and indexes 8 GiB below the lookup table.

// class2_cast.cpp: float-to-int cast of a parameter-derived value (Class II)
// Build: clang++ -O1 -g -fsanitize=address,float-cast-overflow class2_cast.cpp -o class2
// Run:   ./class2 control | zero | nan
#include <cmath>
#include <cstdio>
#include <cstring>
#include <vector>
float dequant_lookup(const std::vector<float>& table, float x, float scale, int zp) {
    int q = (int)(x / scale) + zp;       // UB when x / scale is inf, NaN or huge
    if (q > 255) q = 255;                // one-sided clamp: lower bound is missing
    return table[q];
}
int main(int argc, char** argv) {
    std::vector<float> table(256);
    for (int i = 0; i < 256; ++i) table[i] = i * 0.01f;
    float scale = 0.05f;                 // control: ordinary positive scale
    if (argc > 1 && !strcmp(argv[1], "zero")) scale = 0.0f;
    if (argc > 1 && !strcmp(argv[1], "nan"))  scale = NAN;
    float r = dequant_lookup(table, 1.0f, scale, 0);
    printf("result=%f\n", r);
    return 0;
}

Here the CPU architecture changes the outcome completely. On x86-64 servers, scalar CVTTSS2SI and vector VCVTTPS2DQ emit the Integer Indefinite value 0x80000000 (INT32_MIN = -2147483648) for qNaN, sNaN, ±Inf, and any finite float outside [-2147483648.0f, 2147483520.0f], so every -O1, -O2 and -O3 build we ran bypasses if (q > 255) and faults on table[q] with SIGSEGV at index -2147483648. At -O0 the process aborts one step earlier (SIGABRT): in those builds the libstdc++ 16 vector::operator[] bounds assertion (__n < this->size()) fires first. On ARM64 (Apple Silicon or AWS Graviton), the FCVTZS instruction converts NaN -> 0 (table[0] = 0.0) and saturates +Inf or 1e38f -> INT32_MAX (+2147483647), which a one-sided upper clamp if (idx >= N) idx = N - 1; or if (q > 255) q = 255; catches cleanly (table[255] = 2.55). Meanwhile, NVIDIA CUDA PTX (cvt.rzi.s32.f32 and cvt.rni.s32.f32) also saturates NaN -> 0 and +Inf / 1e38f -> INT32_MAX (+2147483647), yet if a CUDA kernel narrows that 32-bit result directly to int8_t before clamping ((int8_t)__float2int_rn(x / scale)), 0x7FFFFFFF truncates to 0xFF (-1). The same checkpoint or synthetic kernel that passes testing on Apple Silicon or AWS Graviton faults on production x86-64 hosts.

Hanzo AI’s HIP-0113: Cognitive Sidecar & Hanzo Engine Provider Runtime for Thinking Chains, Section 4.5 already warns that non-finite float conversion differs between ARM64 FCVTZS and x86 CVTTSD2SI, requiring rejection before int8 embedding quantization to prevent a consensus split. That is prior art for conversion divergence. The C++ kernels tested here connect it to an unchecked signed index and an out-of-bounds memory access; HIP-0113’s consensus warning does not itself establish that memory-safety consequence.

Class III: Comparator and cumulative-sum collapse in sampling

Top-k and top-p (nucleus) samplers sort logits in descending order and sum softmax probabilities until the cumulative total crosses p. Passing NaN into std::sort violates strict weak ordering (a > b is never true), and adding NaN into the running probability sum ensures cum >= p never triggers.

// class3_selection.cpp: sort-order collapse in a nucleus (top-p) cutoff (Class III)
// Build: clang++ -O1 -g -fsanitize=address,undefined class3_selection.cpp -o class3
// Run:   ./class3 control | nan
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <numeric>
#include <vector>
int nucleus_size(const std::vector<float>& logits, float p) {
    std::vector<int> order(logits.size());
    std::iota(order.begin(), order.end(), 0);
    std::sort(order.begin(), order.end(),
              [&](int a, int b) { return logits[a] > logits[b]; });
    float z = 0.f;
    for (float l : logits) z += expf(l);
    float cum = 0.f;
    int k = 0;
    for (int i : order) {
        cum += expf(logits[i]) / z;
        ++k;
        if (cum >= p) break;                       // with NaN, cum >= p is never true
    }
    return k;
}
int main(int argc, char** argv) {
    std::vector<float> logits(300);
    for (size_t i = 0; i < logits.size(); ++i) logits[i] = (float)((i * 37) % 11) * 0.1f;
    if (argc > 1 && argv[1][0] == 'n')
        for (size_t i = 0; i < logits.size(); i += 3) logits[i] = NAN;
    printf("nucleus_size=%d of %zu\n", nucleus_size(logits, 0.9f), logits.size());
}

Instead of crashing, this kernel fails open. The control run keeps 251 of 300 tokens; the nan run never hits the cum >= p break and keeps all 300. Even compiling with -D_GLIBCXX_DEBUG on GCC 16 or Clang 22 does not catch it: libstdc++ checks whether the comparator is irreflexive (x > x), and since NaN > NaN is false, the debug check passes silently.

In other sorting and sampling patterns across CPU and GPU runtimes, the same NaN input turns from a fail-open into a crash or an inverted winner:

  • Non-irreflexive std::sort comparators (!(a < b) or <=): When a comparator returns true on comp(NaN, NaN) and the array has at least 17 elements (above GNU libstdc++’s _S_threshold = 16 insertion-sort cutoff, as in openxla/xla#52412), __unguarded_partition increments __first with zero bounds checks (while (__comp(*__first, __pivot)) ++__first;) until it walks off the end of the heap or stack buffer. And when a cumulative-sum sampler asserts that sum_run >= sum_tgt succeeded (such as assert(found) in llama_sampler_dist_apply, llama.cpp#29686), a single NaN probability aborts the server process with SIGABRT.
  • GPU radix sort bitwise twiddling (NVIDIA CUB): Unlike comparison sorts, cub::BlockRadixSort::SortDescending and cub::DeviceRadixSort do not execute floating-point comparisons. Their bitwise transformation (Traits<float>::TwiddleIn) reinterprets each float as uint32_t and flips only the sign bit (u ^ 0x80000000) when the sign bit is 0, or all 32 bits (u ^ 0xFFFFFFFF) when the sign bit is 1. That maps positive qNaN (0x7FC00000 -> 0xFFC00000) strictly above +Inf (0x7F800000 -> 0xFF800000), placing +qNaN at index 0 as the #1 highest-scoring winner in GPU top-k sampling and MoE routing.

Class IV: Unchecked integer routing and lookup tensors

Modern architectures increasingly store integer routing tables, permutation maps, or expert lookup arrays directly inside .safetensors checkpoints (dtype: "I32"). Inside the C++ kernel, their elements are used directly as row indices into weight or activation buffers.

// class4_index_table.cpp: an integer parameter tensor used as an index (Class IV)
// Build: clang++ -O1 -g -fsanitize=address class4_index_table.cpp -o class4
// Run:   ./class4 control | crafted
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <vector>
void gather_rows(const std::vector<int32_t>& route, const std::vector<float>& experts,
                 int dim, int token, float* out) {
    int32_t row = route[token];          // value comes from the checkpoint payload
    for (int d = 0; d < dim; ++d)
        out[d] = experts[(size_t)row * dim + d];   // no 0 <= row < num_experts check
}
int main(int argc, char** argv) {
    const int vocab = 64, num_experts = 8, dim = 4;
    std::vector<int32_t> route(vocab);
    for (int t = 0; t < vocab; ++t) route[t] = t % num_experts;
    std::vector<float> experts(num_experts * dim, 1.0f);
    if (argc > 1 && !strcmp(argv[1], "crafted")) route[5] = 1 << 20;
    float out[4];
    gather_rows(route, experts, dim, 5, out);
    printf("out0=%f\n", out[0]);
    return 0;
}

Changing a single int32_t parameter entry leaves the .safetensors header, shape, and byte length completely valid, yet causes an immediate out-of-bounds heap read. A smaller out-of-bounds index may instead read adjacent mapped memory without a crash.

How hardware changes the observed outcome

Sanitizers prove a memory bug exists, but release builds do not run with AddressSanitizer enabled. To see what the same synthetic kernels do in a release-style build, we compiled the four C++ kernels above without any sanitizers and ran them 212 times, hashing each binary before and after every run. On x86-64 Fedora 44 Linux that was 176 runs: GCC 16.2.1 and Clang 22.1.8, -O0 through -O3 (8 configurations), plus -D_GLIBCXX_DEBUG builds for Class III, with every input run twice. On ARM64 macOS (Apple M4 Max) it was 36 runs: Apple clang 21 at -O1 and -O2, every input run twice.

Classx86-64 outcomeARM64 outcome
ISIGABRT, 16/16 runsExit 0; heap under-write
IISIGSEGV at -O1 and aboveExit 0; wrong lookup value
IIIExit 0; retains 300/300 tokensExit 0; retains 300/300 tokens
IVSIGSEGV at -O1 and aboveExit 0; out-of-bounds read

Inputs: I = NaN router; II = zero or NaN divisor scale; III = NaN logits; IV = crafted I32 routing index. All builds in this table omit sanitizers.

For Classes II and IV, the x86-64 -O0 builds instead aborted at the libstdc++ bounds assertion. In Class I, glibc detected heap corruption during free(), while macOS printed selected=-1 count=0 without aborting. Class II returned 0.0 or 2.55 on ARM64; Class IV read a mapped zero-filled page 16 MiB beyond the buffer and printed out0=0.000000.

The same synthetic kernel and poisoned input can exit 0 on an ARM64 laptop and hard-crash on x86-64, or silently read out-of-bounds memory on ARM64 while exiting 0. Testing on one architecture is not evidence about the other.

Cross-ISA conformance: two distinct Class II cases

The retained November 6, 2026 conformance run used the same 824-byte SafeTensors fixture on our ARM64 and x86-64 test hosts. Disassembly records ARM64 fcvtzs w0, s0 and x86-64 cvttss2si %xmm0, %eax. The table gives the measured conversions; the paragraph below explains their consequence for an upper-bound-only check.

CaseFloat32 inputARM64 resultx86-64 result
2AqNaN0INT_MIN
2B+1.0e38INT_MAXINT_MIN

Key: 2A bits = 0x7fc00000; 2B bits = 0x7e967699; INT_MIN = -2,147,483,648; INT_MAX = 2,147,483,647.

With only an idx >= N check and an ordinary positive table length, ARM64’s zero result selects the first entry and its INT_MAX result is rejected. Both negative x86-64 results escape that upper-bound-only check.

Scope of these synthetic kernel tests

Every listing in this article is a self-contained, synthetic C++ kernel that takes a weight value directly as input and reproduces one numerical failure class across x86-64 and ARM64. The pre-load checks in the next section apply to any .safetensors or .gguf checkpoint before a runtime loads it.

The spectral trojan boundary: a nominal crossover, not a safety certificate

Static auditing also has limits when every weight is finite. A planted backdoor can be a single direction added to a weight matrix, and whether a scan of that one file can see it depends on how large the addition is compared with the matrix’s own largest direction. In one planted rank-one edit per model, the perturbation’s sole singular value is sigma_trojan = g · ||W||_F, where g = ||Delta W||_F / ||W||_F is the edit’s size relative to the matrix. Its standalone spectral norm equals the base matrix’s leading singular value at the nominal crossover:

g* = sigma_1(W) / ||W||_F

At the tested layer and placement, g* was 8.65% for SmolLM2-1.7B-Instruct and 6.57% for Qwen3.5-4B. This equality is a reference point, not an exact prediction of the leading direction of W + Delta W: overlap with the base singular directions matters.

Doses are percentages of the base matrix’s Frobenius norm; token = first dose at which the planted trigger token appears in the layer’s top-16 promoted tokens; outlier = first dose at which the layer’s spectral ratio stands out from the same-role layers around it (the automatic sibling test in Part IV).

ModelCrossoverTokenOutlier
SmolLM2-1.7B-Instruct8.65%8%12%
Qwen3.5-4B6.57%8%12%

The 0.10% to 4% sweep produced no signal in either model; the trigger token first entered the promoted top-16 vocabulary projection at 8% dose, while the scalar sibling-outlier test first fired at 12% in both models.

Quantized scales have a benign subnormal background

Scale scans need similar care. Ordinary quantizer-produced Q8_0 and pure Q4_K copies of Qwen3.5-4B contained 272,730 subnormal scales (0.20% of 135,168,000 Q8_0 blocks) and 8,992,791 of 33,792,000 header scale fields (d, dmin) across 16,896,000 Q4_K superblocks (about 0.53 per Q4_K superblock). Across all 10 standard GGUF quantized formats (Q4_0, Q4_1, Q5_0, Q5_1, Q8_0, Q2_K, Q3_K, Q4_K, Q5_K, Q6_K) in our 10-checkpoint verification suite (186,870,528 quantized blocks across 912 quantized tensors, including mixed Q4_K_M shards and TinyLlama-Q2_K), secant sanitize --dry-run scanned 10 / 10 checkpoints with 0 observed false positives (status: "clean"). A filter that rejects every subnormal or zero scale would flag these ordinary quantized copies.

The reason comes directly from how Q4_K super-blocks are quantized. Each 256-weight Q4_K super-block stores d and dmin as IEEE-754 binary16 (ggml_half, where the smallest positive normal number is 2^-14 ≈ 6.1035e-05 at bit pattern 0x0400, and positive subnormals span 0x0001 to 0x03FF), alongside eight sub-blocks with 6-bit integer scales s_j in [0, 63] and mins m_j in [0, 63]. Because the sub-block scale is divided by 63 to form the super-block scale d (and a 4-bit weight’s maximum magnitude max |x_i| is scaled by 15 * 63 = 945), any sub-block whose maximum weight magnitude sits below 945 * 6.1035e-05 ≈ 0.0577 (or a sub-block scale around 2.8e-03 before dividing by 63) produces a legitimate FP16 subnormal d value (for example, across dense blk.*.ffn_down.weight and token_embd.weight in Qwen3.5-4B Q4_K, or d = 4.55e-05 < 6.1035e-05 in blk.0.ffn_down_exps row 0 of llama.cpp’s MoE test fixture), while zero-padded blocks carry d = 0.0 (0x0000).

GGUF weight dequantization multiplies by d and dmin (w = d * s_j * q - dmin * m_j) and never divides by them at inference time. Finite zero and subnormal FP16 block scales in .gguf files cannot trigger division by zero or float-to-integer cast overflow. By contrast, static activation and KV-cache quantization scales (input_scale, k_scale, v_scale) are used as divisors (round(x / scale)), so their safety depends on the numerator range and the consumer’s conversion checks. The synthetic Class II example illustrates the failure when those checks are missing.

Pre-load checks by tensor role

Separating multiplicative block scales, divisor scales, and routing indices gives five checks for the failure classes discussed here. Their applicability depends on the tensor’s role; this is not a claim of zero false positives across all checkpoints.

RuleTensor roleRequirement
S-0Headers and schemaChecked spans and matching shapes
B3-1Float weights and biasesFinite values
B3-2AMultiplicative scalesFinite; zero and subnormal allowed
B3-2BDivisor scalesFinite and above the chosen floor
B3-3Integer indicesLower and upper bounds
  • S-0 (since v0.3.0): Check 64-bit span arithmetic and companion metadata (config.json and generation_config.json) against .safetensors embedding shapes, including 0 <= eos_token_id, bos_token_id, pad_token_id, decoder_start_token_id < vocab_size, 1 <= num_experts_per_tok <= num_local_experts, and 0 <= leading_dense_block <= num_hidden_layers. This covers schema and companion-config constraints relevant to Classes III and IV.
  • B3-1: For Classes I to III, reject NaN and infinity using the exponent fields: F32 (u32 & 0x7F800000) != 0x7F800000, BF16 (u16 & 0x7F80) != 0x7F80, and F16 (u16 & 0x7C00) != 0x7C00.
  • B3-2A: For multiplicative FP16 d and dmin, reject exponent 0x1F with the same F16 mask. Zero and subnormals remain valid, including the Q4_K example above.
  • B3-2B: For activation and KV divisor scales (input_scale, k_scale, v_scale), the proposed floor is isfinite(s) && s >= 1e-12. Its suitability must be checked against the quantization scheme and expected ranges.
  • B3-3: For Class IV indices (g_idx, expert_map, full_row_indices), require membership in [0, rows) before lookup.

In secant sanitize, inspect quantized scale findings by kind (quant_scale_nan, quant_scale_inf, quant_scale_extreme) and offset rather than treating quant_scale_subnormal as a hostile verdict.

Automating Boundary-3 defense with Secant

Secant (documentation) provides a Rust CLI and zero-copy C library (libsecant.a / secant.h) for these checks:

  • secant probe (--class all --format both --out-dir <dir>) generates deterministic, self-verified .safetensors and .gguf witness bundles across all four Boundary-3 failure classes (class1 router/gate NaN/-Inf, class2 scale/bias NaN/+-1e38, class3 NaN/sign/payload/tie arrays, class4 I32/I64 routing tables and zero/subnormal norm vectors) so you can test secant sanitize and stress-test your own model loader and C++/CUDA kernels out of the box.
  • secant sanitize scans unquantized float tensors, all 10 standard GGUF quantized block-scale formats (Q4_0, Q4_1, Q5_0, Q5_1, Q8_0, Q2_K, Q3_K, Q4_K, Q5_K, Q6_K), divisor scales (input_scale, k_scale, v_scale, q_scale, y_scale, prob_scale, *scale_inv, rope_freqs), integer routing/index tensors (tid2eid, expert_id*, g_idx, expert_map, full_row_indices, hash_indices_table), GGUF MoE metadata (s0_expert_count, s0_expert_used_count), and sibling config.json / generation_config.json companion files (s0_companion_vocab_size_mismatch, s0_companion_token_id_oob, s0_companion_expert_count, s0_companion_expert_used_count, s0_companion_leading_dense_block) before load.

During our Boundary-3 audit across inference stacks, three general post-deserialization failure classes demonstrated why format validation alone is not enough:

  1. Companion-config control token ID -> out-of-bounds logits indexing (Rule S-0): When a companion configuration file specifies a stop or control token ID outside [0, vocab_size) (while config.json and .safetensors weights remain 100% valid), an unguarded stop-token suppression path can index a [batch_size, vocab_size] logits buffer out of bounds. Caught statically by secant sanitize (Rule S-0 s0_companion_token_id_oob and s0_companion_vocab_size_mismatch).
  2. Container header or companion-config layer-count relation -> unsigned uint32 wrap (Rule S-0): When a container header or companion config.json specifies a dense-prefix block count larger than the total block count, an unguarded 32-bit unsigned subtraction wraps around to UINT32_MAX (0xFFFFFFFF). Caught statically by secant sanitize (Rule S-0 s0_companion_leading_dense_block).
  3. Integer MoE routing table -> out-of-bounds host or device array indexing (Rule B3-3): When a structurally valid checkpoint stores an integer token-to-expert routing tensor containing an expert ID outside [-1, num_experts), an unguarded loader or MoE dispatch loop that uses the stored integer directly as a table or buffer offset reads or writes out of bounds on the host or device heap. Caught statically by secant sanitize --num-experts <E> (Rule B3-3 routing_id_out_of_range / routing_id_negative).
# 1. Generate deterministic Class I-IV boundary-value fixtures out of the box
$ secant probe --class all --format safetensors \
    --out-dir /tmp/probes --receipt /tmp/probe-receipt.json \
    --attempt-id part1-demo --num-experts 8

# 2. Scan and remediate a poisoned checkpoint before loading
$ secant sanitize \
    --model /tmp/probes/class1-router-nan-neginf.safetensors \
    --receipt /tmp/sanitize-receipt.json --attempt-id part1-demo \
    --num-experts 8 --clamp-nonfinite \
    --out /tmp/clean-class1.safetensors --json
{
  "schema": "secant.wsp.sanitize.v1",
  "attempt_id": "part1-demo",
  "policy_version": "unquantized-static-values-v1",
  "status": "remediated",
  "format": "safetensors",
  "input_unchanged": true,
  "before": {
    "tensors": 3,
    "float_elements": 384,
    "total_findings": 102,
    "counts": { "inf": 51, "nan": 51 }
  },
  "after": {
    "tensors": 3,
    "float_elements": 384,
    "total_findings": 0,
    "counts": {}
  },
  "output_published": true
}

Because the probe fixtures generated by secant probe are deterministic, running the two commands above produces the exact same before and after counts on your machine.

In Part II (Trapped Inside <think>), we turn the same weight-inspection engine from defending C++ kernels to reading and editing the reasoning circuits inside open-weight language models.

References

  1. Rikiya Takehi, Jayden Teoh, Alex Zhang, and Omar Khattab. Sparks of Weight-Space Programming: LLMs can natively read & write neural network weights. MIT CSAIL, November 2, 2026. https://rikiyat.github.io/blog/weight-space-programming/
  2. Hugging Face. Safetensors: Simple, safe way to store and distribute tensors (safetensors/src/tensor.rs, Metadata::validate). https://github.com/huggingface/safetensors
  3. Georgi Gerganov and the ggml / llama.cpp contributors. GGUF v3 Format Specification and ggml Tensor Library. https://github.com/ggml-org/llama.cpp
  4. IEEE Computer Society. IEEE Standard for Floating-Point Arithmetic (IEEE Std 754-2019).
  5. National Vulnerability Database (NVD) & GitHub Security Advisories: CVE-2020-15207 (TFLite), CVE-2022-23560 (TensorFlow), CVE-2025-49847, CVE-2025-53630, CVE-2026-27940, & CVE-2026-33298 (GGUF), CVE-2025-62164, CVE-2026-56340, & CVE-2026-73557 (vLLM), CVE-2026-86288 (GPTQModel), and CVE-2026-84286 / GHSA-mw4q-pwhm-35f7 (ExLlamaV3).
  6. Vincenzo Fornaro. NaN router logits leave MoE top-k selection at best=-1 (GHSA-5xpg-vw35-2687). August 5, 2026.
  7. Hanzo AI. HIP-0113: Cognitive Sidecar & Hanzo Engine Provider Runtime for Thinking Chains. Section 4.5, int8 embedding quantization.
  8. Secant. Paper, Section 5: Static Weight Auditing. Spectral crossover and quantized scale-background experiments.