Follow-up to #3430, where I listed cache_kv as needing "either a new parameter or .len, and that is a decision". It is not. The bound was declared eleven lines above it.
const CONTEXT_LEN : usize = 81; // Max sequence length
Three functions, one file, the same shape
| function |
index |
buffer |
bounded by |
apply_rope_qk |
rope_tables.cos[position * ROPE_PAIRS + pair_idx] |
[CONTEXT_LEN * ROPE_PAIRS]f64 |
nothing — out-of-bounds read |
cache_kv |
cache_k[position * EMBED_DIM + i] |
// [CONTEXT_LEN][EMBED_DIM] |
nothing — out-of-bounds write |
compute_scores |
buffers.scores[h * CONTEXT_LEN + j], j < seq_len |
[NUM_HEADS * CONTEXT_LEN]f64 |
nothing — runs off every heads row |
In the generated Rust each of these panics. In C they do not: void cache_kv(AttentionBuffers* buffers, size_t position, double* cache_k, double* cache_v) — a bare pointer, so the write simply happens.
The intent was already written down, twice
The declaration carries the shape in a comment:
// cache_k: []f32, cache_v: []f32, // [CONTEXT_LEN][EMBED_DIM]
and the specs own test allocates exactly that:
when cache_kv(&buffers, 0, cache_k, [0.0; EMBED_DIM * CONTEXT_LEN])
So the precondition was stated in a comment, exercised by a test, and enforced by nothing.
The fix
Each function guards on CONTEXT_LEN. compute_scores clamps seq_len rather than returning, because a caller asking for more than the buffer holds still wants the rows that exist.
The bound is a constant, not .len, deliberately — the same reasoning as #3431: a []T loses its length at the C ABI, so a .len-based guard would exist in Rust and Zig and be absent from C. With a constant the identical check appears in all three:
void cache_kv(...) { if ((position >= CONTEXT_LEN)) { return ; }
pub fn cache_kv(...) -> () { if (position >= CONTEXT_LEN) { return (); }
fn cache_kv(...) void { ... if (position >= CONTEXT_LEN) { return ; }
rustc errors on the file are unchanged at 23 — those are pre-existing and from other causes.
What this says about the audit one pass ago
I put cache_kv in the "needs a decision" column because I looked at the function and not at the file. The constant it needed was in the same file, the shape was in a comment on the declaration, and the test already allocated to it. Two of the three functions here were not even in that audit — I found them only by reading the neighbours of the one I had flagged.
Follow-up to #3430, where I listed
cache_kvas needing "either a new parameter or.len, and that is a decision". It is not. The bound was declared eleven lines above it.Three functions, one file, the same shape
apply_rope_qkrope_tables.cos[position * ROPE_PAIRS + pair_idx][CONTEXT_LEN * ROPE_PAIRS]f64cache_kvcache_k[position * EMBED_DIM + i]// [CONTEXT_LEN][EMBED_DIM]compute_scoresbuffers.scores[h * CONTEXT_LEN + j],j < seq_len[NUM_HEADS * CONTEXT_LEN]f64In the generated Rust each of these panics. In C they do not:
void cache_kv(AttentionBuffers* buffers, size_t position, double* cache_k, double* cache_v)— a bare pointer, so the write simply happens.The intent was already written down, twice
The declaration carries the shape in a comment:
and the specs own test allocates exactly that:
So the precondition was stated in a comment, exercised by a test, and enforced by nothing.
The fix
Each function guards on
CONTEXT_LEN.compute_scoresclampsseq_lenrather than returning, because a caller asking for more than the buffer holds still wants the rows that exist.The bound is a constant, not
.len, deliberately — the same reasoning as #3431: a[]Tloses its length at the C ABI, so a.len-based guard would exist in Rust and Zig and be absent from C. With a constant the identical check appears in all three:rustc errors on the file are unchanged at 23 — those are pre-existing and from other causes.
What this says about the audit one pass ago
I put
cache_kvin the "needs a decision" column because I looked at the function and not at the file. The constant it needed was in the same file, the shape was in a comment on the declaration, and the test already allocated to it. Two of the three functions here were not even in that audit — I found them only by reading the neighbours of the one I had flagged.