1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
//! libFuzzer entry-point module for `chio-wasm-guards`.
//!
//! Gated behind the `fuzz` Cargo feature so it only compiles into the standalone
//! `chio-fuzz` workspace at `../../fuzz`. Production builds never pull in
//! `arbitrary`, never expose these symbols, and never get recompiled with
//! libFuzzer instrumentation.
//!
//! # Entry point: `ComponentBackend::load_module`
//!
//! The chosen trust-boundary surface is
//! [`crate::component::ComponentBackend::load_module`], the WASM Component
//! Model preinstantiate-validate path that wraps
//! `wasmtime::component::Component::new`. Every arbitrary byte string fed
//! through this path must surface as `Err(WasmGuardError::*)` rather than a
//! panic, abort, or undefined behavior.
//!
//! # Coverage shape
//!
//! On every iteration the same `data` byte slice is driven through three
//! independent surfaces:
//!
//! 1. [`crate::runtime::wasmtime_backend::detect_wasm_format`] - the format
//! sniff via `wasmparser::Parser::is_component` /
//! `Parser::is_core_wasm`.
//! 2. [`crate::component::ComponentBackend::load_module`] - the
//! Component Model preinstantiate-validate path.
//! 3. [`crate::runtime::wasmtime_backend::WasmtimeBackend::load_module`] -
//! the core-module preinstantiate-validate path with the
//! import-namespace check.
//!
//! # Process-wide engine cache
//!
//! `wasmtime::Engine::new` allocates JIT machinery and is far too expensive
//! to repeat per fuzz iteration. The engine is built once per process via a
//! `OnceLock`. Engine construction is fail-closed: if the embedded wasmtime
//! config cannot build an engine at startup we have no fuzz signal and
//! returning keeps libFuzzer happy without aborting.
use Arc;
use OnceLock;
use ;
use crateWasmGuardAbi;
use crateComponentBackend;
use crate;
/// Process-wide shared `Engine` for the fuzz harness.
///
/// Fuel metering on, Component Model on. Built once via `OnceLock` so
/// libFuzzer iterations only pay the JIT-init cost once per process. `None`
/// means engine construction failed at startup.
static ENGINE: = new;
/// Build (or fetch) the process-wide engine. Returns `None` only if the
/// embedded wasmtime config cannot build an engine.
/// Fuel limit handed to `load_module`. The preinstantiate-validate path
/// stores the limit but does not consume fuel; we never call `evaluate`.
/// A non-zero value avoids any short-circuit on zero-fuel pre-checks.
const FUZZ_FUEL_LIMIT: u64 = 1_000_000;
/// Drive arbitrary bytes through the WASM preinstantiate-validate trust
/// boundary.
///
/// Bytes are forwarded through three independent surfaces:
///
/// 1. `detect_wasm_format` (the wasmparser format sniff).
/// 2. `ComponentBackend::load_module` (Component Model parse + validate).
/// 3. `WasmtimeBackend::load_module` (core module parse + validate +
/// import-namespace check).
///
/// Errors at every step are silently consumed: the only outcomes are
/// `Err(WasmGuardError::*)` (good) or a panic / abort (which libFuzzer
/// reports as a crash).
// ---------------------------------------------------------------------------
// WIT host-call boundary deserialization fuzzer
// ---------------------------------------------------------------------------
/// Drive arbitrary bytes through the WIT host-call boundary serde surface.
///
/// The trust boundary is the point at which the host accepts bytes that
/// crossed the WIT marshaller from the guest WASM module and invokes serde
/// to materialize them into typed Rust structs. A panic here lets a
/// malicious guest crash the host process.
///
/// # Chosen ABI surfaces
///
/// 1. [`GuardRequest`](crate::abi::GuardRequest) - the host-to-guest
/// WIT-marshalled request envelope. Exercises the `Deserialize` impl and
/// the nested `serde_json::Value` field (`arguments`) against arbitrary
/// input.
/// 2. [`GuestDenyResponse`](crate::abi::GuestDenyResponse) - the
/// canonical guest-to-host wire payload at
/// `crate::runtime::wasmtime_backend::WasmtimeBackend::read_structured_deny_reason`.
///
/// [`GuardVerdict`](crate::abi::GuardVerdict) is intentionally NOT fuzzed:
/// it crosses the WIT boundary as an `i32` return code, not a serialized
/// struct.
///
/// Errors are silently consumed; the post-condition is "no panic".
// ---------------------------------------------------------------------------
// WASM guard escape fuzzer
// ---------------------------------------------------------------------------
/// Tiny fuel ceiling used for the escape harness `evaluate` step.
///
/// Distinct from [`FUZZ_FUEL_LIMIT`] (which is the preinstantiate-validate
/// fuel field; that path never consumes fuel). The escape harness actually
/// runs guest code, so fuel must be small enough that an adversarial
/// deep-recursion or unbounded-loop module is denied with
/// [`crate::error::WasmGuardError::FuelExhausted`] rather than dragging the
/// fuzzer into a multi-second iteration. 50k units lets a `chio_alloc` stub
/// plus a handful of guest instructions clear without giving an attacker
/// enough budget to amplify a fuel-bomb seed.
const ESCAPE_FUZZ_FUEL_LIMIT: u64 = 50_000;
/// Drive arbitrary bytes through the WASM guard runtime-execution surface.
///
/// Companion to [`fuzz_wasm_preinstantiate_validate`]: that target validates
/// the pre-execution parse + import-namespace check; this one drives the
/// post-load runtime path through a single `evaluate` call. Together they
/// span the eight escape classes:
/// undeclared host imports, oversized linear memory, fuel-budget exhaustion,
/// table grow/abuse, stack overflow via deep recursion, host reentry,
/// malformed component-model encoding, and signed-but-malicious modules.
///
/// # Coverage shape
///
/// On every iteration the same `data` byte slice is driven through:
///
/// 1. [`crate::runtime::wasmtime_backend::detect_wasm_format`] - format
/// sniff; rejects pure garbage early so the escape signal is concentrated
/// on well-formed-but-malicious modules.
/// 2. [`crate::component::ComponentBackend::load_module`] - Component
/// Model preinstantiate-validate; the malformed-component-encoding class
/// lands here.
/// 3. [`crate::runtime::wasmtime_backend::WasmtimeBackend::load_module`] -
/// core-module preinstantiate-validate plus the
/// import-namespace check; the undeclared-imports class lands here.
/// 4. **(Escape-specific)** When step 3 succeeds, drive the loaded module
/// through one `evaluate` call against a constant minimal
/// [`crate::abi::GuardRequest`]. The fuel cap is fixed at
/// [`ESCAPE_FUZZ_FUEL_LIMIT`] so fuel-exhaustion, deep-recursion,
/// table-grow-abuse, and host-reentry escapes surface as typed
/// [`crate::error::WasmGuardError`] values rather than libFuzzer-visible
/// timeouts or aborts.
///
/// # Post-condition
///
/// Every iteration MUST conclude with the host process intact. Errors at
/// every step are silently consumed; the only failures are panics, aborts,
/// or sanitizer-reported memory escapes (which libFuzzer treats as crashes).