Summary
The SR-43 reject-at-load on oversized memory min check (#420) runs post-instantiate, so a
module declaring a large memory min is fully allocated before the rejection fires — the
--memory cap it is supposed to enforce is transiently bypassed. A hostile/faulty module can force
kilnd to allocate multiple GB of RAM despite a tiny --memory cap, then exit "rejected". This is a
memory-exhaustion DoS.
This is the documented follow-up the #420 commit flagged ("rejecting before eager min allocation
needs a pre-instantiate engine API (follow-up)") — filing to track it, with a concrete measurement.
Verified on kiln HEAD 25da5f9 (v0.4.3).
Root cause (source)
kilnd/src/lib.rs:
860 // Instantiate
862 .instantiate(module_handle) // <-- Memory::new allocates the declared `min` HERE
...
875 // SR-43: reject-at-load when the module's DECLARED memory min exceeds the cap
878 // NOTE: this runs post-instantiate, so the declared `min` was
879 // already allocated by Memory::new; rejecting before eager `min`
880 // allocation needs a pre-instantiate engine API (follow-up).
881 let declared_min = mem.0.ty.limits.min;
882 if declared_min > cap_pages { ...reject... } // <-- too late, memory already committed
The --memory cap's runtime limiter (runtime_max_pages, SR-44) guards grow, but the eager
initial min allocation goes through Memory::new at instantiation, which the post-instantiate
check cannot prevent.
Reproduction (executed)
(module (memory 40000) (func (export "start") (drop (memory.size)))) ;; 40000 pages = ~2.56 GB min
$ /usr/bin/time -l kilnd --memory 65536 --function start huge.wasm ;; 64 KB cap
✗ Execution failed: [Resource][E03F8] Module declared memory min exceeds --memory cap (rejected at load)
0.28 real
5298307072 maximum resident set size # ~5.3 GB allocated for a "rejected" module
Contrast: a normal (memory 1) module peaks at ~55 MB RSS. So the "rejected" module resident-allocated
~5.3 GB under a 64 KB --memory cap before the reject fired. On a memory-constrained host this
OOMs/swaps — the exact resource exhaustion --memory is meant to prevent.
Impact
Suggested fix
Enforce the cap before the min is committed: either build the store's ResourceLimiter
(memory_growing, which wasmtime consults for the initial memory allocation too) from
config.max_memory so it denies an over-cap min at Memory::new time, or check
module.memories().map(|m| m.minimum) against the cap on the parsed module before instantiate.
Either rejects pre-allocation and closes the transient-DoS window.
Reported by the pulseengine-challenge harness (executed RSS measurement + source; the documented
post-instantiate follow-up from #420, now tracked).
Summary
The SR-43
reject-at-load on oversized memory mincheck (#420) runs post-instantiate, so amodule declaring a large memory
minis fully allocated before the rejection fires — the--memorycap it is supposed to enforce is transiently bypassed. A hostile/faulty module can forcekilnd to allocate multiple GB of RAM despite a tiny
--memorycap, then exit "rejected". This is amemory-exhaustion DoS.
This is the documented follow-up the #420 commit flagged ("rejecting before eager
minallocationneeds a pre-instantiate engine API (follow-up)") — filing to track it, with a concrete measurement.
Verified on kiln HEAD 25da5f9 (v0.4.3).
Root cause (source)
kilnd/src/lib.rs:The
--memorycap's runtime limiter (runtime_max_pages, SR-44) guardsgrow, but the eagerinitial
minallocation goes throughMemory::newat instantiation, which the post-instantiatecheck cannot prevent.
Reproduction (executed)
Contrast: a normal
(memory 1)module peaks at ~55 MB RSS. So the "rejected" module resident-allocated~5.3 GB under a 64 KB
--memorycap before the reject fired. On a memory-constrained host thisOOMs/swaps — the exact resource exhaustion
--memoryis meant to prevent.Impact
--memorycap is bypassable for the initial declaredmin: reject-at-load prevents executionbut not allocation. Peak allocation is bounded only by the module's declared
min(up to 2^32pages = 256 GB), not by
--memory.table cap. This is specifically the declared-
mineager-allocation path.Suggested fix
Enforce the cap before the min is committed: either build the store's
ResourceLimiter(
memory_growing, which wasmtime consults for the initial memory allocation too) fromconfig.max_memoryso it denies an over-capminatMemory::newtime, or checkmodule.memories().map(|m| m.minimum)against the cap on the parsed module beforeinstantiate.Either rejects pre-allocation and closes the transient-DoS window.
Reported by the pulseengine-challenge harness (executed RSS measurement + source; the documented
post-instantiate follow-up from #420, now tracked).