Memory resource exhaustion via untracked buffers
Description
A vulnerability in the VM's buffer-management system allows an attacker to exceed memory limits through a combination of key-value store operations and raw syscalls. The issue stems from the following mechanisms.
First is the ability to store VM memory pages in a key-value store entry and retrieve them using raw syscalls, leading to an untracked memory allocation when the returned buffer entry is not immediately cleaned up.
fn materialize_duplicates(handle: u32, pages: &mut [u8]) {
unsafe {
// ... SBOR encoding setup ...
wasm_api::kv_entry::kv_entry_write(handle, pages.as_ptr(), i + len);
// Revert to initial value
(pages.as_mut_ptr() as *mut u64).write_volatile(initial_value);
}
for _i in 0..31 { // buffer limit
unsafe {
std::mem::forget(wasm_api::kv_entry::kv_entry_read(handle));
};
}
}
Second is the ability to recursively create new call frames up to the depth limit that maintain separate buffer limits.
unsafe {
wasm_api::blueprint::blueprint_call(
package_address.as_bytes().as_ptr(),
package_address.as_bytes().len(),
blueprint_name.as_ptr(),
blueprint_name.len(),
function_name.as_ptr(),
function_name.len(),
args.as_ptr(),
args.len(),
);
}
The attack works by allocating the maximum available memory in the VM, storing this memory in a key-value store entry, creating duplicates through repeated reads while avoiding buffer cleanup, and recursively spawning new call frames to multiply the effect.
Impact
The vulnerability allows a malicious transaction to do the following:
Consume approximately 480MB of validator memory (8 call frames x 32 buffer limit x 2MB transaction substate limit), which is a ~250x amplification of the intended memory limits per transaction
Cause significant performance degradation with execution times of 5--10 seconds per transaction
Create physical memory pressure on validator nodes through repeated physical page allocations and deallocations
Potentially lead to out-of-memory conditions in validator processes if multiple transactions are executed concurrently
Recommendations
We recommend two potential approaches to resolve this issue:
Buffer slot--limit reduction
The current limit of 32 buffer slots appears unnecessary as the
Buffer
type is only used by the private API. Most WASM APIs return only one buffer, and the application layer forces it to be consumed and cleaned up before returning. Consider reducing the buffer limit to two slots: one slot for general operations and one additional slot to support potential callback scenarios. This would reduce the maximum memory amplification from ~250x to ~16x.Alternative memory management
Consider exporting
malloc
/free
from the guest environment, which would enable proper cost tracking in all scenarios. Memory allocations would be tracked and limited appropriately through the cost-unit system, and it would not require any copies in the host state.
The first approach (reducing buffer slots) is likely the simplest and most effective solution, as there are no known legitimate use cases requiring multiple buffer slots.
Remediation
This issue has been acknowledged by Radix Publishing Limited, and a fix was implemented in commit cd5999b2↗.