Skip to content

fix(wasm-runtime): align host function signatures and add storage host functions - #15

Closed
echobt wants to merge 5 commits into
mainfrom
fix/wasm-host-function-signatures-and-storage
Closed

fix(wasm-runtime): align host function signatures and add storage host functions#15
echobt wants to merge 5 commits into
mainfrom
fix/wasm-host-function-signatures-and-storage

Conversation

@echobt

@echobt echobt commented Feb 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes WASM module instantiation failures caused by host function signature mismatches between the guest SDK and the runtime interface, and implements the missing storage_get/storage_set host functions.

Changes

Network host function signature alignment (network.rs)

  • http_get: Changed from 4 params (req_ptr, req_len, resp_ptr, resp_len) to 3 params (req_ptr, req_len, resp_ptr) to match guest SDK
  • http_post: Changed from 4 params to 5 params (req_ptr, req_len, resp_ptr, resp_len, extra) to match guest SDK
  • dns_resolve: Changed from 4 params (req_ptr, req_len, resp_ptr, resp_len) to 3 params (req_ptr, req_len, resp_ptr) to match guest SDK
  • Added write_result_unbounded and write_bytes_unbounded helpers for writing responses without a length-bounded buffer

Storage host functions (storage.rs)

  • Implemented StorageHostFunctions struct with HostFunctionRegistrar trait
  • Registered storage_get(key_ptr, key_len, value_ptr) → i32 and storage_set(key_ptr, key_len, value_ptr, value_len) → i32 under the platform_storage WASM import namespace
  • Added in-memory key-value data field to StorageHostState for storage operations
  • Added private read_memory, write_to_memory, and get_memory helpers

Runtime state integration (runtime.rs)

  • Added storage_state: Option<StorageHostState> field to RuntimeState
  • Updated RuntimeState::new() constructor to accept and store storage state

Module exports (lib.rs)

  • Exported StorageHostFunctions and HOST_STORAGE_SET from the crate root

Summary by CodeRabbit

  • New Features

    • Added storage set operation to enable key-value storage management in the WASM runtime interface.
    • Enhanced network request handling for HTTP and DNS operations with improved memory management.
  • Improvements

    • Extended runtime state configuration to support optional storage state management.

- http_get: 4 params -> 3 params (req_ptr, req_len, resp_ptr)
- http_post: 4 params -> 5 params (req_ptr, req_len, resp_ptr, resp_len, extra)
- dns_resolve: 4 params -> 3 params (req_ptr, req_len, resp_ptr)
- Add write_result_unbounded/write_bytes_unbounded helpers for functions
  without caller-supplied resp_len, using memory size as bound instead
…rar trait

- Add StorageHostFunctions struct implementing HostFunctionRegistrar
- Register storage_get and storage_set under platform_storage namespace
- Add data HashMap to StorageHostState for in-memory key-value storage
- Implement handle_storage_get: reads key, looks up in data, writes value
- Implement handle_storage_set: validates key/value, inserts into data
- Add memory helper functions (get_memory, read_memory, write_to_memory)
- Add HOST_STORAGE_SET constant
- Export StorageHostFunctions and HOST_STORAGE_SET from lib.rs
@coderabbitai

coderabbitai Bot commented Feb 17, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR extends the WASM runtime interface to support persistent key-value storage operations by introducing a new storage_set host function alongside the existing storage_get, adding memory management utilities for guest memory interaction, extending RuntimeState with optional storage state, and refactoring network host function bindings to use unbounded memory writes.

Changes

Cohort / File(s) Summary
Storage API Expansion
crates/wasm-runtime-interface/src/storage.rs
Introduces storage_set host function, adds memory utilities (get_memory, read_memory, write_to_memory), extends StorageHostState with HashMap-based key-value storage, implements new StorageHostFunctions type as a HostFunctionRegistrar, and adds comprehensive error handling and test coverage.
Public API Re-exports
crates/wasm-runtime-interface/src/lib.rs
Adds public re-exports of StorageHostFunctions and HOST_STORAGE_SET constant from the storage module to expand the public host storage surface.
Runtime State Extension
crates/wasm-runtime-interface/src/runtime.rs
Extends RuntimeState struct with an optional storage_state: Option<StorageHostState> field and updates the constructor to accept and initialize this parameter.
Network Interface Refactoring
crates/wasm-runtime-interface/src/network.rs
Refactors host function bindings by removing resp_len parameter from HTTP GET and DNS RESOLVE functions, adding an extra parameter to HTTP POST, and introducing write_result_unbounded and write_bytes_unbounded helpers for handling unbounded memory writes.

