Skip to content

Commit b215495

Browse files
committed
Relax the block check in OperatorsReader<'_>
This commit relaxes a check added in bytecodealliance#2134 which maintains a stack of frame kinds in the operators reader, in addition to the validator. The goal of bytecodealliance#2134 was to ensure that spec-wise-syntactically-invalid-modules are caught in the parser without the need of the validator, but investigation in bytecodealliance#2180 has shown that this is a source of at least some of a performance regression. The change here is to relax the check to still be able to pass spec tests while making such infrastructure cheaper. The reader now maintains just a small `depth: u32` counter instead of a stack of kinds. This means that the reader can still catch invalid modules such as instructions-after-`end`, but the validator is required to handle situations such as `else` outside of an `if` block. This required some adjustments to tests as well as some workarounds for the upstream spec tests that assert legacy exception-handling instructions are malformed, not invalid.
1 parent 84d3a4c commit b215495

File tree

11 files changed

+63
-77
lines changed

11 files changed

+63
-77
lines changed

ci/generate-spec-tests.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,21 @@ fn copy_test(src: &Path, dst: &Path) {
4444
contents.push_str(";; --assert default \\\n");
4545

4646
// Allow certain assert_malformed tests to be interpreted as assert_invalid
47-
if src.ends_with("binary.wast") || src.ends_with("global.wast") || src.ends_with("select.wast")
47+
if src.ends_with("binary.wast")
48+
|| src.ends_with("global.wast")
49+
|| src.ends_with("select.wast")
50+
|| src.ends_with("try_table.wast")
4851
{
4952
contents.push_str(";; --assert permissive \\\n");
5053
}
5154

55+
// For this test it has legacy instructions which, for roundabout reasons,
56+
// are attempted to be printed in the folded format but that doesn't work.
57+
// Exclude this test for now.
58+
if src.ends_with("try_table.wast") {
59+
contents.push_str(";; --assert no-test-folded \\\n");
60+
}
61+
5262
contents.push_str(";; --snapshot tests/snapshots \\\n");
5363

5464
// This test specifically tests various forms of unicode which are

crates/wasmparser/src/readers/core/operators.rs

Lines changed: 20 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,13 @@ crate::for_each_operator!(define_operator);
336336
#[derive(Clone)]
337337
pub struct OperatorsReader<'a> {
338338
reader: BinaryReader<'a>,
339-
blocks: Vec<FrameKind>,
339+
/// Block depth this reader is currently operating at.
340+
///
341+
/// Increments for instructions like `block` and decrements for instructions
342+
/// like `end`. Used to rule out syntactically invalid modules (as defined
343+
/// by the wasm spec currently) in a cheap but not 100% rigorous fashion.
344+
/// More checks are still performed in the validator about block depth.
345+
depth: u32,
340346
data_index_occurred: Option<usize>,
341347
}
342348

