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
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 ln
# arch b2sum cksum cp csplit date dir dircolors fmt join ln
# 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_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::
1 change: 1 addition & 0 deletions Cargo.lock

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

60 changes: 59 additions & 1 deletion 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, ln, 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, ln, 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 @@ -31,3 +33,59 @@ WASI does not support spawning child processes. Tests that shell out to other co
## WASI: stdin file position not preserved through wasmtime

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: 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`.

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_unsafe_overwrite;
#[cfg(windows)]
pub use self::windows::is_unsafe_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_unsafe_overwrite<I, O>(_input: &I, _output: &O) -> bool {
false
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 @@ -43,6 +43,9 @@ fluent = { workspace = true }
exacl = { workspace = true, optional = true }
nix = { workspace = true, features = ["fs"] }

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

[[bin]]
name = "cp"
path = "src/main.rs"
Expand Down
88 changes: 61 additions & 27 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_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 @@ -1335,9 +1336,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 @@ -1837,15 +1838,50 @@ pub(crate) fn copy_attributes(
})?;

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)?;
#[cfg(target_os = "wasi")]
{
// `filetime`'s WASI backend panics in
// `from_last_{access,modification}_time`. Reach `utimensat` directly
// through `rustix`, converting `SystemTime` → `Timespec` via
// `UNIX_EPOCH` (which matches the `path_filestat_set_times` contract).
use std::time::UNIX_EPOCH;
let to_timespec = |t: std::time::SystemTime| -> io::Result<rustix::fs::Timespec> {
// Pre-epoch source times can't be represented by WASI's
// `path_filestat_set_times` (unsigned nanosecond count).
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 _,
tv_nsec: d.subsec_nanos() as _,
})
};
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(())
}

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(())
}
})?;

#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
Expand Down Expand Up @@ -1896,19 +1932,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 @@ -1930,13 +1956,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
2 changes: 1 addition & 1 deletion src/uu/mktemp/src/mktemp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,7 @@ fn make_temp_dir(dir: &Path, prefix: &str, rand: usize, suffix: &str) -> UResult
// The directory is created with these permission at creation time, using mkdir(3) syscall.
// This is not relevant on Windows systems. See: https://docs.rs/tempfile/latest/tempfile/#security
// `fs` is not imported on Windows anyways.
#[cfg(not(windows))]
#[cfg(unix)]
builder.permissions(fs::Permissions::from_mode(0o700));

match builder.tempdir_in(dir) {
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 @@ -47,6 +46,9 @@ uucore = { workspace = true, features = [
fluent = { workspace = true }
foldhash = { workspace = true }

[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
15 changes: 15 additions & 0 deletions src/uu/sort/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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")
.map(|f| f.split(',').any(|feat| feat == "atomics"))
.unwrap_or(false);

if target_os == "wasi" && !has_atomics {
println!("cargo::rustc-cfg=wasi_no_threads");
}
}
Loading
Loading