Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
77a0b9d
fix(wave): reject invalid `option` and `result` status bytes
rvolosatovs Oct 6, 2026
c847414
refactor(wave): simplify `WaveEncoder`
rvolosatovs Oct 6, 2026
7694517
fix: do not preallocate lists from untrusted length
rvolosatovs Oct 6, 2026
aca6cb2
style: construct `BytesMut` via `Default`
rvolosatovs Oct 6, 2026
14b2c3d
fix(transport): do not preallocate frames from untrusted length
rvolosatovs Oct 6, 2026
505d5ed
build(deps): bump `wasm-tokio` to 0.6.2
rvolosatovs Oct 6, 2026
9cbe5da
refactor: convert to `usize` via `usize::try_from`
rvolosatovs Oct 6, 2026
368849d
fix(wasmtime): bound remote resource handle reads by their length
rvolosatovs Oct 6, 2026
ee85304
fix(go): do not preallocate from untrusted length
rvolosatovs Oct 6, 2026
42ca909
fix(js): do not preallocate arrays from untrusted length
rvolosatovs Oct 6, 2026
19e94f0
fix(wit-bindgen-go): read fixed-width values in full
rvolosatovs Oct 7, 2026
39681ef
fix(transport): cap path buffer preallocation
rvolosatovs Oct 7, 2026
423e32f
build: bump wasm-tokio to 0.7.0
rvolosatovs Oct 7, 2026
0e2d9f3
refactor(transport): use saturating subtraction when reserving data
rvolosatovs Oct 7, 2026
0fbb918
fix(transport): bound framed ingress buffering
rvolosatovs Oct 7, 2026
cdec42f
fix: reset list decoder on error and cap list preallocation
rvolosatovs Oct 7, 2026
ce8b5bd
fix(go): cap preallocation instead of growing from empty
rvolosatovs Oct 7, 2026
8ffb776
fix(go): avoid signed overflow of untrusted lengths
rvolosatovs Oct 7, 2026
2979e58
fix(transport): drop partial list state on decode error
rvolosatovs Oct 7, 2026
04804a6
fix(transport): do not limit ingress path depth
rvolosatovs Oct 7, 2026
7b136c8
fix(js): route frame data as it arrives
rvolosatovs Oct 7, 2026
ef6018c
chore(wave): bump version to 0.2.0
rvolosatovs Oct 7, 2026
c7a2a5b
test: minimize untrusted length and framing tests
rvolosatovs Oct 7, 2026
cdd641f
feat(transport): avoid limiting default decoder frame depth
rvolosatovs Oct 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ url = { version = "2" }
uuid = { version = "1", default-features = false }
wasi = { version = "0.14", default-features = false }
wasi-preview1-component-adapter-provider = { version = "49", default-features = false }
wasm-tokio = { version = "0.6.1", default-features = false }
wasm-tokio = { version = "0.7.0", default-features = false }
wasm-wave = { version = "0.258", default-features = false }
wasmparser = { version = "0.261", default-features = false }
wasmtime = { version = "49", default-features = false }
Expand Down
25 changes: 8 additions & 17 deletions crates/transport/src/frame/codec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ use std::sync::Arc;

use bytes::{Bytes, BytesMut};
use tracing::{instrument, trace};
use wasm_tokio::{Leb128DecoderU32, Leb128DecoderU64, Leb128Encoder};
use wasm_tokio::{DEFAULT_MAX_INITIAL_CAPACITY, Leb128DecoderU32, Leb128DecoderU64, Leb128Encoder};

use super::{Frame, FrameRef};
use crate::frame::{Frame, FrameRef, MAX_INITIAL_PATH_CAPACITY};

