Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions lib/wasix/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ ciborium.workspace = true
strum.workspace = true
dirs.workspace = true
version-compare.workspace = true
async-trait.workspace = true

[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
wasm-bindgen-test.workspace = true
Expand Down
175 changes: 175 additions & 0 deletions lib/wasix/tests/wasm_tests/mock_net.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
//! A mock networking backend used by the `writev_partial_send_error` test.
//!
//! It hands out a TCP socket whose first `try_send` succeeds in full and whose

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Well, this only covers one particular scenario - it's not any generic mocking harness. How much do we benefit from the specific scenario?

//! subsequent `try_send` calls fail with `ConnectionReset`. That deterministically
//! drives fd_write's per-iovec loop down the "a later send errors after an earlier
//! iovec already succeeded" branch, which cannot be triggered reliably over real
//! host sockets (it would depend on an asynchronous RST landing between two
//! back-to-back sends of a single writev - see issue #6785).

use std::mem::MaybeUninit;
use std::net::{Shutdown, SocketAddr};
use std::task::{Context, Poll};
use std::time::Duration;

use wasmer_wasix::virtual_net::{
InterestHandler, NetworkError, Result as NetResult, SocketStatus, VirtualConnectedSocket,
VirtualIoSource, VirtualNetworking, VirtualSocket, VirtualTcpSocket,
};

/// A connected TCP socket whose first `try_send` succeeds and whose following
/// `try_send` calls return `ConnectionReset`.
#[derive(Debug)]
struct FailAfterFirstSendSocket {
local: SocketAddr,
peer: SocketAddr,
sends: usize,
}

impl FailAfterFirstSendSocket {
fn new(local: SocketAddr, peer: SocketAddr) -> Self {
Self {
local,
peer,
sends: 0,
}
}
}

impl VirtualIoSource for FailAfterFirstSendSocket {
fn remove_handler(&mut self) {}

fn poll_read_ready(&mut self, _cx: &mut Context<'_>) -> Poll<NetResult<usize>> {
Poll::Ready(Ok(0))
}

fn poll_write_ready(&mut self, _cx: &mut Context<'_>) -> Poll<NetResult<usize>> {
// Report writable so a blocking connect() completes immediately.
Poll::Ready(Ok(8192))
}
}

impl VirtualSocket for FailAfterFirstSendSocket {
fn set_ttl(&mut self, _ttl: u32) -> NetResult<()> {
Ok(())
}

fn ttl(&self) -> NetResult<u32> {
Ok(64)
}

fn addr_local(&self) -> NetResult<SocketAddr> {
Ok(self.local)
}

fn status(&self) -> NetResult<SocketStatus> {
Ok(SocketStatus::Opened)
}

fn set_handler(&mut self, _handler: Box<dyn InterestHandler + Send + Sync>) -> NetResult<()> {
Ok(())
}
}

impl VirtualConnectedSocket for FailAfterFirstSendSocket {
fn set_linger(&mut self, _linger: Option<Duration>) -> NetResult<()> {
Ok(())
}

fn linger(&self) -> NetResult<Option<Duration>> {
Ok(None)
}

fn try_send(&mut self, data: &[u8]) -> NetResult<usize> {
self.sends += 1;
if self.sends == 1 {
// First iovec is accepted in full.
Ok(data.len())
} else {
// Any later iovec's send fails, exercising the partial-return branch.
Err(NetworkError::ConnectionReset)
}
}

fn try_flush(&mut self) -> NetResult<()> {
Ok(())
}

fn close(&mut self) -> NetResult<()> {
Ok(())
}

fn try_recv(&mut self, _buf: &mut [MaybeUninit<u8>], _peek: bool) -> NetResult<usize> {
Err(NetworkError::WouldBlock)
}
}

impl VirtualTcpSocket for FailAfterFirstSendSocket {
fn set_recv_buf_size(&mut self, _size: usize) -> NetResult<()> {
Ok(())
}

fn recv_buf_size(&self) -> NetResult<usize> {
Ok(0)
}

fn set_send_buf_size(&mut self, _size: usize) -> NetResult<()> {
Ok(())
}

fn send_buf_size(&self) -> NetResult<usize> {
Ok(0)
}

fn set_nodelay(&mut self, _nodelay: bool) -> NetResult<()> {
Ok(())
}

fn nodelay(&self) -> NetResult<bool> {
Ok(false)
}

fn set_keepalive(&mut self, _keepalive: bool) -> NetResult<()> {
Ok(())
}

fn keepalive(&self) -> NetResult<bool> {
Ok(false)
}

fn set_dontroute(&mut self, _dontroute: bool) -> NetResult<()> {
Ok(())
}

fn dontroute(&self) -> NetResult<bool> {
Ok(false)
}

fn addr_peer(&self) -> NetResult<SocketAddr> {
Ok(self.peer)
}

fn shutdown(&mut self, _how: Shutdown) -> NetResult<()> {
Ok(())
}

fn is_closed(&self) -> bool {
false
}
}

/// Networking backend that hands out [`FailAfterFirstSendSocket`]s on connect.
/// Every other operation is left at the `VirtualNetworking` default (unsupported).
#[derive(Debug, Default)]
pub struct FailAfterFirstSendNetworking;

#[async_trait::async_trait]
impl VirtualNetworking for FailAfterFirstSendNetworking {
async fn connect_tcp(
&self,
addr: SocketAddr,
peer: SocketAddr,
) -> NetResult<Box<dyn VirtualTcpSocket + Sync>> {
Ok(Box::new(FailAfterFirstSendSocket::new(addr, peer)))
}
}
65 changes: 65 additions & 0 deletions lib/wasix/tests/wasm_tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ use wasmer_wasix::virtual_fs::{
};

mod error;
mod mock_net;
mod runner;

const TESTED_LIBC_VERSIONS: &[Option<&str>] = &[None, Some("v2026-05-12.1")];
Expand Down Expand Up @@ -1247,6 +1248,19 @@ fn collect_tests(tests: &mut Vec<Trial>) -> Result<()> {
}
}));

