diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f594d634..46631252c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -172,11 +172,10 @@ jobs: - run: ./.github/tools/github_actions_run_cargo clippy --all-targets --all-features $MACOS_AARCH64_CRATES - run: ./.github/tools/github_actions_run_cargo build $MACOS_AARCH64_CRATES - run: ./.github/tools/github_actions_run_cargo nextest $MACOS_AARCH64_CRATES - - name: Test native macOS runner with test-only stdio + - name: Test native macOS runner with in-process broker run: >- ./.github/tools/github_actions_run_cargo nextest - -p litebox_runner_macos_userland --features test-stdio - -E 'test(=static_macho_rewriter_e2e)' + -p litebox_runner_macos_userland --features test-broker - name: Check native macOS platform without subpage compatibility run: | ./.github/tools/github_actions_run_cargo clippy --all-targets --no-default-features -p litebox_platform_macos_userland diff --git a/Cargo.lock b/Cargo.lock index e3b02df1d..1560820b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1622,6 +1622,7 @@ version = "0.1.0" dependencies = [ "bitflags", "litebox", + "litebox_broker_protocol", "litebox_common_linux", "litebox_syscall_rewriter", "object", diff --git a/litebox_common_macos/Cargo.toml b/litebox_common_macos/Cargo.toml index 6b00b57b0..4a2d5f2ff 100644 --- a/litebox_common_macos/Cargo.toml +++ b/litebox_common_macos/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] litebox = { path = "../litebox", version = "0.1.0" } +litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0", default-features = false } bitflags = { version = "2.13.1", default-features = false } litebox_syscall_rewriter = { path = "../litebox_syscall_rewriter", version = "0.1.0", default-features = false } litebox_common_linux = { path = "../litebox_common_linux", version = "0.1.0" } diff --git a/litebox_common_macos/src/errno.rs b/litebox_common_macos/src/errno.rs index cd975f8f2..5b86c0db4 100644 --- a/litebox_common_macos/src/errno.rs +++ b/litebox_common_macos/src/errno.rs @@ -63,6 +63,7 @@ pub enum Errno { ESHUTDOWN = 58, ETIMEDOUT = 60, ECONNREFUSED = 61, + ENAMETOOLONG = 63, ENOTEMPTY = 66, ENOSYS = 78, EOPNOTSUPP = 102, diff --git a/litebox_common_macos/src/lib.rs b/litebox_common_macos/src/lib.rs index 0143d1eaa..463e12170 100644 --- a/litebox_common_macos/src/lib.rs +++ b/litebox_common_macos/src/lib.rs @@ -69,6 +69,48 @@ bitflags::bitflags! { } } +bitflags::bitflags! { + /// Supported Darwin `open` flags. + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub struct OpenFlags: core::ffi::c_int { + // This is the absence of WRONLY/RDWR bits; do not test it with `contains`. + const RDONLY = 0; + const WRONLY = 0x0001; + const RDWR = 0x0002; + const NONBLOCK = 0x0004; + const APPEND = 0x0008; + const NOFOLLOW = 0x0100; + const CREAT = 0x0200; + const TRUNC = 0x0400; + const EXCL = 0x0800; + const NOCTTY = 0x0002_0000; + const DIRECTORY = 0x0010_0000; + const CLOEXEC = 0x0100_0000; + } +} + +bitflags::bitflags! { + /// Darwin descriptor-local flags. + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] + pub struct FileDescriptorFlags: u32 { + const FD_CLOEXEC = 1; + } +} + +/// Darwin pathname limit, including the terminating NUL. +pub const PATH_MAX: usize = 1024; + +bitflags::bitflags! { + /// Supported Darwin `mmap` flags. + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub struct MmapFlags: core::ffi::c_int { + const SHARED = 0x0001; + const PRIVATE = 0x0002; + const FIXED = 0x0010; + const ANONYMOUS = 0x1000; + } +} + /// Native Apple Silicon page size. pub const PAGE_SIZE: usize = 16384; diff --git a/litebox_common_macos/src/syscall.rs b/litebox_common_macos/src/syscall.rs index 8bd994d67..8d7b7a923 100644 --- a/litebox_common_macos/src/syscall.rs +++ b/litebox_common_macos/src/syscall.rs @@ -4,9 +4,11 @@ //! Typed BSD syscall decoding. use litebox::utils::{ReinterpretSignedExt as _, ReinterpretUnsignedExt as _, TruncateExt as _}; +use litebox_broker_protocol::fs::FileMode; use zerocopy::{FromBytes, IntoBytes}; use crate::{ + MmapFlags, OpenFlags, VmProtection, errno::Errno, user_pointers::{UserPtr, UserPtrMut}, }; @@ -16,6 +18,7 @@ pub mod nr { pub const EXIT: usize = 1; pub const READ: usize = 3; pub const WRITE: usize = 4; + pub const OPEN: usize = 5; pub const CLOSE: usize = 6; pub const GETPID: usize = 20; pub const GETUID: usize = 24; @@ -24,8 +27,12 @@ pub mod nr { pub const DUP: usize = 41; pub const GETEGID: usize = 43; pub const GETGID: usize = 47; + pub const MUNMAP: usize = 73; + pub const MPROTECT: usize = 74; + pub const MMAP: usize = 197; pub const READ_NOCANCEL: usize = 396; pub const WRITE_NOCANCEL: usize = 397; + pub const OPEN_NOCANCEL: usize = 398; pub const CLOSE_NOCANCEL: usize = 399; } @@ -69,12 +76,34 @@ pub enum SyscallRequest { buf: UserPtr, count: usize, }, + Open { + path: UserPtr, + flags: OpenFlags, + mode: FileMode, + }, Close { fd: i32, }, Dup { fd: i32, }, + Mmap { + address: usize, + length: usize, + protection: VmProtection, + flags: MmapFlags, + fd: i32, + offset: i64, + }, + Munmap { + address: usize, + length: usize, + }, + Mprotect { + address: usize, + length: usize, + protection: VmProtection, + }, Getpid, Getppid, Getuid, @@ -121,8 +150,30 @@ impl SyscallRequest { buf: UserPtr::from_usize(args[1]), count: args[2], }, + nr::OPEN | nr::OPEN_NOCANCEL => Self::Open { + path: UserPtr::from_usize(args[0]), + flags: OpenFlags::from_bits(int_arg(1)).ok_or(Errno::EINVAL)?, + mode: FileMode::from_u32_bits_truncate(int_arg(2).reinterpret_as_unsigned()), + }, nr::CLOSE | nr::CLOSE_NOCANCEL => Self::Close { fd: int_arg(0) }, nr::DUP => Self::Dup { fd: int_arg(0) }, + nr::MMAP => Self::Mmap { + address: args[0], + length: args[1], + protection: VmProtection::from_bits(int_arg(2)).ok_or(Errno::EINVAL)?, + flags: MmapFlags::from_bits(int_arg(3)).ok_or(Errno::EINVAL)?, + fd: int_arg(4), + offset: args[5].reinterpret_as_signed() as i64, + }, + nr::MUNMAP => Self::Munmap { + address: args[0], + length: args[1], + }, + nr::MPROTECT => Self::Mprotect { + address: args[0], + length: args[1], + protection: VmProtection::from_bits(int_arg(2)).ok_or(Errno::EINVAL)?, + }, nr::GETPID => Self::Getpid, nr::GETPPID => Self::Getppid, nr::GETUID => Self::Getuid, @@ -179,6 +230,33 @@ mod tests { SyscallRequest::from_args(u32::MAX as usize - 2, [0; 8]), Ok(SyscallRequest::MachAbsoluteTime) )); + let request = SyscallRequest::from_args( + nr::MMAP, + [0x4000, 0x8000, 5, 0x12, usize::MAX, 0x1234, 0, 0], + ) + .unwrap(); + assert!(matches!( + request, + SyscallRequest::Mmap { + address: 0x4000, + length: 0x8000, + protection, + flags, + fd: -1, + offset: 0x1234, + } if protection == (VmProtection::READ | VmProtection::EXECUTE) + && flags == (MmapFlags::PRIVATE | MmapFlags::FIXED) + )); + for (protection, flags) in [(8, 2), (1, 4)] { + assert_eq!( + SyscallRequest::from_args( + nr::MMAP, + [0, 0x4000, protection, flags, usize::MAX, 0, 0, 0], + ) + .unwrap_err(), + Errno::EINVAL + ); + } } #[cfg(target_arch = "aarch64")] diff --git a/litebox_runner_macos_userland/Cargo.toml b/litebox_runner_macos_userland/Cargo.toml index 3793d16ff..371dae884 100644 --- a/litebox_runner_macos_userland/Cargo.toml +++ b/litebox_runner_macos_userland/Cargo.toml @@ -21,8 +21,8 @@ litebox_syscall_rewriter = { path = "../litebox_syscall_rewriter", version = "0. litebox = { path = "../litebox", version = "0.1.0" } [features] -# In-process broker fixture with buffered stdin and captured stdout/stderr. -test-stdio = ["dep:litebox", "dep:litebox_broker_core", "dep:litebox_broker_host", "dep:litebox_broker_local", "dep:litebox_broker_protocol"] +# Development-only in-process broker fixture. +test-broker = ["dep:litebox", "dep:litebox_broker_core", "dep:litebox_broker_host", "dep:litebox_broker_local", "dep:litebox_broker_protocol"] [lints] workspace = true diff --git a/litebox_runner_macos_userland/src/lib.rs b/litebox_runner_macos_userland/src/lib.rs index 2e4d7d37a..c4a5123d9 100644 --- a/litebox_runner_macos_userland/src/lib.rs +++ b/litebox_runner_macos_userland/src/lib.rs @@ -8,7 +8,7 @@ use anyhow::{Context as _, Result, bail}; use clap::Parser; use litebox_common_macos::TaskParams; use litebox_platform_macos_userland::{GuestAbi, MacosUserland, set_guest_abi}; -#[cfg(not(feature = "test-stdio"))] +#[cfg(not(feature = "test-broker"))] use litebox_shim_macos::MacosShimBuilder; use std::ffi::CString; @@ -23,7 +23,7 @@ pub struct CliArgs { pub environment_variables: Vec, } -#[cfg(feature = "test-stdio")] +#[cfg(feature = "test-broker")] mod test_broker; pub fn run(cli_args: CliArgs) -> Result { @@ -45,9 +45,9 @@ pub fn run(cli_args: CliArgs) -> Result { .collect::, _>>() .context("NUL in environment entry")?; let platform = MacosUserland::new(); - #[cfg(not(feature = "test-stdio"))] + #[cfg(not(feature = "test-broker"))] let builder = MacosShimBuilder::new(platform); - #[cfg(feature = "test-stdio")] + #[cfg(feature = "test-broker")] let (builder, stdio) = test_broker::setup(platform)?; let program = builder .build() @@ -63,7 +63,7 @@ pub fn run(cli_args: CliArgs) -> Result { unsafe { litebox_platform_macos_userland::run_thread(entrypoints, &mut initial_ctx); } - #[cfg(feature = "test-stdio")] + #[cfg(feature = "test-broker")] test_broker::flush_output(&stdio)?; process .exit_status() diff --git a/litebox_runner_macos_userland/src/test_broker.rs b/litebox_runner_macos_userland/src/test_broker.rs index da97961e9..b65838a2f 100644 --- a/litebox_runner_macos_userland/src/test_broker.rs +++ b/litebox_runner_macos_userland/src/test_broker.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! In-process broker fixture for the test-stdio runner feature. +//! In-process broker fixture for the test-broker runner feature. use anyhow::{Context as _, Result, anyhow}; use litebox::LiteBox; diff --git a/litebox_runner_macos_userland/tests/runner.rs b/litebox_runner_macos_userland/tests/runner.rs index 16142f1b7..ac3bd9a72 100644 --- a/litebox_runner_macos_userland/tests/runner.rs +++ b/litebox_runner_macos_userland/tests/runner.rs @@ -4,7 +4,7 @@ #![cfg(all(target_os = "macos", target_arch = "aarch64"))] use litebox_common_macos::{TaskParams, VmProtection, loader::MachoParsedFile}; -#[cfg(feature = "test-stdio")] +#[cfg(feature = "test-broker")] use std::io::Write as _; use std::{ path::{Path, PathBuf}, @@ -128,20 +128,24 @@ fn assert_svc_gates(original: &[u8], rewritten: &[u8]) -> usize { } /// Both feature configurations exercise the same AOT pipeline and gate ABI. -/// test-stdio adds I/O checks; without it the fixture checks stdio is absent. +/// test-broker adds I/O checks; without it the fixture checks stdio is absent. #[test] fn static_macho_rewriter_e2e() { let dir = tempfile::tempdir().unwrap(); let source = format!( ".set TEST_STDIO, {}\n{}", - usize::from(cfg!(feature = "test-stdio")), + usize::from(cfg!(feature = "test-broker")), include_str!("fixtures/static_macho.S"), ); let binary = assemble(dir.path(), &source); let hooked = rewrite(&binary); let original = std::fs::read(&binary).unwrap(); let rewritten = std::fs::read(&hooked).unwrap(); - let expected_sites = if cfg!(feature = "test-stdio") { 23 } else { 15 }; + let expected_sites = if cfg!(feature = "test-broker") { + 23 + } else { + 15 + }; assert_eq!(assert_svc_gates(&original, &rewritten), expected_sites); let parsed = MachoParsedFile::parse(&original).unwrap(); // Parsing is independent of the byte slice's alignment. @@ -162,12 +166,12 @@ fn static_macho_rewriter_e2e() { .stderr(Stdio::piped()) .spawn() .unwrap(); - #[cfg(feature = "test-stdio")] + #[cfg(feature = "test-broker")] child.stdin.take().unwrap().write_all(b"hello\n").unwrap(); // No input is needed in the default configuration. drop(child.stdin.take()); let output = child.wait_with_output().unwrap(); - #[cfg(feature = "test-stdio")] + #[cfg(feature = "test-broker")] { println!("guest stdout: {}", String::from_utf8_lossy(&output.stdout)); eprintln!("guest stderr: {}", String::from_utf8_lossy(&output.stderr)); @@ -178,7 +182,7 @@ fn static_macho_rewriter_e2e() { "stderr: {}", String::from_utf8_lossy(&output.stderr) ); - let expected: &[u8] = if cfg!(feature = "test-stdio") { + let expected: &[u8] = if cfg!(feature = "test-broker") { b"hello\n" } else { b"" diff --git a/litebox_shim_macos/Cargo.toml b/litebox_shim_macos/Cargo.toml index 6fe792ce5..e6f47e47b 100644 --- a/litebox_shim_macos/Cargo.toml +++ b/litebox_shim_macos/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] litebox = { path = "../litebox", version = "0.1.0" } +litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0", default-features = false } litebox_common_macos = { path = "../litebox_common_macos", version = "0.1.0" } litebox_util_log = { path = "../litebox_util_log", version = "0.1.0" } litebox_syscall_rewriter = { path = "../litebox_syscall_rewriter", version = "0.1.0", default-features = false } @@ -13,7 +14,6 @@ litebox_syscall_rewriter = { path = "../litebox_syscall_rewriter", version = "0. litebox_broker_core = { path = "../litebox_broker_core", version = "0.1.0", features = ["test-support"] } litebox_broker_host = { path = "../litebox_broker_host", version = "0.1.0", features = ["test-support"] } litebox_broker_local = { path = "../litebox_broker_local", version = "0.1.0" } -litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } litebox_platform_macos_userland = { path = "../litebox_platform_macos_userland", version = "0.1.0" } [lints] diff --git a/litebox_shim_macos/src/lib.rs b/litebox_shim_macos/src/lib.rs index 64c148d73..758aa79cb 100644 --- a/litebox_shim_macos/src/lib.rs +++ b/litebox_shim_macos/src/lib.rs @@ -4,7 +4,7 @@ //! Minimal Darwin BSD shim for static AArch64 Mach-O guests. //! //! Guest mappings and file operations use LiteBox. The runner supplies inherited -//! descriptors. Guest file opening and networking are unsupported. +//! descriptors. Networking is unsupported. #![no_std] #![cfg(target_arch = "aarch64")] @@ -257,8 +257,32 @@ impl Task