Sequence Diagram(s)

sequenceDiagram
    participant Guest as Guest WASM
    participant Host as Host Function Handler
    participant Memory as Guest Memory
    participant Storage as Storage HashMap

    Guest->>Host: storage_set(key_ptr, key_len, value_ptr, value_len)
    Host->>Memory: get_memory()
    Memory-->>Host: memory export
    Host->>Memory: read_memory(key_ptr, key_len)
    Memory-->>Host: key bytes
    Host->>Memory: read_memory(value_ptr, value_len)
    Memory-->>Host: value bytes
    Host->>Storage: insert(key, value)
    Storage-->>Host: ok
    Host->>Host: write_result_unbounded(status)
    Host-->>Guest: status code
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 Through memory burrows deep and grand,
The rabbit stores each precious key,
Set and get on borrowed land,
WASM magic, wild and free! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: aligning host function signatures (network) and adding storage host functions, which are the primary objectives of the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/wasm-host-function-signatures-and-storage

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/wasm-runtime-interface/src/storage.rs (1)

1-22: ⚠️ Potential issue | 🟡 Minor

Module doc comment is stale — does not match implemented signatures.

The doc block describes:

  • storage_get(key_ptr, key_len) -> i64 — actual: storage_get(key_ptr, key_len, value_ptr) -> i32
  • storage_propose_write(...) — but storage_set is what's implemented
  • Return format described as packed i64 — but the new functions return plain i32

Please update the doc to reflect the actual host function surface.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/wasm-runtime-interface/src/storage.rs` around lines 1 - 22, Update the
module doc comment to match the implemented host functions and their actual
signatures/return types: replace the described `storage_get(key_ptr, key_len) ->
i64` with the real `storage_get(key_ptr, key_len, value_ptr) -> i32`, rename
`storage_propose_write` to `storage_set` (or document both if both exist) and
document `storage_delete`'s real signature, and remove the packed i64 return
description—describe the concrete i32 return conventions used by `storage_get`,
`storage_set`, and `storage_delete` (e.g., success/error codes and how value_ptr
is used) so the docblock matches the symbols `storage_get`, `storage_set` (or
`storage_propose_write` if present), and `storage_delete`.
🧹 Nitpick comments (2)
crates/wasm-runtime-interface/src/network.rs (2)

622-671: Inconsistent response-write strategy between http_post and http_get/dns_resolve.

handle_http_get and handle_dns_request switched to write_result_unbounded (no resp_len), while handle_http_post retains the bounded write_result with resp_len — plus an unused _extra parameter. This means:

  • GET/DNS: guest supplies no buffer length → host writes unconditionally.
  • POST: guest supplies resp_len → host respects bounded writes.

If this asymmetry is intentional to match the guest SDK, consider documenting why the protocols differ. If unintentional, http_post should likely follow the same pattern as the others.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/wasm-runtime-interface/src/network.rs` around lines 622 - 671, The
handle_http_post function currently uses write_result with a bounded resp_len
and retains an unused _extra parameter, making it inconsistent with
handle_http_get and handle_dns_request which use write_result_unbounded; change
handle_http_post to call write_result_unbounded(caller, resp_ptr, result)
instead of write_result(caller, resp_ptr, resp_len, result), remove the unused
_extra parameter from the signature, and ensure any callers or exports are
updated to match the unbounded response pattern to keep GET/POST/DNS behavior
consistent.

890-905: Extract read_memory and get_memory to a shared module.

Both network.rs and storage.rs contain identical implementations of read_memory and get_memory. Move these functions to a shared location (e.g., runtime.rs or a new memory.rs utility module) to eliminate duplication and maintain a single source of truth for guest-memory I/O operations.

