Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
776c4ec
Add wasm32-wasi platform support
DePasqualeOrg Mar 29, 2026
01b9308
sort: add synchronous fallback for WASI without thread support
DePasqualeOrg Mar 30, 2026
bf85335
sort: add wasi_no_threads cfg alias to reduce predicate verbosity
DePasqualeOrg Apr 8, 2026
5970f8a
Replace unstable wasi_ext with stable libc calls
DePasqualeOrg Apr 8, 2026
b78b514
uucore: use raw pointers for libc stat calls to satisfy clippy
DePasqualeOrg Apr 8, 2026
0fc3e19
Fix clippy warnings for wasm32-wasip1 target
DePasqualeOrg Apr 8, 2026
eebd6be
Add integration tests for cat/sort/tail/touch + underlying fixes
DePasqualeOrg Apr 12, 2026
917ac3e
Annotate more WASI-incompatible tests caught on Linux
DePasqualeOrg Apr 12, 2026
74df0f5
Clarify WASI symlink and guest-path test gaps
DePasqualeOrg Apr 12, 2026
5732fb8
Fix cspell errors in CI
DePasqualeOrg Apr 12, 2026
df2d537
tail: ignore test_gnu_args_f under wasi_runner
DePasqualeOrg Apr 13, 2026
14612f6
tail: ignore test_follow_inotify_only_regular under wasi_runner
DePasqualeOrg Apr 13, 2026
4c09aed
sort: isolate WASI sync code into sibling modules
DePasqualeOrg Apr 20, 2026
9c131a3
util: add run-wasi-tests-docker.sh for local Linux verification
DePasqualeOrg Apr 21, 2026
53c529f
cp: extract WASI timestamp logic into set_timestamps function
DePasqualeOrg Apr 21, 2026
43d95c7
tests/sort: ignore test_consistent_sorting_with_i18n_collate on WASI
DePasqualeOrg May 9, 2026
1a112fe
tests/touch: ignore upstream symlink tests on WASI
DePasqualeOrg Jun 11, 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
13 changes: 6 additions & 7 deletions .github/workflows/wasi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,17 @@ jobs:
# Tests incompatible with WASI are annotated with
# #[cfg_attr(wasi_runner, ignore)] in the test source files.
# TODO: add integration tests for these tools as WASI support is extended:
# arch b2sum cat cksum cp csplit date dir dircolors fmt join
# arch b2sum cksum cp csplit date dir dircolors fmt join
# ls md5sum mkdir mv nproc pathchk pr printenv ptx pwd readlink
# realpath rm rmdir seq sha1sum sha224sum sha256sum sha384sum
# sha512sum shred sleep sort split tail touch tsort uname uniq
# vdir yes
# sha512sum shred sleep split tsort uname uniq vdir yes
UUTESTS_BINARY_PATH="$(pwd)/target/wasm32-wasip1/debug/coreutils.wasm" \
UUTESTS_WASM_RUNNER=wasmtime \
cargo test --test tests -- \
test_base32:: test_base64:: test_basenc:: test_basename:: \
test_comm:: test_cut:: test_dirname:: test_echo:: \
test_cat:: test_comm:: test_cut:: test_dirname:: test_echo:: \
test_expand:: test_factor:: test_false:: test_fold:: \
test_head:: test_link:: test_ln:: test_nl:: test_numfmt:: \
test_od:: test_paste:: test_printf:: test_shuf:: test_sum:: \
test_tee:: test_tr:: test_true:: test_truncate:: \
test_unexpand:: test_unlink:: test_wc::
test_od:: test_paste:: test_printf:: test_shuf:: test_sort:: \
test_sum:: test_tail:: test_tee:: test_touch:: test_tr:: \
test_true:: test_truncate:: test_unexpand:: test_unlink:: test_wc::
60 changes: 57 additions & 3 deletions docs/src/wasi-test-gaps.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<!-- spell-checker:ignore tzdata preopen setrlimit NOFILE filestat kqueue sysinfo EISDIR ELOOP -->

# WASI integration test gaps

Tests annotated with `#[cfg_attr(wasi_runner, ignore = "...")]` are skipped when running integration tests against a WASI binary via wasmtime. This document tracks the reasons so that gaps in WASI support are visible in one place.
Expand All @@ -6,7 +8,7 @@ To find all annotated tests: `grep -rn 'wasi_runner, ignore' tests/`

