-
Notifications
You must be signed in to change notification settings - Fork 0
Testing Strategy
How sandboxd is tested, and why the suite is shaped the way it is. The whole point of the project is an isolation boundary, so the tests are written to prove each guarantee in the Threat Model against a real hostile fixture, not to chase a coverage number.
There are two layers:
-
Integration tests in
tests/sandbox.rs, eleven of them, each driving a.watfixture through the public library API. -
A doc-test in
src/lib.rs, the quick-start example, which proves the README-level snippet actually compiles and runs.
There are no unit tests on private functions. That is deliberate: the contract this project makes is at the public surface (a hostile module is stopped, a clean error is returned), so the tests exercise that surface end to end. Testing classify_trap in isolation would prove less than running an infinite loop and asserting FuelExhausted.
Each fixture in fixtures/ encodes one hostile or well-behaved behaviour, and each is referenced by include_str! at the top of the test file:
const INFINITE_LOOP: &str = include_str!("../fixtures/infinite_loop.wat");
const MEMORY_BOMB: &str = include_str!("../fixtures/memory_bomb.wat");
const DISALLOWED_IMPORT: &str = include_str!("../fixtures/disallowed_import.wat");
const WELL_BEHAVED: &str = include_str!("../fixtures/well_behaved.wat");
const LOGGER: &str = include_str!("../fixtures/logger.wat");This means the attack table in the README and the Threat Model is executable: every row is backed by a test that runs the actual malicious module and asserts the actual error variant. The fixtures are documented in Module Format and WAT.
| Test | Fixture | Asserts |
|---|---|---|
fuel_exhaustion_terminates |
infinite loop | a long timeout but small fuel stops it with FuelExhausted { budget: 1_000_000 }
|
epoch_timeout_terminates |
infinite loop |
u64::MAX fuel but a 100 ms timeout stops it with Timeout { millis: 100 }, and in under five seconds |
memory_cap_enforced |
memory bomb | a 4 MiB cap stops it with MemoryLimitExceeded { limit: 4 MiB }
|
disallowed_import_rejected |
disallowed import | rejected with DisallowedImport { env, secret } before any code runs |
log_import_denied_by_default |
logger | even host::log is denied without a grant: DisallowedImport { host, log }
|
allowed_import_works |
logger | with allow_log, the run succeeds and the sink holds exactly ["hello from the guest"]
|
well_behaved_returns_value |
well-behaved |
add(2, 40) returns I32(42) and reports fuel consumed |
pure_module_is_deterministic |
well-behaved |
fib(20) returns 6765 and burns identical fuel across three fresh sandboxes |
missing_export_is_reported |
well-behaved | calling a non-existent export gives Export, not a panic |
signature_mismatch_is_reported |
well-behaved | wrong argument arity gives Export, not a panic |
invalid_module_is_reported |
(raw bytes) |
b"this is not wasm" gives InvalidModule, not a panic |
Independent fences. The infinite loop appears in two tests on purpose. fuel_exhaustion_terminates gives it a long timeout so only fuel can stop it. epoch_timeout_terminates gives it near-infinite fuel so only the watchdog can stop it. One fixture, two independent mechanisms, each proven alone. This is the redundancy that the whole design rests on.
Determinism. pure_module_is_deterministic runs fib(20) three times in three freshly built sandboxes and asserts not just the same return value but the same fuel_consumed. That is the property that makes fuel usable as a quota, and it is checked rather than assumed.
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#0d1117','primaryTextColor':'#f5f7fa','primaryBorderColor':'#38bdf8','lineColor':'#22d3ee','secondaryColor':'#0f172a','tertiaryColor':'#0d1117','fontFamily':'ui-monospace, monospace'}}}%%
flowchart LR
subgraph fences[one fixture, two fences]
A[infinite_loop.wat] --> B[long timeout, small fuel]
A --> C[huge fuel, short timeout]
B --> D[FuelExhausted]
C --> E[Timeout]
end
export PATH="$HOME/.cargo/bin:/opt/homebrew/bin:$PATH"
cargo test --releaseOn an Apple M3 Pro the integration suite runs in 0.13 s and the doc-test in 0.07 s. The slow part of any change is compiling wasmtime, not running the tests.
The workflow in .github/workflows/ci.yml runs on push and pull request to main, with RUSTFLAGS: "-D warnings" so any warning fails the build. The steps:
-
cargo fmt --all --check, formatting must be clean. -
cargo clippy --all-targets --all-features -- -D warnings, lints must pass with no warnings. -
cargo build --verbose. -
cargo test --verbose.
The registry and target directory are cached on the Cargo.lock hash to keep the wasmtime compile from dominating pipeline time.
-
No fuzzing. A fuzzer feeding random bytes to
compileandrunwould harden theInvalidModuleand trap paths. It is not in the suite today. -
No property tests. The determinism check is by example (
fib(20)three times), not a property over many inputs. - No benchmark regression gate. The numbers in Performance and Benchmarks are measured by hand, not asserted in CI.
These are the kind of additions a contributor could make; see Contributing.
SarmaLinux . sarmalinux.com . repo
SarmaLinux . sarmalinux.com . sandboxd on GitHub
Understand it
Reference
Operate it
Security
Extend it
Decide on it
SarmaLinux . sarmalinux.com