From 69de9ae0818dbbc6dd4c811fc76bdab3c294b3d2 Mon Sep 17 00:00:00 2001 From: Mahdi Ali-Raihan Date: Wed, 29 Jul 2026 13:21:59 -0400 Subject: [PATCH 1/3] Refactored std::fs::set_permissions_nofollow docs clarifying behavior on different platforms + fixed BSD-based systems to use fchmodat with AT_SYMLINK_NOFOLLOW --- library/std/src/fs.rs | 26 +++++++++++--------------- library/std/src/fs/tests.rs | 7 +------ library/std/src/sys/fs/unix.rs | 4 +++- 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/library/std/src/fs.rs b/library/std/src/fs.rs index 4c5cd0e0c9e6a..86e68f8f3ac65 100644 --- a/library/std/src/fs.rs +++ b/library/std/src/fs.rs @@ -3448,17 +3448,12 @@ pub fn set_permissions>(path: P, perm: Permissions) -> io::Result /// /// # Platform-specific behavior /// -/// This function currently corresponds to: -/// * `open` with `O_NOFOLLOW` flag enabled + `fchmod` on WASI -/// * `fchmodat` function with the flag `AT_SYMLINK_NOFOLLOW` enabled -/// on Unix platforms -/// * The flag `FILE_FLAG_OPEN_REPARSE_POINT` is enabled and then the -/// permissions of the file is set through `SetFileInformationByHandle` -/// on Windows. -/// * On all other platforms, the behavior remains the same with -/// [`fs::set_permissions`]. -/// -/// [`fs::set_permissions`]: crate::fs::set_permissions +/// This function currently corresponds to the following underlying operations: +/// * WASI: `open` with `O_NOFOLLOW` followed by `fchmod`. +/// * Unix: `fchmodat` with `AT_SYMLINK_NOFOLLOW` (or no flag is set if `AT_SYMLINK_NOFOLLOW` does +/// not exist on a specific Unix-based platform) +/// * Windows: `CreateFileW` with `FILE_FLAG_OPEN_REPARSE_POINT` followed +/// by `SetFileInformationByHandle`. /// /// Note that, this [may change in the future][changes]. /// @@ -3474,8 +3469,8 @@ pub fn set_permissions>(path: P, perm: Permissions) -> io::Result /// /// Note: On Linux, this will result in a [`Unsupported`] error /// if the final element is a symlink. On BSD-based systems, the -/// behavior can vary from symlink permission bits changing or -/// there being no effects on symlinks +/// behavior in this case can vary: the operation may have no effect at all +/// or it may change the permission bits of the symlink itself. /// /// [`Unsupported`]: crate::io::ErrorKind::Unsupported /// @@ -3488,8 +3483,9 @@ pub fn set_permissions>(path: P, perm: Permissions) -> io::Result /// fn main() -> std::io::Result<()> { /// let mut perms = fs::symlink_metadata("foo.txt")?.permissions(); /// perms.set_readonly(true); -/// // This should result in an error on certain platforms -/// // or succeed in modifying the permissions of a symlink +/// // This should result in an error on certain platforms, +/// // succeed in modifying the permissions of a symlink, +/// // or do nothing at all. /// fs::set_permissions_nofollow("foo.txt", perms)?; /// Ok(()) /// } diff --git a/library/std/src/fs/tests.rs b/library/std/src/fs/tests.rs index f0dbe3e76984a..29d7428f0b5df 100644 --- a/library/std/src/fs/tests.rs +++ b/library/std/src/fs/tests.rs @@ -671,14 +671,9 @@ fn set_get_permissions_nofollows_symlink() { any(windows, target_os = "android", target_os = "macos", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly") => { assert_eq!(result.unwrap(), ()); let metadata0 = check!(fs::symlink_metadata(&symlink_name)); - // So seems like BSD-based systems trying to set permissions - // on symlinks could lead to no effect, so we should expect - // there being no change to BSD-based systems. + // On these systems, it's confirmed the symlink itself is marked readonly // https://superuser.com/questions/1099634/change-permissions-symbolic-link-mac-os - #[cfg(windows)] assert!(metadata0.permissions().readonly()); - #[cfg(not(windows))] - assert!(!metadata0.permissions().readonly()); // Reset the read-only bit under Windows 7: avoids the // `TempDir::drop` from crashing on a permission denial when diff --git a/library/std/src/sys/fs/unix.rs b/library/std/src/sys/fs/unix.rs index 147234f345f45..1edb99deea73c 100644 --- a/library/std/src/sys/fs/unix.rs +++ b/library/std/src/sys/fs/unix.rs @@ -2054,13 +2054,15 @@ pub fn set_perm_nofollow(p: &CStr, perm: FilePermissions) -> io::Result<()> { let os_str = OsStr::from_bytes(bytes); options.open(Path::new(os_str))?.set_permissions(Permissions::from_inner(perm)) } - all(target_os = "linux", not(any(target_os = "espidf", target_os = "horizon"))) => { + all(any(target_os = "linux", target_os = "macos", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly", target_os = "android"), not(any(target_os = "espidf", target_os = "horizon"))) => { cvt_r(|| unsafe { libc::fchmodat(libc::AT_FDCWD, p.as_ptr(), perm.mode, libc::AT_SYMLINK_NOFOLLOW) }) .map(|_| ()) }, _ => { + // These platforms do not have `AT_SYMLINK_NOFOLLOW` but support fchmodat, + // so no flag is set for fchmodat. cvt_r(|| unsafe { libc::fchmodat(libc::AT_FDCWD, p.as_ptr(), perm.mode, 0) }) From a79354689c59b8d7a3abfee88c81ae86b6076feb Mon Sep 17 00:00:00 2001 From: Mahdi Ali-Raihan Date: Thu, 30 Jul 2026 13:07:42 -0400 Subject: [PATCH 2/3] Refactored documentation for std::fs::set_permissions_nofollow, refactored non-BSD-based/non-Linux platforms to use OpenOptions open + set_permissions, and refactored tests accordingly --- library/std/src/fs.rs | 23 +++++++++-------- library/std/src/fs/tests.rs | 7 +++-- library/std/src/path.rs | 2 +- library/std/src/sys/fs/unix.rs | 47 +++++++++++++++++----------------- 4 files changed, 42 insertions(+), 37 deletions(-) diff --git a/library/std/src/fs.rs b/library/std/src/fs.rs index 86e68f8f3ac65..3aa6bd6cfdf74 100644 --- a/library/std/src/fs.rs +++ b/library/std/src/fs.rs @@ -3449,15 +3449,18 @@ pub fn set_permissions>(path: P, perm: Permissions) -> io::Result /// # Platform-specific behavior /// /// This function currently corresponds to the following underlying operations: -/// * WASI: `open` with `O_NOFOLLOW` followed by `fchmod`. -/// * Unix: `fchmodat` with `AT_SYMLINK_NOFOLLOW` (or no flag is set if `AT_SYMLINK_NOFOLLOW` does -/// not exist on a specific Unix-based platform) +/// * Linux, BSD-based platforms, Android: `fchmodat` with `AT_SYMLINK_NOFOLLOW`. +/// * Other Unix-based platforms with symlinks: `open` with `O_NOFOLLOW` followed by behavior +/// denoted in [`fs::set_permissions`]. +/// * Other Unix-based platforms without symlinks: `open` with followed by behavior +/// denoted in [`fs::set_permissions`]. /// * Windows: `CreateFileW` with `FILE_FLAG_OPEN_REPARSE_POINT` followed /// by `SetFileInformationByHandle`. /// /// Note that, this [may change in the future][changes]. /// /// [changes]: io#platform-specific-behavior +/// [`fs::set_permissions`]: crate::fs::set_permissions /// /// # Errors /// @@ -3467,12 +3470,13 @@ pub fn set_permissions>(path: P, perm: Permissions) -> io::Result /// * `path` does not exist. /// * The user lacks the permission to change attributes of the file. /// -/// Note: On Linux, this will result in a [`Unsupported`] error -/// if the final element is a symlink. On BSD-based systems, the -/// behavior in this case can vary: the operation may have no effect at all -/// or it may change the permission bits of the symlink itself. +/// Note: On Linux, this will result in an [`Unsupported`] error +/// if the final element is a symlink. On other Unix-based platforms +/// with symlinks (non-BSD-based), this will result in an [`InvalidInput`] +/// error. /// /// [`Unsupported`]: crate::io::ErrorKind::Unsupported +/// [`InvalidInput`]: crate::io::ErrorKind::InvalidInput /// /// # Examples /// @@ -3483,9 +3487,8 @@ pub fn set_permissions>(path: P, perm: Permissions) -> io::Result /// fn main() -> std::io::Result<()> { /// let mut perms = fs::symlink_metadata("foo.txt")?.permissions(); /// perms.set_readonly(true); -/// // This should result in an error on certain platforms, -/// // succeed in modifying the permissions of a symlink, -/// // or do nothing at all. +/// // This should result in an error on certain platforms or +/// // succeed in modifying the permissions of a symlink /// fs::set_permissions_nofollow("foo.txt", perms)?; /// Ok(()) /// } diff --git a/library/std/src/fs/tests.rs b/library/std/src/fs/tests.rs index 29d7428f0b5df..47c9366ccfe27 100644 --- a/library/std/src/fs/tests.rs +++ b/library/std/src/fs/tests.rs @@ -649,7 +649,10 @@ fn set_get_permissions_nofollows() { // Only Windows and Unix support `fs::set_permissions_nofollow` #[test] -#[cfg(all(any(windows, unix), not(any(target_os = "espidf", target_os = "horizon"))))] +#[cfg(all( + any(windows, unix), + not(any(target_os = "espidf", target_os = "horizon", target_os = "wasi")) +))] fn set_get_permissions_nofollows_symlink() { #[cfg(not(windows))] use crate::os::unix::fs::symlink as symlink_dir; @@ -668,7 +671,7 @@ fn set_get_permissions_nofollows_symlink() { let result = fs::set_permissions_nofollow(&symlink_name, permission_bits); cfg_select! { - any(windows, target_os = "android", target_os = "macos", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly") => { + any(windows, target_os = "macos", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly") => { assert_eq!(result.unwrap(), ()); let metadata0 = check!(fs::symlink_metadata(&symlink_name)); // On these systems, it's confirmed the symlink itself is marked readonly diff --git a/library/std/src/path.rs b/library/std/src/path.rs index be216d87f3241..38e83464294a4 100644 --- a/library/std/src/path.rs +++ b/library/std/src/path.rs @@ -2377,7 +2377,7 @@ pub struct NormalizeError; impl Path { // The following (private!) function allows construction of a path from a u8 // slice, which is only safe when it is known to follow the OsStr encoding. - unsafe fn from_u8_slice(s: &[u8]) -> &Path { + pub(crate) unsafe fn from_u8_slice(s: &[u8]) -> &Path { unsafe { Path::new(OsStr::from_encoded_bytes_unchecked(s)) } } // The following (private!) function reveals the byte encoding used for OsStr. diff --git a/library/std/src/sys/fs/unix.rs b/library/std/src/sys/fs/unix.rs index 1edb99deea73c..87982dcfa6f9b 100644 --- a/library/std/src/sys/fs/unix.rs +++ b/library/std/src/sys/fs/unix.rs @@ -2036,37 +2036,36 @@ pub fn set_perm(p: &CStr, perm: FilePermissions) -> io::Result<()> { } pub fn set_perm_nofollow(p: &CStr, perm: FilePermissions) -> io::Result<()> { - // ESP-IDF and Horizon do not support O_NOFOLLOW, so we skip setting it. - // Their filesystems do not have symbolic links, so no special handling is required. cfg_select! { - // wasm32-wasip1 targets do not support fchmodat, so we fall down to - // open + fchmod - target_os = "wasi" => { - use crate::fs::OpenOptions; - use crate::fs::Permissions; - use crate::os::wasi::ffi::OsStrExt; - use crate::os::wasi::fs::OpenOptionsExt; - - let mut options = OpenOptions::new(); - options.custom_flags(libc::O_NOFOLLOW); - - let bytes = p.to_bytes(); - let os_str = OsStr::from_bytes(bytes); - options.open(Path::new(os_str))?.set_permissions(Permissions::from_inner(perm)) - } - all(any(target_os = "linux", target_os = "macos", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly", target_os = "android"), not(any(target_os = "espidf", target_os = "horizon"))) => { + any(target_os = "linux", target_os = "macos", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly", target_os = "android") => { cvt_r(|| unsafe { libc::fchmodat(libc::AT_FDCWD, p.as_ptr(), perm.mode, libc::AT_SYMLINK_NOFOLLOW) }) .map(|_| ()) }, + // Not all targets support fchmodat, so we fall back to + // open + fchmod. _ => { - // These platforms do not have `AT_SYMLINK_NOFOLLOW` but support fchmodat, - // so no flag is set for fchmodat. - cvt_r(|| unsafe { - libc::fchmodat(libc::AT_FDCWD, p.as_ptr(), perm.mode, 0) - }) - .map(|_| ()) + use crate::fs::OpenOptions; + use crate::fs::Permissions; + let mut options = OpenOptions::new(); + // ESP-IDF and Horizon do not support O_NOFOLLOW, so we skip setting it. + // Their filesystems do not have symbolic links, so no special handling is required. + #[cfg(not(any(target_os = "espidf", target_os = "horizon")))] + { + #[cfg(target_os = "wasi")] + use crate::os::wasi::fs::OpenOptionsExt; + #[cfg(not(target_os = "wasi"))] + use crate::os::unix::fs::OpenOptionsExt; + options.custom_flags(libc::O_NOFOLLOW); + } + + // SAFETY: Since this function is called with `with_native_path` + // and that successfully converted the `&Path` to a `CString`, it + // should be safe to slice away the nul byte from `&CStr` and convert + // it back to a `&Path`. + let path = unsafe { Path::from_u8_slice(p.to_bytes()) }; + options.open(path)?.set_permissions(Permissions::from_inner(perm)) } } } From c1de9c93634abc2aed66f388bd6e08b4d6546d6a Mon Sep 17 00:00:00 2001 From: Mahdi Ali-Raihan Date: Mon, 10 Aug 2026 13:58:40 -0400 Subject: [PATCH 3/3] Add fallback behavior on Linux to use open + fchmod if fchmodat returns ENOTSUP (e.g. for Ubuntu 20.04 returns ENOTSUP on non-symlinks + symlinks when using fchmodat with AT_SYMLINK_NOFOLLOW). Update docs accordingly as well and corrected behavior + docs for other Unix platforms with symlinks should return `FilesystemLoop` error instead of `InvalidInput` due to not setting `OpenOptions` with read enabled. Co-authored-by: Rachel Barker --- library/std/src/fs.rs | 15 +++++++++--- library/std/src/sys/fs/unix.rs | 45 ++++++++++++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/library/std/src/fs.rs b/library/std/src/fs.rs index 3aa6bd6cfdf74..828ab0c0248e6 100644 --- a/library/std/src/fs.rs +++ b/library/std/src/fs.rs @@ -3449,17 +3449,24 @@ pub fn set_permissions>(path: P, perm: Permissions) -> io::Result /// # Platform-specific behavior /// /// This function currently corresponds to the following underlying operations: -/// * Linux, BSD-based platforms, Android: `fchmodat` with `AT_SYMLINK_NOFOLLOW`. +/// * Linux: `fchmodat` with `AT_SYMLINK_NOFOLLOW` with a fallback behavior to use +/// `open` with `O_NOFOLLOW` followed by behavior denoted in [`fs::set_permissions`] when +/// the former `fchmodat` call errors with `ENOTSUP`[^1]. +/// * BSD-based platforms, Android: `fchmodat` with `AT_SYMLINK_NOFOLLOW` /// * Other Unix-based platforms with symlinks: `open` with `O_NOFOLLOW` followed by behavior /// denoted in [`fs::set_permissions`]. -/// * Other Unix-based platforms without symlinks: `open` with followed by behavior +/// * Other Unix-based platforms without symlinks: `open` followed by behavior /// denoted in [`fs::set_permissions`]. /// * Windows: `CreateFileW` with `FILE_FLAG_OPEN_REPARSE_POINT` followed /// by `SetFileInformationByHandle`. /// /// Note that, this [may change in the future][changes]. /// +/// [^1]: Ubuntu 20.04, for example, makes `fchmodat` with `AT_SYMLINK_NOFOLLOW` return `ENOTSUP` +/// on both symlinks and non-symlinks +/// /// [changes]: io#platform-specific-behavior +/// /// [`fs::set_permissions`]: crate::fs::set_permissions /// /// # Errors @@ -3472,11 +3479,11 @@ pub fn set_permissions>(path: P, perm: Permissions) -> io::Result /// /// Note: On Linux, this will result in an [`Unsupported`] error /// if the final element is a symlink. On other Unix-based platforms -/// with symlinks (non-BSD-based), this will result in an [`InvalidInput`] +/// with symlinks (non-BSD-based), this will result in a [`FilesystemLoop`] /// error. /// /// [`Unsupported`]: crate::io::ErrorKind::Unsupported -/// [`InvalidInput`]: crate::io::ErrorKind::InvalidInput +/// [`FilesystemLoop`]: crate::io::ErrorKind::FilesystemLoop /// /// # Examples /// diff --git a/library/std/src/sys/fs/unix.rs b/library/std/src/sys/fs/unix.rs index 87982dcfa6f9b..ab75132231299 100644 --- a/library/std/src/sys/fs/unix.rs +++ b/library/std/src/sys/fs/unix.rs @@ -2037,7 +2037,48 @@ pub fn set_perm(p: &CStr, perm: FilePermissions) -> io::Result<()> { pub fn set_perm_nofollow(p: &CStr, perm: FilePermissions) -> io::Result<()> { cfg_select! { - any(target_os = "linux", target_os = "macos", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly", target_os = "android") => { + target_os = "linux" => { + let res = cvt_r(|| unsafe { + libc::fchmodat(libc::AT_FDCWD, p.as_ptr(), perm.mode, libc::AT_SYMLINK_NOFOLLOW) + }) + .map(|_| ()); + + match res { + Ok(_) => return Ok(()), + Err(err) => { + if err.kind() == crate::io::ErrorKind::Unsupported { + use crate::fs::OpenOptions; + use crate::fs::Permissions; + use crate::os::unix::ffi::OsStrExt; + use crate::os::unix::fs::OpenOptionsExt; + + let mut options = OpenOptions::new(); + options.read(true).custom_flags(libc::O_NOFOLLOW); + + let os_str = OsStr::from_bytes(p.to_bytes()); + let path = Path::new(os_str); + match options.open(path) { + Ok(file) => { + return file.set_permissions(Permissions::from_inner(perm)); + }, + Err(e) => { + if e.kind() == crate::io::ErrorKind::FilesystemLoop { + // When O_NOFOLLOW flag is enabled, if the trailing component of + // a path is a symbolic link, open should fail with ELOOP error + // For consistency with other Linux distributions, we return + // `ErrorKind::Unsupported`. + return Err(err); + } + return Err(e); + } + } + } + + return Err(err); + } + } + } + any(target_os = "macos", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly", target_os = "android") => { cvt_r(|| unsafe { libc::fchmodat(libc::AT_FDCWD, p.as_ptr(), perm.mode, libc::AT_SYMLINK_NOFOLLOW) }) @@ -2057,7 +2098,7 @@ pub fn set_perm_nofollow(p: &CStr, perm: FilePermissions) -> io::Result<()> { use crate::os::wasi::fs::OpenOptionsExt; #[cfg(not(target_os = "wasi"))] use crate::os::unix::fs::OpenOptionsExt; - options.custom_flags(libc::O_NOFOLLOW); + options.read(true).custom_flags(libc::O_NOFOLLOW); } // SAFETY: Since this function is called with `with_native_path`