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
37 changes: 20 additions & 17 deletions library/std/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3448,22 +3448,26 @@ pub fn set_permissions<P: AsRef<Path>>(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:
/// * Linux, BSD-based platforms, Android, QNX: `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].
/// * 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` followed by behavior
/// denoted in [`fs::set_permissions`].
/// * Windows: `CreateFileW` with `FILE_FLAG_OPEN_REPARSE_POINT` followed
/// by `SetFileInformationByHandle`.
Comment on lines +3452 to +3460

@RalfJung RalfJung Aug 19, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// * Linux, BSD-based platforms, Android, QNX: `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].
/// * 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` followed by behavior
/// denoted in [`fs::set_permissions`].
/// * Windows: `CreateFileW` with `FILE_FLAG_OPEN_REPARSE_POINT` followed
/// by `SetFileInformationByHandle`.
/// * Linux, BSD-based platforms, Android, QNX: `fchmodat` with `AT_SYMLINK_NOFOLLOW`. If that is not supported, we fall back to
/// `open` with `O_NOFOLLOW` followed by [`fs::set_permissions`].
/// * Other Unix-based platforms with symlinks: `open` with `O_NOFOLLOW` followed by [`fs::set_permissions`].
/// * Other Unix-based platforms without symlinks: `open` followed by [`fs::set_permissions`].
/// * Windows: `CreateFileW` with `FILE_FLAG_OPEN_REPARSE_POINT` followed
/// by `SetFileInformationByHandle`.

View changes since the review

///
/// 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
///
/// This function will return an error in the following situations, but is not
Expand All @@ -3472,12 +3476,11 @@ pub fn set_permissions<P: AsRef<Path>>(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 can vary from symlink permission bits changing or
/// there being no effects on symlinks
/// Note: On Linux and other Unix-based platforms with symlinks (non-BSD-based),
/// this will result in an [`Unsupported`] error if the final element is a symlink.
///
/// [`Unsupported`]: crate::io::ErrorKind::Unsupported
/// [`FilesystemLoop`]: crate::io::ErrorKind::FilesystemLoop
///
/// # Examples
///
Expand All @@ -3488,8 +3491,8 @@ pub fn set_permissions<P: AsRef<Path>>(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 or
/// // succeed in modifying the permissions of a symlink
/// fs::set_permissions_nofollow("foo.txt", perms)?;
/// Ok(())
/// }
Expand Down
14 changes: 6 additions & 8 deletions library/std/src/fs/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -668,17 +671,12 @@ 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));
// 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
Expand Down
2 changes: 1 addition & 1 deletion library/std/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
93 changes: 67 additions & 26 deletions library/std/src/sys/fs/unix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2036,35 +2036,76 @@ 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;
#[inline]
/// Helper function for fallback open with `O_NOFOLLOW` + `fchmod` behavior
fn open_and_set_permissions(p: &CStr, perm: FilePermissions) -> io::Result<()> {
use crate::fs::{OpenOptions, Permissions};

let mut options = OpenOptions::new();
options.custom_flags(libc::O_NOFOLLOW);
let mut options = OpenOptions::new();

let bytes = p.to_bytes();
let os_str = OsStr::from_bytes(bytes);
options.open(Path::new(os_str))?.set_permissions(Permissions::from_inner(perm))
// 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(not(target_os = "wasi"))]
use crate::os::unix::fs::OpenOptionsExt;
#[cfg(target_os = "wasi")]
use crate::os::wasi::fs::OpenOptionsExt;
options.read(true).custom_flags(libc::O_NOFOLLOW);
}
all(target_os = "linux", 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(|_| ())
},
_ => {
cvt_r(|| unsafe {
libc::fchmodat(libc::AT_FDCWD, p.as_ptr(), perm.mode, 0)
})
.map(|_| ())

#[cfg(not(target_os = "wasi"))]
use crate::os::unix::ffi::OsStrExt;
#[cfg(target_os = "wasi")]
use crate::os::wasi::ffi::OsStrExt;

let os_str = OsStr::from_bytes(p.to_bytes());
options.open(Path::new(os_str))?.set_permissions(Permissions::from_inner(perm))
}

let mut _res: Result<(), core::io::Error> = Err(crate::io::ErrorKind::Unsupported.into());

// These platforms support `fchmodat`, so utilize this syscall over `open` + `fchmod`
#[cfg(any(
target_os = "linux",
target_os = "macos",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
target_os = "android",
target_os = "qnx"
))]
{
_res = cvt_r(|| unsafe {
libc::fchmodat(libc::AT_FDCWD, p.as_ptr(), perm.mode, libc::AT_SYMLINK_NOFOLLOW)
})
.map(|_| ());
}

// If fchmodat fails with `ErrorKind::Unsupported` fallback to using open + fchmod. This is just in case
// for older systems like Ubuntu 20.04 where fchmodat fails with EOPNOTSUPP on both regular files and
// symlinks when AT_SYMLINK_NOFOLLOW is passed in.
match _res {
Ok(_) => Ok(()),
Err(err) => {
if err.kind() == crate::io::ErrorKind::Unsupported {
match open_and_set_permissions(p, perm) {
Ok(_) => return Ok(()),
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`.

@RalfJung RalfJung Aug 19, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this comment. Which "other distributions"? Do different Linuxes behave different here, despite all using the same kernel? Which system returns "unsupported" to indicate a symlink?

View changes since the review

@asder8215 asder8215 Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See Rachel's comment earlier. Ubuntu 20.04 throws ENOTSUP when using fchmodat with AT_SYMLINK_NOFOLLOW regardless if the file is a symlink or not. For that platform specifically, we have to use open with O_NOFOLLOW flag and then fchmod. open with O_NOFOLLOW would throw an ELOOP error in this scenario.

Maybe consistency was a bad choice of word here, but I thought it was better to keep the error message to what fchmodat would fail with (ENOTSUPP/Unsupported) instead of using ELOOP here (other reason why is because FilesystemLoop error is unstable and Unsupported is stable, so maybe convenient for those to error handle this without needing nightly features).

@RalfJung RalfJung Aug 19, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't that just mean that Ubuntu 20 has an old kernel that does not yet support fchmodat? We then trigger the fallback behavior anyway so the user never sees that. What kind of consistency are you trying to achieve here...?

@asder8215 asder8215 Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fchmodat with AT_SYMLINK_NOFOLLOW in later versions of Ubuntu functions normally with setting permissions on regular files/directories and throws Unsupported on symlinks.

And the consistency thing is bad wording on my end. I might've even wrote that comment before getting rid of the cfg_select branches because we had other Unix/Linux platforms solely calling fchmodat and other platforms just calling fchmodat with fallback behavior to open and fchmod on Unsupported error. Now, I just have every platform that supports fchmodat call fchmodat and everything will fall to use open and fchmod.

I'll make changes to the comment in an amended commit later today.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fchmodat with AT_SYMLINK_NOFOLLOW in later versions of Ubuntu functions normally with setting permissions on regular files/directories and throws Unsupported on symlinks.

I assume not just later Ubuntu, but all recent Linux distros? Ubuntu uses a fairly standard kernel AFAIK?

The man page just says

     AT_SYMLINK_NOFOLLOW
            If path is a symbolic link, do not dereference it: instead operate on the link itself.

so it is a bit odd that we get "unsupported" at all...

return Err(err);
}
return Err(e);
}
}
}

Err(err)
}
}
}
Expand Down
Loading