Skip to content

Commit 693d590

Browse files
chore: simplify parser limits
Signed-off-by: Henry <mail@henrygressmann.de>
1 parent 6d8072c commit 693d590

9 files changed

Lines changed: 393 additions & 506 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ Applications that only load `.twasm` can remove the parser and validator from th
7575

7676
WebAssembly [validation](https://webassembly.github.io/spec/core/valid/index.html) is enabled by default through the `validate` feature. Keep this feature enabled and leave `ParserOptions::validation` enabled for modules from untrusted sources. Without validation, parsing can produce modules that violate runtime assumptions and may panic during instantiation or execution.
7777

78-
Validation does not limit parsing or execution resources. Hosts that run untrusted code should also set opt-in `parser::ParseLimits` for input and known parse-time amplification points, configure stack and `ResourceLimiter` limits, and use fuel- or time-budgeted execution as needed.
78+
Validation does not limit parsing or execution resources. Hosts that run untrusted code should also configure `parser::ParseLimits`, stack and `ResourceLimiter` limits, and fuel- or time-budgeted execution as needed.
7979

8080
Loading `.twasm` checks the archive header and encoding but does not run WebAssembly validation or verify TinyWasm's runtime invariants. Load archives only from trusted sources. For untrusted input, parse a WebAssembly binary with validation enabled.
8181

‎crates/parser/README.md‎

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ This crate provides the parser and lowering pipeline that converts WebAssembly b
1212
## Usage
1313

1414
```rust
15-
use tinywasm_parser::{ParseLimits, Parser, ParserOptions};
15+
use tinywasm_parser::{Parser, ParserOptions};
1616

1717
let bytes = include_bytes!("./file.wasm");
1818

@@ -22,23 +22,11 @@ let module = parser.parse_module_bytes(bytes)?;
2222
let parser = Parser::new(ParserOptions::default().with_optimize(false));
2323
let module = parser.parse_module_bytes(bytes)?;
2424

25-
// Select explicit bounds before accepting untrusted input. Keep validation on.
26-
let limits = ParseLimits::new()
27-
.with_max_module_bytes(16 * 1024 * 1024)
28-
.with_max_section_items(100_000)
29-
.with_max_function_locals(10_000)
30-
.with_max_br_table_targets(4_096)
31-
.with_max_array_new_fixed_elements(4_096);
32-
let parser = Parser::new(ParserOptions::default().with_limits(limits));
33-
let module = parser.parse_module_bytes(bytes)?;
34-
3525
let module = parser.parse_module_file("path/to/file.wasm")?;
3626
let mut stream = std::fs::File::open("path/to/file.wasm")?;
3727
let module = parser.parse_module_stream(&mut stream)?;
3828
```
3929

4030
If you just want the default configuration, the top-level `parse_bytes`, `parse_file`, and `parse_stream` helpers are thin wrappers around `Parser::default()`.
4131

42-
The limits are opt-in and disabled by default. They bound encoded input and
43-
known parse-time amplification points; they are not a hard cap on peak memory
44-
or elapsed time. A host should also bound guest runtime resources after parsing.
32+
For untrusted modules, keep validation enabled and configure [`ParseLimits`](https://docs.rs/tinywasm-parser/latest/tinywasm_parser/struct.ParseLimits.html).

‎crates/parser/src/conversion.rs‎

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -165,11 +165,7 @@ pub(crate) fn convert_module_code(
165165
) -> Result<(FunctionCode, Option<FuncValidatorAllocations>, OperatorsReaderAllocations)> {
166166
let mut locals_reader = func.get_locals_reader()?;
167167
let mut local_types = metadata.signature(context.ty_idx)?.params.clone();
168-
crate::check_parse_limit(
169-
crate::ParseLimitKind::FunctionLocals,
170-
options.limits.max_function_locals,
171-
local_types.len(),
172-
)?;
168+
options.limits.check(crate::ParseLimitKind::FunctionLocals, local_types.len())?;
173169

174170
#[cfg(feature = "validate")]
175171
let mut validator = validator;
@@ -182,7 +178,7 @@ pub(crate) fn convert_module_code(
182178
.len()
183179
.checked_add(local.0 as usize)
184180
.ok_or_else(|| crate::ParseError::Other("function local count overflow".into()))?;
185-
crate::check_parse_limit(crate::ParseLimitKind::FunctionLocals, options.limits.max_function_locals, expanded)?;
181+
options.limits.check(crate::ParseLimitKind::FunctionLocals, expanded)?;
186182
#[cfg(feature = "validate")]
187183
if let Some(validator) = validator.as_mut() {
188184
validator.define_locals(position, local.0, local.1)?;

‎crates/parser/src/error.rs‎

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,8 @@
1+
use crate::ParseLimitKind;
12
use alloc::string::{String, ToString};
23
use core::fmt::{Debug, Display};
34
use wasmparser::Encoding;
45

5-
/// The input or parse-time expansion limit that was exceeded.
6-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7-
pub enum ParseLimitKind {
8-
/// Encoded module bytes.
9-
ModuleBytes,
10-
/// Materialized entries in one module section.
11-
SectionItems,
12-
/// Parameters plus declared locals in one function.
13-
FunctionLocals,
14-
/// Explicit targets in one `br_table`.
15-
BrTableTargets,
16-
/// Elements in one `array.new_fixed`.
17-
ArrayNewFixedElements,
18-
}
19-
20-
impl Display for ParseLimitKind {
21-
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
22-
let name = match self {
23-
Self::ModuleBytes => "module bytes",
24-
Self::SectionItems => "section items",
25-
Self::FunctionLocals => "function locals",
26-
Self::BrTableTargets => "br_table targets",
27-
Self::ArrayNewFixedElements => "array.new_fixed elements",
28-
};
29-
f.write_str(name)
30-
}
31-
}
32-
336
#[derive(Debug, PartialEq, Eq)]
347
/// Errors that can occur when parsing a WebAssembly module
358
pub enum ParseError {

‎crates/parser/src/lib.rs‎

Lines changed: 13 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,18 @@ pub(crate) mod log {
3232
mod conversion;
3333
mod emitter;
3434
mod error;
35+
mod limits;
3536
mod macros;
3637
mod module;
3738
mod selection;
3839
mod validation;
3940
mod visit;
4041

41-
#[cfg(all(test, feature = "std"))]
42-
mod tests;
43-
4442
#[cfg(parallel_parser)]
4543
mod parallel;
4644

4745
pub use error::*;
46+
pub use limits::{ParseLimitKind, ParseLimits};
4847
use module::ModuleReader;
4948
use validation::Validator;
5049

@@ -53,79 +52,6 @@ use wasmparser::WasmFeatures;
5352

5453
pub use tinywasm_types::Module;
5554

56-
/// Optional limits for parsing modules from untrusted sources.
57-
///
58-
/// These limit encoded input and specific forms of parse-time expansion. They do
59-
/// not constitute a hard bound on the parser's total memory use. Validation
60-
/// should remain enabled for untrusted modules.
61-
#[non_exhaustive]
62-
#[derive(Debug, Clone, Copy, Default)]
63-
pub struct ParseLimits {
64-
/// Maximum encoded module size in bytes, including custom sections.
65-
pub max_module_bytes: Option<usize>,
66-
/// Maximum materialized entries in any one section. Recursive type groups
67-
/// and compact imports count by their expanded entries.
68-
pub max_section_items: Option<usize>,
69-
/// Maximum parameters plus declared locals in any one function.
70-
pub max_function_locals: Option<usize>,
71-
/// Maximum explicit targets in one `br_table` (excluding its default).
72-
pub max_br_table_targets: Option<usize>,
73-
/// Maximum elements in one `array.new_fixed`.
74-
pub max_array_new_fixed_elements: Option<usize>,
75-
}
76-
77-
impl ParseLimits {
78-
/// Create limits with every bound disabled.
79-
pub const fn new() -> Self {
80-
Self {
81-
max_module_bytes: None,
82-
max_section_items: None,
83-
max_function_locals: None,
84-
max_br_table_targets: None,
85-
max_array_new_fixed_elements: None,
86-
}
87-
}
88-
89-
/// Bound the encoded size of a module.
90-
pub const fn with_max_module_bytes(mut self, limit: usize) -> Self {
91-
self.max_module_bytes = Some(limit);
92-
self
93-
}
94-
95-
/// Bound the number of materialized entries in each section.
96-
pub const fn with_max_section_items(mut self, limit: usize) -> Self {
97-
self.max_section_items = Some(limit);
98-
self
99-
}
100-
101-
/// Bound parameters plus declared locals in each function.
102-
pub const fn with_max_function_locals(mut self, limit: usize) -> Self {
103-
self.max_function_locals = Some(limit);
104-
self
105-
}
106-
107-
/// Bound explicit targets in each `br_table`.
108-
pub const fn with_max_br_table_targets(mut self, limit: usize) -> Self {
109-
self.max_br_table_targets = Some(limit);
110-
self
111-
}
112-
113-
/// Bound elements in each `array.new_fixed`.
114-
pub const fn with_max_array_new_fixed_elements(mut self, limit: usize) -> Self {
115-
self.max_array_new_fixed_elements = Some(limit);
116-
self
117-
}
118-
}
119-
120-
pub(crate) fn check_parse_limit(kind: ParseLimitKind, limit: Option<usize>, observed: usize) -> Result<()> {
121-
if let Some(limit) = limit
122-
&& observed > limit
123-
{
124-
return Err(ParseError::LimitExceeded { kind, limit });
125-
}
126-
Ok(())
127-
}
128-
12955
/// Parser optimization and lowering options.
13056
#[non_exhaustive]
13157
#[derive(Debug, Clone)]
@@ -279,33 +205,19 @@ impl Parser {
279205

280206
#[cfg(feature = "std")]
281207
fn read_more(
208+
&self,
282209
stream: &mut impl std::io::Read,
283210
buffer: &mut alloc::vec::Vec<u8>,
284211
hint: usize,
285212
total_read: &mut usize,
286-
max_module_bytes: Option<usize>,
287213
) -> Result<usize> {
288214
let len = buffer.len();
289215
// Size hints can come from untrusted section lengths.
290216
let mut increment = hint.clamp(1, 64 * 1024);
291-
if let Some(limit) = max_module_bytes {
217+
if let Some(limit) = self.options.limits.max_module_bytes {
292218
let remaining = limit.saturating_sub(*total_read);
293-
if remaining == 0 {
294-
// A module exactly at the limit may still need an EOF probe.
295-
let mut extra = [0];
296-
let read_bytes = loop {
297-
match stream.read(&mut extra) {
298-
Ok(read_bytes) => break read_bytes,
299-
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
300-
Err(e) => return Err(ParseError::Other(alloc::format!("Error reading from stream: {e}"))),
301-
}
302-
};
303-
if read_bytes != 0 {
304-
return Err(ParseError::LimitExceeded { kind: ParseLimitKind::ModuleBytes, limit });
305-
}
306-
return Ok(0);
307-
}
308-
increment = increment.min(remaining);
219+
// Leave room for one byte beyond the limit to distinguish EOF.
220+
increment = increment.min(remaining.saturating_add(1));
309221
}
310222
let new_len =
311223
len.checked_add(increment).ok_or_else(|| ParseError::Other("stream buffer is too large".into()))?;
@@ -324,15 +236,17 @@ impl Parser {
324236
}
325237
};
326238
buffer.truncate(len + read_bytes);
327-
*total_read =
239+
let next_total =
328240
total_read.checked_add(read_bytes).ok_or_else(|| ParseError::Other("stream byte count overflow".into()))?;
241+
self.options.limits.check(ParseLimitKind::ModuleBytes, next_total)?;
242+
*total_read = next_total;
329243
Ok(read_bytes)
330244
}
331245

332246
/// Parse a [`Module`] from bytes
333247
pub fn parse_module_bytes(&self, wasm: impl AsRef<[u8]>) -> Result<Module> {
334248
let wasm = wasm.as_ref();
335-
check_parse_limit(ParseLimitKind::ModuleBytes, self.options.limits.max_module_bytes, wasm.len())?;
249+
self.options.limits.check(ParseLimitKind::ModuleBytes, wasm.len())?;
336250
let mut validator = self.validator();
337251
let mut reader = ModuleReader::default();
338252

@@ -383,13 +297,7 @@ impl Parser {
383297
buffer.truncate(buffer.len() - buffer_offset);
384298
buffer_offset = 0;
385299
}
386-
let read_bytes = Self::read_more(
387-
&mut stream,
388-
&mut buffer,
389-
hint,
390-
&mut total_read,
391-
self.options.limits.max_module_bytes,
392-
)?;
300+
let read_bytes = self.read_more(&mut stream, &mut buffer, hint, &mut total_read)?;
393301
eof = read_bytes == 0;
394302
}
395303
wasmparser::Chunk::Parsed { consumed, payload } => {
@@ -422,13 +330,7 @@ impl Parser {
422330
if let Some((count, section_size)) = deferred_code_section {
423331
while buffer.len() - buffer_offset < section_size {
424332
let remaining = section_size - (buffer.len() - buffer_offset);
425-
let read_bytes = Self::read_more(
426-
&mut stream,
427-
&mut buffer,
428-
remaining,
429-
&mut total_read,
430-
self.options.limits.max_module_bytes,
431-
)?;
333+
let read_bytes = self.read_more(&mut stream, &mut buffer, remaining, &mut total_read)?;
432334
if read_bytes == 0 {
433335
return Err(ParseError::ParseError {
434336
message: "unexpected end-of-file".into(),
@@ -452,13 +354,7 @@ impl Parser {
452354
}
453355

454356
if !eof {
455-
let read_bytes = Self::read_more(
456-
&mut stream,
457-
&mut buffer,
458-
1,
459-
&mut total_read,
460-
self.options.limits.max_module_bytes,
461-
)?;
357+
let read_bytes = self.read_more(&mut stream, &mut buffer, 1, &mut total_read)?;
462358
eof = read_bytes == 0;
463359

464360
if !eof {

0 commit comments

Comments
 (0)