## Tools not yet covered by integration tests

arch, b2sum, cat, cksum, cp, csplit, date, dir, dircolors, fmt, join, ls, md5sum, mkdir, mv, nproc, pathchk, pr, printenv, ptx, pwd, readlink, realpath, rm, rmdir, seq, sha1sum, sha224sum, sha256sum, sha384sum, sha512sum, shred, sleep, sort, split, tail, touch, tsort, uname, uniq, vdir, yes
arch, b2sum, cksum, cp, csplit, date, dir, dircolors, fmt, join, ls, md5sum, mkdir, mv, nproc, pathchk, pr, printenv, ptx, pwd, readlink, realpath, rm, rmdir, seq, sha1sum, sha224sum, sha256sum, sha384sum, sha512sum, shred, sleep, split, tsort, uname, uniq, vdir, yes

## WASI sandbox: host paths not visible

Expand All @@ -32,6 +34,58 @@ WASI does not support spawning child processes. Tests that shell out to other co

When stdin is a seekable file, wasmtime does not preserve the file position between the host and guest. Tests that validate stdin offset behavior after `head` reads are skipped.

## WASI: read_link on absolute paths fails under wasmtime via spawned test harness
## WASI: absolute symlink targets fail under wasmtime

Under wasmtime, symlinks whose stored target is an absolute guest path (for example `bar -> /foo`) fail in cases that work on POSIX: `readlink bar` exits 1 and `readlink -v bar` reports `Permission denied`, while the equivalent relative symlink (`bar -> foo`) succeeds. This reproduces even when `wasmtime` is invoked directly, so it is not specific to the cargo-test harness. `realpath` and symlink-heavy `cp` paths inherit the same limitation, and individual symptom tests under this umbrella are annotated with narrower reasons describing the observed errno mismatch.

## WASI: no Unix domain socket support

WASI does not support Unix domain sockets. Tests that create or read from `AF_UNIX` sockets are skipped.

## WASI: no locale data

The WASI sandbox does not ship locale data, so `setlocale`/`LC_ALL` have no effect and sorting falls back to byte comparison. Tests that depend on locale-aware collation or month-name translation are skipped.

## WASI: tail follow mode disabled

`tail -f` / `tail -F` (follow mode) requires change-notification mechanisms (`inotify`, `kqueue`) and signal handling that WASI does not provide, so follow is disabled on WASI and a warning is emitted. Tests that exercise follow behavior are skipped.

## WASI: cannot detect unsafe overwrite

`is_unsafe_overwrite` (used by `cat` to detect input-is-output situations) is stubbed to return `false` on WASI because the required `stat` / device-and-inode comparison is not available. Tests that assert this error path are skipped.

## WASI: pre-epoch timestamps not representable

WASI Preview 1 `Timestamp` is a `u64` nanosecond count since the Unix epoch, so `path_filestat_set_times` (and therefore `touch -t` with a two-digit year ≥ 69) cannot express dates before 1970. Tests that set pre-epoch timestamps are skipped.

## WASI: no timezone database

wasi-libc does not ship tzdata, so `TZ` is not honoured and timezone-dependent validation (e.g. `touch -t` rejecting a nonexistent local time during a DST transition) does not happen. Tests that rely on this are skipped.

## WASI: guest root is a writable preopen

The test harness maps the per-test working directory as the guest's `/`. That makes `/` writable inside the guest, so GNU-style protections against operating on the system root (e.g. `touch /` failing) cannot be exercised. It also means guest-visible absolute paths are rooted at `/`, not at the host tempdir. Tests that compare against host `canonicalize()` results or pass host absolute paths into the guest (for example some `cp --parents`, `readlink`, `realpath`, `pwd`, and `ls` cases) need guest-aware expectations or separate coverage. Tests that assert the root-protection behavior are skipped.

## WASI: `touch -` (stdout) unsupported

On WASI, `touch -` returns `UnsupportedPlatformFeature` because the guest cannot reliably locate the host file backing stdout. Tests that exercise `touch -` are skipped.

## WASI: rlimit/setrlimit not supported

