From 78d85a59615bd8df6bff6ef8a164c9f2cefa7a2a Mon Sep 17 00:00:00 2001 From: Alexander Shishenko Date: Tue, 4 Aug 2026 21:16:25 +0300 Subject: [PATCH 1/2] uucore: fix OsStr byte conversions on wasip2 and wasip3 `target_os = "wasi"` is true for p1, p2 and p3, but `std::os::wasi` is unstable on p2 and not compiled at all on p3. The helpers gating on `target_env = "p1"` therefore misbehaved on the other two: - `os_str_from_bytes`, `os_string_from_vec` and `os_string_to_vec` took the UTF-8-validating path and rejected byte strings that are valid on WASI, even though `os_str_as_bytes` already returned raw bytes there. - `path_ends_with_terminator` gated its `as_encoded_bytes` branch to exactly p2, so on p3 it used `as_bytes` with no `OsStrExt` in scope and did not compile. Use the `OsStr::as_encoded_bytes` family instead: stable since 1.74 and uniform across p1/p2/p3. On WASI `OsStr` is byte-based, so `from_encoded_bytes_unchecked` is sound. unix and Windows are unchanged. Verified with a non-UTF-8 round-trip built for wasm32-wasip2 and run under wasmtime: rejected before, preserved after. wasip3 is not compile-tested, as std does not yet build for that target. --- src/uucore/src/lib/features/fs.rs | 17 ++---------- src/uucore/src/lib/lib.rs | 43 +++++++++++++++++-------------- 2 files changed, 25 insertions(+), 35 deletions(-) diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index a15bf0f2b2..99f8a4eb6c 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -790,23 +790,10 @@ pub fn are_hardlinks_or_one_way_symlink_to_same_file(source: &Path, target: &Pat /// * `path` - A reference to the path to be checked. #[cfg(any(unix, target_os = "wasi"))] pub fn path_ends_with_terminator(path: &Path) -> bool { - #[cfg(unix)] - use std::os::unix::prelude::OsStrExt; - #[cfg(all(target_os = "wasi", target_env = "p1"))] - use std::os::wasi::ffi::OsStrExt; - - #[cfg(all(target_os = "wasi", target_env = "p2"))] - return path - .as_os_str() + path.as_os_str() .as_encoded_bytes() .last() - .is_some_and(|&byte| byte == b'/'); - #[cfg(not(all(target_os = "wasi", target_env = "p2")))] - return path - .as_os_str() - .as_bytes() - .last() - .is_some_and(|&byte| byte == b'/'); + .is_some_and(|&byte| byte == b'/') } #[cfg(windows)] diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index aeacfe01fc..2437312507 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -151,8 +151,6 @@ use std::io::{BufRead, BufReader}; use std::iter; #[cfg(unix)] use std::os::unix::ffi::{OsStrExt, OsStringExt}; -#[cfg(all(target_os = "wasi", target_env = "p1"))] -use std::os::wasi::ffi::{OsStrExt, OsStringExt}; use std::str; use std::str::Utf8Chunk; use std::sync::{LazyLock, atomic::Ordering}; @@ -485,15 +483,19 @@ pub fn os_str_as_bytes_lossy(os_string: &OsStr) -> Cow<'_, [u8]> { /// /// This always succeeds on unix platforms, /// and fails on other platforms if the bytes can't be parsed as UTF-8. -#[cfg_attr( - any(unix, all(target_os = "wasi", target_env = "p1")), - expect(clippy::unnecessary_wraps) -)] +#[cfg_attr(any(unix, target_os = "wasi"), expect(clippy::unnecessary_wraps))] pub fn os_str_from_bytes(bytes: &[u8]) -> error::UResult> { - #[cfg(any(unix, all(target_os = "wasi", target_env = "p1")))] + #[cfg(unix)] return Ok(Cow::Borrowed(OsStr::from_bytes(bytes))); - #[cfg(not(any(unix, all(target_os = "wasi", target_env = "p1"))))] + #[cfg(target_os = "wasi")] + return Ok(Cow::Borrowed( + // SAFETY: on WASI `OsStr` is a plain byte string with no encoding + // invariant, so every byte sequence is a valid `OsStr`. + unsafe { OsStr::from_encoded_bytes_unchecked(bytes) }, + )); + + #[cfg(not(any(unix, target_os = "wasi")))] Ok(Cow::Owned(OsString::from(str::from_utf8(bytes).map_err( |_| error::UUsageError::new(1, "Unable to transform bytes into OsStr"), )?))) @@ -503,15 +505,17 @@ pub fn os_str_from_bytes(bytes: &[u8]) -> error::UResult> { /// /// This always succeeds on unix platforms, /// and fails on other platforms if the bytes can't be parsed as UTF-8. -#[cfg_attr( - any(unix, all(target_os = "wasi", target_env = "p1")), - expect(clippy::unnecessary_wraps) -)] +#[cfg_attr(any(unix, target_os = "wasi"), expect(clippy::unnecessary_wraps))] pub fn os_string_from_vec(vec: Vec) -> error::UResult { - #[cfg(any(unix, all(target_os = "wasi", target_env = "p1")))] + #[cfg(unix)] return Ok(OsString::from_vec(vec)); - #[cfg(not(any(unix, all(target_os = "wasi", target_env = "p1"))))] + #[cfg(target_os = "wasi")] + // SAFETY: on WASI `OsString` is a plain byte string with no encoding + // invariant, so every byte sequence is a valid `OsString`. + return Ok(unsafe { OsString::from_encoded_bytes_unchecked(vec) }); + + #[cfg(not(any(unix, target_os = "wasi")))] Ok(OsString::from(String::from_utf8(vec).map_err(|_| { error::UUsageError::new(1, "invalid UTF-8 was detected in one or more arguments") })?)) @@ -521,14 +525,13 @@ pub fn os_string_from_vec(vec: Vec) -> error::UResult { /// /// This always succeeds on unix platforms, /// and fails on other platforms if the bytes can't be parsed as UTF-8. -#[cfg_attr( - any(unix, all(target_os = "wasi", target_env = "p1")), - expect(clippy::unnecessary_wraps) -)] +#[cfg_attr(any(unix, target_os = "wasi"), expect(clippy::unnecessary_wraps))] pub fn os_string_to_vec(s: OsString) -> error::UResult> { - #[cfg(any(unix, all(target_os = "wasi", target_env = "p1")))] + #[cfg(unix)] let v = s.into_vec(); - #[cfg(not(any(unix, all(target_os = "wasi", target_env = "p1"))))] + #[cfg(target_os = "wasi")] + let v = s.into_encoded_bytes(); + #[cfg(not(any(unix, target_os = "wasi")))] let v = s .into_string() .map_err(|_| { From d8e25d86a2cf150726411611c7617adda3f626ea Mon Sep 17 00:00:00 2001 From: Alexander Shishenko Date: Tue, 4 Aug 2026 21:23:50 +0300 Subject: [PATCH 2/2] uucore: add tests for OsStr byte conversions on WASI Assert that `os_string_from_vec`/`os_string_to_vec` and `os_str_from_bytes`/`os_str_as_bytes` round-trip non-UTF-8 bytes on byte-oriented platforms, which is unix and every WASI environment. Built for wasm32-wasip2 and run under wasmtime, they fail before the preceding commit and pass after it: before: Unable to transform bytes into OsStr after: 2 passed On unix they pass either way and guard against a future regression. --- src/uucore/src/lib/lib.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 2437312507..40b2228b5d 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -763,6 +763,35 @@ mod tests { test_invalid_utf8_args_ignore(os_str); } + /// On byte-oriented platforms — unix and every WASI environment — the + /// `OsString` helpers must round-trip arbitrary bytes, including sequences + /// that are not valid UTF-8. This regressed on wasm32-wasip2 and + /// wasm32-wasip3, where the conversions fell back to the UTF-8-validating + /// path and returned an error instead. + #[cfg(any(unix, target_os = "wasi"))] + #[test] + fn os_string_from_vec_roundtrips_non_utf8() { + let source = vec![0x66, 0x6f, 0x80, 0x6f]; + + let os_string = os_string_from_vec(source.clone()).unwrap(); + // The value really does hold bytes that are not valid UTF-8. + assert!(os_string.to_str().is_none()); + assert_eq!(os_string_to_vec(os_string).unwrap(), source); + } + + /// Same invariant for the borrowed `OsStr` conversions, which must agree + /// with `os_string_from_vec` on what is representable. + #[cfg(any(unix, target_os = "wasi"))] + #[test] + fn os_str_from_bytes_roundtrips_non_utf8() { + let source = [0x66, 0x6f, 0x80, 0x6f]; + + let os_str = os_str_from_bytes(&source).unwrap(); + assert!(os_str.to_str().is_none()); + assert_eq!(os_str_as_bytes(&os_str).unwrap(), &source); + assert_eq!(os_str_as_bytes_lossy(&os_str).into_owned(), source); + } + #[test] fn test_format_usage() { assert_eq!(format_usage("expr EXPRESSION"), "expr EXPRESSION");