/// [Frame] decoder
pub struct Decoder {
Expand All @@ -31,7 +31,7 @@ impl Decoder {

impl Default for Decoder {
fn default() -> Self {
Self::new(32, u32::MAX.into())
Self::new(u32::MAX, u32::MAX.into())
}
}

Expand Down Expand Up @@ -59,27 +59,19 @@ impl tokio_util::codec::Decoder for Decoder {
),
));
}
let n = n
.try_into()
let n = usize::try_from(n)
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))?;
self.path_cap = n;
Vec::with_capacity(n)
Vec::with_capacity(n.min(MAX_INITIAL_PATH_CAPACITY))
};
let n = self.path_cap.saturating_sub(src.len());
if n > 0 {
src.reserve(n);
self.path = Some(path);
return Ok(None);
}
while self.path_cap > 0 {
trace!(self.path_cap, "decoding path element");
let Some(i) = Leb128DecoderU32.decode(src)? else {
self.path = Some(path);
return Ok(None);
};
trace!(i, "decoded path element");
let i = i
.try_into()
let i = usize::try_from(i)
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))?;
path.push(i);
self.path_cap -= 1;
Expand All @@ -100,8 +92,7 @@ impl tokio_util::codec::Decoder for Decoder {
),
));
}
let n = n
.try_into()
let n = usize::try_from(n)
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))?;
self.data_len = n;
if n == 0 {
Expand All @@ -113,7 +104,7 @@ impl tokio_util::codec::Decoder for Decoder {
}
let n = self.data_len.saturating_sub(src.len());
if n > 0 {
src.reserve(n);
src.reserve(n.min(DEFAULT_MAX_INITIAL_CAPACITY));
self.path = Some(path);
return Ok(None);
}
Expand Down
86 changes: 70 additions & 16 deletions crates/transport/src/frame/conn/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use core::task::{Context, Poll, ready};

use std::sync::Arc;

use bytes::{Buf as _, BufMut as _, Bytes, BytesMut};
use bytes::{Buf as _, Bytes, BytesMut};
use futures::Sink as _;
use pin_project_lite::pin_project;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt as _};
Expand All @@ -16,7 +16,11 @@ use tokio_util::codec::Encoder;
use tokio_util::io::StreamReader;
use tokio_util::sync::PollSender;
use tracing::{Instrument as _, Span, debug, error, instrument, trace};
use wasm_tokio::{AsyncReadCore as _, AsyncReadLeb128 as _, Leb128Encoder};
use wasm_tokio::{
AsyncReadCore as _, AsyncReadLeb128 as _, DEFAULT_MAX_INITIAL_CAPACITY, Leb128Encoder,
};

use crate::frame::MAX_INITIAL_PATH_CAPACITY;

mod client;
mod server;
Expand Down Expand Up @@ -581,14 +585,13 @@ async fn ingress(
let n = AsyncReadExt::chain([b].as_slice(), &mut rx)
.read_u32_leb128()
.await?;
let n = n
.try_into()
let n = usize::try_from(n)
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))?;
trace!(n, "read path length");
let tx = if n == 0 {
param_tx
} else {
let mut path = Vec::with_capacity(n);
let mut path = Vec::with_capacity(n.min(MAX_INITIAL_PATH_CAPACITY));
for i in 0..n {
trace!(i, "reading path element");
let p = rx.read_u32_leb128().await?;
Expand All @@ -611,18 +614,24 @@ async fn ingress(
};
trace!("reading data length");
let n = rx.read_u32_leb128().await?;
let n = n
.try_into()
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))?;
trace!(n, "read data length");
let mut buf = BytesMut::with_capacity(n);
buf.put_bytes(0, n);
trace!("reading data");
rx.read_exact(&mut buf).await?;
trace!(?buf, "read data");
tx.send(Ok(buf.freeze())).await.map_err(|_| {
std::io::Error::new(std::io::ErrorKind::BrokenPipe, "stream receiver closed")
})?;
let mut data = (&mut rx).take(n.into());
while data.limit() > 0 {
let k = usize::try_from(data.limit())
.unwrap_or(usize::MAX)
.min(DEFAULT_MAX_INITIAL_CAPACITY);
let mut buf = BytesMut::with_capacity(k);
trace!(len = k, "reading data chunk");
while buf.len() < k {
if data.read_buf(&mut buf).await? == 0 {
return Err(std::io::ErrorKind::UnexpectedEof.into());
}
}
trace!(?buf, "read data chunk");
tx.send(Ok(buf.freeze())).await.map_err(|_| {
std::io::Error::new(std::io::ErrorKind::BrokenPipe, "stream receiver closed")
})?;
}
}
}

Expand Down Expand Up @@ -676,3 +685,48 @@ pub trait ConnHandler<Rx, Tx> {
}

impl<Rx, Tx> ConnHandler<Rx, Tx> for () {}