@@ -345,7 +351,7 @@ impl<'a> OperatorsReader<'a> {
345351
pub fn new(reader: BinaryReader<'a>) -> OperatorsReader<'a> {
346352
OperatorsReader {
347353
reader,
348-
blocks: vec![FrameKind::Block],
354+
depth: 1,
349355
data_index_occurred: None,
350356
}
351357
}
@@ -377,7 +383,7 @@ impl<'a> OperatorsReader<'a> {
377383
}
378384

379385
fn ensure_stack_empty(&self) -> Result<()> {
380-
if !self.blocks.is_empty() {
386+
if self.depth != 0 {
381387
bail!(
382388
self.original_position(),
383389
"control frames remain at end of function body or expression"
@@ -410,33 +416,6 @@ impl<'a> OperatorsReader<'a> {
410416
Ok((self.read()?, pos))
411417
}
412418

413-
fn enter(&mut self, k: FrameKind) {
414-
self.blocks.push(k)
415-
}
416-
417-
fn expect_block(&mut self, k: FrameKind, found: &str) -> Result<()> {
418-
match self.blocks.last() {
419-
None => bail!(
420-
self.original_position(),
421-
"empty stack found where {:?} expected",
422-
k
423-
),
424-
Some(x) if *x == k => Ok(()),
425-
Some(_) => bail!(
426-
self.original_position(),
427-
"`{}` found outside `{:?}` block",
428-
found,
429-
k
430-
),
431-
}
432-
}
433-
434-
fn end(&mut self) -> Result<()> {
435-
assert!(!self.blocks.is_empty());
436-
self.blocks.pop();
437-
Ok(())
438-
}
439-
440419
/// Visit the next available operator with the specified [`VisitOperator`] instance.
441420
///
442421
/// Note that this does not implicitly propagate any additional information such as instruction
@@ -486,7 +465,7 @@ impl<'a> OperatorsReader<'a> {
486465
where
487466
T: VisitOperator<'a>,
488467
{
489-
if self.blocks.is_empty() {
468+
if self.depth == 0 {
490469
bail!(
491470
self.original_position(),
492471
"operators remaining after end of function body or expression"
@@ -498,46 +477,28 @@ impl<'a> OperatorsReader<'a> {
498477
0x00 => visitor.visit_unreachable(),
499478
0x01 => visitor.visit_nop(),
500479
0x02 => {
501-
self.enter(FrameKind::Block);
480+
self.depth += 1;
502481
visitor.visit_block(self.reader.read_block_type()?)
503482
}
504483
0x03 => {
505-
self.enter(FrameKind::Loop);
484+
self.depth += 1;
506485
visitor.visit_loop(self.reader.read_block_type()?)
507486
}
508487
0x04 => {
509-
self.enter(FrameKind::If);
488+
self.depth += 1;
510489
visitor.visit_if(self.reader.read_block_type()?)
511490
}
512-
0x05 => {
513-
self.expect_block(FrameKind::If, "else")?;
514-
visitor.visit_else()
515-
}
491+
0x05 => visitor.visit_else(),
516492
0x06 => {
517-
if !self.reader.legacy_exceptions() {
518-
bail!(
519-
pos,
520-
"legacy_exceptions feature required for try instruction"
521-
);
522-
}
523-
self.enter(FrameKind::LegacyTry);
493+
self.depth += 1;
524494
visitor.visit_try(self.reader.read_block_type()?)
525495
}
526-
0x07 => {
527-
if !self.reader.legacy_exceptions() {
528-
bail!(
529-
pos,
530-
"legacy_exceptions feature required for catch instruction"
531-
);
532-
}
533-
self.expect_block(FrameKind::LegacyTry, "catch")?;
534-
visitor.visit_catch(self.reader.read_var_u32()?)
535-
}
496+
0x07 => visitor.visit_catch(self.reader.read_var_u32()?),
536497
0x08 => visitor.visit_throw(self.reader.read_var_u32()?),
537498
0x09 => visitor.visit_rethrow(self.reader.read_var_u32()?),
538499
0x0a => visitor.visit_throw_ref(),
539500
0x0b => {
540-
self.end()?;
501+
self.depth -= 1;
541502
visitor.visit_end()
542503
}
543504
0x0c => visitor.visit_br(self.reader.read_var_u32()?),
@@ -558,20 +519,10 @@ impl<'a> OperatorsReader<'a> {
558519
0x14 => visitor.visit_call_ref(self.reader.read()?),
559520
0x15 => visitor.visit_return_call_ref(self.reader.read()?),
560521
0x18 => {
561-
self.expect_block(FrameKind::LegacyTry, "delegate")?;
562-
self.blocks.pop();
522+
self.depth -= 1;
563523
visitor.visit_delegate(self.reader.read_var_u32()?)
564524
}
565-
0x19 => {
566-
if !self.reader.legacy_exceptions() {
567-
bail!(
568-
pos,
569-
"legacy_exceptions feature required for catch_all instruction"
570-
);
571-
}
572-
self.expect_block(FrameKind::LegacyTry, "catch_all")?;
573-
visitor.visit_catch_all()
574-
}
525+
0x19 => visitor.visit_catch_all(),
575526
0x1a => visitor.visit_drop(),
576527
0x1b => visitor.visit_select(),
577528
0x1c => {
@@ -590,7 +541,7 @@ impl<'a> OperatorsReader<'a> {
590541
}
591542
}
592543
0x1f => {
593-
self.enter(FrameKind::TryTable);
544+
self.depth += 1;
594545
visitor.visit_try_table(self.reader.read()?)
595546
}
596547

src/bin/wasm-tools/wast.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,12 @@ impl Opts {
251251
// two error messages are swapped to `assert_invalid`.
252252
"malformed mutability",
253253
"integer too large",
254+
// The upstream specification requires that the legacy
255+
// exceptions proposal is textually invalid (e.g.
256+
// `assert_malformed`). This crate supports it though as
257+
// "just another proposal", so it's not malformed but it's
258+
// invalid.
259+
"unexpected token",
254260
];
255261
if self.assert(Assert::Permissive) && permissive_error_messages.contains(&message) {
256262
return self.test_wast_directive(

tests/cli/invalid.wast

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
;; RUN: wast --assert default --snapshot tests/snapshots %
1+
;; RUN: wast --assert default,no-test-folded --snapshot tests/snapshots %
22

33
(assert_invalid
44
(module
55
(table 1 funcref)
66
(func table.init 0 100))
77
"unknown elem segment")
88

9-
(assert_malformed
9+
(assert_invalid
1010
(module
1111
(func else))
12-
"`else` found outside `If` block")
12+
"else found outside of an `if` block ")
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
;; RUN: wast %
2+
3+
(assert_invalid
4+
(module (func try end))
5+
"legacy exceptions support is not enabled")
6+
7+
(assert_invalid
8+
(module (func catch 0))
9+
"legacy exceptions support is not enabled")
10+
11+
(assert_invalid
12+
(module (func catch_all))
13+
"legacy exceptions support is not enabled")

tests/cli/print-no-panic-dangling-else.wat

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
;; FAIL: print %
1+
;; RUN: print %
22

33
(module
44
(func else)

tests/cli/print-no-panic-dangling-else.wat.stderr

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
(module
22
(type (;0;) (func))
33
(func (;0;) (type 0)
4+
else
5+
)
6+
)

tests/cli/spec/proposals/exception-handling/try_table.wast

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
;; RUN: wast \
22
;; --assert default \
3+
;; --assert permissive \
4+
;; --assert no-test-folded \
35
;; --snapshot tests/snapshots \
46
;; --ignore-error-messages \
57
;; --features=wasm2,exceptions,tail-call \

tests/cli/spec/proposals/wasm-3.0/try_table.wast

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
;; RUN: wast \
22
;; --assert default \
3+
;; --assert permissive \
4+
;; --assert no-test-folded \
35
;; --snapshot tests/snapshots \
46
;; --ignore-error-messages \
57
;; --features=wasm3 \

tests/snapshots/cli/invalid.wast.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
"text": "unknown elem segment"
1010
},
1111
{
12-
"type": "assert_malformed",
12+
"type": "assert_invalid",
1313
"line": 10,
1414
"filename": "invalid.1.wasm",
1515
"module_type": "binary",
16-
"text": "`else` found outside `If` block"
16+
"text": "else found outside of an `if` block "
1717
}
1818
]
1919
}

0 commit comments

Comments
 (0)