Skip to content

Commit 49bf50b

Browse files
authored
[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>
1 parent 107e38d commit 49bf50b

1 file changed

Lines changed: 58 additions & 0 deletions

File tree

‎crates/ruff_db/src/parsed.rs‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,14 @@ mod indexed {
497497
let chunk_index = index / Self::CHUNK_LEN;
498498
let entry_index = index % Self::CHUNK_LEN;
499499
let chunk = &self.chunks[chunk_index];
500+
501+
// A partial chunk's trailing bits are padding, not indexed nodes.
502+
assert!(
503+
entry_index < usize::from(chunk.entry_count),
504+
"index out of bounds: the len is {} but the index is {index}",
505+
chunk_index * Self::CHUNK_LEN + usize::from(chunk.entry_count),
506+
);
507+
500508
let words = &self.words[chunk.word_start as usize..];
501509

502510
match chunk.layout {
@@ -805,6 +813,56 @@ mod indexed {
805813
mod tests {
806814
use super::*;
807815

816+
#[test]
817+
#[should_panic(expected = "index out of bounds: the len is 1 but the index is 1")]
818+
fn indexed_nodes_relative_tail_bounds() {
819+
let index = IndexedNodes {
820+
chunks: vec![IndexChunk {
821+
base: 0x1000,
822+
word_start: 0,
823+
entry_bits: IndexedNodes::KIND_BITS,
824+
entry_count: 1,
825+
layout: IndexChunkLayout::Relative,
826+
}]
827+
.into_boxed_slice(),
828+
words: vec![u64::from(RootNodeKind::Stmt as u8)].into_boxed_slice(),
829+
};
830+
831+
assert_eq!(index.get(0), (0x1000, RootNodeKind::Stmt));
832+
index.get(1);
833+
}
834+
835+
#[test]
836+
#[should_panic(expected = "index out of bounds: the len is 1 but the index is 1")]
837+
fn indexed_nodes_wide_tail_bounds() {
838+
let index = IndexedNodes {
839+
chunks: vec![IndexChunk {
840+
base: 0,
841+
word_start: 0,
842+
entry_bits: IndexedNodes::KIND_BITS,
843+
entry_count: 1,
844+
layout: IndexChunkLayout::Wide,
845+
}]
846+
.into_boxed_slice(),
847+
words: vec![0x1000, u64::from(RootNodeKind::Stmt as u8)].into_boxed_slice(),
848+
};
849+
850+
assert_eq!(index.get(0), (0x1000, RootNodeKind::Stmt));
851+
index.get(1);
852+
}
853+
854+
#[test]
855+
#[should_panic(expected = "index out of bounds: the len is 65 but the index is 65")]
856+
fn indexed_nodes_chunk_boundary() {
857+
let parsed = ruff_python_parser::parse_module(&"pass\n".repeat(65)).unwrap();
858+
let indexed = IndexedModule::new(parsed);
859+
860+
assert_eq!(indexed.index.len(), 65);
861+
assert_eq!(indexed.index.get(63).1, RootNodeKind::Stmt);
862+
assert_eq!(indexed.index.get(64).1, RootNodeKind::Stmt);
863+
indexed.index.get(65);
864+
}
865+
808866
#[test]
809867
fn indexed_nodes_round_trip() {
810868
let parsed = ruff_python_parser::parse_module(

0 commit comments

Comments
 (0)