From 85c5e889fbe9dd7c126c7dfb58207ce647b35723 Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Thu, 18 Sep 2025 02:25:57 -0400 Subject: [PATCH] Implement fchmodat Capability based `fchmod`. I also implemented unit tests for chmod, fchmod, and fchmodat. The unit tests check Redox-specific behavior, so some of the tests are disabled for Linux. --- src/header/sys_stat/mod.rs | 13 + src/platform/linux/mod.rs | 13 + src/platform/pal/mod.rs | 1 + src/platform/redox/mod.rs | 13 + tests/Makefile | 1 + .../bins_dynamic/sys_stat/chmod.stderr | 0 .../bins_dynamic/sys_stat/chmod.stdout | 0 .../bins_static/sys_stat/chmod.stderr | 0 .../bins_static/sys_stat/chmod.stdout | 0 tests/sys_stat/chmod.c | 254 ++++++++++++++++++ 10 files changed, 295 insertions(+) create mode 100644 tests/expected/bins_dynamic/sys_stat/chmod.stderr create mode 100644 tests/expected/bins_dynamic/sys_stat/chmod.stdout create mode 100644 tests/expected/bins_static/sys_stat/chmod.stderr create mode 100644 tests/expected/bins_static/sys_stat/chmod.stdout create mode 100644 tests/sys_stat/chmod.c diff --git a/src/header/sys_stat/mod.rs b/src/header/sys_stat/mod.rs index dc4c656405..9ffed145b0 100644 --- a/src/header/sys_stat/mod.rs +++ b/src/header/sys_stat/mod.rs @@ -82,6 +82,19 @@ pub extern "C" fn fchmod(fildes: c_int, mode: mode_t) -> c_int { Sys::fchmod(fildes, mode).map(|()| 0).or_minus_one_errno() } +#[no_mangle] +pub unsafe extern "C" fn fchmodat( + dirfd: c_int, + path: *const c_char, + mode: mode_t, + flags: c_int, +) -> c_int { + let path = CStr::from_nullable_ptr(path); + Sys::fchmodat(dirfd, path, mode, flags) + .map(|()| 0) + .or_minus_one_errno() +} + #[no_mangle] pub unsafe extern "C" fn fstat(fildes: c_int, buf: *mut stat) -> c_int { let buf = Out::nonnull(buf); diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs index 6bf219543e..dfeb2b4e3b 100644 --- a/src/platform/linux/mod.rs +++ b/src/platform/linux/mod.rs @@ -171,6 +171,19 @@ impl Pal for Sys { e_raw(unsafe { syscall!(FCHMOD, fildes, mode) }).map(|_| ()) } + fn fchmodat(dirfd: c_int, path: Option, mode: mode_t, flags: c_int) -> Result<()> { + e_raw(unsafe { + syscall!( + FCHMODAT, + dirfd, + path.map_or(core::ptr::null(), |p| p.as_ptr()), + mode, + flags + ) + }) + .map(|_| ()) + } + fn fchown(fildes: c_int, owner: uid_t, group: gid_t) -> Result<()> { e_raw(unsafe { syscall!(FCHOWN, fildes, owner, group) }).map(|_| ()) } diff --git a/src/platform/pal/mod.rs b/src/platform/pal/mod.rs index de64a37a8d..f012a549fb 100644 --- a/src/platform/pal/mod.rs +++ b/src/platform/pal/mod.rs @@ -67,6 +67,7 @@ pub trait Pal { fn fchdir(fildes: c_int) -> Result<()>; fn fchmod(fildes: c_int, mode: mode_t) -> Result<()>; + fn fchmodat(dirfd: c_int, path: Option, mode: mode_t, flags: c_int) -> Result<()>; fn fchown(fildes: c_int, owner: uid_t, group: gid_t) -> Result<()>; diff --git a/src/platform/redox/mod.rs b/src/platform/redox/mod.rs index 9d915a6a54..c5c0ee4d16 100644 --- a/src/platform/redox/mod.rs +++ b/src/platform/redox/mod.rs @@ -259,6 +259,19 @@ impl Pal for Sys { Ok(()) } + fn fchmodat(dirfd: c_int, path: Option, mode: mode_t, flags: c_int) -> Result<()> { + const MASK: c_int = !(fcntl::AT_SYMLINK_NOFOLLOW | fcntl::AT_EMPTY_PATH); + if MASK & flags != 0 { + return Err(Errno(EOPNOTSUPP)); + } + let path = path + .and_then(|cs| str::from_utf8(cs.to_bytes()).ok()) + .ok_or(Errno(ENOENT))?; + let file = cap_path_at(dirfd, path, flags, 0)?; + syscall::fchmod(*file as usize, mode as u16)?; + Ok(()) + } + fn fchown(fd: c_int, owner: uid_t, group: gid_t) -> Result<()> { syscall::fchown(fd as usize, owner as u32, group as u32)?; Ok(()) diff --git a/tests/Makefile b/tests/Makefile index 1eb8d2ee83..c63daa4e8a 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -109,6 +109,7 @@ EXPECT_NAMES=\ string/stpcpy \ string/stpncpy \ strings \ + sys_stat/chmod \ sys_stat/lstat \ sys_stat/fstatat \ sys_syslog/syslog \ diff --git a/tests/expected/bins_dynamic/sys_stat/chmod.stderr b/tests/expected/bins_dynamic/sys_stat/chmod.stderr new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/expected/bins_dynamic/sys_stat/chmod.stdout b/tests/expected/bins_dynamic/sys_stat/chmod.stdout new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/expected/bins_static/sys_stat/chmod.stderr b/tests/expected/bins_static/sys_stat/chmod.stderr new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/expected/bins_static/sys_stat/chmod.stdout b/tests/expected/bins_static/sys_stat/chmod.stdout new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/sys_stat/chmod.c b/tests/sys_stat/chmod.c new file mode 100644 index 0000000000..c89f892cd8 --- /dev/null +++ b/tests/sys_stat/chmod.c @@ -0,0 +1,254 @@ +#define _GNU_SOURCE // Linux to run locally + +#include +#include +#include +#include +#include +#include +#include + +#include + +/* #if defined(__linux__) && !defined(__redox__) */ +/* #include */ +/* #else */ +/* #define KERNEL_VERSION(x,y,z) (0) */ +/* #endif */ + +__attribute__((nonnull(2))) +static bool check_mode( + int dir, + const char path[], + int flags, + mode_t expected +) { + struct stat stat = {0}; + if (fstatat(dir, path, &stat, flags) == -1) { + perror("fstatat"); + return false; + } + + if ((stat.st_mode & 0777) != expected) { + fprintf( + stderr, + "Mode mismatch\n\tExpected: %o\n\tActual: %o\n", + expected, + stat.st_mode + ); + return false; + } + + return true; +} + +int main(void) { + int status = EXIT_FAILURE; + + char template[] = "/tmp/chmtest.XXXXXX"; + if (!mkdtemp(template)) { + perror("mkdtemp"); + goto bye; + } + size_t len = sizeof(template) - 1; + + // Create a file and a link to chmod. + const char file_name[] = "king_crimson"; + char file_path[PATH_MAX] = {0}; + memcpy(file_path, template, len); + file_path[len] = '/'; + memcpy(&file_path[len + 1], file_name, sizeof(file_name)); + + int filefd = open(file_path, O_CREAT); + if (filefd == -1) { + perror("open (file, O_CREAT)"); + goto rmtempdir; + } + + const char link_name[] = "red"; + char link_path[PATH_MAX] = {0}; + memcpy(link_path, template, len); + link_path[len] = '/'; + memcpy(&link_path[len + 1], link_name, sizeof(link_name)); + + if (symlink(file_path, link_path) == -1) { + perror("symlink"); + goto rmfiles; + } + #ifdef __redox__ + int linkfd = open(link_path, O_PATH | O_NOFOLLOW | O_SYMLINK); + if (linkfd == -1) { + perror("open (link, O_NOFOLLOW)"); + goto rmfiles; + } + #endif + + int dir = open(template, O_DIRECTORY); + if (dir == -1) { + perror("open (temp dir)"); + goto closelink; + } + + // chmod + + // File + if (chmod(file_path, 0666) == -1) { + perror("chmod file 0666"); + goto closetemp; + } + if (!check_mode(dir, file_name, 0, 0666)) { + fprintf(stderr, "Context: chmod %s\n", file_name); + goto closetemp; + } + + // Link (deferenced) + if (chmod(link_path, 0777) == -1) { + perror("chmod link 0777"); + goto closetemp; + } + if (!check_mode(dir, file_name, 0, 0777)) { + fprintf(stderr, "Context: chmod %s\n", link_name); + goto closetemp; + } + + // Dir + if (chmod(template, 0766) == -1) { + perror("chmod directory 0766"); + goto closetemp; + } + if (!check_mode(dir, "", AT_EMPTY_PATH, 0766)) { + fprintf(stderr, "Context: chmod %s\n", template); + goto closetemp; + } + + // fchmod + + // File + if (fchmod(filefd, 0644) == -1) { + perror("fchmod file 0644"); + goto closetemp; + } + if (!check_mode(dir, file_name, 0, 0644)) { + fprintf(stderr, "Context: fchmod %s\n", file_name); + goto closetemp; + } + + // Link (not followed) + #ifdef __redox__ + if (fchmod(linkfd, 0666) == -1) { + perror("fchmod link 0666"); + goto closetemp; + } + if (!check_mode(dir, link_name, AT_SYMLINK_NOFOLLOW, 0666)) { + fprintf(stderr, "Context: fchmod %s\n", link_name); + goto closetemp; + } + #endif + + // Directory + if (fchmod(dir, 0777) == -1) { + perror("fchmod directory 0777"); + goto closetemp; + } + if (!check_mode(dir, "", AT_EMPTY_PATH, 0777)) { + fprintf(stderr, "Context: fchmod %s\n", template); + goto closetemp; + } + + // fchmodat + + // File (relative) + if (fchmodat(dir, file_name, 0666, 0) == -1) { + perror("fchmodat directory 0666"); + goto closetemp; + } + if (!check_mode(dir, file_name, 0, 0666)) { + fprintf(stderr, "Context: fchmodat %s\n", file_name); + goto closetemp; + } + + // File (absolute) + if (fchmodat(dir, file_path, 0777, 0) == -1) { + perror("fchmodat file 0777"); + goto closetemp; + } + if (!check_mode(dir, file_name, 0, 0777)) { + fprintf(stderr, "Context: fchmodat %s\n", file_path); + } + + // Link (followed) + if (fchmodat(dir, link_name, 0666, 0) == -1) { + perror("fchmodat link 0666"); + goto closetemp; + } + if (!check_mode(dir, file_name, 0, 0666)) { + fprintf(stderr, "Context: fchmodat %s\n", link_name); + goto closetemp; + } + + // Link (not followed) + #ifdef __redox__ + if (fchmodat(dir, link_name, 0777, AT_SYMLINK_NOFOLLOW) == -1) { + perror("fchmodat link 0777"); + goto closetemp; + } + if (!check_mode(dir, link_name, AT_SYMLINK_NOFOLLOW, 0777)) { + fprintf(stderr, "Context: fchmodat %s\n", link_name); + goto closetemp; + } + #endif + + // Directory + // AT_EMPTY_PATH support is relatively new so this fails in CI. + #if defined(__redox__) // || LINUX_VERSION_CODE >= KERNEL_VERSION(6,6,0) + if (fchmodat(dir, "", 0700, AT_EMPTY_PATH) == -1) { + perror("fchmodat directory 0700"); + goto closetemp; + } + if (!check_mode(dir, "", AT_EMPTY_PATH, 0700)) { + fprintf(stderr, "Context: fchmodat %s\n", template); + goto closetemp; + } + + // Directory (cwd) + char old_cwd[PATH_MAX] = {0}; + if (!getcwd(old_cwd, PATH_MAX)) { + perror("getcwd"); + goto closetemp; + } + if (chdir(template) == -1) { + perror("chdir (temp dir)"); + goto closetemp; + } + + if (fchmodat(AT_FDCWD, "", 0777, AT_EMPTY_PATH) == -1) { + perror("fchmodat cwd 0777"); + goto closetemp; + } + if (!check_mode(AT_FDCWD, "", AT_EMPTY_PATH, 0777)) { + fprintf(stderr, "Context: fchmodat %s\n", template); + goto closetemp; + } + + if (chdir(old_cwd) == -1) { + perror("chdir (old cwd)"); + goto closetemp; + } + #endif + + status = EXIT_SUCCESS; +closetemp: + close(dir); +closelink: + #ifdef __redox__ + close(linkfd); + #endif +rmfiles: + close(filefd); + unlink(file_path); + unlink(link_path); +rmtempdir: + rmdir(template); +bye: + return status; +}