From 8145b07ebcd1f6553a417a009091c53d7610a04f Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Fri, 3 Jul 2026 20:34:01 -0700 Subject: [PATCH] fix(registry): coreutils stat/chmod/ls show real permission bits --- .../execution/assets/runners/wasm-runner.mjs | 18 +++- packages/browser/src/runtime.ts | 16 +++- .../native/crates/libs/builtins/src/lib.rs | 12 ++- .../native/crates/libs/shims/src/which.rs | 12 ++- .../brush-interactive/0001-wasi-support.patch | 16 ---- .../crates/uu_chmod/0001-wasi-compat.patch | 89 +++++++++++++++---- .../0001-wasi-host-fs-mode-display.patch | 42 ++++++--- .../uu_stat/0001-wasi-metadata-compat.patch | 49 +++++++--- registry/native/scripts/patch-vendor.sh | 9 +- registry/native/stubs/uucore/build.rs | 30 ++++--- .../stubs/uucore/src/lib/mods/locale.rs | 26 ++++++ 11 files changed, 241 insertions(+), 78 deletions(-) diff --git a/crates/execution/assets/runners/wasm-runner.mjs b/crates/execution/assets/runners/wasm-runner.mjs index a324ce61b..31e2a84bc 100644 --- a/crates/execution/assets/runners/wasm-runner.mjs +++ b/crates/execution/assets/runners/wasm-runner.mjs @@ -2204,6 +2204,17 @@ function resolveSyntheticHostMapping(value, fromGuestDir = '/') { return resolveModuleGuestPathToHostMapping(guestPath); } +function chmodMappedGuestPath(guestPath, hostPath, mode) { + fsModule.chmodSync(hostPath, mode); + try { + if (typeof guestPath === 'string' && guestPath.length > 0) { + fsModule.chmodSync(guestPath, mode); + } + } catch { + // Best effort: host-mapped paths may not also exist as direct kernel paths. + } +} + function maybeCreateSyntheticCommandResult(command, args, cwd) { const basename = path.posix.basename(String(command || '')); @@ -2218,6 +2229,7 @@ function maybeCreateSyntheticCommandResult(command, args, cwd) { const mode = Number.parseInt(modeArg, 8) >>> 0; try { for (const targetArg of args.slice(1)) { + const guestPath = resolveSyntheticGuestPath(targetArg, cwd || '/'); const mapping = resolveSyntheticHostMapping(targetArg, cwd || '/'); if (!mapping || typeof mapping.hostPath !== 'string') { throw new Error(`No such file or directory: ${targetArg}`); @@ -2227,7 +2239,7 @@ function maybeCreateSyntheticCommandResult(command, args, cwd) { error.code = 'EROFS'; throw error; } - fsModule.chmodSync(mapping.hostPath, mode); + chmodMappedGuestPath(guestPath, mapping.hostPath, mode); } return { exitCode: 0, stdout: '', stderr: '' }; } catch (error) { @@ -4407,7 +4419,7 @@ const hostFsImport = { hostPath: mapping.hostPath, mode: Number(mode) >>> 0, }); - fsModule.chmodSync(mapping.hostPath, Number(mode) >>> 0); + chmodMappedGuestPath(target, mapping.hostPath, Number(mode) >>> 0); return 0; } catch { traceHostProcess('host-fs-chmod-fault', {}); @@ -4426,7 +4438,7 @@ const hostFsImport = { if (!mapping || typeof mapping.hostPath !== 'string' || mapping.readOnly) { return 1; } - fsModule.chmodSync(mapping.hostPath, Number(mode) >>> 0); + chmodMappedGuestPath(handle.guestPath, mapping.hostPath, Number(mode) >>> 0); return 0; } const targetFd = diff --git a/packages/browser/src/runtime.ts b/packages/browser/src/runtime.ts index a7f2de3c2..3e77ac6b8 100644 --- a/packages/browser/src/runtime.ts +++ b/packages/browser/src/runtime.ts @@ -3247,7 +3247,10 @@ export const POLYFILL_CODE_MAP: Record = { } return 0o100644; }, - path_mode(pathPtr, pathLen, followSymlinks) { + // Signature must match the node runner's host_fs.path_mode + // (fd, pathPtr, pathLen, followSymlinks). The guest passes the + // directory fd first (3 = cwd preopen); the path is at args 2/3. + path_mode(_fd, pathPtr, pathLen, followSymlinks) { try { const guestPath = resolveGuestPath(readString(pathPtr, pathLen)); const stat = Number(followSymlinks) === 0 @@ -3258,6 +3261,17 @@ export const POLYFILL_CODE_MAP: Record = { return 0; } }, + // Matches node runner host_fs.chmod(fd, pathPtr, pathLen, mode): + // 0 on success, 1 on failure. + chmod(_fd, pathPtr, pathLen, mode) { + try { + const guestPath = resolveGuestPath(readString(pathPtr, pathLen)); + fs().chmodSync(guestPath, Number(mode) >>> 0); + return 0; + } catch { + return 1; + } + }, }, host_process: { proc_spawn(argvPtr, argvLen, envpPtr, envpLen, stdinFd, stdoutFd, stderrFd, cwdPtr, cwdLen, retPid) { diff --git a/registry/native/crates/libs/builtins/src/lib.rs b/registry/native/crates/libs/builtins/src/lib.rs index 5585bc4aa..dee2b00af 100644 --- a/registry/native/crates/libs/builtins/src/lib.rs +++ b/registry/native/crates/libs/builtins/src/lib.rs @@ -15,7 +15,14 @@ use std::os::unix::fs::MetadataExt; mod host_fs { #[link(wasm_import_module = "host_fs")] unsafe extern "C" { - pub fn path_mode(path_ptr: *const u8, path_len: u32, follow_symlinks: u32) -> u32; + // Signature must match the sidecar host_fs.path_mode + // (dir_fd, path_ptr, path_len, follow_symlinks). + pub fn path_mode( + dir_fd: u32, + path_ptr: *const u8, + path_len: u32, + follow_symlinks: u32, + ) -> u32; } } @@ -380,7 +387,8 @@ fn permission_allows(_path: &str, metadata: &Metadata, requested_bit: u32) -> bo #[cfg(target_os = "wasi")] fn wasi_path_mode(path: &str) -> Option { let bytes = path.as_bytes(); - let mode = unsafe { host_fs::path_mode(bytes.as_ptr(), bytes.len() as u32, 1) }; + // dir_fd 3 = cwd preopen; absolute paths ignore it. + let mode = unsafe { host_fs::path_mode(3, bytes.as_ptr(), bytes.len() as u32, 1) }; if mode == 0 { None } else { diff --git a/registry/native/crates/libs/shims/src/which.rs b/registry/native/crates/libs/shims/src/which.rs index bd91954b3..c12871463 100644 --- a/registry/native/crates/libs/shims/src/which.rs +++ b/registry/native/crates/libs/shims/src/which.rs @@ -17,7 +17,14 @@ use std::os::unix::fs::MetadataExt; mod host_fs { #[link(wasm_import_module = "host_fs")] unsafe extern "C" { - pub fn path_mode(path_ptr: *const u8, path_len: u32, follow_symlinks: u32) -> u32; + // Signature must match the sidecar host_fs.path_mode + // (dir_fd, path_ptr, path_len, follow_symlinks). + pub fn path_mode( + dir_fd: u32, + path_ptr: *const u8, + path_len: u32, + follow_symlinks: u32, + ) -> u32; } } @@ -45,7 +52,8 @@ fn executable_mode_bits(path: &Path, _metadata: &fs::Metadata) -> bool { let Ok(path_len) = u32::try_from(bytes.len()) else { return false; }; - let mode = unsafe { host_fs::path_mode(bytes.as_ptr(), path_len, 1) }; + // dir_fd 3 = cwd preopen; absolute paths ignore it. + let mode = unsafe { host_fs::path_mode(3, bytes.as_ptr(), path_len, 1) }; (mode & 0o111) != 0 } diff --git a/registry/native/patches/crates/brush-interactive/0001-wasi-support.patch b/registry/native/patches/crates/brush-interactive/0001-wasi-support.patch index 3a0be1c35..bd64fe387 100644 --- a/registry/native/patches/crates/brush-interactive/0001-wasi-support.patch +++ b/registry/native/patches/crates/brush-interactive/0001-wasi-support.patch @@ -139,19 +139,3 @@ diff -ruN '--exclude=*.orig' a/src/reedline/validator.rs b/src/reedline/validato let shell = tokio::task::block_in_place(|| { tokio::runtime::Handle::current().block_on(self.shell.lock()) }); ---- a/Cargo.toml -+++ b/Cargo.toml -@@ -91,6 +91,13 @@ - "signal", - ] - -+[target."cfg(target_arch = \"wasm32\")".dependencies.tokio] -+version = "1.48.0" -+features = [ -+ "macros", -+ "sync", -+] -+ - [target."cfg(unix)".dependencies.nix] - version = "0.30.1" - features = ["term"] diff --git a/registry/native/patches/crates/uu_chmod/0001-wasi-compat.patch b/registry/native/patches/crates/uu_chmod/0001-wasi-compat.patch index c8297fe54..8e41964b6 100644 --- a/registry/native/patches/crates/uu_chmod/0001-wasi-compat.patch +++ b/registry/native/patches/crates/uu_chmod/0001-wasi-compat.patch @@ -1,6 +1,6 @@ --- a/src/chmod.rs +++ b/src/chmod.rs -@@ -8,16 +8,70 @@ +@@ -8,16 +8,87 @@ use clap::{Arg, ArgAction, Command}; use std::ffi::OsString; use std::fs; @@ -27,23 +27,42 @@ + mod host_fs { + #[link(wasm_import_module = "host_fs")] + unsafe extern "C" { -+ pub fn chmod(path_ptr: *const u8, path_len: u32, mode: u32) -> u32; ++ pub fn chmod(fd: u32, path_ptr: *const u8, path_len: u32, mode: u32) -> u32; ++ pub fn path_mode( ++ fd: u32, ++ path_ptr: *const u8, ++ path_len: u32, ++ follow_symlinks: u32, ++ ) -> u32; + } + } + -+ /// Extension trait to provide mode() on WASI Metadata. -+ pub trait MetadataMode { -+ fn mode(&self) -> u32; ++ fn fallback_mode(meta: &fs::Metadata) -> u32 { ++ let ft = meta.file_type(); ++ let base = if ft.is_dir() { 0o755 } else { 0o644 }; ++ if meta.permissions().readonly() { ++ base & !0o222 ++ } else { ++ base ++ } + } -+ impl MetadataMode for fs::Metadata { -+ fn mode(&self) -> u32 { -+ let ft = self.file_type(); -+ let base = if ft.is_dir() { 0o755 } else { 0o644 }; -+ if self.permissions().readonly() { -+ base & !0o222 -+ } else { -+ base -+ } ++ ++ pub fn mode_for_path(path: &Path, meta: &fs::Metadata, follow_symlinks: bool) -> u32 { ++ let Some(path_str) = path.to_str() else { ++ return fallback_mode(meta); ++ }; ++ let mode = unsafe { ++ host_fs::path_mode( ++ 3, ++ path_str.as_ptr(), ++ path_str.len() as u32, ++ if follow_symlinks { 1 } else { 0 }, ++ ) ++ }; ++ if mode == 0 { ++ fallback_mode(meta) ++ } else { ++ mode & 0o7777 + } + } + @@ -55,7 +74,7 @@ + .to_str() + .ok_or_else(|| Error::new(ErrorKind::InvalidInput, "path is not valid UTF-8"))? + .as_bytes(); -+ let status = unsafe { host_fs::chmod(bytes.as_ptr(), bytes.len() as u32, mode) }; ++ let status = unsafe { host_fs::chmod(3, bytes.as_ptr(), bytes.len() as u32, mode) }; + if status == 0 { + return Ok(()); + } @@ -65,13 +84,47 @@ + fs::set_permissions(path, perms) + } +} -+#[cfg(target_os = "wasi")] -+use wasi_compat::MetadataMode; + #[cfg(all(unix, not(target_os = "redox")))] use uucore::safe_traversal::{DirFd, SymlinkBehavior}; use uucore::{format_usage, show, show_error}; -@@ -683,7 +737,12 @@ +@@ -121,7 +192,16 @@ + let preserve_root = matches.get_flag(options::PRESERVE_ROOT); + let fmode = match matches.get_one::(options::REFERENCE) { + Some(fref) => match fs::metadata(fref) { +- Ok(meta) => Some(meta.mode() & 0o7777), ++ Ok(meta) => { ++ #[cfg(target_os = "wasi")] ++ { ++ Some(wasi_compat::mode_for_path(Path::new(fref), &meta, true)) ++ } ++ #[cfg(not(target_os = "wasi"))] ++ { ++ Some(meta.mode() & 0o7777) ++ } ++ } + Err(_) => { + return Err(ChmodError::CannotStat(fref.into()).into()); + } +@@ -623,7 +703,16 @@ + let metadata = get_metadata(file, dereference); + + let fperm = match metadata { +- Ok(meta) => meta.mode() & 0o7777, ++ Ok(meta) => { ++ #[cfg(target_os = "wasi")] ++ { ++ wasi_compat::mode_for_path(file, &meta, dereference) ++ } ++ #[cfg(not(target_os = "wasi"))] ++ { ++ meta.mode() & 0o7777 ++ } ++ } + Err(err) => { + // Handle dangling symlinks or other errors + return if file.is_symlink() && !dereference { +@@ -683,7 +772,12 @@ // Use the helper method for consistent reporting self.report_permission_change(file, fperm, mode); Ok(()) diff --git a/registry/native/patches/crates/uu_ls/0001-wasi-host-fs-mode-display.patch b/registry/native/patches/crates/uu_ls/0001-wasi-host-fs-mode-display.patch index 460cf4f41..ff68f349e 100644 --- a/registry/native/patches/crates/uu_ls/0001-wasi-host-fs-mode-display.patch +++ b/registry/native/patches/crates/uu_ls/0001-wasi-host-fs-mode-display.patch @@ -9,7 +9,7 @@ fsext::{MetadataTimeField, metadata_get_time}, line_ending::LineEnding, os_str_as_bytes_lossy, -@@ -77,6 +77,38 @@ +@@ -77,6 +77,60 @@ translate, version_cmp::version_cmp, }; @@ -18,37 +18,59 @@ + +#[cfg(target_os = "wasi")] +mod wasi_host_fs { ++ use std::env; ++ + use super::{Metadata, Path}; + + mod host_fs { + #[link(wasm_import_module = "host_fs")] + unsafe extern "C" { -+ pub fn path_mode(path_ptr: *const u8, path_len: u32, follow_symlinks: u32) -> u32; ++ pub fn path_mode( ++ fd: u32, ++ path_ptr: *const u8, ++ path_len: u32, ++ follow_symlinks: u32, ++ ) -> u32; + } + } + -+ pub fn mode_for_path(path: &Path, metadata: &Metadata, follow_symlinks: bool) -> u32 { ++ fn fallback_mode(metadata: &Metadata) -> u32 { ++ if metadata.is_dir() { 0o040755 } else { 0o100644 } ++ } ++ ++ fn raw_mode_for_path(path: &Path, follow_symlinks: bool) -> u32 { + let Some(path_str) = path.to_str() else { -+ return if metadata.is_dir() { 0o040755 } else { 0o100644 }; ++ return 0; + }; -+ let mode = unsafe { ++ unsafe { + host_fs::path_mode( ++ 3, + path_str.as_ptr(), + path_str.len() as u32, + if follow_symlinks { 1 } else { 0 }, + ) -+ }; -+ if mode == 0 { -+ if metadata.is_dir() { 0o040755 } else { 0o100644 } -+ } else { ++ } ++ } ++ ++ pub fn mode_for_path(path: &Path, metadata: &Metadata, follow_symlinks: bool) -> u32 { ++ let mode = raw_mode_for_path(path, follow_symlinks); ++ if mode != 0 { + mode ++ } else if path.is_relative() { ++ env::current_dir() ++ .ok() ++ .map(|cwd| raw_mode_for_path(&cwd.join(path), follow_symlinks)) ++ .filter(|mode| *mode != 0) ++ .unwrap_or_else(|| fallback_mode(metadata)) ++ } else { ++ fallback_mode(metadata) + } + } +} mod dired; use dired::{DiredOutput, is_dired_arg_present}; -@@ -2982,7 +3014,15 @@ +@@ -2982,7 +3036,15 @@ let is_acl_set = false; #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] let is_acl_set = has_acl(item.path()); diff --git a/registry/native/patches/crates/uu_stat/0001-wasi-metadata-compat.patch b/registry/native/patches/crates/uu_stat/0001-wasi-metadata-compat.patch index 4aa333abb..fc4a10e43 100644 --- a/registry/native/patches/crates/uu_stat/0001-wasi-metadata-compat.patch +++ b/registry/native/patches/crates/uu_stat/0001-wasi-metadata-compat.patch @@ -25,7 +25,7 @@ use uucore::{entries, format_usage, show_error, show_warning}; use clap::{Arg, ArgAction, ArgMatches, Command}; -@@ -23,10 +29,79 @@ +@@ -23,10 +29,85 @@ use std::ffi::{OsStr, OsString}; use std::fs::{FileType, Metadata}; use std::io::Write; @@ -79,7 +79,12 @@ + mod host_fs { + #[link(wasm_import_module = "host_fs")] + unsafe extern "C" { -+ pub fn path_mode(path_ptr: *const u8, path_len: u32, follow_symlinks: u32) -> u32; ++ pub fn path_mode( ++ fd: u32, ++ path_ptr: *const u8, ++ path_len: u32, ++ follow_symlinks: u32, ++ ) -> u32; + } + } + @@ -89,6 +94,7 @@ + }; + let mode = unsafe { + host_fs::path_mode( ++ 3, + path_str.as_ptr(), + path_str.len() as u32, + if follow_symlinks { 1 } else { 0 }, @@ -105,15 +111,16 @@ use thiserror::Error; use uucore::time::{FormatSystemTimeFallback, format_system_time, system_time_to_sec}; -@@ -1048,11 +1123,16 @@ - precision, - format, +@@ -1032,6 +1113,7 @@ + display_name: &str, + file: &OsString, + file_type: FileType, ++ effective_mode: u32, + from_user: bool, + #[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))] + follow_symbolic_links: bool, +@@ -1050,9 +1132,9 @@ } => { -+ #[cfg(target_os = "wasi")] -+ let effective_mode = -+ wasi_host_fs::mode_for_path(Path::new(file), meta, self.follow); -+ #[cfg(not(target_os = "wasi"))] -+ let effective_mode = meta.mode(); let output = match format { // access rights in octal - 'a' => OutputType::UnsignedOct(0o7777 & meta.mode()), @@ -124,7 +131,7 @@ // number of blocks allocated (see %B) 'b' => OutputType::Unsigned(meta.blocks()), -@@ -1096,9 +1176,9 @@ +@@ -1096,9 +1178,9 @@ // device number in hex 'D' => OutputType::UnsignedHex(meta.dev()), // raw mode in hex @@ -136,3 +143,23 @@ // group ID of owner 'g' => OutputType::Unsigned(meta.gid() as u64), // group name of owner +@@ -1234,6 +1316,11 @@ + match result { + Ok(meta) => { + let file_type = meta.file_type(); ++ #[cfg(target_os = "wasi")] ++ let effective_mode = ++ wasi_host_fs::mode_for_path(Path::new(&file), &meta, follow_symbolic_links); ++ #[cfg(not(target_os = "wasi"))] ++ let effective_mode = meta.mode(); + let tokens = if self.from_user + || !(file_type.is_char_device() || file_type.is_block_device()) + { +@@ -1249,6 +1336,7 @@ + &display_name, + &file, + file_type, ++ effective_mode, + self.from_user, + follow_symbolic_links, + ) { diff --git a/registry/native/scripts/patch-vendor.sh b/registry/native/scripts/patch-vendor.sh index 1c0f3c819..7a9b4f2aa 100755 --- a/registry/native/scripts/patch-vendor.sh +++ b/registry/native/scripts/patch-vendor.sh @@ -113,14 +113,15 @@ for CRATE_DIR in $CRATE_DIRS; do patch -p1 -d "$VENDOR_CRATE" < "$PATCH" > /dev/null 2>&1 echo "applied" elif patch --dry-run -R -p1 -d "$VENDOR_CRATE" < "$PATCH" > /dev/null 2>&1; then - patch -R -p1 -d "$VENDOR_CRATE" < "$PATCH" > /dev/null 2>&1 - patch -p1 -d "$VENDOR_CRATE" < "$PATCH" > /dev/null 2>&1 - echo "reapplied" + echo "already applied" else # Mixed state (e.g. an interrupted earlier run left some # hunks applied): apply the remaining hunks, tolerating # already-applied ones; fail only on genuine rejects. - OUT=$(patch -p1 -N -r /dev/null -d "$VENDOR_CRATE" < "$PATCH" 2>&1); RC=$? + set +e + OUT=$(patch -p1 -N -r /dev/null -d "$VENDOR_CRATE" < "$PATCH" 2>&1) + RC=$? + set -e if [ $RC -le 1 ] && ! echo "$OUT" | grep -q "FAILED"; then echo "converged (mixed state)" else diff --git a/registry/native/stubs/uucore/build.rs b/registry/native/stubs/uucore/build.rs index f4b36c0ac..2af31f6f2 100644 --- a/registry/native/stubs/uucore/build.rs +++ b/registry/native/stubs/uucore/build.rs @@ -234,9 +234,18 @@ fn embed_static_utility_locales( )?; let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap_or_default(); - let Some(registry_dir) = Path::new(&manifest_dir).parent() else { + let Some(native_dir) = Path::new(&manifest_dir) + .parent() + .and_then(Path::parent) + else { return Ok(()); // nothing to scan }; + let vendor_dir = native_dir.join("vendor"); + let registry_dir = if vendor_dir.exists() { + vendor_dir.as_path() + } else { + native_dir + }; // First, try to embed uucore locales - critical for common translations like "Usage:" embed_component_locales(embedded_file, locales_to_embed, "uucore", |locale| { @@ -252,16 +261,15 @@ fn embed_static_utility_locales( for entry in entries { let file_name = entry.file_name(); if let Some(dir_name) = file_name.to_str() { - // Match uu_- - if let Some((util_part, _)) = dir_name.split_once('-') { - if let Some(util_name) = util_part.strip_prefix("uu_") { - embed_component_locales( - embedded_file, - locales_to_embed, - util_name, - |locale| entry.path().join(format!("locales/{locale}.ftl")), - )?; - } + // Match cargo-vendor's uu_ directories and registry uu_- names. + let util_part = dir_name.split_once('-').map_or(dir_name, |(name, _)| name); + if let Some(util_name) = util_part.strip_prefix("uu_") { + embed_component_locales( + embedded_file, + locales_to_embed, + util_name, + |locale| entry.path().join(format!("locales/{locale}.ftl")), + )?; } } } diff --git a/registry/native/stubs/uucore/src/lib/mods/locale.rs b/registry/native/stubs/uucore/src/lib/mods/locale.rs index 7dfcdb25f..e50789f47 100644 --- a/registry/native/stubs/uucore/src/lib/mods/locale.rs +++ b/registry/native/stubs/uucore/src/lib/mods/locale.rs @@ -288,12 +288,38 @@ fn create_english_bundle_from_embedded( } fn get_message_internal(id: &str, args: Option) -> String { + ensure_localization_from_argv(); LOCALIZER.with(|lock| { lock.get() .map_or_else(|| id.to_string(), |loc| loc.format(id, args.as_ref())) // Return the key ID if localizer not initialized }) } +fn ensure_localization_from_argv() { + if LOCALIZER.with(|lock| lock.get().is_some()) { + return; + } + + thread_local! { + static LOCALIZATION_AUTO_INIT_ATTEMPTED: Cell = const { Cell::new(false) }; + } + if LOCALIZATION_AUTO_INIT_ATTEMPTED.with(|flag| flag.replace(true)) { + return; + } + + let Some(program) = std::env::args_os().next() else { + return; + }; + let Some(util_name) = Path::new(&program) + .file_name() + .and_then(|name| name.to_str()) + .map(crate::get_canonical_util_name) + else { + return; + }; + let _ = setup_localization(util_name); +} + /// Retrieves a localized message by its identifier. /// /// Looks up a message with the given ID in the current locale bundle and returns