Skip to content

Commit 3fa11bf

Browse files
hyperpolymathclaude
andcommitted
fix(parser): never panic on malformed/truncated .twasm (reinforce v0)
The error path in expect() sliced self.src[pos..pos+20], panicking with an out-of-bounds (or non-UTF-8-boundary) slice when a parse error occurred near end-of-input. peek_word() had the same self.src[start..] hazard. A malformed .twasm must be a clean Err diagnostic, never a crash. - expect/peek_word now use self.src.get(..) and a length-clamped error window. - regression test parser_never_panics_on_malformed_input feeds char-boundary truncations of all six examples + adversarial fragments (unbalanced delimiters, UTF-8, partial type forms); previously 6 inputs panicked, now 0. 32/32 codegen tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0c15133 commit 3fa11bf

2 files changed

Lines changed: 41 additions & 5 deletions

File tree

crates/typed-wasm-codegen/src/parser.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@ impl<'a> Parser<'a> {
8383

8484
fn peek_word(&mut self, word: &str) -> bool {
8585
let start = self.pos;
86-
if self.src[start..].starts_with(word) {
86+
// Panic-safe: `get` returns None on an out-of-range / non-char-boundary
87+
// index instead of panicking on malformed or truncated input.
88+
if self.src.get(start..).map_or(false, |rest| rest.starts_with(word)) {
8789
let next_char = self.src.as_bytes().get(start + word.len());
8890
if next_char.is_none() || !next_char.unwrap().is_ascii_alphabetic() {
8991
return true;
@@ -132,15 +134,18 @@ impl<'a> Parser<'a> {
132134

133135
fn expect(&mut self, s: &str) -> Result<(), String> {
134136
self.skip_whitespace();
135-
if self.src[self.pos..].starts_with(s) {
137+
if self.src.get(self.pos..).map_or(false, |rest| rest.starts_with(s)) {
136138
self.pos += s.len();
137139
Ok(())
138140
} else {
141+
// Panic-safe error context: clamp to the end of input and fall back
142+
// if the window straddles a UTF-8 boundary, so malformed/truncated
143+
// input yields an Err, never an out-of-bounds slice panic.
144+
let end = (self.pos + 20).min(self.src.len());
145+
let found = self.src.get(self.pos..end).unwrap_or("<end-of-input>");
139146
Err(format!(
140147
"Expected '{}' at position {}, found '{}'",
141-
s,
142-
self.pos,
143-
&self.src[self.pos..self.pos + 20]
148+
s, self.pos, found
144149
))
145150
}
146151
}

crates/typed-wasm-codegen/tests/corpus.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,37 @@ fn parsed_example_corpus_round_trips() {
176176
}
177177
}
178178

179+
/// Reinforce: a malformed or truncated `.twasm` must yield `Ok`/`Err`, never a
180+
/// panic. Feeds char-boundary truncations of every example plus adversarial
181+
/// fragments (unbalanced delimiters, UTF-8, partial type forms) through the
182+
/// parser; the test fails if any input panics (out-of-bounds slice, etc.).
183+
#[test]
184+
fn parser_never_panics_on_malformed_input() {
185+
let examples: [&str; 6] = [
186+
include_str!("../../../examples/01-single-module.twasm"),
187+
include_str!("../../../examples/02-multi-module.twasm"),
188+
include_str!("../../../examples/03-ownership-linearity.twasm"),
189+
include_str!("../../../examples/04-ecs-game.twasm"),
190+
include_str!("../../../examples/05-tropical-cost.twasm"),
191+
include_str!("../../../examples/06-epistemic-sync.twasm"),
192+
];
193+
for src in examples {
194+
for cut in (0..src.len()).step_by(7) {
195+
if src.is_char_boundary(cut) {
196+
let _ = parser::parse_module(&src[..cut]); // must not panic
197+
}
198+
}
199+
}
200+
for frag in [
201+
"", "region", "region X", "region X {", "region X { f: ",
202+
"region X { f: i32", "fn", "fn f(", "fn f(&mut region<",
203+
"fn f() -> ", "import region", "import region X from \"",
204+
"opt<ptr<", "区域 region", "{{{{{", "<<<<<", "region X { invariant {",
205+
] {
206+
let _ = parser::parse_module(frag); // must not panic
207+
}
208+
}
209+
179210
#[test]
180211
fn generated_corpus_round_trips() {
181212
for seed in 0..512u64 {

0 commit comments

Comments
 (0)