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
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Craton Software Company
//! Bounded-cache regression tests (exec S-5 + exec S-10).
//!
//! Pre-fix the executor held two unbounded `DashMap`s:
//! * `module_cache: DashMap<digest, Module>` — a misbehaving tenant
//! could pin arbitrarily many compiled modules (each multi-MiB of
//! host RAM) by submitting unique wasm bytes in a loop (S-5).
//! * `instances: DashMap<InstanceId, ...>` — the same loop, applied
//! to `spawn_instance`, could exhaust host RAM with live `Store`s
//! (S-10).
//!
//! The fix bounds the module cache with an LRU and the instance
//! registry with an atomic admission counter. These tests pin the
//! observable side of both bounds.
use std::sync::Arc;
use tensor_wasm_core::types::TenantId;
use tensor_wasm_exec::engine::{EngineConfig, TensorWasmEngine};
use tensor_wasm_exec::executor::{ExecError, SpawnConfig, TensorWasmExecutor};
/// Build a syntactically-distinct trivial module by varying an internal
/// export name. Each variant compiles to a different wasm byte sequence
/// and therefore hashes to a different BLAKE3 digest — guaranteeing
/// independent slots in the module cache.
fn distinct_trivial_wasm(tag: u32) -> Vec<u8> {
let src = format!(r#"(module (func (export "noop_{tag}")))"#);
wat::parse_str(&src).expect("valid wat")
}
#[tokio::test]
async fn module_cache_evicts_lru_at_cap() {
// Cap at 2 — the third distinct module compile must evict the
// oldest (LRU) entry, holding the cache size at 2.
let cfg = EngineConfig {
max_module_cache_entries: 2,
..EngineConfig::default()
};
let engine = Arc::new(TensorWasmEngine::with_config(cfg).expect("engine"));
let exec = TensorWasmExecutor::new(engine);
let w1 = distinct_trivial_wasm(1);
let w2 = distinct_trivial_wasm(2);
let w3 = distinct_trivial_wasm(3);
// Spawn (and immediately terminate) each variant. We terminate after
// each spawn so the live-instance counter does not bound this test;
// the assertion is purely about module-cache occupancy.
let id1 = exec
.spawn_instance(SpawnConfig::for_tenant(TenantId(1)), &w1)
.await
.expect("spawn w1");
exec.terminate(id1).await.expect("terminate w1");
let id2 = exec
.spawn_instance(SpawnConfig::for_tenant(TenantId(1)), &w2)
.await
.expect("spawn w2");
exec.terminate(id2).await.expect("terminate w2");
// Two distinct modules → cache full but not yet over cap.
assert_eq!(
exec.module_cache_len(),
2,
"cache should hold both modules before eviction"
);
// Third spawn pushes the cache over its cap, evicting w1 (the
// least-recently-used). The cache MUST stay at the configured cap.
let id3 = exec
.spawn_instance(SpawnConfig::for_tenant(TenantId(1)), &w3)
.await
.expect("spawn w3");
exec.terminate(id3).await.expect("terminate w3");
assert_eq!(
exec.module_cache_len(),
2,
"LRU eviction must keep the cache at its configured cap",
);
// Verify w1 was the evicted entry: re-spawning it now compiles
// afresh (the cache no longer contains it) and pushes the cache
// back to its cap by evicting whichever entry is now oldest. The
// observable signal is simply that `module_cache_len` is still 2 —
// if w1 had survived, the recompile would be a hit (no insert) and
// the cache would stay at 2 trivially. To distinguish a hit from a
// miss-then-insert-then-evict we inspect that BOTH w2 and w3 still
// resolve from cache (a `module_cache_len` of 2 after touching all
// three variants is only possible if eviction is actually firing).
let id1_again = exec
.spawn_instance(SpawnConfig::for_tenant(TenantId(1)), &w1)
.await
.expect("re-spawn w1");
exec.terminate(id1_again).await.expect("terminate w1 again");
assert_eq!(
exec.module_cache_len(),
2,
"after touching all 3 distinct modules with cap=2 the cache must stay bounded",
);
}
#[tokio::test]
async fn module_cache_stays_bounded_when_filled_past_cap() {
// Companion to `module_cache_evicts_lru_at_cap`: drive a larger number
// of distinct modules (well past the cap) through the executor and
// assert the cache never grows beyond `max_module_cache_entries`. Uses
// `cached_module_count()` (the alias surfaced for operators) to confirm
// the S-5 bound holds under sustained unique-module pressure — the
// exact DoS shape the LRU cap defends against (a tenant submitting
// unique wasm bytes in a loop to pin compiled modules).
const CAP: usize = 4;
const DISTINCT_MODULES: u32 = 32;
let cfg = EngineConfig {
max_module_cache_entries: CAP,
..EngineConfig::default()
};
let engine = Arc::new(TensorWasmEngine::with_config(cfg).expect("engine"));
let exec = TensorWasmExecutor::new(engine);
for tag in 0..DISTINCT_MODULES {
let wasm = distinct_trivial_wasm(tag);
let id = exec
.spawn_instance(SpawnConfig::for_tenant(TenantId(1)), &wasm)
.await
.expect("spawn distinct module");
// Terminate immediately so the live-instance cap never bounds this
// test — the assertion is purely about module-cache occupancy.
exec.terminate(id).await.expect("terminate");
// After every spawn the cache must be at or below the cap; it can
// never exceed it regardless of how many distinct modules we feed.
assert!(
exec.cached_module_count() <= CAP,
"cache exceeded cap {CAP} after {} distinct modules: count={}",
tag + 1,
exec.cached_module_count(),
);
}
// After filling well past the cap the cache must sit exactly at the cap
// (we touched far more distinct modules than CAP, so eviction must have
// fired and the cache must be saturated).
assert_eq!(
exec.cached_module_count(),
CAP,
"cache should be saturated at its cap after sustained unique-module pressure",
);
}
#[tokio::test]
async fn instances_capacity_exhausted_returns_typed_error() {
// Cap at 2 live instances — the third spawn must fail with the
// typed `CapacityExhausted` error, not a generic wasmtime error.
let cfg = EngineConfig {
max_instances: Some(2),
..EngineConfig::default()
};
let engine = Arc::new(TensorWasmEngine::with_config(cfg).expect("engine"));
let exec = TensorWasmExecutor::new(engine);
// Trivial module — `(module)` is valid wasm with no exports / imports;
// it doesn't need a start function so instantiation is essentially free.
let wasm = wat::parse_str("(module)").expect("valid wat");
let _id1 = exec
.spawn_instance(SpawnConfig::for_tenant(TenantId(1)), &wasm)
.await
.expect("first spawn");
let _id2 = exec
.spawn_instance(SpawnConfig::for_tenant(TenantId(1)), &wasm)
.await
.expect("second spawn");
assert_eq!(exec.instances_len(), 2);
// Third spawn must be rejected at admission time, before any
// compile/instantiate work touches the host.
let err = exec
.spawn_instance(SpawnConfig::for_tenant(TenantId(1)), &wasm)
.await
.expect_err("third spawn must fail with CapacityExhausted");
match err {
ExecError::CapacityExhausted { active, limit } => {
assert_eq!(limit, 2);
assert!(
active > limit,
"active={active} must exceed limit={limit} on rejection",
);
}
other => panic!("expected CapacityExhausted, got {other:?}"),
}
// Most important invariant: the failed spawn must not have leaked
// an admission slot — `instances_len` should still be exactly 2,
// otherwise the cap erodes one slot per failed spawn and a tenant
// could deny service to themselves by repeatedly tripping the limit.
assert_eq!(
exec.instances_len(),
2,
"failed spawn must roll back its admission slot",
);
}