Skip to content

WASIX POSIX compatibility tests#6070

Draft
artemyarulin wants to merge 22 commits intomainfrom
tests/wasix-tests
Draft

WASIX POSIX compatibility tests#6070
artemyarulin wants to merge 22 commits intomainfrom
tests/wasix-tests

Conversation

@artemyarulin
Copy link
Copy Markdown
Contributor

@artemyarulin artemyarulin commented Jan 15, 2026

As we've discussed - this PR uses a lot of open source project like LTP (Linux Test Project) as a basis for new WASIX tests. The idea is cover all the syscalls and ensure POSIX compatibility and also achieve great test coverage on WASIX.

This is the draft PR as we've agreed I'll slowly convert all the tests and once all syscalls are covered then I'll open it for the review.

And we are done 🎉 Next step is to split it into much smaller chunks (probably one PR per syscall) and start slowly reviewing that.


Total syscalls: 135
Tested syscalls: 133
Untested syscalls: 2
Coverage: 98.5%
Syscalls tested: 133/135 (98.5%)
Total tests added: 769 (149 Rust + 620 C)
Total lines added: 23,211

  args_get.rs                               12 tests
  args_sizes_get.rs                         13 tests
  clock_res_get.rs                           8 tests
  clock_time_get.rs                         10 tests
  environ_get.rs                            12 tests
  environ_sizes_get.rs                      10 tests
  fd_dup.rs                                 11 tests
  fd_fdstat_get.rs                          22 tests
  fd_prestat_dir_name.rs                    42 tests
  random_get.rs                              9 tests
  call_dynamic.c                             8 test scenarios
  callback_signal.c                          2 test scenarios
  chdir.c                                    8 test scenarios
  clock_time_get.c                           4 test scenarios
  clock_time_set.c                           4 test scenarios
  closure_allocate.c                         3 test scenarios
  closure_free.c                            14 test scenarios
  closure_prepare.c                          2 test scenarios
  context_create.c                           4 test scenarios
  context_destroy.c                          2 test scenarios
  context_switch.c                           6 test scenarios
  dlsym.c                                    5 test scenarios
  epoll_create.c                             3 test scenarios
  epoll_ctl.c                                5 test scenarios
  epoll_wait.c                               6 test scenarios
  fd_advise.c                                6 test scenarios
  fd_allocate.c                              5 test scenarios
  fd_close.c                                 5 test scenarios
  fd_datasync.c                              5 test scenarios
  fd_dup.c                                   2 test scenarios
  fd_dup2.c                                  5 test scenarios
  fd_event.c                                 6 test scenarios
  fd_fdflags_get.c                          24 test scenarios
  fd_fdflags_set.c                           4 test scenarios
  fd_fdstat_get.c                           24 test scenarios
  fd_fdstat_set_flags.c                      2 test scenarios
  fd_fdstat_set_rights.c                     2 test scenarios
  fd_filestat_get.c                          5 test scenarios
  fd_filestat_set_size.c                     7 test scenarios
  fd_filestat_set_times.c                    3 test scenarios
  fd_pipe.c                                  4 test scenarios
  fd_read.c                                 11 test scenarios
  fd_readdir.c                               7 test scenarios
  fd_renumber.c                              4 test scenarios
  fd_seek.c                                  5 test scenarios
  fd_sync.c                                  4 test scenarios
  fd_tell.c                                 13 test scenarios
  fd_write.c                                11 test scenarios
  futex_wait.c                               5 test scenarios
  futex_wake.c                               5 test scenarios
  futex_wake_all.c                           3 test scenarios
  getcwd.c                                   8 test scenarios
  path_create_directory.c                   11 test scenarios
  path_filestat_get.c                        5 test scenarios
  path_filestat_set_times.c                  7 test scenarios
  path_link.c                               15 test scenarios
  path_open.c                               14 test scenarios
  path_open2.c                               7 test scenarios
  path_readlink.c                            4 test scenarios
  path_remove_directory.c                    8 test scenarios
  path_rename.c                             18 test scenarios
  path_symlink.c                             5 test scenarios
  path_unlink_file.c                         5 test scenarios
  poll_oneoff.c                              3 test scenarios
  port_addr_add.c                            3 test scenarios
  port_addr_clear.c                          1 test scenarios
  port_addr_list.c                           4 test scenarios
  port_addr_remove.c                         2 test scenarios
  port_bridge.c                              2 test scenarios
  port_gateway_set.c                         3 test scenarios
  port_mac.c                                 1 test scenarios
  port_route_add.c                           5 test scenarios
  port_route_list.c                          4 test scenarios
  port_route_remove.c                        3 test scenarios
  port_unbridge.c                            1 test scenarios
  proc_id.c                                  8 test scenarios
  proc_join.c                                7 test scenarios
  proc_parent.c                              2 test scenarios
  proc_raise.c                               3 test scenarios
  proc_signal.c                              5 test scenarios
  proc_signals_get.c                         2 test scenarios
  proc_signals_sizes_get.c                   3 test scenarios
  proc_spawn.c                               1 test scenarios
  proc_spawn2.c                              4 test scenarios
  random_get.c                               3 test scenarios
  reflect_signature.c                        8 test scenarios
  resolve.c                                  7 test scenarios
  sched_yield.c                              8 test scenarios
  sock_accept.c                              6 test scenarios
  sock_addr_peer.c                           4 test scenarios
  sock_bind.c                                7 test scenarios
  sock_connect.c                             8 test scenarios
  sock_get_opt_flag.c                        5 test scenarios
  sock_get_opt_size.c                        6 test scenarios
  sock_get_opt_time.c                        5 test scenarios
  sock_join_multicast.c                      8 test scenarios
  sock_leave_multicast_v4.c                  4 test scenarios
  sock_leave_multicast_v6.c                  4 test scenarios
  sock_listen.c                              4 test scenarios
  sock_open.c                               10 test scenarios
  sock_pair.c                                6 test scenarios
  sock_recv.c                                7 test scenarios
  sock_recv_from.c                           8 test scenarios
  sock_send.c                                9 test scenarios
  sock_send_file.c                           4 test scenarios
  sock_send_to.c                             9 test scenarios
  sock_shutdown.c                            7 test scenarios
  sock_status.c                              6 test scenarios
  thread_exit.c                              3 test scenarios
  thread_id.c                                8 test scenarios
  thread_join.c                              4 test scenarios
  thread_parallelism.c                       3 test scenarios
  thread_signal.c                            4 test scenarios
  thread_sleep.c                             3 test scenarios
  thread_spawn.c                             5 test scenarios
  tty_get.c                                  3 test scenarios
  tty_set.c                                  7 test scenarios