WASI has no concept of per-process resource limits, so `setrlimit` (and the `rlimit` crate that wraps it) has no effect. Tests that set `RLIMIT_NOFILE` to verify behavior under restricted file-descriptor budgets are skipped.

## WASI: sysinfo/meminfo not available

WASI has no `sysinfo`/`/proc/meminfo` equivalent, so features that size buffers as a percentage of system memory (e.g. `sort -S 10%`) cannot resolve the limit and fail. Tests that exercise percentage-based sizing are skipped.

## WASI: errno/error-message mismatches

Several error paths surface different errno values (and therefore different error messages) through wasmtime than on POSIX. Observed cases:

- Opening a directory as a file returns `EBADF` rather than `EISDIR`.
- Redirecting a directory into stdin returns `ENOENT` rather than `EISDIR`.
- Filesystem permission errors surface as `ENOENT` rather than `EACCES`.
- Symlink-loop traversal does not reliably surface `ELOOP` ("Too many levels of symbolic links").
- Opening a symlink-to-directory does not reliably surface `EISDIR`.

`fs::read_link` on an absolute path inside the sandbox (e.g. `/file2`) returns `EPERM` when the WASI binary is launched through `std::process::Command` from the test harness, even though the same call works when wasmtime is invoked directly. This breaks `uucore::fs::canonicalize` for symlink sources, so tests that rely on following a symlink to compute a relative path are skipped.
Tests that assert specific error text for these paths are skipped.
6 changes: 5 additions & 1 deletion src/uu/cat/src/platform/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ pub use self::unix::is_safe_overwrite;
#[cfg(windows)]
pub use self::windows::is_safe_overwrite;

