[ty] Check packed AST index bounds (#27849)

An out-of-range `NodeIndex` in the final packed chunk can read padding
instead of rejecting the lookup. That padding can decode to an address
and node kind that were never recorded together, violating the invariant
used to reconstruct an AST reference.

Check the chunk's actual entry count before decoding either
representation, so invalid lookups fail before reconstructing a
reference.

Co-authored-by: zaniebot <242828183+zaniebot@users.noreply.github.com>
This commit is contained in:
zaniebotandzaniebot authored and GitHub committed 2026-08-18 19:17:54 -04:00
1 parent 107e38d045
commit 49bf50b1c7
1 file changed
+58
+58
View File
@@ -497,6 +497,14 @@ mod indexed {
let chunk_index = index / Self::CHUNK_LEN;
let entry_index = index % Self::CHUNK_LEN;
let chunk = &self.chunks[chunk_index];
// A partial chunk's trailing bits are padding, not indexed nodes.
assert!(
entry_index < usize::from(chunk.entry_count),
"index out of bounds: the len is {} but the index is {index}",
chunk_index * Self::CHUNK_LEN + usize::from(chunk.entry_count),
);
let words = &self.words[chunk.word_start as usize..];
match chunk.layout {
@@ -805,6 +813,56 @@ mod indexed {
mod tests {
use super::*;
#[test]
#[should_panic(expected = "index out of bounds: the len is 1 but the index is 1")]
fn indexed_nodes_relative_tail_bounds() {
let index = IndexedNodes {
chunks: vec![IndexChunk {
base: 0x1000,
word_start: 0,
entry_bits: IndexedNodes::KIND_BITS,
entry_count: 1,
layout: IndexChunkLayout::Relative,
}]
.into_boxed_slice(),
words: vec![u64::from(RootNodeKind::Stmt as u8)].into_boxed_slice(),
};
assert_eq!(index.get(0), (0x1000, RootNodeKind::Stmt));
index.get(1);
}
#[test]
#[should_panic(expected = "index out of bounds: the len is 1 but the index is 1")]
fn indexed_nodes_wide_tail_bounds() {
let index = IndexedNodes {
chunks: vec![IndexChunk {
base: 0,
word_start: 0,
entry_bits: IndexedNodes::KIND_BITS,
entry_count: 1,
layout: IndexChunkLayout::Wide,
}]
.into_boxed_slice(),
words: vec![0x1000, u64::from(RootNodeKind::Stmt as u8)].into_boxed_slice(),
};
assert_eq!(index.get(0), (0x1000, RootNodeKind::Stmt));
index.get(1);
}
#[test]
#[should_panic(expected = "index out of bounds: the len is 65 but the index is 65")]
fn indexed_nodes_chunk_boundary() {
let parsed = ruff_python_parser::parse_module(&"pass\n".repeat(65)).unwrap();
let indexed = IndexedModule::new(parsed);
assert_eq!(indexed.index.len(), 65);
assert_eq!(indexed.index.get(63).1, RootNodeKind::Stmt);
assert_eq!(indexed.index.get(64).1, RootNodeKind::Stmt);
indexed.index.get(65);
}
#[test]
fn indexed_nodes_round_trip() {
let parsed = ruff_python_parser::parse_module(