vt::cpu::PagedAttentionKernel reads btab[r * bt_row + (j / block_size) * bt_col] for every j < seq_lens[r] and never checks that the block table has that many columns. A caller that supplies a table shorter than ceil(seq_lens[r] / block_size) gets an out-of-bounds read of the tensor's own storage, a plausible-looking block index out of it, and attention over the wrong page. Nothing faults and nothing is reported.
tests/vllm/models/test_qwen3_5_decode_graph_seam.cpp supplies exactly that. Its SpecAttnMeta helper hardcoded block_table_num_cols = 1 with one entry per request, while shape C of the W6 case sits at pos = 20 with q = 4, so seq_lens = 24 against CachePool's block_size = 16 and the kernel reads column 1 of a one-column table.
It was silent until the neighbouring bytes moved. Found while fixing #1380: a DevicePool change altered which pooled block holds the table and therefore what follows it, and the same read became a SIGSEGV on thor:gpu0. gdb at src/vt/cpu/cpu_paged_attn.cpp:224, one frame under FullAttnBlockPaged, reached from the eager fallback of Qwen3_5DecodeGraph::Step for that shape. The case passes at origin/main (8 cases / 138 assertions) purely because the read landed on bytes that decoded to an in-range block index.
Both halves are fixed in the same flow, per AGENTS.md: the kernel refuses a short table with one compare per request outside the token loop, and the test's helper sizes its block table for the sequence length it declares.
FOLLOWING_AGENTS_PROTOCOL
Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]
vt::cpu::PagedAttentionKernelreadsbtab[r * bt_row + (j / block_size) * bt_col]for everyj < seq_lens[r]and never checks that the block table has that many columns. A caller that supplies a table shorter thanceil(seq_lens[r] / block_size)gets an out-of-bounds read of the tensor's own storage, a plausible-looking block index out of it, and attention over the wrong page. Nothing faults and nothing is reported.tests/vllm/models/test_qwen3_5_decode_graph_seam.cppsupplies exactly that. ItsSpecAttnMetahelper hardcodedblock_table_num_cols = 1with one entry per request, while shape C of the W6 case sits atpos = 20withq = 4, soseq_lens = 24againstCachePool'sblock_size = 16and the kernel reads column 1 of a one-column table.It was silent until the neighbouring bytes moved. Found while fixing #1380: a
DevicePoolchange altered which pooled block holds the table and therefore what follows it, and the same read became a SIGSEGV onthor:gpu0.gdbatsrc/vt/cpu/cpu_paged_attn.cpp:224, one frame underFullAttnBlockPaged, reached from the eager fallback ofQwen3_5DecodeGraph::Stepfor that shape. The case passes atorigin/main(8 cases / 138 assertions) purely because the read landed on bytes that decoded to an in-range block index.Both halves are fixed in the same flow, per AGENTS.md: the kernel refuses a short table with one compare per request outside the token loop, and the test's helper sizes its block table for the sequence length it declares.
FOLLOWING_AGENTS_PROTOCOL
Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]