Skip to content

Commit bd1904d

Browse files
committed
Harden Wasm runtime resource boundaries
1 parent 87b1a1f commit bd1904d

9 files changed

Lines changed: 355 additions & 51 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ behavior when the change improves security or project direction.
4444
- Make directory-listing timestamp formatting checked and preserve the
4545
`SafeRelativePath` invariant through validated incremental components,
4646
preventing panic-abort and latent traversal paths in static serving.
47+
- Add crate-level Wasm resource ceilings, checked execution deadlines,
48+
semaphore-capacity validation, fallible compile-worker creation, and
49+
before/after deadline checks for the finite non-blocking host-call boundary.
4750

4851
## 1.7.7 - 2026-07-10
4952

crates/fluxheim-config/src/config_tests_wasm.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,44 @@ fn wasm_registry_builds_loader_manifest_with_plugin_overrides() {
288288
fluxheim_wasm::validate_plugin_manifest(manifests[0].clone(), false).unwrap();
289289
}
290290

291+
#[cfg(feature = "wasm")]
292+
#[test]
293+
fn wasm_registry_rejects_limits_above_runtime_hard_ceiling() {
294+
let mut cases = Vec::new();
295+
296+
let mut config = base_wasm_config("");
297+
config.wasm.default_limits.max_module_bytes = super::ByteSize::from_bytes(16 * 1024 * 1024 + 1);
298+
cases.push(("max_module_bytes", config));
299+
300+
let mut config = base_wasm_config("");
301+
config.wasm.default_limits.max_memory_bytes =
302+
super::ByteSize::from_bytes(256 * 1024 * 1024 + 1);
303+
cases.push(("max_memory_bytes", config));
304+
305+
let mut config = base_wasm_config("");
306+
config.wasm.default_limits.max_table_elements = 100_001;
307+
cases.push(("max_table_elements", config));
308+
309+
let mut config = base_wasm_config("");
310+
config.wasm.default_limits.fuel = 100_000_001;
311+
cases.push(("fuel", config));
312+
313+
let mut config = base_wasm_config("");
314+
config.wasm.default_limits.timeout_ms = 5_001;
315+
cases.push(("timeout_ms", config));
316+
317+
let mut config = base_wasm_config("");
318+
config.wasm.default_limits.compile_timeout_ms = 10_001;
319+
cases.push(("compile_timeout_ms", config));
320+
321+
for (expected_field, config) in cases {
322+
assert!(matches!(
323+
config.validate(),
324+
Err(ConfigError::InvalidWasmPolicy { field, .. }) if field == expected_field
325+
));
326+
}
327+
}
328+
291329
#[cfg(all(feature = "wasm", not(feature = "wasm-proxy-abi")))]
292330
#[test]
293331
fn wasm_proxy_preview_namespace_requires_feature() {

crates/fluxheim-config/src/config_wasm.rs

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,24 @@ pub const MAX_WASM_MAX_CONCURRENT_EXECUTIONS: u32 = 256;
1616
pub const MAX_WASM_MAX_TOTAL_PREVIEW_CONCURRENT_EXECUTIONS: u32 = 32;
1717
pub const MAX_WASM_QUEUE_LIMIT: u32 = 256;
1818
pub const MAX_WASM_ATTACHMENT_PRIORITY: u32 = 1_000_000;
19+
pub const MAX_WASM_MODULE_BYTES: u64 = 16 * 1024 * 1024;
20+
pub const MAX_WASM_MEMORY_BYTES: u64 = 256 * 1024 * 1024;
21+
pub const MAX_WASM_TABLE_ELEMENTS: u32 = 100_000;
22+
pub const MAX_WASM_FUEL: u64 = 100_000_000;
23+
pub const MAX_WASM_TIMEOUT_MS: u64 = 5_000;
24+
pub const MAX_WASM_COMPILE_TIMEOUT_MS: u64 = 10_000;
25+
26+
#[cfg(feature = "wasm")]
27+
const _: () = {
28+
assert!(MAX_WASM_MODULE_BYTES == fluxheim_wasm::HARD_MAX_MODULE_BYTES);
29+
assert!(MAX_WASM_MEMORY_BYTES as usize == fluxheim_wasm::HARD_MAX_MEMORY_BYTES);
30+
assert!(MAX_WASM_TABLE_ELEMENTS as usize == fluxheim_wasm::HARD_MAX_TABLE_ELEMENTS);
31+
assert!(MAX_WASM_FUEL == fluxheim_wasm::HARD_MAX_FUEL);
32+
assert!(MAX_WASM_TIMEOUT_MS == fluxheim_wasm::HARD_MAX_TIMEOUT.as_millis() as u64);
33+
assert!(
34+
MAX_WASM_COMPILE_TIMEOUT_MS == fluxheim_wasm::HARD_MAX_COMPILE_TIMEOUT.as_millis() as u64
35+
);
36+
};
1937

2038
const DEFAULT_WASM_MAX_MODULE_BYTES: u64 = 1_048_576;
2139
const DEFAULT_WASM_MAX_MEMORY_BYTES: u64 = 16 * 1024 * 1024;
@@ -633,46 +651,50 @@ impl Default for WasmSandboxLimitsConfig {
633651

634652
impl WasmSandboxLimitsConfig {
635653
fn validate(&self, field: &'static str) -> Result<(), ConfigError> {
636-
if self.max_module_bytes.as_u64() == 0 {
654+
if self.max_module_bytes.as_u64() == 0
655+
|| self.max_module_bytes.as_u64() > MAX_WASM_MODULE_BYTES
656+
{
637657
return Err(ConfigError::InvalidWasmPolicy {
638658
scope: field.to_owned(),
639659
field: "max_module_bytes",
640-
reason: "must be greater than zero",
660+
reason: "must be between 1 byte and 16 MiB",
641661
});
642662
}
643-
if self.max_memory_bytes.as_u64() == 0 {
663+
if self.max_memory_bytes.as_u64() == 0
664+
|| self.max_memory_bytes.as_u64() > MAX_WASM_MEMORY_BYTES
665+
{
644666
return Err(ConfigError::InvalidWasmPolicy {
645667
scope: field.to_owned(),
646668
field: "max_memory_bytes",
647-
reason: "must be greater than zero",
669+
reason: "must be between 1 byte and 256 MiB",
648670
});
649671
}
650-
if self.max_table_elements == 0 {
672+
if self.max_table_elements == 0 || self.max_table_elements > MAX_WASM_TABLE_ELEMENTS {
651673
return Err(ConfigError::InvalidWasmPolicy {
652674
scope: field.to_owned(),
653675
field: "max_table_elements",
654-
reason: "must be greater than zero",
676+
reason: "must be between 1 and 100000",
655677
});
656678
}
657-
if self.fuel == 0 {
679+
if self.fuel == 0 || self.fuel > MAX_WASM_FUEL {
658680
return Err(ConfigError::InvalidWasmPolicy {
659681
scope: field.to_owned(),
660682
field: "fuel",
661-
reason: "must be greater than zero",
683+
reason: "must be between 1 and 100000000",
662684
});
663685
}
664-
if self.timeout_ms == 0 {
686+
if self.timeout_ms == 0 || self.timeout_ms > MAX_WASM_TIMEOUT_MS {
665687
return Err(ConfigError::InvalidWasmPolicy {
666688
scope: field.to_owned(),
667689
field: "timeout_ms",
668-
reason: "must be greater than zero",
690+
reason: "must be between 1 and 5000",
669691
});
670692
}
671-
if self.compile_timeout_ms == 0 {
693+
if self.compile_timeout_ms == 0 || self.compile_timeout_ms > MAX_WASM_COMPILE_TIMEOUT_MS {
672694
return Err(ConfigError::InvalidWasmPolicy {
673695
scope: field.to_owned(),
674696
field: "compile_timeout_ms",
675-
reason: "must be greater than zero",
697+
reason: "must be between 1 and 10000",
676698
});
677699
}
678700
Ok(())

crates/fluxheim-wasm/src/lib.rs

Lines changed: 92 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ pub const DEFAULT_MAX_TABLE_ELEMENTS: usize = 10_000;
3434
pub const DEFAULT_FUEL: u64 = 5_000_000;
3535
pub const DEFAULT_TIMEOUT: Duration = Duration::from_millis(50);
3636
pub const DEFAULT_COMPILE_TIMEOUT: Duration = Duration::from_millis(500);
37+
pub const HARD_MAX_MODULE_BYTES: u64 = 16 * 1024 * 1024;
38+
pub const HARD_MAX_MEMORY_BYTES: usize = 256 * 1024 * 1024;
39+
pub const HARD_MAX_TABLE_ELEMENTS: usize = 100_000;
40+
pub const HARD_MAX_FUEL: u64 = 100_000_000;
41+
pub const HARD_MAX_TIMEOUT: Duration = Duration::from_secs(5);
42+
pub const HARD_MAX_COMPILE_TIMEOUT: Duration = Duration::from_secs(10);
3743

3844
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
3945
pub struct WasmSandboxLimits {
@@ -60,40 +66,40 @@ impl Default for WasmSandboxLimits {
6066

6167
impl WasmSandboxLimits {
6268
pub fn validate(self) -> Result<Self, WasmPluginError> {
63-
if self.max_module_bytes == 0 {
69+
if self.max_module_bytes == 0 || self.max_module_bytes > HARD_MAX_MODULE_BYTES {
6470
return Err(WasmPluginError::InvalidLimit {
6571
field: "max_module_bytes",
66-
reason: "must be greater than zero",
72+
reason: "must be between 1 byte and 16 MiB",
6773
});
6874
}
69-
if self.max_memory_bytes == 0 {
75+
if self.max_memory_bytes == 0 || self.max_memory_bytes > HARD_MAX_MEMORY_BYTES {
7076
return Err(WasmPluginError::InvalidLimit {
7177
field: "max_memory_bytes",
72-
reason: "must be greater than zero",
78+
reason: "must be between 1 byte and 256 MiB",
7379
});
7480
}
75-
if self.max_table_elements == 0 {
81+
if self.max_table_elements == 0 || self.max_table_elements > HARD_MAX_TABLE_ELEMENTS {
7682
return Err(WasmPluginError::InvalidLimit {
7783
field: "max_table_elements",
78-
reason: "must be greater than zero",
84+
reason: "must be between 1 and 100000",
7985
});
8086
}
81-
if self.fuel == 0 {
87+
if self.fuel == 0 || self.fuel > HARD_MAX_FUEL {
8288
return Err(WasmPluginError::InvalidLimit {
8389
field: "fuel",
84-
reason: "must be greater than zero",
90+
reason: "must be between 1 and 100000000",
8591
});
8692
}
87-
if self.timeout.is_zero() {
93+
if self.timeout.is_zero() || self.timeout > HARD_MAX_TIMEOUT {
8894
return Err(WasmPluginError::InvalidLimit {
8995
field: "timeout",
90-
reason: "must be greater than zero",
96+
reason: "must be between 1ns and 5s",
9197
});
9298
}
93-
if self.compile_timeout.is_zero() {
99+
if self.compile_timeout.is_zero() || self.compile_timeout > HARD_MAX_COMPILE_TIMEOUT {
94100
return Err(WasmPluginError::InvalidLimit {
95101
field: "compile_timeout",
96-
reason: "must be greater than zero",
102+
reason: "must be between 1ns and 10s",
97103
});
98104
}
99105
Ok(self)
@@ -121,4 +127,78 @@ mod tests {
121127
}
122128
));
123129
}
130+
131+
#[test]
132+
fn sandbox_limits_reject_values_above_hard_ceiling() {
133+
let cases = [
134+
(
135+
"max_module_bytes",
136+
WasmSandboxLimits {
137+
max_module_bytes: HARD_MAX_MODULE_BYTES + 1,
138+
..WasmSandboxLimits::default()
139+
},
140+
),
141+
(
142+
"max_memory_bytes",
143+
WasmSandboxLimits {
144+
max_memory_bytes: HARD_MAX_MEMORY_BYTES + 1,
145+
..WasmSandboxLimits::default()
146+
},
147+
),
148+
(
149+
"max_table_elements",
150+
WasmSandboxLimits {
151+
max_table_elements: HARD_MAX_TABLE_ELEMENTS + 1,
152+
..WasmSandboxLimits::default()
153+
},
154+
),
155+
(
156+
"fuel",
157+
WasmSandboxLimits {
158+
fuel: HARD_MAX_FUEL + 1,
159+
..WasmSandboxLimits::default()
160+
},
161+
),
162+
(
163+
"timeout",
164+
WasmSandboxLimits {
165+
timeout: HARD_MAX_TIMEOUT + Duration::from_nanos(1),
166+
..WasmSandboxLimits::default()
167+
},
168+
),
169+
(
170+
"compile_timeout",
171+
WasmSandboxLimits {
172+
compile_timeout: HARD_MAX_COMPILE_TIMEOUT + Duration::from_nanos(1),
173+
..WasmSandboxLimits::default()
174+
},
175+
),
176+
];
177+
178+
for (field, limits) in cases {
179+
assert!(matches!(
180+
limits.validate(),
181+
Err(WasmPluginError::InvalidLimit {
182+
field: invalid,
183+
..
184+
}) if invalid == field
185+
));
186+
}
187+
}
188+
189+
#[test]
190+
fn sandbox_limits_accept_values_at_hard_ceiling() {
191+
assert!(
192+
WasmSandboxLimits {
193+
max_module_bytes: HARD_MAX_MODULE_BYTES,
194+
max_memory_bytes: HARD_MAX_MEMORY_BYTES,
195+
max_table_elements: HARD_MAX_TABLE_ELEMENTS,
196+
fuel: HARD_MAX_FUEL,
197+
timeout: HARD_MAX_TIMEOUT,
198+
compile_timeout: HARD_MAX_COMPILE_TIMEOUT,
199+
}
200+
.validate()
201+
.is_ok()
202+
);
203+
}
124204
}

0 commit comments

Comments
 (0)