Affected locations:

  • network.rs: lines 890–905 (read_memory) and 1001–1006 (get_memory)
  • storage.rs: lines 558–573 (read_memory) and 551–556 (get_memory)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/wasm-runtime-interface/src/network.rs` around lines 890 - 905, Extract
the duplicate read_memory and get_memory implementations into a single shared
module (e.g., create crates/wasm-runtime-interface/src/memory.rs or add them to
runtime.rs), make them appropriately visible (pub(crate) or pub as needed), and
remove the copies from network.rs and storage.rs; then update both network.rs
and storage.rs to import and call the shared get_memory and read_memory
functions instead of their local versions, and run cargo build/tests to ensure
no visibility or import errors remain.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/wasm-runtime-interface/src/network.rs`:
- Around line 975-999: The write_bytes_unbounded function can overwrite adjacent
guest memory because it never verifies the actual guest buffer size; update
write_bytes_unbounded to perform the same safety check as write_bytes by either
(a) require and accept a guest-supplied resp_len and return -(needed_len) when
the provided buffer is too small, (b) call the guest allocator (storage_alloc)
to obtain a buffer large enough before writing, or (c) explicitly document and
enforce a precondition that the guest must allocate sufficient space—implement
one of these approaches in the write_bytes_unbounded implementation (referencing
write_bytes and storage_alloc to mirror the two-phase/allocator pattern) so the
host never blindly writes past the guest buffer.

In `@crates/wasm-runtime-interface/src/storage.rs`:
- Around line 454-499: The return-value collides because handle_storage_get
returns val.len() as i32 for success but StorageHostStatus::NotFound.to_i32() ==
1, making a 1-byte value indistinguishable from NotFound; change the status
encoding so error/status codes are negative (e.g., make
StorageHostStatus::NotFound a negative value) and update the
StorageHostStatus::to_i32/from_i32 mappings accordingly so positive returns from
handle_storage_get always mean bytes written and negative returns indicate
error/status; update all callsites that interpret StorageHostStatus integers to
use the new negative status convention (references: function handle_storage_get,
enum StorageHostStatus and its to_i32/from_i32 conversions).
- Around line 501-549: handle_storage_set currently inserts without enforcing
StorageHostConfig limits; before inserting, check
storage_state.config.max_keys_per_challenge against current
storage_state.data.len() (or add/update a keys_count) and check
storage_state.bytes_written (or a new cumulative total_storage_bytes) +
value.len() <= storage_state.config.max_total_storage; if either limit would be
exceeded return the appropriate StorageHostStatus (e.g., QuotaExceeded or a
mapped error) instead of inserting; also update the cumulative
total_storage_bytes (or bytes_written) on successful insert/remove so future
checks are O(1) rather than O(n).

---

Outside diff comments:
In `@crates/wasm-runtime-interface/src/storage.rs`:
- Around line 1-22: Update the module doc comment to match the implemented host
functions and their actual signatures/return types: replace the described
`storage_get(key_ptr, key_len) -> i64` with the real `storage_get(key_ptr,
key_len, value_ptr) -> i32`, rename `storage_propose_write` to `storage_set` (or
document both if both exist) and document `storage_delete`'s real signature, and
remove the packed i64 return description—describe the concrete i32 return
conventions used by `storage_get`, `storage_set`, and `storage_delete` (e.g.,
success/error codes and how value_ptr is used) so the docblock matches the
symbols `storage_get`, `storage_set` (or `storage_propose_write` if present),
and `storage_delete`.

---

Nitpick comments:
In `@crates/wasm-runtime-interface/src/network.rs`:
- Around line 622-671: The handle_http_post function currently uses write_result
with a bounded resp_len and retains an unused _extra parameter, making it
inconsistent with handle_http_get and handle_dns_request which use
write_result_unbounded; change handle_http_post to call
write_result_unbounded(caller, resp_ptr, result) instead of write_result(caller,
resp_ptr, resp_len, result), remove the unused _extra parameter from the
signature, and ensure any callers or exports are updated to match the unbounded
response pattern to keep GET/POST/DNS behavior consistent.
- Around line 890-905: Extract the duplicate read_memory and get_memory
implementations into a single shared module (e.g., create
crates/wasm-runtime-interface/src/memory.rs or add them to runtime.rs), make
them appropriately visible (pub(crate) or pub as needed), and remove the copies
from network.rs and storage.rs; then update both network.rs and storage.rs to
import and call the shared get_memory and read_memory functions instead of their
local versions, and run cargo build/tests to ensure no visibility or import
errors remain.

