diff --git a/bindings/rust/lib.rs b/bindings/rust/lib.rs index 776346ee..57e88450 100644 --- a/bindings/rust/lib.rs +++ b/bindings/rust/lib.rs @@ -57,4 +57,67 @@ mod tests { .set_language(&super::LANGUAGE.into()) .expect("Error loading Ruby parser"); } + + extern "C" { + fn tree_sitter_ruby_external_scanner_create() -> *mut std::ffi::c_void; + fn tree_sitter_ruby_external_scanner_destroy(payload: *mut std::ffi::c_void); + fn tree_sitter_ruby_external_scanner_serialize( + payload: *mut std::ffi::c_void, + buffer: *mut u8, + ) -> u32; + fn tree_sitter_ruby_external_scanner_deserialize( + payload: *mut std::ffi::c_void, + buffer: *const u8, + length: u32, + ); + } + + /// The external scanner must treat its serialization buffer as untrusted: + /// tree-sitter hands back whatever bytes it was given, and a heredoc record + /// carries a 32-bit word length that used to be copied without a bounds + /// check. Feeding arbitrary buffers must never read past `length`, and must + /// leave the scanner in a state that can still be serialized. + #[test] + fn test_external_scanner_rejects_malformed_state() { + // A pseudo-random stream keeps the test deterministic without a dependency. + let mut state = 0x2545_f491_4f6c_dd1d_u64; + let mut next_byte = || { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + (state >> 33) as u8 + }; + + let mut out = vec![0_u8; 1024]; + for _ in 0..20_000 { + let length = (next_byte() % 64) as usize; + let buffer: Vec = (0..length).map(|_| next_byte()).collect(); + + unsafe { + let scanner = tree_sitter_ruby_external_scanner_create(); + tree_sitter_ruby_external_scanner_deserialize( + scanner, + buffer.as_ptr(), + length as u32, + ); + let written = + tree_sitter_ruby_external_scanner_serialize(scanner, out.as_mut_ptr()); + assert!(written as usize <= out.len()); + tree_sitter_ruby_external_scanner_destroy(scanner); + } + } + } + + /// Round-tripping a well-formed state must survive validation. + #[test] + fn test_external_scanner_round_trips_heredoc_state() { + let mut parser = tree_sitter::Parser::new(); + parser + .set_language(&super::LANGUAGE.into()) + .expect("Error loading Ruby parser"); + + let source = "a = <<~DOC\n body\nDOC\n"; + let tree = parser.parse(source, None).unwrap(); + assert!(!tree.root_node().has_error()); + } } diff --git a/src/scanner.c b/src/scanner.c index b3ed0549..e94532f1 100644 --- a/src/scanner.c +++ b/src/scanner.c @@ -138,6 +138,11 @@ static inline unsigned serialize(Scanner *scanner, char *buffer) { return size; } +// The serialized state comes back from the caller, which may hand over a buffer +// that a previous (or corrupted) run produced. Every read is bounds-checked and +// any inconsistency drops the scanner back to its empty state rather than +// restoring garbage: an over-long word length would otherwise read past the +// buffer, and a truncated record would leave a half-built heredoc on the stack. static inline void deserialize(Scanner *scanner, const char *buffer, unsigned length) { unsigned size = 0; scanner->has_leading_whitespace = false; @@ -147,10 +152,19 @@ static inline void deserialize(Scanner *scanner, const char *buffer, unsigned le return; } - uint8_t literal_depth = buffer[size++]; + uint8_t literal_depth = (uint8_t)buffer[size++]; + if (length - size < (unsigned)literal_depth * 5) { + reset(scanner); + return; + } for (unsigned j = 0; j < literal_depth; j++) { Literal literal = {0}; - literal.type = (TokenType)(buffer[size++]); + uint8_t literal_type = (uint8_t)buffer[size++]; + if (literal_type > NONE) { + reset(scanner); + return; + } + literal.type = (TokenType)literal_type; literal.open_delimiter = (unsigned char)buffer[size++]; literal.close_delimiter = (unsigned char)buffer[size++]; literal.nesting_depth = (unsigned char)buffer[size++]; @@ -158,26 +172,45 @@ static inline void deserialize(Scanner *scanner, const char *buffer, unsigned le array_push(&scanner->literal_stack, literal); } - uint8_t open_heredoc_count = buffer[size++]; + if (size == length) { + reset(scanner); + return; + } + + uint8_t open_heredoc_count = (uint8_t)buffer[size++]; for (unsigned j = 0; j < open_heredoc_count; j++) { + if (length - size < 4 + sizeof(uint32_t)) { + reset(scanner); + return; + } + Heredoc heredoc = {0}; heredoc.end_word_indentation_allowed = buffer[size++]; heredoc.allows_interpolation = buffer[size++]; heredoc.started = buffer[size++]; heredoc.open_depth = (uint8_t)buffer[size++]; - heredoc.word = (String)array_new(); uint32_t word_length; memcpy(&word_length, &buffer[size], sizeof(uint32_t)); size += sizeof(uint32_t); - array_reserve(&heredoc.word, word_length); - memcpy(heredoc.word.contents, &buffer[size], word_length); + if (length - size < word_length) { + reset(scanner); + return; + } + + heredoc.word = (String)array_new(); + if (word_length > 0) { + array_reserve(&heredoc.word, word_length); + memcpy(heredoc.word.contents, &buffer[size], word_length); + } heredoc.word.size = word_length; size += word_length; array_push(&scanner->open_heredocs, heredoc); } - assert(size == length); + if (size != length) { + reset(scanner); + } } // `open_heredocs` keeps the heredocs whose bodies have started first, innermost