Skip to content

fix(spec): bound attention's position and seq_len by CONTEXT_LEN - #3433

Merged
gHashTag merged 1 commit into
masterfrom
spec/cache-kv-bound
Sep 7, 2026
Merged

fix(spec): bound attention's position and seq_len by CONTEXT_LEN#3433
gHashTag merged 1 commit into
masterfrom
spec/cache-kv-bound

Conversation

@gHashTag

@gHashTag gHashTag commented Sep 7, 2026

Copy link
Copy Markdown
Owner

Closes #3432 · Refs #3430

One pass ago I listed cache_kv as needing "either a new parameter or .len, and that is a decision". It was 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
apply_rope_qk rope_tables.cos[position * ROPE_PAIRS + pair_idx] [CONTEXT_LEN * ROPE_PAIRS]f64 out-of-bounds read
cache_kv cache_k[position * EMBED_DIM + i] // [CONTEXT_LEN][EMBED_DIM] out-of-bounds write
compute_scores scores[h * CONTEXT_LEN + j], j < seq_len [NUM_HEADS * CONTEXT_LEN]f64 runs off every heads row

Two of the three were not in the #3430 audit at all. I found them by reading the neighbours of the one I had flagged.

In the generated Rust each panics. In C they do not — void cache_kv(AttentionBuffers* buffers, size_t position, double* cache_k, double* cache_v) is a bare pointer, so the write simply happens.

The intent was already written down twice

//   cache_k: []f32, cache_v: []f32,  // [CONTEXT_LEN][EMBED_DIM]
when cache_kv(&buffers, 0, cache_k, [0.0; EMBED_DIM * CONTEXT_LEN])

Stated in a comment on the declaration, allocated to by the specs own test, enforced by nothing.

The guard uses the constant, not .len

Same reasoning as #3431: a []T loses its length at the C ABI, so a .len-based guard would live in the Rust and Zig outputs and be absent from C. With a constant the identical check reaches all three:

void cache_kv(...) { if ((position >= CONTEXT_LEN)) { return ; }
pub fn cache_kv(...) -> () { if (position >= CONTEXT_LEN) { return (); }

compute_scores clamps seq_len rather than returning: a caller asking for more rows than the buffer holds still wants the ones that exist.

rustc errors on the file are unchanged at 23, all pre-existing and from other causes. All three guards green — STALE 0, 936/936, 80/80, CONVERGED pairs run: 2 — and both seals refreshed.

What this says about my own audit

I read the function and not the file. Twice now the thing I called a decision was a constant already in scope — this, and the CI cost two passes ago.

Closes #3432
Refs #3430

One pass ago I listed cache_kv as needing "either a new parameter or .len, and
that is a decision". It was not: `const CONTEXT_LEN : usize = 81;  // Max
sequence length` is declared eleven lines above it in the same file.

Three functions share the shape, and two of them were not in that audit at all
-- they were found by reading the neighbours of the one I had flagged:

  apply_rope_qk    rope_tables.cos[position * ROPE_PAIRS + pair_idx]
                   against [CONTEXT_LEN * ROPE_PAIRS]  -- out-of-bounds READ
  cache_kv         cache_k[position * EMBED_DIM + i]
                   against // [CONTEXT_LEN][EMBED_DIM] -- out-of-bounds WRITE
  compute_scores   scores[h * CONTEXT_LEN + j], j < seq_len
                   against [NUM_HEADS * CONTEXT_LEN]   -- runs off every row

Nothing bounded `position` or `seq_len`. In the generated Rust each panics; in
C it does not, the parameter being a bare pointer.

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 spec's own test allocates exactly `[0.0; EMBED_DIM * CONTEXT_LEN]`.
Stated in a comment, exercised by a test, enforced by nothing.

The guard uses the CONSTANT, not `.len`, for the same reason as #3431: a []T
loses its length at the C ABI, so a .len-based guard would exist in the Rust and
Zig outputs and be absent from C. With a constant the identical check appears in
all three. compute_scores clamps seq_len rather than returning, because a caller
asking for more rows than the buffer holds still wants the ones that exist.

rustc errors on the file are unchanged at 23; those are pre-existing and from
other causes.

The lesson is that I read the function and not the file. Twice now the thing I
called a decision was a constant already in scope.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gHashTag
gHashTag enabled auto-merge (squash) September 7, 2026 23:14
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

📓 NotebookLM Notebook linked to this PR

This notebook contains session context, decisions, and artifacts for this work.

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-09-07 23:14:44 UTC

Summary

Status Count
Total Open PRs 16
PRs with Failing Checks 13
PRs with All Checks Green 3
READY 2
FAILING 13
PENDING 0
NO CHECKS YET 0

These columns do not partition: 2 + 13 + 0 + 0 = 15, and there are 16 open PRs. A PR is being counted twice or not at all.

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=1d58d30ea13e != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

@gHashTag
gHashTag merged commit f4dc7ac into master Sep 7, 2026
26 of 29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

attention.t27: three functions index by an unbounded position or seq_len, past buffers the same file sizes by CONTEXT_LEN

1 participant