Comment on lines +975 to +999
fn write_bytes_unbounded(caller: &mut Caller<RuntimeState>, resp_ptr: i32, bytes: &[u8]) -> i32 {
if resp_ptr < 0 {
return -1;
}
if bytes.len() > i32::MAX as usize {
return -1;
}

let memory = match get_memory(caller) {
Some(memory) => memory,
None => return -1,
};

let ptr = resp_ptr as usize;
let end = match ptr.checked_add(bytes.len()) {
Some(end) => end,
None => return -1,
};
let data = memory.data_mut(caller);
if end > data.len() {
return -(bytes.len() as i32);
}
data[ptr..end].copy_from_slice(bytes);
bytes.len() as i32
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

write_bytes_unbounded can silently corrupt guest memory.

This function writes a response of arbitrary length to a guest-supplied pointer without any buffer-size check from the guest side. The only guard (Line 994) catches writes past the total WASM linear memory, but if the response is larger than the buffer the guest actually allocated at resp_ptr, adjacent guest data (heap, stack, globals) will be silently overwritten.

Contrast with the bounded write_bytes (Line 940) which uses the guest-supplied resp_len to return a negative "need this much space" indicator before writing. The unbounded variant skips this safeguard.

Consider one of:

  1. A two-phase protocol: first return the required size, then let the guest call back with a sufficiently sized buffer.
  2. Having the guest provide an allocation function the host can call (e.g., storage_alloc, which is already defined as a constant).
  3. At minimum, documenting the contract that the guest must pre-allocate a buffer large enough for the maximum possible response.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/wasm-runtime-interface/src/network.rs` around lines 975 - 999, The
write_bytes_unbounded function can overwrite adjacent guest memory because it
never verifies the actual guest buffer size; update write_bytes_unbounded to
perform the same safety check as write_bytes by either (a) require and accept a
guest-supplied resp_len and return -(needed_len) when the provided buffer is too
small, (b) call the guest allocator (storage_alloc) to obtain a buffer large
enough before writing, or (c) explicitly document and enforce a precondition
that the guest must allocate sufficient space—implement one of these approaches
in the write_bytes_unbounded implementation (referencing write_bytes and
storage_alloc to mirror the two-phase/allocator pattern) so the host never
blindly writes past the guest buffer.

Comment on lines +454 to +499
fn handle_storage_get(
caller: &mut Caller<RuntimeState>,
key_ptr: i32,
key_len: i32,
value_ptr: i32,
) -> i32 {
let key = match read_memory(caller, key_ptr, key_len) {
Ok(bytes) => bytes,
Err(err) => {
warn!(error = %err, "storage_get: memory read failed");
return StorageHostStatus::InternalError.to_i32();
}
};

let storage_state = match caller.data().storage_state.as_ref() {
Some(state) => state,
None => {
warn!("storage_get: storage state not initialized");
return StorageHostStatus::InternalError.to_i32();
}
};

if let Err(err) = storage_state.config.validate_key(&key) {
let status: StorageHostStatus = err.into();
return status.to_i32();
}

let value = storage_state.data.get(&key).cloned();

let storage_state = caller.data_mut().storage_state.as_mut().unwrap();
storage_state.operations_count = storage_state.operations_count.saturating_add(1);

match value {
Some(val) => {
if let Err(err) = write_to_memory(caller, value_ptr, &val) {
warn!(error = %err, "storage_get: memory write failed");
return StorageHostStatus::InternalError.to_i32();
}
let len = val.len();
let storage_state = caller.data_mut().storage_state.as_mut().unwrap();
storage_state.bytes_read = storage_state.bytes_read.saturating_add(len as u64);
len as i32
}
None => StorageHostStatus::NotFound.to_i32(),
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Return-value collision: NotFound (1) is indistinguishable from a successful 1-byte read.

handle_storage_get returns val.len() as i32 on success (Line 495) and StorageHostStatus::NotFound.to_i32() (= 1) when the key isn't found (Line 497). A guest reading a key whose stored value is exactly 1 byte will receive 1 — the same return code as "not found."

The guest has no way to distinguish these two cases from the return value alone.

Options to fix:

  • Reserve positive return values exclusively for byte lengths (0 = empty value, which is allowed by validate_value) and use a distinct negative code for NotFound.
  • Return the length via a separate out-pointer and use the return value only for status codes.
Possible fix: change NotFound to a negative status code

If NotFound were assigned a negative value (like the other error statuses), e.g., -9, there would be no ambiguity:

 pub enum StorageHostStatus {
     Success = 0,
-    NotFound = 1,
+    NotFound = -9,
     KeyTooLarge = -1,

Then positive return values always mean "bytes written" and negative values always mean "error/status." You'd also need to update from_i32 accordingly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/wasm-runtime-interface/src/storage.rs` around lines 454 - 499, The
return-value collides because handle_storage_get returns val.len() as i32 for
success but StorageHostStatus::NotFound.to_i32() == 1, making a 1-byte value
indistinguishable from NotFound; change the status encoding so error/status
codes are negative (e.g., make StorageHostStatus::NotFound a negative value) and
update the StorageHostStatus::to_i32/from_i32 mappings accordingly so positive
returns from handle_storage_get always mean bytes written and negative returns
indicate error/status; update all callsites that interpret StorageHostStatus
integers to use the new negative status convention (references: function
handle_storage_get, enum StorageHostStatus and its to_i32/from_i32 conversions).

Comment on lines +501 to +549
fn handle_storage_set(
caller: &mut Caller<RuntimeState>,
key_ptr: i32,
key_len: i32,
value_ptr: i32,
value_len: i32,
) -> i32 {
let key = match read_memory(caller, key_ptr, key_len) {
Ok(bytes) => bytes,
Err(err) => {
warn!(error = %err, "storage_set: key memory read failed");
return StorageHostStatus::InternalError.to_i32();
}
};

let value = match read_memory(caller, value_ptr, value_len) {
Ok(bytes) => bytes,
Err(err) => {
warn!(error = %err, "storage_set: value memory read failed");
return StorageHostStatus::InternalError.to_i32();
}
};

let storage_state = match caller.data().storage_state.as_ref() {
Some(state) => state,
None => {
warn!("storage_set: storage state not initialized");
return StorageHostStatus::InternalError.to_i32();
}
};

if let Err(err) = storage_state.config.validate_key(&key) {
let status: StorageHostStatus = err.into();
return status.to_i32();
}

if let Err(err) = storage_state.config.validate_value(&value) {
let status: StorageHostStatus = err.into();
return status.to_i32();
}

let val_len = value.len() as u64;
let storage_state = caller.data_mut().storage_state.as_mut().unwrap();
storage_state.data.insert(key, value);
storage_state.bytes_written = storage_state.bytes_written.saturating_add(val_len);
storage_state.operations_count = storage_state.operations_count.saturating_add(1);

StorageHostStatus::Success.to_i32()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

handle_storage_set does not enforce max_keys_per_challenge or max_total_storage.

StorageHostConfig defines max_keys_per_challenge (default: 10,000) and max_total_storage (default: 100 MB), but handle_storage_set never checks these limits before inserting into the data map. A guest module could exhaust host memory by inserting unbounded keys/data within the configured per-value limit.

Proposed enforcement sketch
     if let Err(err) = storage_state.config.validate_value(&value) {
         let status: StorageHostStatus = err.into();
         return status.to_i32();
     }
 
+    // Enforce per-challenge key limit
+    if !storage_state.data.contains_key(&key)
+        && storage_state.data.len() >= storage_state.config.max_keys_per_challenge
+    {
+        return StorageHostStatus::QuotaExceeded.to_i32();
+    }
+
+    // Enforce total storage limit (approximate)
+    let current_total: usize = storage_state.data.iter().map(|(k, v)| k.len() + v.len()).sum();
+    let new_total = current_total + key.len() + value.len();
+    if new_total > storage_state.config.max_total_storage {
+        return StorageHostStatus::QuotaExceeded.to_i32();
+    }
+
     let val_len = value.len() as u64;
     let storage_state = caller.data_mut().storage_state.as_mut().unwrap();

Note: The total-storage check above is O(n) per write. For better performance, track cumulative size in StorageHostState and update it on insert/remove.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn handle_storage_set(
caller: &mut Caller<RuntimeState>,
key_ptr: i32,
key_len: i32,
value_ptr: i32,
value_len: i32,
) -> i32 {
let key = match read_memory(caller, key_ptr, key_len) {
Ok(bytes) => bytes,
Err(err) => {
warn!(error = %err, "storage_set: key memory read failed");
return StorageHostStatus::InternalError.to_i32();
}
};
let value = match read_memory(caller, value_ptr, value_len) {
Ok(bytes) => bytes,
Err(err) => {
warn!(error = %err, "storage_set: value memory read failed");
return StorageHostStatus::InternalError.to_i32();
}
};
let storage_state = match caller.data().storage_state.as_ref() {
Some(state) => state,
None => {
warn!("storage_set: storage state not initialized");
return StorageHostStatus::InternalError.to_i32();
}
};
if let Err(err) = storage_state.config.validate_key(&key) {
let status: StorageHostStatus = err.into();
return status.to_i32();
}
if let Err(err) = storage_state.config.validate_value(&value) {
let status: StorageHostStatus = err.into();
return status.to_i32();
}
let val_len = value.len() as u64;
let storage_state = caller.data_mut().storage_state.as_mut().unwrap();
storage_state.data.insert(key, value);
storage_state.bytes_written = storage_state.bytes_written.saturating_add(val_len);
storage_state.operations_count = storage_state.operations_count.saturating_add(1);
StorageHostStatus::Success.to_i32()
}
fn handle_storage_set(
caller: &mut Caller<RuntimeState>,
key_ptr: i32,
key_len: i32,
value_ptr: i32,
value_len: i32,
) -> i32 {
let key = match read_memory(caller, key_ptr, key_len) {
Ok(bytes) => bytes,
Err(err) => {
warn!(error = %err, "storage_set: key memory read failed");
return StorageHostStatus::InternalError.to_i32();
}
};
let value = match read_memory(caller, value_ptr, value_len) {
Ok(bytes) => bytes,
Err(err) => {
warn!(error = %err, "storage_set: value memory read failed");
return StorageHostStatus::InternalError.to_i32();
}
};
let storage_state = match caller.data().storage_state.as_ref() {
Some(state) => state,
None => {
warn!("storage_set: storage state not initialized");
return StorageHostStatus::InternalError.to_i32();
}
};
if let Err(err) = storage_state.config.validate_key(&key) {
let status: StorageHostStatus = err.into();
return status.to_i32();
}
if let Err(err) = storage_state.config.validate_value(&value) {
let status: StorageHostStatus = err.into();
return status.to_i32();
}
// Enforce per-challenge key limit
if !storage_state.data.contains_key(&key)
&& storage_state.data.len() >= storage_state.config.max_keys_per_challenge
{
return StorageHostStatus::QuotaExceeded.to_i32();
}
// Enforce total storage limit (approximate)
let current_total: usize = storage_state.data.iter().map(|(k, v)| k.len() + v.len()).sum();
let new_total = current_total + key.len() + value.len();
if new_total > storage_state.config.max_total_storage {
return StorageHostStatus::QuotaExceeded.to_i32();
}
let val_len = value.len() as u64;
let storage_state = caller.data_mut().storage_state.as_mut().unwrap();
storage_state.data.insert(key, value);
storage_state.bytes_written = storage_state.bytes_written.saturating_add(val_len);
storage_state.operations_count = storage_state.operations_count.saturating_add(1);
StorageHostStatus::Success.to_i32()
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/wasm-runtime-interface/src/storage.rs` around lines 501 - 549,
handle_storage_set currently inserts without enforcing StorageHostConfig limits;
before inserting, check storage_state.config.max_keys_per_challenge against
current storage_state.data.len() (or add/update a keys_count) and check
storage_state.bytes_written (or a new cumulative total_storage_bytes) +
value.len() <= storage_state.config.max_total_storage; if either limit would be
exceeded return the appropriate StorageHostStatus (e.g., QuotaExceeded or a
mapped error) instead of inserting; also update the cumulative
total_storage_bytes (or bytes_written) on successful insert/remove so future
checks are O(1) rather than O(n).

@echobt echobt closed this Feb 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant