Skip to content

Commit d9b20bc

Browse files
feat: add validate feature
Signed-off-by: Henry <mail@henrygressmann.de>
1 parent b6a6cef commit d9b20bc

15 files changed

Lines changed: 262 additions & 85 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Added support for the WebAssembly function-references proposal
1313
- Added `WasmValue::ty` and `WasmValue::matches_type`
14+
- Added a `validate` feature to `tinywasm` and `tinywasm-parser` (enabled by default) to optionally skip wasmparser validation for faster parsing of trusted modules.
1415

1516
### Changed
1617

‎README.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,11 @@ See the [examples](./examples) directory and [documentation](https://docs.rs/tin
4444
- **`std`:** Enables `std` and parsing from files and streams. Enabled by default.
4545
- **`log`:** Enables integration with the `log` crate. Enabled by default.
4646
- **`parser`:** Enables `tinywasm-parser` and top-level parse helpers. Enabled by default.
47+
- **`validate`:** Enables WebAssembly validation while parsing. Enabled by default and configurable through `ParserOptions`.
4748
- **`archive`:** Enables serialization and deserialization of the internal `twasm` format. Enabled by default.
4849
- **`canonicalize-nans`:** Canonicalizes NaN values. Enabled by default.
4950
- **`debug`:** Derives `Debug` for runtime types. Enabled by default.
50-
- **`parallel-parser`:** Parallelizes function parsing and validation when `std` is enabled. Enabled by default.
51+
- **`parallel-parser`:** Parallelizes function parsing when `std` is enabled. Enabled by default.
5152
- **`guest-debug`:** Exposes module-internal by-index inspection APIs (`*_by_index`).
5253
- **`simd-x86`:** Enables x86-specific SIMD intrinsics and uses `unsafe` internally.
5354

@@ -65,7 +66,7 @@ TinyWasm also has its own internal bytecode format, `twasm`. WebAssembly modules
6566

6667
## Safety
6768

68-
TinyWasm only uses safe Rust by default. The optional `simd-x86` feature enables x86-specific SIMD intrinsics and uses `unsafe` internally. WebAssembly input is validated by default and runs inside a sandbox: untrusted Wasm should not be able to access host memory, escape the sandbox, or cause undefined behavior in the runtime. Validation should only be disabled for trusted input.
69+
TinyWasm only uses safe Rust by default. The optional `simd-x86` feature enables x86-specific SIMD intrinsics and uses `unsafe` internally. WebAssembly input is validated by default through the `validate` feature. Disabling validation should not let Wasm access host memory or escape the sandbox, but malformed input may panic or otherwise crash the process, so only disable it for trusted input.
6970

7071
The internal `twasm` bytecode format is not currently validated as an untrusted input format. Malformed `twasm` may panic, but should not compromise memory safety or allow sandbox escape. Only run trusted `twasm` bytecode, or generate it through TinyWasm from Wasm input.
7172

‎crates/cli/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ tinywasm = { workspace = true, features = [
3737
"parallel-parser",
3838
"parser",
3939
"std",
40+
"validate",
4041
] }
4142
wast = { workspace = true, optional = true }
4243
wat = { workspace = true, optional = true }

‎crates/parser/Cargo.toml‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ categories.workspace = true
1313
[dependencies]
1414
log = { workspace = true, optional = true }
1515
tinywasm-types = { workspace = true }
16-
wasmparser = { workspace = true, features = ["features", "simd", "validate"] }
16+
wasmparser = { workspace = true, features = ["simd"] }
1717

1818
[features]
19-
default = ["log", "parallel", "std"]
19+
default = ["log", "parallel", "std", "validate"]
2020
log = ["dep:log"]
2121
parallel = ["std"]
2222
std = ["tinywasm-types/std", "wasmparser/std"]
23+
validate = ["wasmparser/features", "wasmparser/validate"]

‎crates/parser/README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ This crate provides the parser and lowering pipeline that converts WebAssembly b
66

77
- `std`: Enables the use of `std` and `std::io` for parsing from files and streams.
88
- `log`: Enables logging of the parsing process using the `log` crate.
9-
- `parallel`: Enables multithreaded parsing and validation when `std` is available.
9+
- `parallel`: Enables multithreaded function parsing. Requires `std`.
10+
- `validate`: Enables `wasmparser` validation. Enabled by default and configurable through `ParserOptions`.
1011

1112
## Usage
1213

‎crates/parser/src/conversion.rs‎

Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
use crate::{Result, module::FunctionCode, visit::process_operators_and_validate};
1+
use crate::validation::{FuncValidator, FuncValidatorAllocations, ValidatorResources};
2+
#[cfg(feature = "validate")]
3+
use crate::visit::process_operators_and_validate;
4+
use crate::{Result, module::FunctionCode, visit::process_operators};
25
use alloc::{boxed::Box, format, string::ToString, vec::Vec};
36
use tinywasm_types::*;
4-
use wasmparser::{
5-
CompositeInnerType, FuncValidator, FuncValidatorAllocations, OperatorsReader, OperatorsReaderAllocations,
6-
ValidatorResources,
7-
};
7+
use wasmparser::{CompositeInnerType, OperatorsReader, OperatorsReaderAllocations};
88

99
pub(crate) fn convert_module_element(element: wasmparser::Element<'_>) -> Result<tinywasm_types::Element> {
1010
let kind = match element.kind {
@@ -116,28 +116,48 @@ pub(crate) fn convert_module_export(export: wasmparser::Export<'_>) -> Result<Ex
116116
Ok(Export { index: export.index, name: Box::from(export.name), kind })
117117
}
118118

119+
fn extend_local_types(
120+
local_types: &mut Vec<crate::visit::OperandSize>,
121+
count: u32,
122+
ty: wasmparser::ValType,
123+
) -> Result<()> {
124+
let size = crate::visit::OperandSize::from(ty);
125+
let count =
126+
usize::try_from(count).map_err(|_| crate::ParseError::Other("local declaration count is too large".into()))?;
127+
local_types.reserve(count);
128+
local_types.extend(core::iter::repeat_n(size, count));
129+
Ok(())
130+
}
131+
119132
pub(crate) fn convert_module_code(
120133
func: wasmparser::FunctionBody<'_>,
121-
mut validator: Option<FuncValidator<ValidatorResources>>,
134+
validator: Option<FuncValidator<ValidatorResources>>,
122135
reader_allocs: OperatorsReaderAllocations,
123136
metadata: &crate::visit::ModuleMetadata,
124137
ty_idx: u32,
125138
) -> Result<(FunctionCode, Option<FuncValidatorAllocations>, OperatorsReaderAllocations)> {
126139
let locals_reader = func.get_locals_reader()?;
127-
let pos = locals_reader.original_position();
140+
#[cfg(feature = "validate")]
141+
let locals_position = locals_reader.original_position();
128142
let signature = metadata.signature(ty_idx)?.clone();
129143
let mut local_types = signature.params.clone();
130144

131-
for (i, local) in locals_reader.into_iter().enumerate() {
145+
#[cfg(feature = "validate")]
146+
let mut validator = validator;
147+
148+
#[cfg(feature = "validate")]
149+
for (local_index, local) in locals_reader.into_iter().enumerate() {
132150
let local = local?;
133151
if let Some(validator) = validator.as_mut() {
134-
validator.define_locals(pos + i, local.0, local.1)?;
152+
validator.define_locals(locals_position + local_index, local.0, local.1)?;
135153
}
136-
let size = crate::visit::OperandSize::from(local.1);
137-
let count = usize::try_from(local.0)
138-
.map_err(|_| crate::ParseError::Other("local declaration count is too large".into()))?;
139-
local_types.reserve(count);
140-
local_types.extend(core::iter::repeat_n(size, count));
154+
extend_local_types(&mut local_types, local.0, local.1)?;
155+
}
156+
157+
#[cfg(not(feature = "validate"))]
158+
for local in locals_reader {
159+
let local = local?;
160+
extend_local_types(&mut local_types, local.0, local.1)?;
141161
}
142162

143163
// maps a local's address to the index in the type's locals array
@@ -154,8 +174,33 @@ pub(crate) fn convert_module_code(
154174
*count = count.checked_add(1).ok_or_else(|| crate::ParseError::Other(error.into()))?;
155175
}
156176

157-
let (body, data, validator_allocs, reader_allocs) =
158-
process_operators_and_validate(validator, func, local_types, local_addr_map, metadata, ty_idx, reader_allocs)?;
177+
#[cfg(feature = "validate")]
178+
let (body, data, validator_allocs, reader_allocs) = match validator {
179+
Some(validator) => {
180+
let (body, data, validator_allocs, reader_allocs) = process_operators_and_validate(
181+
validator,
182+
func,
183+
local_types,
184+
local_addr_map,
185+
metadata,
186+
ty_idx,
187+
reader_allocs,
188+
)?;
189+
(body, data, Some(validator_allocs), reader_allocs)
190+
}
191+
None => {
192+
let (body, data, reader_allocs) =
193+
process_operators(func, local_types, local_addr_map, metadata, ty_idx, reader_allocs)?;
194+
(body, data, None, reader_allocs)
195+
}
196+
};
197+
#[cfg(not(feature = "validate"))]
198+
let (body, data, validator_allocs, reader_allocs) = {
199+
let _ = validator;
200+
let (body, data, reader_allocs) =
201+
process_operators(func, local_types, local_addr_map, metadata, ty_idx, reader_allocs)?;
202+
(body, data, None, reader_allocs)
203+
};
159204
Ok((
160205
FunctionCode { instructions: body, data, locals: local_counts, uses_local_memory: false },
161206
validator_allocs,

‎crates/parser/src/lib.rs‎

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,29 @@ mod error;
3434
mod macros;
3535
mod module;
3636
mod optimize;
37+
mod validation;
3738
mod visit;
3839

3940
#[cfg(parallel_parser)]
4041
mod parallel;
4142

4243
pub use error::*;
4344
use module::ModuleReader;
44-
use wasmparser::{Validator, WasmFeatures};
45+
use validation::Validator;
46+
47+
#[cfg(feature = "validate")]
48+
use wasmparser::WasmFeatures;
4549

4650
pub use tinywasm_types::Module;
4751

4852
/// Parser optimization and lowering options.
4953
#[non_exhaustive]
5054
#[derive(Debug, Clone)]
5155
pub struct ParserOptions {
52-
/// Whether to validate modules while parsing. Enabled by default.
56+
/// Whether to validate modules while parsing. Enabled by default when the
57+
/// `validate` feature is enabled.
58+
///
59+
/// Requires the `validate` feature to have any effect.
5360
///
5461
/// Disable this only for trusted input. Parsing without validation may produce
5562
/// a module that violates runtime assumptions.
@@ -73,7 +80,7 @@ pub struct ParserOptions {
7380
impl Default for ParserOptions {
7481
fn default() -> Self {
7582
Self {
76-
validation: true,
83+
validation: cfg!(feature = "validate"),
7784
optimize_local_memory_allocation: true,
7885
optimize_rewrite: true,
7986
#[cfg(parallel_parser)]
@@ -85,9 +92,12 @@ impl Default for ParserOptions {
8592
impl ParserOptions {
8693
/// Enable or disable WebAssembly validation.
8794
///
95+
/// Requires the `validate` feature to have any effect.
96+
///
8897
/// Disable this only for trusted input. Parsing without validation may produce
8998
/// a module that violates runtime assumptions.
9099
pub const fn with_validation(mut self, enabled: bool) -> Self {
100+
assert!(!enabled || cfg!(feature = "validate"), "validation requires the `validate` feature");
91101
self.validation = enabled;
92102
self
93103
}
@@ -157,27 +167,35 @@ impl Parser {
157167
&self.options
158168
}
159169

160-
fn create_validator(_options: ParserOptions) -> Validator {
161-
let features = WasmFeatures::CALL_INDIRECT_OVERLONG
162-
| WasmFeatures::BULK_MEMORY_OPT
163-
| WasmFeatures::RELAXED_SIMD
164-
| WasmFeatures::GC_TYPES
165-
| WasmFeatures::REFERENCE_TYPES
166-
| WasmFeatures::MUTABLE_GLOBAL
167-
| WasmFeatures::MULTI_VALUE
168-
| WasmFeatures::FLOATS
169-
| WasmFeatures::BULK_MEMORY
170-
| WasmFeatures::SATURATING_FLOAT_TO_INT
171-
| WasmFeatures::SIGN_EXTENSION
172-
| WasmFeatures::EXTENDED_CONST
173-
| WasmFeatures::FUNCTION_REFERENCES
174-
| WasmFeatures::TAIL_CALL
175-
| WasmFeatures::MULTI_MEMORY
176-
| WasmFeatures::SIMD
177-
| WasmFeatures::MEMORY64
178-
| WasmFeatures::CUSTOM_PAGE_SIZES
179-
| WasmFeatures::WIDE_ARITHMETIC;
180-
Validator::new_with_features(features)
170+
fn validator(&self) -> Option<Validator> {
171+
#[cfg(feature = "validate")]
172+
{
173+
let features = WasmFeatures::CALL_INDIRECT_OVERLONG
174+
| WasmFeatures::BULK_MEMORY_OPT
175+
| WasmFeatures::RELAXED_SIMD
176+
| WasmFeatures::GC_TYPES
177+
| WasmFeatures::REFERENCE_TYPES
178+
| WasmFeatures::MUTABLE_GLOBAL
179+
| WasmFeatures::MULTI_VALUE
180+
| WasmFeatures::FLOATS
181+
| WasmFeatures::BULK_MEMORY
182+
| WasmFeatures::SATURATING_FLOAT_TO_INT
183+
| WasmFeatures::SIGN_EXTENSION
184+
| WasmFeatures::EXTENDED_CONST
185+
| WasmFeatures::FUNCTION_REFERENCES
186+
| WasmFeatures::TAIL_CALL
187+
| WasmFeatures::MULTI_MEMORY
188+
| WasmFeatures::SIMD
189+
| WasmFeatures::MEMORY64
190+
| WasmFeatures::CUSTOM_PAGE_SIZES
191+
| WasmFeatures::WIDE_ARITHMETIC;
192+
self.options.validation().then(|| Validator::new_with_features(features))
193+
}
194+
#[cfg(not(feature = "validate"))]
195+
{
196+
assert!(!self.options.validation(), "validation requires the `validate` feature");
197+
None
198+
}
181199
}
182200

183201
#[cfg(feature = "std")]
@@ -194,7 +212,7 @@ impl Parser {
194212
/// Parse a [`Module`] from bytes
195213
pub fn parse_module_bytes(&self, wasm: impl AsRef<[u8]>) -> Result<Module> {
196214
let wasm = wasm.as_ref();
197-
let mut validator = self.options.validation().then(|| Self::create_validator(self.options.clone()));
215+
let mut validator = self.validator();
198216
let mut reader = ModuleReader::default();
199217

200218
for payload in wasmparser::Parser::new(0).parse_all(wasm) {
@@ -228,7 +246,7 @@ impl Parser {
228246
#[cfg(feature = "std")]
229247
/// Parse a [`Module`] from a stream. Requires `std` feature.
230248
pub fn parse_module_stream(&self, mut stream: impl std::io::Read) -> Result<Module> {
231-
let mut validator = self.options.validation().then(|| Self::create_validator(self.options.clone()));
249+
let mut validator = self.validator();
232250
let mut reader = ModuleReader::default();
233251
let mut buffer = alloc::vec::Vec::new();
234252
let mut parser = wasmparser::Parser::new(0);

‎crates/parser/src/macros.rs‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,24 @@
11
pub(crate) mod visit {
2+
#[cfg(feature = "validate")]
23
macro_rules! validate_then_visit {
34
($( @$proposal:ident $op:ident $({ $($arg:ident: $argty:ty),* })? => $visit:ident ($($ann:tt)*))*) => {$(
45
fn $visit(&mut self $($(,$arg: $argty)*)?) -> Self::Output {
5-
if let Some(validator) = self.validator.as_mut() {
6-
if let Err(e) = validator.visitor(self.position).$visit($($($arg.clone()),*)?) {
7-
core::hint::cold_path();
8-
return Err(crate::ParseError::ParseError { message: e.to_string(), offset: self.position });
9-
}
6+
if let Err(e) = self.validator.visitor(self.position).$visit($($($arg.clone()),*)?) {
7+
core::hint::cold_path();
8+
return Err(crate::ParseError::ParseError { message: e.to_string(), offset: self.position });
109
}
1110
self.builder.$visit($($($arg),*)?)
1211
}
1312
)*};
1413
}
1514

15+
#[cfg(feature = "validate")]
1616
macro_rules! validate_then_visit_simd {
1717
($( @$proposal:ident $op:ident $({ $($arg:ident: $argty:ty),* })? => $visit:ident ($($ann:tt)*))*) => {$(
1818
fn $visit(&mut self $($(,$arg: $argty)*)?) -> Self::Output {
19-
if let Some(validator) = self.validator.as_mut() {
20-
if let Err(e) = validator.simd_visitor(self.position).$visit($($($arg.clone()),*)?) {
21-
core::hint::cold_path();
22-
return Err(crate::ParseError::ParseError { message: e.to_string(), offset: self.position });
23-
}
19+
if let Err(e) = self.validator.simd_visitor(self.position).$visit($($($arg.clone()),*)?) {
20+
core::hint::cold_path();
21+
return Err(crate::ParseError::ParseError { message: e.to_string(), offset: self.position });
2422
}
2523
self.builder.$visit($($($arg),*)?)
2624
}
@@ -138,7 +136,9 @@ pub(crate) mod visit {
138136
};
139137
}
140138

141-
pub(crate) use {impl_visit_operator, lowering_ops, validate_then_visit, validate_then_visit_simd};
139+
pub(crate) use {impl_visit_operator, lowering_ops};
140+
#[cfg(feature = "validate")]
141+
pub(crate) use {validate_then_visit, validate_then_visit_simd};
142142
}
143143

144144
pub(crate) mod optimize {

0 commit comments

Comments
 (0)