// WASI: no fstat-based device/inode checks available; assume safe.
// WASI: when stdout is inherited from a host file descriptor, wasmtime
// reports its fstat as all-zero (st_dev == st_ino == 0), so the dev/inode
// comparison against any input file descriptor can never match. There is
// no reliable way to detect unsafe overwrite here; assume safe rather than
// risk a spurious error.
#[cfg(target_os = "wasi")]
pub fn is_safe_overwrite<I, O>(_input: &I, _output: &O) -> bool {
true
Expand Down
3 changes: 3 additions & 0 deletions src/uu/cp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ windows-sys = { workspace = true, features = [
"Win32_Storage_FileSystem",
] }

[target.'cfg(target_os = "wasi")'.dependencies]
rustix = { workspace = true, features = ["fs"] }

[[bin]]
name = "cp"
path = "src/main.rs"
Expand Down
100 changes: 70 additions & 30 deletions src/uu/cp/src/cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore (ToDO) copydir ficlone fiemap ftruncate linkgs lstat nlink nlinks pathbuf pwrite reflink strs xattrs symlinked deduplicated advcpmv nushell IRWXG IRWXO IRWXU IRWXUGO IRWXU IRWXG IRWXO IRWXUGO sflag
// spell-checker:ignore (ToDO) copydir ficlone fiemap filestat ftruncate linkgs lstat nlink nlinks pathbuf pwrite reflink strs utimensat xattrs symlinked deduplicated advcpmv nushell IRWXG IRWXO IRWXU IRWXUGO IRWXU IRWXG IRWXO IRWXUGO sflag

use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
Expand All @@ -20,6 +20,7 @@ use uucore::fsxattr::{copy_acls, copy_xattrs, copy_xattrs_skip_selinux};
use uucore::translate;

use clap::{Arg, ArgAction, ArgMatches, Command, builder::ValueParser, value_parser};
#[cfg(not(target_os = "wasi"))]
use filetime::FileTime;
use indicatif::{ProgressBar, ProgressStyle};
#[cfg(unix)]
Expand Down Expand Up @@ -1367,9 +1368,9 @@ fn parse_path_args(
/// Check if an error is ENOTSUP/EOPNOTSUPP (operation not supported).
/// This is used to suppress xattr errors on filesystems that don't support them.
fn is_enotsup_error(error: &CpError) -> bool {
#[cfg(unix)]
#[cfg(any(unix, target_os = "wasi"))]
const EOPNOTSUPP: i32 = libc::EOPNOTSUPP;
#[cfg(not(unix))]
#[cfg(not(any(unix, target_os = "wasi")))]
const EOPNOTSUPP: i32 = 95;

match error {
Expand Down Expand Up @@ -1788,6 +1789,55 @@ fn copy_extended_attrs(source: &Path, dest: &Path, skip_selinux: bool) -> CopyRe
Ok(())
}

/// Copy the access and modification timestamps from `source_metadata` onto `dest`.
/// If `dest` is a symlink, the symlink's own timestamps are set rather than the
/// target's.
///
/// On WASI this calls `rustix::fs::utimensat` directly because `filetime`'s
/// WASI backend panics in `from_last_{access,modification}_time`. `SystemTime`
/// values are converted to `Timespec` against `UNIX_EPOCH`, matching WASI's
/// `path_filestat_set_times` contract (unsigned nanosecond count — pre-epoch
/// source times can't be represented).
fn set_timestamps(source_metadata: &Metadata, dest: &Path) -> CopyResult<()> {
#[cfg(target_os = "wasi")]
{
use std::time::UNIX_EPOCH;
let to_timespec = |t: std::time::SystemTime| -> io::Result<rustix::fs::Timespec> {
let d = t
.duration_since(UNIX_EPOCH)
.map_err(|e| io::Error::new(io::ErrorKind::Unsupported, e))?;
Ok(rustix::fs::Timespec {
tv_sec: d.as_secs() as i64,
tv_nsec: d.subsec_nanos() as i32,
})
};
let timestamps = rustix::fs::Timestamps {
last_access: to_timespec(source_metadata.accessed()?)?,
last_modification: to_timespec(source_metadata.modified()?)?,
};
let flags = if dest.is_symlink() {
rustix::fs::AtFlags::SYMLINK_NOFOLLOW
} else {
rustix::fs::AtFlags::empty()
};
rustix::fs::utimensat(rustix::fs::CWD, dest, &timestamps, flags)
.map_err(io::Error::from)?;
Ok(())
}

#[cfg(not(target_os = "wasi"))]
{
let atime = FileTime::from_last_access_time(source_metadata);
let mtime = FileTime::from_last_modification_time(source_metadata);
if dest.is_symlink() {
filetime::set_symlink_file_times(dest, atime, mtime)?;
} else {
filetime::set_file_times(dest, atime, mtime)?;
}
Ok(())
}
}

/// Copy the specified attributes from one path to another.
/// If `skip_selinux_xattr` is true, the security.selinux xattr will not be copied
/// (used when -Z is specified to set the default context instead).
Expand Down Expand Up @@ -1901,16 +1951,8 @@ pub(crate) fn copy_attributes(
Ok(())
})?;

handle_preserve(attributes.timestamps, || -> CopyResult<()> {
let atime = FileTime::from_last_access_time(&source_metadata);
let mtime = FileTime::from_last_modification_time(&source_metadata);
if dest.is_symlink() {
filetime::set_symlink_file_times(dest, atime, mtime)?;
} else {
filetime::set_file_times(dest, atime, mtime)?;
}

Ok(())
handle_preserve(attributes.timestamps, || {
set_timestamps(&source_metadata, dest)
})?;

#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
Expand Down Expand Up @@ -1961,19 +2003,9 @@ pub(crate) fn copy_attributes(
fn symlink_file(
source: &Path,
dest: &Path,
#[cfg(not(target_os = "wasi"))] symlinked_files: &mut HashSet<FileInformation>,
#[cfg(target_os = "wasi")] _symlinked_files: &mut HashSet<FileInformation>,
symlinked_files: &mut HashSet<FileInformation>,
) -> CopyResult<()> {
#[cfg(target_os = "wasi")]
{
Err(CpError::IoErrContext(
io::Error::new(io::ErrorKind::Unsupported, "symlinks not supported"),
translate!("cp-error-cannot-create-symlink",
"dest" => get_filename(dest).unwrap_or("?").quote(),
"source" => get_filename(source).unwrap_or("?").quote()),
))
}
#[cfg(not(any(windows, target_os = "wasi")))]
#[cfg(unix)]
{
std::os::unix::fs::symlink(source, dest).map_err(|e| {
CpError::IoErrContext(
Expand All @@ -1995,13 +2027,21 @@ fn symlink_file(
)
})?;
}
#[cfg(not(target_os = "wasi"))]
#[cfg(target_os = "wasi")]
{
if let Ok(file_info) = FileInformation::from_path(dest, false) {
symlinked_files.insert(file_info);
}
Ok(())
rustix::fs::symlink(source, dest).map_err(|e| {
CpError::IoErrContext(
io::Error::from(e),
translate!("cp-error-cannot-create-symlink",
"dest" => get_filename(dest).unwrap_or("?").quote(),
"source" => get_filename(source).unwrap_or("?").quote()),
)
})?;
}
if let Ok(file_info) = FileInformation::from_path(dest, false) {
symlinked_files.insert(file_info);
}
Ok(())
}

fn context_for(src: &Path, dest: &Path) -> String {
Expand Down
4 changes: 3 additions & 1 deletion src/uu/env/src/native_int_str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
// this conversion needs to be done only once in the beginning and at the end.

use std::ffi::OsString;
#[cfg(not(target_os = "windows"))]
#[cfg(unix)]
use std::os::unix::ffi::{OsStrExt, OsStringExt};
#[cfg(target_os = "wasi")]
use std::os::wasi::ffi::{OsStrExt, OsStringExt};
#[cfg(target_os = "windows")]
use std::os::windows::prelude::*;
use std::{borrow::Cow, ffi::OsStr};
Expand Down
4 changes: 3 additions & 1 deletion src/uu/sort/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ compare = { workspace = true }
itertools = { workspace = true }
memchr = { workspace = true }
rand = { workspace = true }
rayon = { workspace = true }
self_cell = { workspace = true }
tempfile = { workspace = true }
thiserror = { workspace = true }
Expand All @@ -50,6 +49,9 @@ foldhash = { workspace = true }
[target.'cfg(all(unix, not(any(target_os = "redox", target_os = "fuchsia", target_os = "haiku", target_os = "solaris", target_os = "illumos"))))'.dependencies]
rustix = { workspace = true, features = ["system", "process"] }

[target.'cfg(not(all(target_os = "wasi", not(target_feature = "atomics"))))'.dependencies]
rayon = { workspace = true }

[target.'cfg(not(any(target_os = "redox", target_os = "wasi")))'.dependencies]
ctrlc = { workspace = true }

Expand Down
14 changes: 14 additions & 0 deletions src/uu/sort/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
fn main() {
// Set a short alias for the WASI-without-threads configuration so that
// source files can use `#[cfg(wasi_no_threads)]` instead of the verbose
// `#[cfg(all(target_os = "wasi", not(target_feature = "atomics")))]`.
println!("cargo::rustc-check-cfg=cfg(wasi_no_threads)");

let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
let has_atomics = std::env::var("CARGO_CFG_TARGET_FEATURE")
.is_ok_and(|f| f.split(',').any(|feat| feat == "atomics"));

if target_os == "wasi" && !has_atomics {
println!("cargo::rustc-cfg=wasi_no_threads");
}
}
44 changes: 44 additions & 0 deletions src/uu/sort/src/check/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

//! Check if a file is ordered.
//!
//! On most platforms this uses a multi-threaded reader. On WASI without
//! atomics, a synchronous variant is used instead. The two implementations
//! live in sibling modules and are selected via cfg at the module boundary.

use std::cmp::Ordering;
use std::ffi::OsStr;

use uucore::error::UResult;

use crate::{GlobalSettings, open};

#[cfg(not(wasi_no_threads))]
mod threaded;
#[cfg(not(wasi_no_threads))]
use threaded as runner;

#[cfg(wasi_no_threads)]
mod sync;
#[cfg(wasi_no_threads)]
use sync as runner;

/// Check if the file at `path` is ordered.
pub fn check(path: &OsStr, settings: &GlobalSettings) -> UResult<()> {
let max_allowed_cmp = if settings.unique {
Ordering::Less
} else {
Ordering::Equal
};
let file = open(path)?;
let chunk_size = if settings.buffer_size < 100 * 1024 {
settings.buffer_size
} else {
100 * 1024
};

runner::check(path, settings, max_allowed_cmp, file, chunk_size)
}
Loading
Loading