{ let bytes = buf.to_owned_slice::

(length).ok_or(Errno::EFAULT)?; self.do_write(&fd, &bytes) } + SyscallRequest::Open { path, flags, mode } => { + let path = self.read_path(path)?; + self.sys_open(path, flags, mode).to_syscall_result() + } SyscallRequest::Close { fd } => self.sys_close(fd).to_syscall_result(), SyscallRequest::Dup { fd } => self.sys_dup(fd).to_syscall_result(), + SyscallRequest::Mmap { + address, + length, + protection, + flags, + fd, + offset, + } => self + .sys_mmap(address, length, protection, flags, fd, offset) + .to_syscall_result(), + SyscallRequest::Munmap { address, length } => { + self.sys_munmap(address, length).to_syscall_result() + } + SyscallRequest::Mprotect { + address, + length, + protection, + } => self + .sys_mprotect(address, length, protection) + .to_syscall_result(), SyscallRequest::Getpid => Ok(self.sys_getpid().cast_unsigned() as usize), SyscallRequest::Getppid => Ok(self.sys_getppid().cast_unsigned() as usize), SyscallRequest::Getuid => Ok(self.sys_getuid() as usize), diff --git a/litebox_shim_macos/src/syscalls.rs b/litebox_shim_macos/src/syscalls.rs index 150815ba1..5bd2e98e0 100644 --- a/litebox_shim_macos/src/syscalls.rs +++ b/litebox_shim_macos/src/syscalls.rs @@ -10,6 +10,7 @@ use litebox::{ use litebox_common_macos::{KernReturn, syscall::MachTimebaseInfo, user_pointers::UserPtrMut}; pub(crate) mod file; +pub(crate) mod mm; impl Task

{ pub(crate) fn sys_exit(&self, status: i32) { diff --git a/litebox_shim_macos/src/syscalls/file.rs b/litebox_shim_macos/src/syscalls/file.rs index 755cd7ac6..d2e3109ae 100644 --- a/litebox_shim_macos/src/syscalls/file.rs +++ b/litebox_shim_macos/src/syscalls/file.rs @@ -4,17 +4,20 @@ //! Guest descriptor namespace and LiteBox file operations. use crate::{ShimPlatform, Task}; -use alloc::{sync::Arc, vec::Vec}; +use alloc::{string::String, sync::Arc, vec::Vec}; use litebox::{ LiteBox, fd::RawDescriptorStorage, fs::{ BrokerFile, FileFd, - errors::{ReadError, WriteError}, + errors::{OpenError, PathError, ReadError, TruncateError, WriteError}, }, sync::RwLock, }; -use litebox_common_macos::errno::Errno; +use litebox_broker_protocol::fs::{FileAccessMode, FileMode, FileOpenFlags, FileUser}; +use litebox_common_macos::{ + FileDescriptorFlags, OpenFlags, PAGE_SIZE, PATH_MAX, errno::Errno, user_pointers::UserPtr, +}; const MAX_FDS: usize = 1024; @@ -87,6 +90,98 @@ impl Drop for FilesState

{ } impl Task

{ + pub(crate) fn sys_open( + &self, + path: impl litebox::path::Arg, + flags: OpenFlags, + mode: FileMode, + ) -> Result { + let path = path.as_rust_str().map_err(|_| Errno::EINVAL)?; + if path.is_empty() { + return Err(Errno::ENOENT); + } + if path.len() >= PATH_MAX { + return Err(Errno::ENAMETOOLONG); + } + if path.as_bytes().contains(&0) { + return Err(Errno::EINVAL); + } + let access = match flags.bits() & 3 { + 0 => FileAccessMode::ReadOnly, + 1 => FileAccessMode::WriteOnly, + 2 => FileAccessMode::ReadWrite, + _ => return Err(Errno::EINVAL), + }; + let mut open_flags = FileOpenFlags::NONE; + for (guest, broker) in [ + (OpenFlags::CREAT, FileOpenFlags::CREATE), + (OpenFlags::TRUNC, FileOpenFlags::TRUNCATE), + (OpenFlags::EXCL, FileOpenFlags::EXCLUSIVE), + (OpenFlags::APPEND, FileOpenFlags::APPEND), + (OpenFlags::NONBLOCK, FileOpenFlags::NONBLOCKING), + (OpenFlags::NOFOLLOW, FileOpenFlags::NO_FOLLOW), + (OpenFlags::NOCTTY, FileOpenFlags::NO_CONTROLLING_TERMINAL), + (OpenFlags::DIRECTORY, FileOpenFlags::DIRECTORY), + ] { + if flags.contains(guest) { + open_flags = open_flags.union(broker); + } + } + let mut context = litebox::fs::Context::new(); + context.set_acting_user(FileUser { + user: u16::try_from(self.params.euid).map_err(|_| Errno::EINVAL)?, + group: u16::try_from(self.params.egid).map_err(|_| Errno::EINVAL)?, + }); + // Until chdir/umask are supported, use cwd "/" and the Linux shim's default umask. + let mode = mode & !(FileMode::WGRP | FileMode::WOTH); + // Keep the free slot exclusive until open succeeds and the FD is published. + // TODO: reserve a slot without holding the FD-table lock across broker IPC. + let mut raw = self.files.raw.write(); + if raw.iter_alive().count() >= MAX_FDS { + return Err(Errno::EMFILE); + } + let file = self + .global + .litebox + .open_file(&context, path, access, open_flags, mode) + .map_err(open_error)?; + if flags.contains(OpenFlags::CLOEXEC) { + // TODO: honor FD_CLOEXEC when macOS exec is implemented. + let old = self + .global + .litebox + .descriptor_table_mut() + .set_fd_metadata(&file, FileDescriptorFlags::FD_CLOEXEC); + assert!(old.is_none()); + } + Ok(u32::try_from(raw.fd_into_raw_integer(file)).expect("fd bounded by MAX_FDS")) + } + + pub(crate) fn read_path(&self, path: UserPtr) -> Result { + let mut bytes = Vec::new(); + while bytes.len() < PATH_MAX { + let address = path + .as_usize() + .checked_add(bytes.len()) + .ok_or(Errno::EFAULT)?; + let length = (PAGE_SIZE - address % PAGE_SIZE).min(PATH_MAX - bytes.len()); + self.check_user_buffer( + address, + length, + litebox::platform::page_mgmt::MemoryRegionPermissions::READ, + )?; + let chunk = UserPtr::::from_usize(address) + .to_owned_slice::

(length) + .ok_or(Errno::EFAULT)?; + if let Some(end) = chunk.iter().position(|&byte| byte == 0) { + bytes.extend_from_slice(&chunk[..end]); + return String::from_utf8(bytes).map_err(|_| Errno::EINVAL); + } + bytes.extend_from_slice(&chunk); + } + Err(Errno::ENAMETOOLONG) + } + pub(crate) fn do_read(&self, fd: &FileFd, buf: &mut [u8]) -> Result { let size = self .global @@ -119,6 +214,28 @@ impl Task

{ } } +fn open_error(error: OpenError) -> Errno { + match error { + OpenError::AccessNotAllowed | OpenError::NoWritePerms => Errno::EACCES, + OpenError::ReadOnlyFileSystem => Errno::EROFS, + OpenError::AlreadyExists => Errno::EEXIST, + OpenError::PathError(error) => match error { + PathError::NoSuchFileOrDirectory | PathError::MissingComponent => Errno::ENOENT, + PathError::NoSearchPerms { .. } => Errno::EACCES, + PathError::InvalidPathname => Errno::EINVAL, + PathError::ComponentNotADirectory => Errno::ENOTDIR, + }, + OpenError::TruncateError(error) => match error { + TruncateError::ClosedFd => Errno::EBADF, + TruncateError::IsDirectory => Errno::EISDIR, + TruncateError::NotForWriting => Errno::EACCES, + TruncateError::IsTerminalDevice => Errno::EINVAL, + TruncateError::Io => Errno::EIO, + }, + _ => Errno::EIO, + } +} + fn read_error(error: ReadError) -> Errno { match error { ReadError::ClosedFd | ReadError::NotForReading => Errno::EBADF, @@ -160,7 +277,9 @@ mod tests { use litebox_broker_protocol::fs::{ FileAccessMode, FileMode, FileOpenFlags, FileSeekWhence, FileUser, }; - use litebox_common_macos::{PAGE_SIZE, PtRegs, TaskParams, syscall::nr}; + use litebox_common_macos::{ + MmapFlags, PAGE_SIZE, PtRegs, TaskParams, VmProtection, syscall::nr, + }; use litebox_platform_macos_userland::MacosUserland as Platform; #[test] @@ -288,18 +407,9 @@ mod tests { assert_eq!(invoke(nr::CLOSE, 2, 0), Err(Errno::EBADF)); // Invalid output buffers must not advance a file's offset. - let fd = task - .global - .litebox - .open_file( - &context, - "/data", - FileAccessMode::ReadOnly, - FileOpenFlags::NONE, - FileMode::empty(), - ) - .unwrap(); - let fresh = task.files.insert_file(fd).unwrap() as usize; + let fresh = task + .sys_open("/data", OpenFlags::RDONLY, FileMode::empty()) + .unwrap() as usize; // SAFETY: the test owns this idle mapping. unsafe { task.global @@ -318,6 +428,98 @@ mod tests { assert_eq!(invoke(nr::READ, fresh, 2), Ok(2)); assert_eq!(&*buf.to_owned_slice(2).unwrap(), b"ab"); + let open = |number, path, flags: OpenFlags, mode| { + let mut ctx = PtRegs::default(); + ctx.regs[16] = number; + ctx.regs[0] = path; + ctx.regs[1] = flags.bits().cast_unsigned() as usize; + ctx.regs[2] = mode; + task.do_syscall(&ctx) + }; + // The terminator is the last mapped byte: open must not read the next page. + let path_offset = MAX_KERNEL_BUF_SIZE - b"/new\0".len(); + buf.copy_from_slice(path_offset, b"/new\0").unwrap(); + let path = buf.as_usize() + path_offset; + let flags = OpenFlags::RDWR | OpenFlags::CREAT | OpenFlags::EXCL | OpenFlags::CLOEXEC; + let created = open(nr::OPEN, path, flags, 0o666).unwrap(); + let descriptor_flags = |fd| { + let fd = task.files.typed_fd(i32::try_from(fd).unwrap()).unwrap(); + task.global + .litebox + .descriptor_table() + .with_metadata(&fd, |flags: &FileDescriptorFlags| *flags) + }; + let duplicate = invoke(nr::DUP, created, 0).unwrap(); + assert!(matches!( + descriptor_flags(duplicate), + Err(litebox::fd::MetadataError::NoSuchMetadata) + )); + assert_eq!( + descriptor_flags(created).unwrap(), + FileDescriptorFlags::FD_CLOEXEC + ); + assert_eq!(invoke(nr::CLOSE, duplicate, 0), Ok(0)); + let status = task + .global + .litebox + .file_status( + &task + .files + .typed_fd(i32::try_from(created).unwrap()) + .unwrap(), + ) + .unwrap(); + assert_eq!(status.mode, FileMode::from_u32_bits_truncate(0o644)); + assert_eq!( + status.owner, + FileUser { + user: 1000, + group: 1000 + } + ); + assert_eq!( + open(nr::OPEN_NOCANCEL, path, flags, 0o666), + Err(Errno::EEXIST) + ); + buf.copy_from_slice(0, b"new").unwrap(); + assert_eq!(invoke(nr::WRITE, created, 3), Ok(3)); + assert_eq!(invoke(nr::CLOSE, created, 0), Ok(0)); + let reopened = open(nr::OPEN_NOCANCEL, path, OpenFlags::RDONLY, 0).unwrap(); + assert!(matches!( + descriptor_flags(reopened), + Err(litebox::fd::MetadataError::NoSuchMetadata) + )); + assert_eq!(invoke(nr::READ, reopened, 3), Ok(3)); + assert_eq!(&*buf.to_owned_slice(3).unwrap(), b"new"); + assert_eq!(invoke(nr::CLOSE_NOCANCEL, reopened, 0), Ok(0)); + assert_eq!(open(nr::OPEN, 0, OpenFlags::RDONLY, 0), Err(Errno::EFAULT)); + buf.copy_from_slice(0, &[b'x'; PATH_MAX]).unwrap(); + assert_eq!( + open(nr::OPEN, buf.as_usize(), OpenFlags::RDONLY, 0), + Err(Errno::ENAMETOOLONG) + ); + buf.copy_from_slice(0, b"\0").unwrap(); + assert_eq!( + open(nr::OPEN, buf.as_usize(), OpenFlags::RDONLY, 0), + Err(Errno::ENOENT) + ); + + let directory = task + .sys_open("/", OpenFlags::RDONLY, FileMode::empty()) + .unwrap(); + assert_eq!( + task.sys_mmap( + 0, + PAGE_SIZE, + VmProtection::READ, + MmapFlags::PRIVATE, + i32::try_from(directory).unwrap(), + 0, + ), + Err(Errno::EINVAL) + ); + task.sys_close(i32::try_from(directory).unwrap()).unwrap(); + // Publishing a duplicate is atomic with the descriptor limit check. std::thread::scope(|scope| { for _ in 0..4 { @@ -334,7 +536,40 @@ mod tests { assert_eq!(task.sys_dup(1), Ok(u32::try_from(expected).unwrap())); } assert_eq!(task.sys_dup(1), Err(Errno::EMFILE)); + buf.copy_from_slice(0, b"/data\0").unwrap(); + assert_eq!( + open( + nr::OPEN, + buf.as_usize(), + OpenFlags::WRONLY | OpenFlags::TRUNC, + 0 + ), + Err(Errno::EMFILE), + ); + let mut contents = [0; 6]; + assert_eq!( + task.global + .litebox + .read_file(&task.files.typed_fd(1).unwrap(), &mut contents, Some(0),) + .unwrap(), + 6 + ); + assert_eq!(&contents, b"abcdXY"); + buf.copy_from_slice(0, b"/not-created\0").unwrap(); + assert_eq!( + open( + nr::OPEN_NOCANCEL, + buf.as_usize(), + OpenFlags::WRONLY | OpenFlags::CREAT, + 0o600 + ), + Err(Errno::EMFILE), + ); task.sys_close(12).unwrap(); + assert_eq!( + open(nr::OPEN, buf.as_usize(), OpenFlags::RDONLY, 0), + Err(Errno::ENOENT), + ); assert_eq!(task.sys_dup(1), Ok(12)); } } diff --git a/litebox_shim_macos/src/syscalls/mm.rs b/litebox_shim_macos/src/syscalls/mm.rs new file mode 100644 index 000000000..66a422ff8 --- /dev/null +++ b/litebox_shim_macos/src/syscalls/mm.rs @@ -0,0 +1,251 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Darwin virtual-memory syscalls. + +use litebox::{ + fs::errors::ReadError, + mm::linux::{ + CreatePagesFlags, MappingError, NonZeroAddress, NonZeroPageSize, VmemProtectError, + }, + platform::{ + RawConstPointer as _, RawMutPointer as _, page_mgmt::MemoryRegionPermissions as Permissions, + }, +}; +use litebox_common_macos::{MmapFlags, PAGE_SIZE, VmProtection, errno::Errno}; + +use crate::{ShimPlatform, Task}; + +fn permissions(protection: VmProtection) -> Permissions { + let mut permissions = Permissions::empty(); + permissions.set(Permissions::READ, protection.contains(VmProtection::READ)); + permissions.set(Permissions::WRITE, protection.contains(VmProtection::WRITE)); + permissions.set( + Permissions::EXEC, + protection.contains(VmProtection::EXECUTE), + ); + permissions +} + +fn mapping_flags(flags: MmapFlags, file_backed: bool) -> CreatePagesFlags { + let mut result = CreatePagesFlags::POPULATE_PAGES_IMMEDIATELY; + result.set( + CreatePagesFlags::FIXED_ADDR, + flags.contains(MmapFlags::FIXED), + ); + result.set(CreatePagesFlags::SHARED, flags.contains(MmapFlags::SHARED)); + result.set(CreatePagesFlags::MAP_FILE, file_backed); + result +} + +fn mapping_error(error: MappingError) -> Errno { + match error { + MappingError::BadFD(_) => Errno::EBADF, + MappingError::NotForReading => Errno::EACCES, + MappingError::OutOfMemory | MappingError::MapError(_) => Errno::ENOMEM, + _ => Errno::EINVAL, + } +} + +fn mmap_read_error(error: ReadError) -> Errno { + match error { + ReadError::ClosedFd => Errno::EBADF, + ReadError::NotAFile => Errno::EINVAL, + ReadError::NotForReading => Errno::EACCES, + _ => Errno::EIO, + } +} + +fn protection_error(error: VmemProtectError) -> Errno { + use litebox::platform::page_mgmt::PermissionUpdateError; + + match error { + VmemProtectError::InvalidRange(_) => Errno::ENOMEM, + VmemProtectError::NoAccess { .. } => Errno::EACCES, + VmemProtectError::UnAligned(_) => Errno::EINVAL, + VmemProtectError::ProtectError(error) => match error { + PermissionUpdateError::Unallocated | PermissionUpdateError::OutOfMemory => { + Errno::ENOMEM + } + PermissionUpdateError::PermissionDenied => Errno::EACCES, + PermissionUpdateError::Unaligned | PermissionUpdateError::PlatformFailure => { + Errno::EINVAL + } + _ => Errno::EINVAL, + }, + } +} + +impl Task

{ + pub(crate) fn sys_mmap( + &self, + address: usize, + length: usize, + protection: VmProtection, + flags: MmapFlags, + fd: i32, + offset: i64, + ) -> Result { + if length == 0 + || !address.is_multiple_of(PAGE_SIZE) + || flags.contains(MmapFlags::SHARED) == flags.contains(MmapFlags::PRIVATE) + { + return Err(Errno::EINVAL); + } + let length = length + .checked_next_multiple_of(PAGE_SIZE) + .ok_or(Errno::ENOMEM)?; + let offset = usize::try_from(offset).map_err(|_| Errno::EINVAL)?; + if !offset.is_multiple_of(PAGE_SIZE) + || offset.checked_add(length).is_none() + || address.checked_add(length).is_none() + { + return Err(Errno::EINVAL); + } + if flags.contains(MmapFlags::SHARED) + && protection.contains(VmProtection::WRITE) + && !flags.contains(MmapFlags::ANONYMOUS) + { + return Err(Errno::ENOTSUP); + } + let suggested = NonZeroAddress::new(address); + let length = NonZeroPageSize::new(length).ok_or(Errno::EINVAL)?; + if flags.contains(MmapFlags::ANONYMOUS) { + // SAFETY: MAP_FIXED has Darwin's replacement semantics; otherwise + // PageManager treats `suggested` only as an allocation hint. + return unsafe { + self.global.pm.create_pages_with_permissions( + suggested, + length, + mapping_flags(flags, false), + permissions(protection), + |_| Ok(0), + ) + } + .map(|pointer| pointer.as_usize()) + .map_err(mapping_error); + } + + let file = self.files.typed_fd(fd)?; + let mut file_offset = offset; + let mut buffer = [0; PAGE_SIZE]; + let mut initialization_error = None; + // SAFETY: MAP_FIXED has Darwin's replacement semantics. Initialization + // runs while the new mapping is private to this syscall and still RW. + let result = unsafe { + self.global.pm.create_pages_with_permissions( + suggested, + length, + mapping_flags(flags, true), + permissions(protection), + |pointer| { + let mut copied = 0; + while copied < length.as_usize() { + let chunk = (length.as_usize() - copied).min(buffer.len()); + let read = self + .global + .litebox + .read_file(&file, &mut buffer[..chunk], Some(file_offset)) + .map_err(|error| { + initialization_error = Some(error); + MappingError::NotForReading + })?; + if read == 0 { + break; + } + pointer + .copy_from_slice(copied, &buffer[..read]) + .ok_or(MappingError::OutOfMemory)?; + copied += read; + file_offset = file_offset + .checked_add(read) + .ok_or(MappingError::OutOfMemory)?; + } + Ok(copied) + }, + ) + }; + match (result, initialization_error) { + (_, Some(error)) => Err(mmap_read_error(error)), + (Ok(pointer), None) => Ok(pointer.as_usize()), + (Err(error), None) => Err(mapping_error(error)), + } + } + + pub(crate) fn sys_munmap(&self, address: usize, length: usize) -> Result<(), Errno> { + let length = length + .checked_next_multiple_of(PAGE_SIZE) + .filter(|length| *length != 0) + .ok_or(Errno::EINVAL)?; + address.checked_add(length).ok_or(Errno::EINVAL)?; + // SAFETY: Darwin munmap relinquishes the caller-selected guest range. + unsafe { + self.global + .pm + .remove_pages(P::RawMutPointer::from_usize(address), length) + } + .map_err(|_| Errno::EINVAL) + } + + pub(crate) fn sys_mprotect( + &self, + address: usize, + length: usize, + protection: VmProtection, + ) -> Result<(), Errno> { + if !address.is_multiple_of(PAGE_SIZE) { + return Err(Errno::EINVAL); + } + if length == 0 { + return Ok(()); + } + let length = length + .checked_next_multiple_of(PAGE_SIZE) + .ok_or(Errno::EINVAL)?; + address.checked_add(length).ok_or(Errno::EINVAL)?; + // SAFETY: PageManager validates the tracked range and maximum allowed + // permissions. + unsafe { + self.global.pm.change_page_permissions( + P::RawMutPointer::from_usize(address), + length, + permissions(protection), + ) + } + .map_err(protection_error) + } +} + +#[cfg(all(test, target_os = "macos"))] +mod tests { + extern crate std; + + use super::*; + use alloc::sync::Arc; + use core::sync::atomic::AtomicI32; + use litebox_common_macos::TaskParams; + use litebox_platform_macos_userland::MacosUserland as Platform; + + use crate::{MacosShimBuilder, Process}; + + #[test] + fn mprotect_validates_ranges() { + let platform = Platform::new(); + let shim = MacosShimBuilder::new(platform).build(); + let task = Task { + global: shim.global, + files: shim.files, + params: TaskParams::default(), + process: Process(Arc::new(AtomicI32::new(-1))), + }; + + assert_eq!( + task.sys_mprotect(PAGE_SIZE, PAGE_SIZE, VmProtection::READ), + Err(Errno::ENOMEM) + ); + assert_eq!( + task.sys_mprotect(1, 0, VmProtection::READ), + Err(Errno::EINVAL) + ); + } +}