Skip to content

harden decoders - #1515

Merged
rvolosatovs merged 24 commits into
mainfrom
fix/decoder
Oct 7, 2026
Merged

rvolosatovs merged 24 commits into
mainfrom
fix/decoder

Conversation

@rvolosatovs

Copy link
Copy Markdown
Member

No description provided.

`read_value` accepted any non-zero byte as `option::some` and silently
treated any byte other than 0 as `result::err`. Use the same
`read_option_status` and `read_result_status` helpers as the `wasmtime`
codec so that malformed status bytes fail with `InvalidData`, and read
at least one byte for `flags` like the `wasmtime` codec does.

Assisted-by: claude:claude-fable-5-1
Comment thread crates/wave/src/core/encode.rs Outdated
Comment thread crates/wasmtime/src/codec.rs Outdated
Look up variant and enum discriminants and flag bits with the same
helpers and width-specific branches as the `wasmtime` codec, and encode
list, record and tuple elements by reference rather than cloning them
first.

Encoding a `result` or `variant` payload whose presence does not match
the type now fails like the `wasmtime` codec does, instead of silently
producing undecodable output.

Drop `with_type`, which was `WaveEncoder::new` under another name, skip
the `dst` buffer in the trace span and make the doc examples run.

Assisted-by: claude:claude-fable-5-1
The `wasmtime` and `wave` `read_value` implementations and the
transport `ListDecoder` all reserved capacity for the list length read
off the wire before reading any element, so a few bytes claiming
`u32::MAX` elements forced a multi-gigabyte allocation. Let the vectors
grow as elements are actually decoded instead.

Assisted-by: claude:claude-fable-5-1
Assisted-by: claude:claude-fable-5-1
`ingress` reserved capacity for the path length and zero-filled a
buffer for the data length read off the wire before reading either, so a
few bytes claiming `u32::MAX` forced a multi-gigabyte allocation. Grow
the path as elements are read and read the data through `take`, failing
with `UnexpectedEof` if the stream ends early.

Assisted-by: claude:claude-opus-5-5
Pulls in the fix avoiding preallocation from untrusted `core:name` and
`core:vec` lengths.

Assisted-by: claude:claude-opus-5-5
Spell out the target type at the conversion site instead of relying on
`try_into()` inference or `as` casts.

Assisted-by: claude:claude-opus-5-5
The remote resource handle arm of `read_value` preallocated the
handle length read off the wire and then read to the end of the
stream, ignoring that length and swallowing any data following the
handle. Read exactly the declared number of bytes instead.

Assisted-by: claude:claude-opus-5-5
The Go runtime list, byte list and frame readers as well as the
generated string, byte list, list, stream chunk and resource handle
readers allocated the length read off the wire up front, so a few
bytes claiming `u32::MAX` elements forced a multi-gigabyte allocation.
Grow the buffers as data is actually read instead. This also fixes
string and handle readers relying on a single `Read` call filling the
buffer.

Assisted-by: claude:claude-opus-5-5
Generated `f32`, `f64` and flags readers used a single `Read` call,
which the `io.Reader` contract allows to return short with a `nil`
error, silently zero-padding the value and desynchronizing the decoder
on the following bytes. Use `io.ReadFull` instead.

Assisted-by: claude:claude-fable-5-1
Path buffers in the frame decoder were preallocated from the
wire-provided path length, bounded only by the configurable maximum
depth. Cap the initial allocation at `MAX_INITIAL_PATH_CAPACITY`, the
same way frame data is capped at `MAX_INITIAL_DATA_CAPACITY`, and
preallocate the ingress path up to the same cap.

Assisted-by: claude:claude-fable-5-1
wasm-tokio 0.7.0 caps decoder preallocation at a const-generic
`MAX_INITIAL_CAPACITY` (1 MiB by default), so list and byte decoders no
longer reserve memory based solely on an untrusted length prefix.

Assisted-by: claude:claude-fable-5-1
@rvolosatovs rvolosatovs changed the title various decoder fixes harden decoders Oct 7, 2026
@rvolosatovs
rvolosatovs marked this pull request as ready for review October 7, 2026 11:13
Reject paths deeper than the decoder's default maximum depth and forward
frame data in chunks of at most `DEFAULT_MAX_INITIAL_CAPACITY` bytes
instead of buffering whole frames of up to 4 GiB.

Also drop the redundant path reservation in the frame decoder and reuse
`wasm_tokio::DEFAULT_MAX_INITIAL_CAPACITY`.

Assisted-by: claude:claude-opus-5-5
Reset `ListDecoder` state when an element fails to decode, matching
`wasm_tokio::CoreVecDecoder`, and preallocate list buffers up to
`DEFAULT_MAX_INITIAL_CAPACITY` bytes rather than not at all.

Assisted-by: claude:claude-opus-5-5
Add `ReadBytes`, which preallocates at most 1 MiB and reads the rest in
chunks via `io.ReadFull`, and `NewSlice`, which returns a non-nil slice
with capped capacity. Use them in the library and generated bindings,
restoring non-nil empty lists and frame paths.

Assisted-by: claude:claude-opus-5-5
Compare untrusted `uint32` lengths before converting to `int`, which
would go negative on 32-bit targets and make `make` panic.

Assisted-by: claude:claude-opus-5-5
Clear buffered elements and deferred handlers when a list element fails
to decode, so a subsequent empty list does not hand out stale deferred
handlers via `take_deferred`.

Assisted-by: claude:claude-opus-5-5
The spec defines no maximum path depth and neither the Go nor JS
implementations enforce one, so cap the path preallocation instead of
rejecting deep paths. Also read data chunks into uninitialized buffers
rather than zero-filling them first.

Assisted-by: claude:claude-opus-5-5
Forward frame payloads to sub-streams chunk by chunk instead of
buffering whole frames, avoiding quadratic copying of large frames
arriving over many transport chunks.

Assisted-by: claude:claude-opus-5-5
`WaveEncoder::with_type` was removed, which is a breaking change.

Assisted-by: claude:claude-opus-5-5
Collapse the Go `ReadBytes`/`NewSlice` tests into a single case that
still crosses the 1 MiB chunk boundary, and merge the two JS framing
tests into one that only passes when frame data is routed before the
frame is complete.

Assisted-by: claude:claude-fable-5-1
Signed-off-by: Roman Volosatovs <rvolosatovs@riseup.net>
@rvolosatovs
rvolosatovs enabled auto-merge October 7, 2026 11:59
@rvolosatovs
rvolosatovs added this pull request to the merge queue Oct 7, 2026
Merged via the queue into main with commit bd1bcf7 Oct 7, 2026
40 checks passed
@rvolosatovs
rvolosatovs deleted the fix/decoder branch October 7, 2026 12:10
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