Found while reviewing #98. Not introduced there — #98 added guards to list_subscriptions and, per the systematic-debugging rule, the same pattern should have been grepped for at the time. It was not, so filing it.
The pattern
A host function returns an i64/i32 length, and the delegate allocates or truncates on it without validating. Two consequences:
as usize truncates on wasm32, where usize is 32 bits. A returned 2^32 becomes 0, which surfaces to the caller as an empty result rather than an error — indistinguishable from a genuine empty.
Vec::truncate with an oversized argument is a documented no-op, so a host that reports writing more than the buffer holds leaves the zero-filled tail in place, to be decoded as valid-looking data.
The sites
All in rust/src/delegate_host.rs:
| Function |
Issue |
list_secrets (~:523) |
vec![0u8; len as usize] on an unvalidated positive i64; then truncate(written as usize) with written never checked against len |
get_contract_state (~:572) |
same shape |
ctx_read (~:331) |
buf.truncate(read.max(0) as usize), read unchecked against the buffer |
get_secret (~:420) |
same shape |
Why it matters less than it looks, and still matters
The host is trusted, so this is not an attack surface — it is a malfunctioning-host surface, and the failure is silent in the direction that matters: a caller receives a plausible empty or short result instead of an error. That is the same conflation #98 argued was worth a Result return type for list_subscriptions.
Suggested fix
#98 extracted the decisions from list_subscriptions into two pure functions, validate_list_len and resolve_written, specifically so they are compiled and tested on the host target — the wasm-only bodies are otherwise type-checked by CI and executed by nothing. The same treatment generalises: one shared validation helper, host-side table-driven tests, and the unsafe extern call left as the only wasm-gated part.
Note this interacts with #100: cargo test runs on the host only, so any logic left inside #[cfg(target_family = "wasm")] has no test coverage at all today.
[AI-assisted - Claude]
Found while reviewing #98. Not introduced there — #98 added guards to
list_subscriptionsand, per the systematic-debugging rule, the same pattern should have been grepped for at the time. It was not, so filing it.The pattern
A host function returns an
i64/i32length, and the delegate allocates or truncates on it without validating. Two consequences:as usizetruncates on wasm32, whereusizeis 32 bits. A returned2^32becomes0, which surfaces to the caller as an empty result rather than an error — indistinguishable from a genuine empty.Vec::truncatewith an oversized argument is a documented no-op, so a host that reports writing more than the buffer holds leaves the zero-filled tail in place, to be decoded as valid-looking data.The sites
All in
rust/src/delegate_host.rs:list_secrets(~:523)vec![0u8; len as usize]on an unvalidated positivei64; thentruncate(written as usize)withwrittennever checked againstlenget_contract_state(~:572)ctx_read(~:331)buf.truncate(read.max(0) as usize),readunchecked against the bufferget_secret(~:420)Why it matters less than it looks, and still matters
The host is trusted, so this is not an attack surface — it is a malfunctioning-host surface, and the failure is silent in the direction that matters: a caller receives a plausible empty or short result instead of an error. That is the same conflation #98 argued was worth a
Resultreturn type forlist_subscriptions.Suggested fix
#98 extracted the decisions from
list_subscriptionsinto two pure functions,validate_list_lenandresolve_written, specifically so they are compiled and tested on the host target — the wasm-only bodies are otherwise type-checked by CI and executed by nothing. The same treatment generalises: one shared validation helper, host-side table-driven tests, and theunsafeextern call left as the only wasm-gated part.Note this interacts with #100:
cargo testruns on the host only, so any logic left inside#[cfg(target_family = "wasm")]has no test coverage at all today.[AI-assisted - Claude]