#[cfg(test)]
mod tests {
use super::*;

#[test_log::test(tokio::test)]
async fn ingress_truncated_path() {
let index = std::sync::Mutex::new(IndexTrie::Empty);
let (tx, _rx) = mpsc::channel(1);
let err = ingress([0x02, 0x00].as_slice(), &index, &tx)
.await
.unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::UnexpectedEof);
}

#[test_log::test(tokio::test)]
async fn ingress_chunked_data() {
let index = std::sync::Mutex::new(IndexTrie::Empty);
let (tx, mut rx) = mpsc::channel(4);
let n = DEFAULT_MAX_INITIAL_CAPACITY + 1;
let mut frame = vec![0x00, 0x81, 0x80, 0x40];
frame.resize(frame.len() + n, 0x42);
ingress(frame.as_slice(), &index, &tx).await.unwrap();
drop(tx);
let mut chunks = vec![];
while let Some(chunk) = rx.recv().await {
chunks.push(chunk.unwrap().len());
}
assert_eq!(chunks, [DEFAULT_MAX_INITIAL_CAPACITY, 1]);
}

#[test_log::test(tokio::test)]
async fn ingress_truncated_data() {
let index = std::sync::Mutex::new(IndexTrie::Empty);
let (tx, _rx) = mpsc::channel(1);
let err = ingress(
[0x00, 0xff, 0xff, 0xff, 0xff, 0x0f, 0x42].as_slice(),
&index,
&tx,
)
.await
.unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::UnexpectedEof);
}
}
2 changes: 2 additions & 0 deletions crates/transport/src/frame/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ pub use oneshot::*;
/// Framing protocol version
pub const PROTOCOL: u8 = 0;

const MAX_INITIAL_PATH_CAPACITY: usize = 32;

