From 31dd86017045e2591a56240433d1deae375aadac Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Mon, 21 Sep 2026 02:28:14 +0000 Subject: [PATCH 1/2] gate unix-only hardening helpers on windows --- cmd/soroban-cli/src/commands/contract/build/source_archive.rs | 1 + cmd/soroban-cli/src/config/locator.rs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/cmd/soroban-cli/src/commands/contract/build/source_archive.rs b/cmd/soroban-cli/src/commands/contract/build/source_archive.rs index 02093fe794..e7fa82dd8c 100644 --- a/cmd/soroban-cli/src/commands/contract/build/source_archive.rs +++ b/cmd/soroban-cli/src/commands/contract/build/source_archive.rs @@ -525,6 +525,7 @@ fn gzip(bytes: &[u8]) -> Result, Error> { #[cfg(test)] mod tests { use super::*; + #[cfg(unix)] use crate::config::locator::{enforce_hardened_tree, FileMode}; use sha2::{Digest, Sha256}; diff --git a/cmd/soroban-cli/src/config/locator.rs b/cmd/soroban-cli/src/config/locator.rs index f7dbb7078d..6f0b3ff941 100644 --- a/cmd/soroban-cli/src/config/locator.rs +++ b/cmd/soroban-cli/src/config/locator.rs @@ -644,6 +644,7 @@ impl Pwd for Args { /// How `enforce_hardened_tree` normalizes a file's owner bits (group/other are /// always stripped regardless). #[derive(Clone, Copy)] +#[cfg_attr(not(unix), allow(dead_code))] pub(crate) enum FileMode { /// Force every file to exactly `0o600`. Used for config files, which are /// data (never executable) and must stay owner-writable so the CLI can @@ -669,6 +670,7 @@ pub(crate) enum FileMode { /// On non-unix platforms this is a no-op; tempdirs / config dirs there rely /// on filesystem ACLs created by the higher-level APIs. #[allow(clippy::unnecessary_wraps)] +#[cfg_attr(not(unix), allow(dead_code))] pub(crate) fn enforce_hardened_tree( root: &Path, file_mode: FileMode, From bec99020804ef288dd2d4e04019837dda6ef931b Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Mon, 21 Sep 2026 07:58:47 +0000 Subject: [PATCH 2/2] put unix-only hardening helpers behind cfg(unix) --- cmd/soroban-cli/src/config/locator.rs | 81 ++++++++++++--------------- 1 file changed, 37 insertions(+), 44 deletions(-) diff --git a/cmd/soroban-cli/src/config/locator.rs b/cmd/soroban-cli/src/config/locator.rs index 6f0b3ff941..e7054a0dc9 100644 --- a/cmd/soroban-cli/src/config/locator.rs +++ b/cmd/soroban-cli/src/config/locator.rs @@ -643,8 +643,8 @@ impl Pwd for Args { /// How `enforce_hardened_tree` normalizes a file's owner bits (group/other are /// always stripped regardless). +#[cfg(unix)] #[derive(Clone, Copy)] -#[cfg_attr(not(unix), allow(dead_code))] pub(crate) enum FileMode { /// Force every file to exactly `0o600`. Used for config files, which are /// data (never executable) and must stay owner-writable so the CLI can @@ -667,59 +667,52 @@ pub(crate) enum FileMode { /// Best-effort: an entry whose `chmod` fails is skipped and traversal continues, /// so one unfixable file can't leave the rest of the tree group/other-readable. /// -/// On non-unix platforms this is a no-op; tempdirs / config dirs there rely -/// on filesystem ACLs created by the higher-level APIs. +/// Unix-only: mode bits aren't a thing on other platforms, so this doesn't +/// exist there; tempdirs / config dirs on non-unix rely on filesystem ACLs +/// created by the higher-level APIs. +#[cfg(unix)] #[allow(clippy::unnecessary_wraps)] -#[cfg_attr(not(unix), allow(dead_code))] pub(crate) fn enforce_hardened_tree( root: &Path, file_mode: FileMode, ) -> io::Result<(Vec, Vec)> { - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let mut changed_dirs = Vec::new(); - let mut changed_files = Vec::new(); - let mut stack = vec![root.to_path_buf()]; - while let Some(p) = stack.pop() { - let Ok(meta) = std::fs::symlink_metadata(&p) else { - continue; - }; - if meta.file_type().is_symlink() { - continue; + use std::os::unix::fs::PermissionsExt; + let mut changed_dirs = Vec::new(); + let mut changed_files = Vec::new(); + let mut stack = vec![root.to_path_buf()]; + while let Some(p) = stack.pop() { + let Ok(meta) = std::fs::symlink_metadata(&p) else { + continue; + }; + if meta.file_type().is_symlink() { + continue; + } + let current = meta.permissions().mode() & 0o777; + if meta.is_dir() { + if current != 0o700 + && std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o700)).is_ok() + { + changed_dirs.push(p.clone()); } - let current = meta.permissions().mode() & 0o777; - if meta.is_dir() { - if current != 0o700 - && std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o700)).is_ok() - { - changed_dirs.push(p.clone()); - } - if let Ok(entries) = std::fs::read_dir(&p) { - for entry in entries.filter_map(Result::ok) { - stack.push(entry.path()); - } - } - } else { - let target = match file_mode { - FileMode::Exact => 0o600, - // Keep the owner's bits (notably execute) but drop group/other. - FileMode::PreserveOwner => current & 0o700, - }; - if current != target - && std::fs::set_permissions(&p, std::fs::Permissions::from_mode(target)).is_ok() - { - changed_files.push(p); + if let Ok(entries) = std::fs::read_dir(&p) { + for entry in entries.filter_map(Result::ok) { + stack.push(entry.path()); } } + } else { + let target = match file_mode { + FileMode::Exact => 0o600, + // Keep the owner's bits (notably execute) but drop group/other. + FileMode::PreserveOwner => current & 0o700, + }; + if current != target + && std::fs::set_permissions(&p, std::fs::Permissions::from_mode(target)).is_ok() + { + changed_files.push(p); + } } - Ok((changed_dirs, changed_files)) - } - #[cfg(not(unix))] - { - let _ = (root, file_mode); - Ok((Vec::new(), Vec::new())) } + Ok((changed_dirs, changed_files)) } #[cfg(unix)]