Skip to content

fix(s3): hash upload bodies on the host, so large uploads no longer run out of fuel - #237

Open
ndreno wants to merge 1 commit into
mainfrom
fix/s3-body-hash-on-host
Open

ndreno wants to merge 1 commit into
mainfrom
fix/s3-body-hash-on-host

Conversation

@ndreno

@ndreno ndreno commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Fixes #236.

Cause

SigV4 signs the payload hash. The s3 dispatcher computed it in WASM (software SHA-256, about 120 fuel per byte), while a call's fuel budget (100M) does not grow with the body, which travels outside the metered JSON. Uploads over about 750 KB ran out of fuel; outside dev mode the 500 left no trace.

Change

  • host_sha256(data_ptr, data_len, out_ptr) -> i32, behind a new hash capability, hashes a range of plugin memory natively. Both ranges are checked before anything is written; a negative or out-of-bounds range returns -1 and the plugin keeps running; input and digest may overlap. It refreshes the call's deadline afterwards, as the other blocking host calls do, since hashing a large input takes wall-clock time the plugin did not spend running.
  • SDK: hash::sha256 / hash::sha256_hex. In-process outside WASM, so plugin unit tests see the same digests.
  • s3: hashes the payload through the SDK and declares hash. The canonical request stays small and is still hashed by barbacane-sigv4.
  • Logging: every request failing inside the gateway (dev_error_response, 17 call sites) is logged at error with its cause, which previously reached only a dev-mode caller. A dispatch failure names its plugin.
  • Trap causes: out of fuel says so and states the budget, a missed deadline is WasmError::Timeout (defined, previously never produced), and other traps keep wasmtime's full cause chain instead of only the "error while executing at wasm backtrace" header.
  • Docs: capability tables (CLAUDE.md, architecture), a Hashing section in the plugin guide, changelog.

Compatibility: this s3.wasm imports host_sha256, so it needs a gateway of this version or later. An older s3.wasm keeps working on the new gateway.

Tests

Host function, pure (barbacane-wasm/src/hash.rs, 13): FIPS 180-2 vectors, a million as, every length from 0 to 257 bytes (across three 64-byte padding boundaries) against ring as an independent oracle, 20 MiB against ring, a range mid-memory with the bytes around it untouched, empty input at the start, middle and end, digest overwriting its input, digest ending exactly at the end of memory; refused: negative pointer, length or output, input one past the end, digest one byte short, memory smaller than a digest, overflowing ends. Every refused case checks memory is unchanged.

Through the linker (barbacane-wasm/tests/host_sha256.rs, 10, WAT modules): digest of a data segment, empty range, the whole memory to its last byte, out of bounds / digest not fitting / each negative argument returning -1 without a trap. Budgets: 12 MiB hashed with a 100k-fuel budget, paired with a control that touches each byte in WASM under the same budget and runs out; a large input under a 30 ms time budget, with the input sized at run time so that hashing it takes at least three budgets on the machine running the test, paired with a control that overruns it and must report Timeout.

Capability (6): host_sha256 refused with no capability, log or verify_signature; accepted with hash; hash grants nothing else; plugin.toml accepts hash.

SDK (3) and s3 plugin (4 new, 28 total): payload hash equals the body's at 1, 55, 56, 63, 64, 65 bytes, 1 KiB, 1 MiB and 8 MiB; no body and an empty body sign the empty hash; a known vector; the signature changes when the last byte of a 1 MiB body changes.

End to end (barbacane-test/tests/s3_dispatch.rs, 5): the gateway with the real s3.wasm in front of a wiremock S3 that accepts a PUT only if x-amz-content-sha256 is the SHA-256 of the bytes it received and Authorization equals the SigV4 signature recomputed with barbacane-sigv4. Uploads of 1 B, 64 KB, 700 KB, 800 KB, 1 MiB, 4 MiB, 20 MiB and 24 MiB arrive byte-for-byte and correctly signed; an empty upload is signed over the empty hash; a body exactly at --max-body-size is stored; one byte over is a 413 and never reaches S3; a control shows the mock refuses a mismatched hash.

Logging (barbacane-test/tests/plugin_failures.rs, 1): a dispatcher trapping on unreachable gives 500, and the log line names plugin 'trapper' dispatch failed and the trap.

Mutation-checked

Mutation Result
s3 plugin hashing in WASM again end-to-end fails at exactly 819,200 bytes: plugin 's3' dispatch failed: WASM execution trapped: out of fuel: the call used all 100000000 instructions of its budget
no deadline refresh in host_sha256 2 fail
timeout reported as a generic trap 1 fails
no log line in dev_error_response 1 fails

The first deadline test used a 1 ms budget and flaked under the full workspace run; it now sizes its input to the machine, and passed in isolation and in two full workspace runs. During development, one run of s3_dispatch failed right after swapping s3.wasm builds, with the name not captured; it did not recur in 11 later runs, three of them as three concurrent copies of the suite (15 gateways).

Checks

cargo fmt, both CI clippy steps, cargo test --workspace --exclude barbacane-test (18 suites), the s3 plugin's fmt/clippy/tests, SDK cargo check --target wasm32-unknown-unknown, and the s3_dispatch, plugin_failures, plugins, compilation, proxy, validation integration suites with --test-threads=2.

…un out of fuel

Fixes #236. The s3 dispatcher signs each request with SigV4, which
includes the SHA-256 of the body. It hashed the body in WASM, at about
120 fuel per byte, while a call's fuel budget (100M) does not grow with
the body: the body travels outside the metered JSON. Uploads over about
750 KB ran out of fuel and failed with 500.

- host_sha256(data_ptr, data_len, out_ptr) -> i32, behind a new `hash`
  capability, hashes a range of plugin memory natively and refreshes the
  call's deadline, as the other blocking host calls do. Ranges are
  checked before anything is written; -1 for a negative or out-of-bounds
  range, and the plugin keeps running.
- The SDK wraps it as hash::sha256 / hash::sha256_hex, hashing in-process
  outside WASM so plugin unit tests see the same digests.
- The s3 plugin hashes the payload through it and declares `hash`.

Also, as the issue noted, a failure was invisible outside dev mode:
- Every request that fails inside the gateway is logged at error with its
  cause; a dispatch failure names its plugin.
- A trap reports its cause: out of fuel says so and states the budget, a
  missed deadline is a Timeout, other traps keep wasmtime's full chain
  instead of only the backtrace header.

Plugin lockfiles pick up the SDK's sha2 dependency; they also recorded
the SDK at 0.7.0.
@coderabbitai

coderabbitai Bot commented Sep 25, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (1)
  • deep-review

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository: barbacane-dev/barbacane/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 5da840dd-759c-4450-8f85-be24c47d2991

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

This branch has not been deployed

No deployments
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.

s3 dispatcher: uploads over ~750 KB fail with 500, fuel runs out hashing the body for SigV4

1 participant