@zebreus zebreus self-requested a review January 15, 2026 15:58
@zebreus
Copy link
Copy Markdown
Contributor

zebreus commented Jan 16, 2026

related to #6055

@marxin marxin marked this pull request as ready for review February 5, 2026 08:46
@marxin marxin requested a review from syrusakbary as a code owner February 5, 2026 08:46
Copilot AI review requested due to automatic review settings February 5, 2026 08:46
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds comprehensive POSIX compatibility tests for WASIX syscalls, drawing from established test suites like LTP (Linux Test Project) and stress-ng. The implementation introduces 566 new tests (149 Rust + 417 C) covering 81 out of 135 syscalls to ensure POSIX compatibility and improve test coverage.

Changes:

  • Added extensive C-based test suite for WASIX syscalls covering networking, file operations, process management, and signal handling
  • Fixed multiple syscall implementations to align with POSIX behavior (error codes, validation, memory safety)
  • Enhanced linker and module loading to support RTLD_GLOBAL flag and proper symbol resolution

Reviewed changes

Copilot reviewed 164 out of 182 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
tests/c-wasi-tests/*.c New POSIX compatibility tests for various syscalls
lib/wasix/src/syscalls/wasix/sock_*.rs Fixed socket option handling and validation
lib/wasix/src/syscalls/wasix/proc_*.rs Improved process signal handling and validation
lib/wasix/src/syscalls/wasi/path_*.rs Enhanced path operations with better error handling
lib/wasix/src/syscalls/wasi/fd_*.rs Fixed file descriptor operations and rights checking
lib/wasix/src/state/linker.rs Added RTLD_GLOBAL support and function table deduplication
lib/virtual-fs/src/pipe.rs Implemented proper pipe buffering with PIPE_MAX_BYTES limit
lib/virtual-net/src/host.rs Added IP list and route list implementations

Comment on lines +41 to +49
if !matches!(
opt,
Sockoption::RecvBufSize
| Sockoption::SendBufSize
| Sockoption::Ttl
| Sockoption::MulticastTtlV4
) {
return Ok(Err(Errno::Inval));
}
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The validation logic checks for valid socket options but then converts to WasiSocketOption and matches again in the closure. This creates duplicate validation. Consider consolidating the validation or removing the redundant check.

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +20
if !ctx.data().process.signal_thread(&tid, sig) {
return Ok(Errno::Srch);
}
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The function now returns a boolean to indicate success/failure, but the error handling immediately after could be simplified by having signal_thread return a Result<(), Errno> instead of a boolean.

Copilot uses AI. Check for mistakes.
if sig_u8 > Signal::Sigsys as u8 {
return Ok(Errno::Inval);
}
let sig = Signal::try_from(sig_u8).unwrap_or(Signal::Signone);
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The validation logic converts i32 to u8, checks bounds, then converts to Signal with unwrap_or. Consider using Signal::try_from directly on the bounded sig_u8 and handling the error case explicitly instead of using unwrap_or, as this would make the intent clearer.

Suggested change
let sig = Signal::try_from(sig_u8).unwrap_or(Signal::Signone);
let sig = match Signal::try_from(sig_u8) {
Ok(sig) => sig,
Err(_) => return Ok(Errno::Inval),
};

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +15
fn resolve_symlink_path(
state: &WasiState,
inodes: &WasiInodes,
path: &str,
) -> Result<String, Errno> {
let mut seen: HashSet<PathBuf> = HashSet::new();
let mut current = PathBuf::from(path);

for _ in 0..MAX_SYMLINKS {
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The function resolve_symlink_path lacks documentation explaining its purpose, parameters, and return value. Given its complexity with symlink resolution and loop detection, it should include a docstring describing the MAX_SYMLINKS limit and loop detection mechanism.

Copilot uses AI. Check for mistakes.
Comment on lines +83 to +106

if st_size > old_size {
handle
.seek(std::io::SeekFrom::Start(old_size))
.await
.map_err(map_io_err)?;

let mut remaining = st_size - old_size;
let zeros = [0u8; 8192];
while remaining > 0 {
let chunk = std::cmp::min(remaining, zeros.len() as u64) as usize;
handle
.write_all(&zeros[..chunk])
.await
.map_err(map_io_err)?;
remaining -= chunk as u64;
}

handle
.seek(std::io::SeekFrom::Start(original_offset))
.await
.map_err(map_io_err)?;
}

Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The file extension logic manually writes zeros in 8KB chunks. Consider using set_len or a platform-specific sparse file API to extend the file more efficiently, especially for large files where writing zeros could be slow.

Suggested change
if st_size > old_size {
handle
.seek(std::io::SeekFrom::Start(old_size))
.await
.map_err(map_io_err)?;
let mut remaining = st_size - old_size;
let zeros = [0u8; 8192];
while remaining > 0 {
let chunk = std::cmp::min(remaining, zeros.len() as u64) as usize;
handle
.write_all(&zeros[..chunk])
.await
.map_err(map_io_err)?;
remaining -= chunk as u64;
}
handle
.seek(std::io::SeekFrom::Start(original_offset))
.await
.map_err(map_io_err)?;
}

Copilot uses AI. Check for mistakes.
Comment on lines +148 to +156
if !is_stdio {
let guard = inode.read();
match guard.deref() {
Kind::Dir { .. } | Kind::Root { .. } => return Ok(Err(Errno::Isdir)),
Kind::PipeTx { .. } => return Ok(Err(Errno::Badf)),
Kind::File { .. } => {
if fd_entry.open_flags & crate::fs::Fd::READ == 0 {
return Ok(Err(Errno::Badf));
}
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The rights checking logic is now split between the File case (lines 154-160) and the default case (lines 161-165). Consider extracting this into a helper function to reduce duplication and improve maintainability.

Copilot uses AI. Check for mistakes.
Comment on lines +2580 to +2597
fn find_function_in_table(
&self,
store: &mut impl AsStoreMut,
func: &Function,
) -> Option<u32> {
let table = &self.indirect_function_table;
let target_raw = unsafe { Value::FuncRef(Some(func.clone())).as_raw(store).funcref };
let size = table.size(store);
for index in 0..size {
if let Some(Value::FuncRef(Some(existing))) = table.get(store, index) {
let existing_raw = unsafe { Value::FuncRef(Some(existing)).as_raw(store).funcref };
if existing_raw == target_raw {
return Some(index);
}
}
}
None
}
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The function performs a linear search through the entire function table. For modules with many functions, this could become a bottleneck. Consider maintaining a HashMap of function pointers to indices for O(1) lookups.

Copilot uses AI. Check for mistakes.
Comment on lines +246 to +258
let Some(rx_end) = self.rx_end.upgrade() else {
return Err(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"PipeRx is closed",
));
};
{
let rx = rx_end.lock().unwrap();
if rx.pending_bytes.saturating_add(buf.len()) > PIPE_MAX_BYTES {
return Err(std::io::ErrorKind::WouldBlock.into());
}
}
self.send_bytes(buf)
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

There's a race condition between checking pending_bytes (line 254) and calling send_bytes (line 258). Another thread could write to the pipe between these operations, causing the buffer to exceed PIPE_MAX_BYTES. The lock should be held across both operations.

Suggested change
let Some(rx_end) = self.rx_end.upgrade() else {
return Err(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"PipeRx is closed",
));
};
{
let rx = rx_end.lock().unwrap();
if rx.pending_bytes.saturating_add(buf.len()) > PIPE_MAX_BYTES {
return Err(std::io::ErrorKind::WouldBlock.into());
}
}
self.send_bytes(buf)
let Some(ref tx) = self.tx else {
return Err(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"PipeTx is closed",
));
};
let Some(rx_end) = self.rx_end.upgrade() else {
return Err(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"PipeRx is closed",
));
};
{
let mut rx = rx_end.lock().unwrap();
let new_pending = rx.pending_bytes.saturating_add(buf.len());
if new_pending > PIPE_MAX_BYTES {
return Err(std::io::ErrorKind::WouldBlock.into());
}
rx.pending_bytes = new_pending;
}
if let Err(_) = tx.send(buf.to_vec()) {
if let Some(rx_end) = self.rx_end.upgrade() {
let mut rx = rx_end.lock().unwrap();
rx.pending_bytes = rx.pending_bytes.saturating_sub(buf.len());
}
return Err(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"PipeTx is closed",
));
}
self.mark_other_end_readable();
Ok(buf.len())

Copilot uses AI. Check for mistakes.
…d_advice, fd_close, proc_parent, proc_signals_sizes_get, thread_parallelism
…e, fd_allocate, fd_fdstat_set_rights, fd_filestat_set_size, fd_filestat_set_times, fd_seek, path_create_directory, path_filestat_get, path_filestat_set_times, path_open2, path_readlink, path_remove_directory, path_symlink, path_unlink_file
…2, proc_signals_get, sock_get_opt, sock_listen, sock_set_opt, sock_shutdown, sock_stats, thread_sleep
…imes, path_link, proc_raise, port_addr_clear, port_add_remove, port_route_clear, sock_bind
… port_addr_add, port_gateway_set, port_route_remove, port_unbridge, resolve, sock_accept
…, path_open2, sock_connect, sock_join_multicast, sock_pair, sock_recv_from, sock_send_to, thread_join, thread_spawn
…wn2, proc_exec, proc_exec2, proc_exec3, proc_exec3_errors, proc_exec3_empty_argv, proc_snapshot, sock_send_to, sock_send_file
@artemyarulin
Copy link
Copy Markdown
Contributor Author

Python upstream result: NO_CHANGE

Baseline: main @ 0e9da5e
Candidate: tests/wasix-tests @ d239123

Summary:

  • Regressions: 0
  • Improvements: 0
  • Added tests: 0
  • Removed tests: 0
  • PASS: 2688 -> 2688
  • FAIL: 156 -> 156
  • TIMEOUT: 1986 -> 1986
  • SKIP: 170 -> 170

Results commit:

  • c3cf10e911f279be79b0b2d280a489f73b8de43e

More info:

@artemyarulin
Copy link
Copy Markdown
Contributor Author

Python upstream result: REGRESSION

Baseline: main @ 19299bc
Candidate: tests/wasix-tests @ d239123

Summary:

  • Regressions: 63
  • Improvements: 857
  • Added tests: 0
  • Removed tests: 5
  • PASS: 28305 -> 29032
  • FAIL: 2653 -> 1821
  • TIMEOUT: 4874 -> 4769
  • SKIP: 2111 -> 2316

Results commit:

  • 554065a91c351655ef0fc8739fd0585c6d145c21

Top regressions:

  • test.test_dbm_sqlite3.Misuse.test_misuse_empty_filename (PASS -> FAIL)
  • test.test_dbm_sqlite3.Misuse.test_misuse_invalid_flag (PASS -> FAIL)
  • test.test_dbm_sqlite3.Misuse.test_misuse_invalid_key (PASS -> FAIL)
  • test.test_dbm_sqlite3.Misuse.test_misuse_iter_close1 (PASS -> FAIL)
  • test.test_dbm_sqlite3.Misuse.test_misuse_iter_close2 (PASS -> FAIL)
  • test.test_dbm_sqlite3.Misuse.test_misuse_reinit (PASS -> FAIL)
  • test.test_dbm_sqlite3.Misuse.test_misuse_use_after_close (PASS -> FAIL)
  • test.test_dbm_sqlite3.ReadOnly.test_readonly_delete (PASS -> FAIL)
  • test.test_dbm_sqlite3.ReadOnly.test_readonly_iter (PASS -> FAIL)
  • test.test_dbm_sqlite3.ReadOnly.test_readonly_keys (PASS -> FAIL)

Top improvements:

  • test.test_cprofile.CProfileTest.test_output_file_when_changing_directory (FAIL -> PASS)
  • test.test_filecmp.DirCompareTestCase.test_cmpfiles (FAIL -> PASS)
  • test.test_filecmp.DirCompareTestCase.test_cmpfiles_invalid_names (FAIL -> PASS)
  • test.test_filecmp.DirCompareTestCase.test_default_ignores (FAIL -> PASS)
  • test.test_filecmp.DirCompareTestCase.test_dircmp_different_directories (FAIL -> PASS)
  • test.test_filecmp.DirCompareTestCase.test_dircmp_different_file (FAIL -> PASS)
  • test.test_filecmp.DirCompareTestCase.test_dircmp_identical_directories (FAIL -> PASS)
  • test.test_filecmp.DirCompareTestCase.test_dircmp_invalid_names (FAIL -> PASS)
  • test.test_filecmp.DirCompareTestCase.test_dircmp_no_shallow_different_file (FAIL -> PASS)
  • test.test_filecmp.DirCompareTestCase.test_dircmp_shallow_is_keyword_only (FAIL -> PASS)

Top removed tests:

  • tearDownClass (test.test_tarfile.NoneInfoExtractTests_Data) (FAIL)
  • tearDownClass (test.test_tarfile.NoneInfoExtractTests_Default) (FAIL)
  • tearDownClass (test.test_tarfile.NoneInfoExtractTests_FullyTrusted) (FAIL)
  • tearDownClass (test.test_tarfile.NoneInfoExtractTests_Tar) (FAIL)
  • tearDownModule (test.test_tarfile) (FAIL)

More info:

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.

3 participants