/// Owned wRPC frame
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Frame {
Expand Down
74 changes: 56 additions & 18 deletions crates/transport/src/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ use wasm_tokio::cm::{
};
use wasm_tokio::{
CoreNameDecoder, CoreNameEncoder, CoreVecDecoder, CoreVecDecoderBytes, CoreVecEncoderBytes,
Leb128DecoderI8, Leb128DecoderI16, Leb128DecoderI32, Leb128DecoderI64, Leb128DecoderI128,
Leb128DecoderU8, Leb128DecoderU16, Leb128DecoderU32, Leb128DecoderU64, Leb128DecoderU128,
Leb128Encoder, Utf8Codec,
DEFAULT_MAX_INITIAL_CAPACITY, Leb128DecoderI8, Leb128DecoderI16, Leb128DecoderI32,
Leb128DecoderI64, Leb128DecoderI128, Leb128DecoderU8, Leb128DecoderU16, Leb128DecoderU32,
Leb128DecoderU64, Leb128DecoderU128, Leb128Encoder, Utf8Codec,
};

use crate::BufferedIncoming;
Expand Down Expand Up @@ -801,16 +801,27 @@ where
if len == 0 {
return Ok(Some(Vec::default()));
}
let len = len
.try_into()
let len = usize::try_from(len)
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))?;
self.ret = Vec::with_capacity(len);
self.deferred = Vec::with_capacity(len);
self.ret = Vec::with_capacity(
len.min(DEFAULT_MAX_INITIAL_CAPACITY / mem::size_of::<T::Item>().max(1)),
);
self.deferred = Vec::with_capacity(len.min(
DEFAULT_MAX_INITIAL_CAPACITY
/ mem::size_of::<Option<DeferredFn<BufferedIncoming>>>(),
));
self.cap = len;
}
while self.cap > 0 {
let Some(v) = self.dec.decode(src)? else {
return Ok(None);
let v = match self.dec.decode(src) {
Ok(Some(v)) => v,
Ok(None) => return Ok(None),
Err(err) => {
self.cap = 0;
self.ret.clear();
self.deferred.clear();
return Err(err);
}
};
self.ret.push(v);
self.deferred.push(self.dec.take_deferred());
Expand Down Expand Up @@ -1804,8 +1815,7 @@ where
for (i, item) in zip(i.., chunk) {
enc.encode(item, &mut buf)?;
if let Some(f) = enc.take_deferred() {
let i = i
.try_into()
let i = usize::try_from(i)
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))?;
let w = w.index(&[i]).map_err(std::io::Error::other)?;
trace!("spawning transmit task");
Expand Down Expand Up @@ -2265,9 +2275,37 @@ mod tests {

use super::*;

#[test_log::test(tokio::test)]
async fn list_decoder_reset_on_error() -> anyhow::Result<()> {
let mut dec = <Vec<Vec<String>> as Decode>::Decoder::default();
let mut buf = BytesMut::from(&[0x02, 0x01, 0x01, b'a', 0x01, 0x01, 0xff][..]);
assert!(tokio_util::codec::Decoder::decode(&mut dec, &mut buf).is_err());
let mut buf = BytesMut::from(&[0x01, 0x01, 0x01, b'b'][..]);
assert_eq!(
tokio_util::codec::Decoder::decode(&mut dec, &mut buf)?,
Some(vec![vec!["b".to_string()]])
);
Ok(())
}

#[test_log::test(tokio::test)]
async fn list_decoder_reset_deferred_on_error() -> anyhow::Result<()> {
let mut dec =
<Vec<Pin<Box<dyn Stream<Item = Bytes> + Send>>> as Decode>::Decoder::default();
let mut buf = BytesMut::from(&[0x02, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff][..]);
assert!(tokio_util::codec::Decoder::decode(&mut dec, &mut buf).is_err());
let mut buf = BytesMut::from(&[0x00][..]);
let vs = tokio_util::codec::Decoder::decode(&mut dec, &mut buf)?;
assert!(vs.is_some_and(|vs| vs.is_empty()));
if let Some(_f) = Deferred::<BufferedIncoming>::take_deferred(&mut dec) {
bail!("no deferred read should have been returned");
}
Ok(())
}

#[test_log::test(tokio::test)]
async fn codec() -> anyhow::Result<()> {
let mut buf = BytesMut::new();
let mut buf = BytesMut::default();
let mut enc = <(u8, u32) as Encode>::Encoder::default();
enc.encode((0x42, 0x42), &mut buf)?;
if let Some(_f) = Deferred::<Outgoing>::take_deferred(&mut enc) {
Expand All @@ -2282,17 +2320,17 @@ mod tests {
let mut enc = <f32 as Encode>::Encoder::default();

// A non-canonical (e.g. signalling) `NaN` is canonicalized on encode.
let mut buf = BytesMut::new();
let mut buf = BytesMut::default();
enc.encode(f32::from_bits(0x7f80_0001), &mut buf).unwrap();
assert_eq!(buf.as_ref(), CANONICAL_NAN_F32.to_le_bytes());

// A negative `NaN` is canonicalized to the (positive) canonical `NaN`.
let mut buf = BytesMut::new();
let mut buf = BytesMut::default();
enc.encode(f32::from_bits(0xffc0_0000), &mut buf).unwrap();
assert_eq!(buf.as_ref(), CANONICAL_NAN_F32.to_le_bytes());

// Non-`NaN` values are encoded unchanged.
let mut buf = BytesMut::new();
let mut buf = BytesMut::default();
enc.encode(1.5_f32, &mut buf).unwrap();
assert_eq!(buf.as_ref(), 1.5_f32.to_bits().to_le_bytes());
}
Expand All @@ -2301,17 +2339,17 @@ mod tests {
fn canonical_nan_f64() {
let mut enc = <f64 as Encode>::Encoder::default();

let mut buf = BytesMut::new();
let mut buf = BytesMut::default();
enc.encode(f64::from_bits(0x7ff0_0000_0000_0001), &mut buf)
.unwrap();
assert_eq!(buf.as_ref(), CANONICAL_NAN_F64.to_le_bytes());

let mut buf = BytesMut::new();
let mut buf = BytesMut::default();
enc.encode(f64::from_bits(0xfff8_0000_0000_0000), &mut buf)
.unwrap();
assert_eq!(buf.as_ref(), CANONICAL_NAN_F64.to_le_bytes());

let mut buf = BytesMut::new();
let mut buf = BytesMut::default();
enc.encode(1.5_f64, &mut buf).unwrap();
assert_eq!(buf.as_ref(), 1.5_f64.to_bits().to_le_bytes());
}
Expand Down
Loading
Loading