diff --git a/src/cage/src/cage.rs b/src/cage/src/cage.rs index f57fd4fd7..783c4dcf5 100644 --- a/src/cage/src/cage.rs +++ b/src/cage/src/cage.rs @@ -9,7 +9,7 @@ pub use once_cell::sync::Lazy; /// interaction and increases efficiency. pub use parking_lot::{Mutex, RwLock}; pub use std::path::{Path, PathBuf}; -pub use std::sync::atomic::{AtomicBool, AtomicI32, AtomicPtr, AtomicU64, Ordering}; +pub use std::sync::atomic::{AtomicBool, AtomicI32, AtomicPtr, AtomicU32, AtomicU64, Ordering}; pub use std::sync::Arc; use sysdefs::constants::lind_platform_const::MAX_CAGEID; use sysdefs::constants::sys_const::EXIT_SUCCESS; @@ -251,6 +251,9 @@ pub struct Cage { /// Could also be moved to wasmtime/crate/lind-3i if grate_inflight /// tracking is considered a VMContext-level concern. pub grate_inflight: AtomicU64, + /// The file mode creation mask for this cage. Applied during file/directory + /// creation to restrict permissions. Mirrors Linux per-process umask. + pub umask: AtomicU32, } /// We achieve an O(1) complexity for our cage map implementation through the following three approaches: @@ -464,6 +467,7 @@ mod tests { exit_group_initiated: AtomicBool::new(false), is_dead: AtomicBool::new(false), grate_inflight: AtomicU64::new(0), + umask: AtomicU32::new(0o022), }; add_cage(2, test_cage); diff --git a/src/glibc/lind_syscall/lind_syscall_num.h b/src/glibc/lind_syscall/lind_syscall_num.h index 04eeff2a7..0ac1a9a46 100644 --- a/src/glibc/lind_syscall/lind_syscall_num.h +++ b/src/glibc/lind_syscall/lind_syscall_num.h @@ -101,6 +101,7 @@ #define FCHMOD_SYSCALL 91 #define CHOWN_SYSCALL 92 #define LCHOWN_SYSCALL 94 +#define UMASK_SYSCALL 95 #define GETUID_SYSCALL 102 #define GETGID_SYSCALL 104 diff --git a/src/glibc/sysdeps/unix/sysv/linux/umask.c b/src/glibc/sysdeps/unix/sysv/linux/umask.c index 3872dfbbc..0be21f4bb 100644 --- a/src/glibc/sysdeps/unix/sysv/linux/umask.c +++ b/src/glibc/sysdeps/unix/sysv/linux/umask.c @@ -1,9 +1,13 @@ #include #include #include +#include +#include mode_t umask (mode_t mask) { - return -1; + return (mode_t) MAKE_LEGACY_SYSCALL ( + UMASK_SYSCALL, "syscall|umask", (uint64_t) mask, + NOTUSED, NOTUSED, NOTUSED, NOTUSED, NOTUSED, TRANSLATE_ERRNO_ON); } diff --git a/src/rawposix/src/fs_calls.rs b/src/rawposix/src/fs_calls.rs index 22acff909..1e6b897ec 100644 --- a/src/rawposix/src/fs_calls.rs +++ b/src/rawposix/src/fs_calls.rs @@ -6,6 +6,8 @@ use cage::{ use dashmap::mapref::entry::Entry::{Occupied, Vacant}; use fdtables; use libc::c_void; +use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU32, AtomicU64}; +use std::sync::atomic::{Ordering, Ordering::*}; use std::sync::Arc; use sysdefs::constants::err_const::{get_errno, handle_errno, syscall_error, Errno}; use sysdefs::constants::fs_const::{ @@ -86,6 +88,7 @@ pub extern "C" fn openat_syscall( }; let oflag = sc_convert_sysarg_to_i32(oflag_arg, oflag_cageid, cageid); let mode = sc_convert_sysarg_to_u32(mode_arg, mode_cageid, cageid); + if !(sc_unusedarg(arg5, arg5_cageid) && sc_unusedarg(arg6, arg6_cageid)) { panic!( "{}: unused arguments contain unexpected values -- security violation", @@ -128,8 +131,11 @@ pub extern "C" fn openat_syscall( Err(_) => return syscall_error(Errno::EINVAL, "openat", "invalid path"), }; - let kernel_fd = - unsafe { libc::openat(host_fd, c_path.as_ptr(), oflag, mode as libc::mode_t) }; + let cage = get_cage(cageid).unwrap(); + let umask = cage.umask.load(Ordering::Relaxed); + let masked_mode = (mode & !umask) as libc::mode_t; + let kernel_fd = unsafe { libc::openat(host_fd, c_path.as_ptr(), oflag, masked_mode) }; + if kernel_fd < 0 { return handle_errno(get_errno(), "openat_syscall"); } @@ -201,6 +207,10 @@ pub extern "C" fn open_syscall( ); } + let cage = get_cage(cageid).unwrap(); + let umask = cage.umask.load(Ordering::Relaxed); + let mode = mode & !umask; + // Get the kernel fd first let kernel_fd = unsafe { libc::open(path.as_ptr(), oflag, mode) }; @@ -502,6 +512,10 @@ pub extern "C" fn mkdir_syscall( ); } + let cage = get_cage(cageid).unwrap(); + let umask = cage.umask.load(Ordering::Relaxed); + let mode = mode & !umask; + let ret = unsafe { libc::mkdir(path.as_ptr(), mode) }; // Error handling if ret < 0 { @@ -557,6 +571,9 @@ pub extern "C" fn mknod_syscall( "mknod_syscall" ); } + let cage = get_cage(cageid).unwrap(); + let umask = cage.umask.load(Ordering::Relaxed); + let mode = mode & !umask; let ret = unsafe { libc::mknod(path.as_ptr(), mode, dev) }; if ret < 0 { @@ -5524,3 +5541,52 @@ pub extern "C" fn listxattr_syscall( // Convert ssize_t to i32 safely ret.try_into().unwrap_or(i32::MAX) } + +/// Reference to Linux: https://man7.org/linux/man-pages/man2/umask.2.html +/// +/// Linux `umask()` syscall sets the calling process's file mode creation mask +/// to `mask & 0777` and returns the previous value of the mask. The umask is +/// applied during file/directory creation (open, mkdir, mknod) to restrict +/// permissions. Since each cage is isolated, the umask is stored per-cage in +/// the Cage struct rather than delegating to the host kernel. +/// +/// ## Input: +/// - cageid: current cage identifier +/// - mask_arg: the new umask value (only the lower 9 bits are used) +/// - arg2-arg6: additional arguments which are expected to be unused +/// +/// ## Returns: +/// - Always succeeds and returns the previous umask value. +pub extern "C" fn umask_syscall( + cageid: u64, + mask_arg: u64, + mask_cageid: u64, + arg2: u64, + arg2_cageid: u64, + arg3: u64, + arg3_cageid: u64, + arg4: u64, + arg4_cageid: u64, + arg5: u64, + arg5_cageid: u64, + arg6: u64, + arg6_cageid: u64, +) -> i32 { + let mask = sc_convert_sysarg_to_u32(mask_arg, mask_cageid, cageid); + + if !(sc_unusedarg(arg2, arg2_cageid) + && sc_unusedarg(arg3, arg3_cageid) + && sc_unusedarg(arg4, arg4_cageid) + && sc_unusedarg(arg5, arg5_cageid) + && sc_unusedarg(arg6, arg6_cageid)) + { + panic!( + "{}: unused arguments contain unexpected values -- security violation", + "umask_syscall" + ); + } + + let cage = get_cage(cageid).unwrap(); + let old_mask = cage.umask.swap(mask & 0o777, Ordering::SeqCst); + old_mask as i32 +} diff --git a/src/rawposix/src/init.rs b/src/rawposix/src/init.rs index 5cf3f5423..1ec15ea4e 100644 --- a/src/rawposix/src/init.rs +++ b/src/rawposix/src/init.rs @@ -7,7 +7,7 @@ use fdtables; use parking_lot::{Mutex, RwLock}; use std::ffi::CString; use std::path::PathBuf; -use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU64, Ordering::*}; +use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU32, AtomicU64, Ordering::*}; use std::sync::Arc; use sysdefs::constants::{ EXIT_SUCCESS, FDKIND_KERNEL, INIT_CAGEID, MAIN_THREADID, RAWPOSIX_CAGEID, STDERR_FILENO, @@ -287,6 +287,7 @@ pub fn rawposix_start(verbosity: isize) { exit_group_initiated: AtomicBool::new(false), is_dead: AtomicBool::new(false), grate_inflight: AtomicU64::new(0), + umask: AtomicU32::new(0o022), // POSIX default: masks group-write and other-write bits }; // Add cage to cagetable diff --git a/src/rawposix/src/sys_calls.rs b/src/rawposix/src/sys_calls.rs index 48d6f59e6..f7ccbc6ca 100644 --- a/src/rawposix/src/sys_calls.rs +++ b/src/rawposix/src/sys_calls.rs @@ -12,8 +12,8 @@ use libc::sched_yield; use parking_lot::{Mutex, RwLock}; use std::ffi::CString; use std::path::PathBuf; -use std::sync::atomic::Ordering::*; -use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU64}; +use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU32, AtomicU64}; +use std::sync::atomic::{Ordering, Ordering::*}; use std::sync::Arc; use std::time::Duration; use sysdefs::constants::err_const::{syscall_error, Errno, VERBOSE}; @@ -148,6 +148,7 @@ pub extern "C" fn fork_syscall( exit_group_initiated: AtomicBool::new(false), is_dead: AtomicBool::new(false), grate_inflight: AtomicU64::new(0), + umask: AtomicU32::new(selfcage.umask.load(Ordering::SeqCst)), }; // increment child counter for parent diff --git a/src/rawposix/src/syscall_table.rs b/src/rawposix/src/syscall_table.rs index 0d80dc8a3..f18ff9d5e 100644 --- a/src/rawposix/src/syscall_table.rs +++ b/src/rawposix/src/syscall_table.rs @@ -16,8 +16,8 @@ use super::fs_calls::{ read_syscall, readlink_syscall, readlinkat_syscall, readv_syscall, rename_syscall, renameat2_syscall, renameat_syscall, rmdir_syscall, setxattr_syscall, shmat_syscall, shmctl_syscall, shmdt_syscall, shmget_syscall, stat_syscall, statfs_syscall, symlink_syscall, - symlinkat_syscall, sync_file_range_syscall, truncate_syscall, unlink_syscall, unlinkat_syscall, - utimensat_syscall, write_syscall, writev_syscall, + symlinkat_syscall, sync_file_range_syscall, truncate_syscall, umask_syscall, unlink_syscall, + unlinkat_syscall, utimensat_syscall, write_syscall, writev_syscall, }; use super::init::RawCallFunc; use super::net_calls::{ @@ -129,6 +129,7 @@ pub const SYSCALL_TABLE: &[(u64, RawCallFunc)] = &[ (syscall_const::FCHMOD_SYSCALL as u64, fchmod_syscall), (syscall_const::CHOWN_SYSCALL as u64, chown_syscall), (syscall_const::LCHOWN_SYSCALL as u64, lchown_syscall), + (syscall_const::UMASK_SYSCALL as u64, umask_syscall), (syscall_const::GETUID_SYSCALL as u64, getuid_syscall), (syscall_const::GETGID_SYSCALL as u64, getgid_syscall), (syscall_const::GETEUID_SYSCALL as u64, geteuid_syscall), diff --git a/src/sysdefs/src/constants/syscall_const.rs b/src/sysdefs/src/constants/syscall_const.rs index fb90c4a76..6ab4b5b47 100644 --- a/src/sysdefs/src/constants/syscall_const.rs +++ b/src/sysdefs/src/constants/syscall_const.rs @@ -81,6 +81,7 @@ pub const CHMOD_SYSCALL: i32 = 90; pub const FCHMOD_SYSCALL: i32 = 91; pub const CHOWN_SYSCALL: i32 = 92; pub const LCHOWN_SYSCALL: i32 = 94; +pub const UMASK_SYSCALL: i32 = 95; pub const GETUID_SYSCALL: i32 = 102; pub const GETGID_SYSCALL: i32 = 104; pub const GETEUID_SYSCALL: i32 = 107; diff --git a/tests/unit-tests/file_tests/deterministic/umask.c b/tests/unit-tests/file_tests/deterministic/umask.c new file mode 100644 index 000000000..f54a471cc --- /dev/null +++ b/tests/unit-tests/file_tests/deterministic/umask.c @@ -0,0 +1,88 @@ +/* + * Before running this test: + * 1. Ensure the test directory exists in $LIND_FS_ROOT. + * 2. No pre-existing files named "umask_test_file.txt" or "umask_test_dir". + */ + + +#include +#include +#include +#include + +int main() { + mode_t old_mask; + int fd; + struct stat st; + + /* Cleanup from any previous run */ + rmdir("testfiles/umask_test_dir"); + unlink("testfiles/umask_test_file.txt"); + unlink("testfiles/umask_test_file2.txt"); + + + /* Test 1: umask returns previous mask */ + old_mask = umask(0022); + if (old_mask != 0022) { + printf("FAIL: expected initial umask 0022, got %04o\n", old_mask); + return 1; + } + umask(old_mask); /* restore */ + printf("PASS: umask returns previous mask\n"); + + /* Test 2: umask applied on open — file created with 0666 & ~0022 = 0644 */ + umask(0022); + fd = open("testfiles/umask_test_file.txt", O_CREAT | O_WRONLY, 0666); + if (fd == -1) { + perror("open failed"); + return 1; + } + close(fd); + + if (stat("testfiles/umask_test_file.txt", &st) == -1) { + perror("stat failed"); + return 1; + } + if ((st.st_mode & 0777) != 0644) { + printf("FAIL: expected file perms 0644, got %04o\n", st.st_mode & 0777); + return 1; + } + printf("PASS: umask applied correctly on open (0644)\n"); + + /* Test 3: umask applied on mkdir — dir created with 0777 & ~0022 = 0755 */ + umask(0022); + if (mkdir("testfiles/umask_test_dir", 0777) == -1) { + perror("mkdir failed"); + return 1; + } + if (stat("testfiles/umask_test_dir", &st) == -1) { + perror("stat on dir failed"); + return 1; + } + if ((st.st_mode & 0777) != 0755) { + printf("FAIL: expected dir perms 0755, got %04o\n", st.st_mode & 0777); + return 1; + } + printf("PASS: umask applied correctly on mkdir (0755)\n"); + + /* Test 4: changing umask affects subsequent creates */ + umask(0077); + fd = open("testfiles/umask_test_file2.txt", O_CREAT | O_WRONLY, 0666); + if (fd == -1) { + perror("open failed"); + return 1; + } + close(fd); + if (stat("testfiles/umask_test_file2.txt", &st) == -1) { + perror("stat failed"); + return 1; + } + if ((st.st_mode & 0777) != 0600) { + printf("FAIL: expected file perms 0600, got %04o\n", st.st_mode & 0777); + return 1; + } + printf("PASS: umask 0077 applied correctly on open (0600)\n"); + + printf("All umask tests passed.\n"); + return 0; +}