on this page
Series Overview · Part I: The Illusion of Safe Tensors · Part II: Trapped Inside <think> · Part III: Project-Then-Heal · Part IV: Spectral Trojans & Pre-Load Sanitization · Secant Repo · Secant Docs
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:
- Boundary 1 (Container / Deserializer - file format parsing): Can the file parser (
SafeTensors::deserialize,gguf_init_from_file, or apickle/protobuf reader) be tricked into executing code or corrupting memory while reading header lengths and byte spans? Eliminatingpicklein favor of.safetensorsclosed Boundary 1. - 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)? - Boundary 3 (Weight-to-Kernel / Config-to-Runtime Seam): What happens after a
.safetensorsor.gguffile 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_Kscale headers, or router biases) or companion config (eos_token_idingeneration_config.json,leading_dense_block) and uses it directly as a C++/CUDA array index, loop bound, or divisor?
| Boundary | Plain-English Layer | What It Checks | Owner |
|---|---|---|---|
| 1. Framing | File format parsing | Magic bytes, offsets, lengths, no pickle opcodes | Format parser (safetensors, gguf) |
| 2. Schema & Runtime | Model graph & generation loops | Tensor names, dtypes, shapes, token-budget loops | Model loader & sampler |
| 3. Values & Seams | Weight/config numbers used as kernel indices or divisors | Finite weights/scales, divisor floors, routing & vocab index bounds | secant 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.
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.
JSON header ≤ 100 MB; byte offsets fit file length.
PASS in the synthetic fixturemoe.gate.bias shape [8], dtype F32 matches graph.
IEEE-754 NaN comparisons always return false, so idx stays at the -1 sentinel and counts[-1] is incremented.
HEAP UNDER-WRITE (4 BYTES).safetensors tensor "moe.gate.bias" (F32, [8]): [ NaN, NaN, NaN, NaN, ... ]
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 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:
- Offset contiguity: Each tensor’s
data_offsets(s, e)must start at the previous tensor’s end (s == start) and satisfye >= s(SafeTensorError::InvalidOffset). - Shape-product overflow: Multiplying non-zero dimensions in
info.shapebyinfo.dtype.bitsize()viatry_foldmust not overflowusize(SafeTensorError::ValidationOverflow). - Bit-count byte alignment: The total bit count
nbitsmust be byte-aligned (nbits % 8 == 0, raisingSafeTensorError::MisalignedSliceotherwise). - Byte-span equality: The byte count
nbits / 8must equal the offset spane - s(SafeTensorError::TensorInvalidInfo). - Final buffer length: The end of the last tensor (
buffer_end + 8 + n) must equalbuffer.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 TensorFlowSavedModelgraphs for unsafe Python opcodes (REDUCE,GLOBAL,INST,OBJ), KerasLambdalayers, and file I/O operators. It skips.gguffiles entirely and excludes.safetensors,.gguf, and.onnxnumerical payloads from its inspection domain (arXiv:2608.27424).Fickling 0.1.6+(Trail of Bits) andModelAudit 0.2: Decompile Python pickle streams into abstract syntax trees to enforce import allowlists, and explicitly recommend.safetensorsas a safe alternative without parsing.safetensorsor.gguftensor values.- Hugging Face Hub
picklescan,ClamAV, and the model-viewer metadata parser:picklescanscans Python pickle opcodes,ClamAVscans for known malware signatures, and the Hub’s model-viewer metadata parser reads.safetensorsand.ggufheaders to render tensor shapes and chat templates in the web UI, while none of the three scans binary.safetensorsor.gguftensor 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::sortcomparators (!(a < b)or<=): When a comparator returnstrueoncomp(NaN, NaN)and the array has at least 17 elements (above GNUlibstdc++’s_S_threshold = 16insertion-sort cutoff, as inopenxla/xla#52412),__unguarded_partitionincrements__firstwith 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 thatsum_run >= sum_tgtsucceeded (such asassert(found)inllama_sampler_dist_apply,llama.cpp#29686), a singleNaNprobability aborts the server process withSIGABRT. - GPU radix sort bitwise twiddling (
NVIDIA CUB): Unlike comparison sorts,cub::BlockRadixSort::SortDescendingandcub::DeviceRadixSortdo not execute floating-point comparisons. Their bitwise transformation (Traits<float>::TwiddleIn) reinterprets eachfloatasuint32_tand flips only the sign bit (u ^ 0x80000000) when the sign bit is0, or all 32 bits (u ^ 0xFFFFFFFF) when the sign bit is1. That maps positiveqNaN(0x7FC00000->0xFFC00000) strictly above+Inf(0x7F800000->0xFF800000), placing+qNaNat index0as the #1 highest-scoring winner in GPU top-ksampling 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.
| Class | x86-64 outcome | ARM64 outcome |
|---|---|---|
| I | SIGABRT, 16/16 runs | Exit 0; heap under-write |
| II | SIGSEGV at -O1 and above | Exit 0; wrong lookup value |
| III | Exit 0; retains 300/300 tokens | Exit 0; retains 300/300 tokens |
| IV | SIGSEGV at -O1 and above | Exit 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.
| Case | Float32 input | ARM64 result | x86-64 result |
|---|---|---|---|
| 2A | qNaN | 0 | INT_MIN |
| 2B | +1.0e38 | INT_MAX | INT_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).
| Model | Crossover | Token | Outlier |
|---|---|---|---|
| SmolLM2-1.7B-Instruct | 8.65% | 8% | 12% |
| Qwen3.5-4B | 6.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.
| Rule | Tensor role | Requirement |
|---|---|---|
| S-0 | Headers and schema | Checked spans and matching shapes |
| B3-1 | Float weights and biases | Finite values |
| B3-2A | Multiplicative scales | Finite; zero and subnormal allowed |
| B3-2B | Divisor scales | Finite and above the chosen floor |
| B3-3 | Integer indices | Lower and upper bounds |
- S-0 (since v0.3.0): Check 64-bit span arithmetic and companion metadata (
config.jsonandgeneration_config.json) against.safetensorsembedding shapes, including0 <= eos_token_id, bos_token_id, pad_token_id, decoder_start_token_id < vocab_size,1 <= num_experts_per_tok <= num_local_experts, and0 <= 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
danddmin, reject exponent0x1Fwith 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 isisfinite(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.safetensorsand.ggufwitness bundles across all four Boundary-3 failure classes (class1router/gateNaN/-Inf,class2scale/biasNaN/+-1e38,class3NaN/sign/payload/tie arrays,class4I32/I64routing tables and zero/subnormal norm vectors) so you can testsecant sanitizeand stress-test your own model loader and C++/CUDA kernels out of the box.secant sanitizescans 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 siblingconfig.json/generation_config.jsoncompanion 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:
- 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)(whileconfig.jsonand.safetensorsweights remain 100% valid), an unguarded stop-token suppression path can index a[batch_size, vocab_size]logits buffer out of bounds. Caught statically bysecant sanitize(Rule S-0s0_companion_token_id_oobands0_companion_vocab_size_mismatch). - Container header or companion-config layer-count relation -> unsigned
uint32wrap (Rule S-0): When a container header or companionconfig.jsonspecifies a dense-prefix block count larger than the total block count, an unguarded 32-bit unsigned subtraction wraps around toUINT32_MAX(0xFFFFFFFF). Caught statically bysecant sanitize(Rule S-0s0_companion_leading_dense_block). - 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 bysecant sanitize --num-experts <E>(Rule B3-3routing_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
- 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/
- Hugging Face. Safetensors: Simple, safe way to store and distribute tensors (
safetensors/src/tensor.rs,Metadata::validate). https://github.com/huggingface/safetensors - Georgi Gerganov and the
ggml/llama.cppcontributors. GGUF v3 Format Specification andggmlTensor Library. https://github.com/ggml-org/llama.cpp - IEEE Computer Society. IEEE Standard for Floating-Point Arithmetic (IEEE Std 754-2019).
- 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), andCVE-2026-84286/GHSA-mw4q-pwhm-35f7(ExLlamaV3). - Vincenzo Fornaro. NaN router logits leave MoE top-k selection at best=-1 (
GHSA-5xpg-vw35-2687). August 5, 2026. - Hanzo AI. HIP-0113: Cognitive Sidecar & Hanzo Engine Provider Runtime for Thinking Chains. Section 4.5, int8 embedding quantization.
- Secant. Paper, Section 5: Static Weight Auditing. Spectral crossover and quantized scale-background experiments.