tests.push(libtest_mimic::Trial::test(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Don't like this (apparently second one) special-case. If have have a special need from the harness, we should introduce a new directive and annotate the corresponding expectations in the test-case.

"wasm/writev_partial_send_error",
{
let tests_dir = tests_dir.clone();
let tests_build_root = tests_build_root.clone();
move || {
run_writev_partial_send_error(&tests_dir, &tests_build_root)
.map(|_| ())
.map_err(|e| libtest_mimic::Failed::from(format!("{e:?}")))
}
},
));

for entry in WalkDir::new(&tests_dir)
.into_iter()
.filter_map(Result::ok)
Expand Down Expand Up @@ -1415,3 +1429,54 @@ fn run_dynamic_runtime_hook_smoke(

Ok(libtest_mimic::Completion::Completed)
}

/// Drives the stream writev partial-success path where a *later* per-iovec
/// send() errors after an earlier iovec was fully sent. This cannot be
/// triggered deterministically over real host sockets (it would race an
/// asynchronous RST between two back-to-back sends of a single writev - see
/// issue #6785), so it runs against a mock networking backend whose TCP socket
/// succeeds on the first send and returns ECONNRESET afterwards. fd_write must
/// return the bytes already transferred (the first iovec length) rather than
/// failing the whole syscall.
fn run_writev_partial_send_error(
tests_dir: &Path,
tests_build_root: &Path,
) -> Result<libtest_mimic::Completion> {
if cfg!(target_os = "windows") {
return Ok(libtest_mimic::Completion::ignored_with(
"WASIXCC toolchain does not cover Windows yet",
));
}

let source_dir = tests_dir.join("socket/writev-partial-send-error");
let config = Config::new(
PrimarySource::CSourceFile("main.c".to_owned()),
source_dir,
tests_build_root.to_path_buf(),
"writev_partial_send_error".to_owned(),
);
let wasm = run_build_script(&config)?;
let run_dir = config.build_path();

let result = runner::run_wasm_with_runner_and_runtime_config(
&wasm,
&run_dir,
config.engine,
config.program_name.as_deref(),
false,
|_| Ok(()),
|runtime| {
runtime.set_networking_implementation(mock_net::FailAfterFirstSendNetworking);
Ok(())
},
)?;

ensure!(
result.exit_code == 0,
"writev partial send error exited with {}\n{}",
result.exit_code,
runner::format_captured_output(&result),
);

Ok(libtest_mimic::Completion::Completed)
}
Loading
Loading