diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2255b608..efb5b72d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -259,6 +259,9 @@ jobs: # - `litebox_platform_linux_userland` is allowed to have `std` access, # since it is a purely-userland implementation. # + # - `litebox_platform_freebsd_userland` is allowed to have `std` access, + # since it is a purely-userland implementation. + # # - `litebox_platform_windows_userland` is allowed to have `std` access, # since it is a purely-userland implementation. # @@ -311,6 +314,7 @@ jobs: find . -type f -name 'Cargo.toml' \ -not -path './Cargo.toml' \ -not -path './litebox_platform_linux_userland/Cargo.toml' \ + -not -path './litebox_platform_freebsd_userland/Cargo.toml' \ -not -path './litebox_platform_windows_userland/Cargo.toml' \ -not -path './litebox_runner_linux_on_windows_userland/Cargo.toml' \ -not -path './litebox_platform_lvbs/Cargo.toml' \ diff --git a/Cargo.lock b/Cargo.lock index 5535e5194..7b625e1dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -923,6 +923,19 @@ dependencies = [ "tar", ] +[[package]] +name = "litebox_platform_freebsd_userland" +version = "0.1.0" +dependencies = [ + "bitflags 2.9.4", + "getrandom 0.3.4", + "libc", + "litebox", + "litebox_common_linux", + "thiserror", + "zerocopy", +] + [[package]] name = "litebox_platform_linux_kernel" version = "0.1.0" @@ -994,6 +1007,7 @@ version = "0.1.0" dependencies = [ "cfg-if", "litebox", + "litebox_platform_freebsd_userland", "litebox_platform_linux_kernel", "litebox_platform_linux_userland", "litebox_platform_lvbs", diff --git a/Cargo.toml b/Cargo.toml index 258dd51e9..9264eb039 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "litebox", "litebox_common_linux", "litebox_common_optee", + "litebox_platform_freebsd_userland", "litebox_platform_linux_kernel", "litebox_platform_linux_userland", "litebox_platform_windows_userland", @@ -28,6 +29,7 @@ default-members = [ "litebox", "litebox_common_linux", "litebox_common_optee", + "litebox_platform_freebsd_userland", "litebox_platform_linux_kernel", "litebox_platform_linux_userland", "litebox_platform_windows_userland", diff --git a/dev_tests/src/ratchet.rs b/dev_tests/src/ratchet.rs index 276452d4d..8b4c91743 100644 --- a/dev_tests/src/ratchet.rs +++ b/dev_tests/src/ratchet.rs @@ -35,6 +35,7 @@ fn ratchet_globals() -> Result<()> { &[ ("dev_bench/", 1), ("litebox/", 9), + ("litebox_platform_freebsd_userland/", 7), ("litebox_platform_linux_kernel/", 6), ("litebox_platform_linux_userland/", 5), ("litebox_platform_lvbs/", 23), @@ -71,6 +72,7 @@ fn ratchet_maybe_uninit() -> Result<()> { &[ ("dev_tests/", 1), ("litebox/", 1), + ("litebox_platform_freebsd_userland/", 2), ("litebox_platform_linux_userland/", 3), ("litebox_shim_linux/", 5), ], diff --git a/litebox/src/mm/exception_table.rs b/litebox/src/mm/exception_table.rs index 203130eec..336443052 100644 --- a/litebox/src/mm/exception_table.rs +++ b/litebox/src/mm/exception_table.rs @@ -18,7 +18,7 @@ use crate::utils::TruncateExt as _; -#[cfg(any(target_os = "linux", target_os = "none"))] +#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "none"))] macro_rules! ex_table_section { () => { // a = allocate, R = retain: don't discard on linking. @@ -214,7 +214,7 @@ struct ExceptionTableEntry { /// Returns the exception table, found by linker-defined symbols marking the /// start and end of the section. -#[cfg(any(target_os = "linux", target_os = "none"))] +#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "none"))] fn exception_table() -> &'static [ExceptionTableEntry] { // SAFETY: the linker automatically defines these symbols when the section // is non-empty. diff --git a/litebox_platform_freebsd_userland/Cargo.toml b/litebox_platform_freebsd_userland/Cargo.toml new file mode 100644 index 000000000..2f2475a59 --- /dev/null +++ b/litebox_platform_freebsd_userland/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "litebox_platform_freebsd_userland" +version = "0.1.0" +edition = "2024" + +[dependencies] +libc = { version = "0.2.169", default-features = false } +litebox = { path = "../litebox/", version = "0.1.0" } +litebox_common_linux = { path = "../litebox_common_linux", version = "0.1.0" } +thiserror = { version = "2.0.6", default-features = false } +bitflags = "2.6.0" +getrandom = "0.3.4" +zerocopy = { version = "0.8", default-features = false, features = ["derive"] } + +[lints] +workspace = true diff --git a/litebox_platform_freebsd_userland/README.md b/litebox_platform_freebsd_userland/README.md new file mode 100644 index 000000000..056d84e7e --- /dev/null +++ b/litebox_platform_freebsd_userland/README.md @@ -0,0 +1,3 @@ +# LiteBox Platform: FreeBSD Userland + +A [LiteBox platform](../litebox/platform/index.html) for running LiteBox on userland FreeBSD. diff --git a/litebox_platform_freebsd_userland/src/errno/generated.rs b/litebox_platform_freebsd_userland/src/errno/generated.rs new file mode 100644 index 000000000..51df1020f --- /dev/null +++ b/litebox_platform_freebsd_userland/src/errno/generated.rs @@ -0,0 +1,323 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Generated code for the [`super::Errno`] constants. +//! +//! This particular module itself is private, but defines all of the below within the public +//! [`super::Errno`] type, so as to have them all be exposed, but still keep the auto-generated code +//! restricted to this single file. +//! + +impl super::Errno { + /// Human-friendly readable version of `self`. + /// + /// Generated from FreeBSD 14.3 errno manual page + /// https://man.freebsd.org/cgi/man.cgi?errno + pub(crate) const fn as_str(self) -> &'static str { + match self.value.get() { + 1 => "EPERM: Operation not permitted", + 2 => "ENOENT: No such file or directory", + 3 => "ESRCH: No such process", + 4 => "EINTR: Interrupted system call", + 5 => "EIO: Input/output error", + 6 => "ENXIO: Device not configured", + 7 => "E2BIG: Argument list too long", + 8 => "ENOEXEC: Exec format error", + 9 => "EBADF: Bad file descriptor", + 10 => "ECHILD: No child processes", + 11 => "EDEADLK: Resource deadlock avoided", + 12 => "ENOMEM: Cannot allocate memory", + 13 => "EACCES: Permission denied", + 14 => "EFAULT: Bad address", + 15 => "ENOTBLK: Block device required", + 16 => "EBUSY: Device busy", + 17 => "EEXIST: File exists", + 18 => "EXDEV: Cross-device link", + 19 => "ENODEV: Operation not supported by device", + 20 => "ENOTDIR: Not a directory", + 21 => "EISDIR: Is a directory", + 22 => "EINVAL: Invalid argument", + 23 => "ENFILE: Too many open files in system", + 24 => "EMFILE: Too many open files", + 25 => "ENOTTY: Inappropriate ioctl for device", + 26 => "ETXTBSY: Text file busy", + 27 => "EFBIG: File too large", + 28 => "ENOSPC: No space left on device", + 29 => "ESPIPE: Illegal seek", + 30 => "EROFS: Read-only file system", + 31 => "EMLINK: Too many links", + 32 => "EPIPE: Broken pipe", + 33 => "EDOM: Numerical argument out of domain", + 34 => "ERANGE: Result too large", + 35 => "EAGAIN: Resource temporarily unavailable", + 36 => "EINPROGRESS: Operation now in progress", + 37 => "EALREADY: Operation already in progress", + 38 => "ENOTSOCK: Socket operation on non-socket", + 39 => "EDESTADDRREQ: Destination address required", + 40 => "EMSGSIZE: Message too long", + 41 => "EPROTOTYPE: Protocol wrong type for socket", + 42 => "ENOPROTOOPT: Protocol not available", + 43 => "EPROTONOSUPPORT: Protocol not supported", + 44 => "ESOCKTNOSUPPORT: Socket type not supported", + 45 => "EOPNOTSUPP: Operation not supported", + 46 => "EPFNOSUPPORT: Protocol family not supported", + 47 => "EAFNOSUPPORT: Address family not supported by protocol family", + 48 => "EADDRINUSE: Address already in use", + 49 => "EADDRNOTAVAIL: Can't assign requested address", + 50 => "ENETDOWN: Network is down", + 51 => "ENETUNREACH: Network is unreachable", + 52 => "ENETRESET: Network dropped connection on reset", + 53 => "ECONNABORTED: Software caused connection abort", + 54 => "ECONNRESET: Connection reset by peer", + 55 => "ENOBUFS: No buffer space available", + 56 => "EISCONN: Socket is already connected", + 57 => "ENOTCONN: Socket is not connected", + 58 => "ESHUTDOWN: Can't send after socket shutdown", + 60 => "ETIMEDOUT: Operation timed out", + 61 => "ECONNREFUSED: Connection refused", + 62 => "ELOOP: Too many levels of symbolic links", + 63 => "ENAMETOOLONG: File name too long", + 64 => "EHOSTDOWN: Host is down", + 65 => "EHOSTUNREACH: No route to host", + 66 => "ENOTEMPTY: Directory not empty", + 67 => "EPROCLIM: Too many processes", + 68 => "EUSERS: Too many users", + 69 => "EDQUOT: Disc quota exceeded", + 70 => "ESTALE: Stale NFS file handle", + 72 => "EBADRPC: RPC struct is bad", + 73 => "ERPCMISMATCH: RPC version wrong", + 74 => "EPROGUNAVAIL: RPC prog. not avail", + 75 => "EPROGMISMATCH: Program version wrong", + 76 => "EPROCUNAVAIL: Bad procedure for program", + 77 => "ENOLCK: No locks available", + 78 => "ENOSYS: Function not implemented", + 79 => "EFTYPE: Inappropriate file type or format", + 80 => "EAUTH: Authentication error", + 81 => "ENEEDAUTH: Need authenticator", + 82 => "EIDRM: Identifier removed", + 83 => "ENOMSG: No message of desired type", + 84 => "EOVERFLOW: Value too large to be stored in data type", + 85 => "ECANCELED: Operation canceled", + 86 => "EILSEQ: Illegal byte sequence", + 87 => "ENOATTR: Attribute not found", + 88 => "EDOOFUS: Programming error", + 89 => "EBADMSG: Bad message", + 90 => "EMULTIHOP: Multihop attempted", + 91 => "ENOLINK: Link has been severed", + 92 => "EPROTO: Protocol error", + 93 => "ENOTCAPABLE: Capabilities insufficient", + 94 => "ECAPMODE: Not permitted in capability mode", + 95 => "ENOTRECOVERABLE: State not recoverable", + 96 => "EOWNERDEAD: Previous owner died", + 97 => "EINTEGRITY: Integrity check failed", + _ => unreachable!(), + } + } +} + +/// The associated constants for [`super::Errno`] are generated from FreeBSD errno values +/// https://man.freebsd.org/cgi/man.cgi?errno +#[expect( + unused, + reason = "Generated code that is not used in the current context, but useful for error handling later on." +)] +impl super::Errno { + /// Operation not permitted + pub(crate) const EPERM: Self = Self::from_const(1); + /// No such file or directory + pub(crate) const ENOENT: Self = Self::from_const(2); + /// No such process + pub(crate) const ESRCH: Self = Self::from_const(3); + /// Interrupted system call + pub(crate) const EINTR: Self = Self::from_const(4); + /// Input/output error + pub(crate) const EIO: Self = Self::from_const(5); + /// Device not configured + pub(crate) const ENXIO: Self = Self::from_const(6); + /// Argument list too long + pub(crate) const E2BIG: Self = Self::from_const(7); + /// Exec format error + pub(crate) const ENOEXEC: Self = Self::from_const(8); + /// Bad file descriptor + pub(crate) const EBADF: Self = Self::from_const(9); + /// No child processes + pub(crate) const ECHILD: Self = Self::from_const(10); + /// Resource deadlock avoided + pub(crate) const EDEADLK: Self = Self::from_const(11); + /// Cannot allocate memory + pub(crate) const ENOMEM: Self = Self::from_const(12); + /// Permission denied + pub(crate) const EACCES: Self = Self::from_const(13); + /// Bad address + pub(crate) const EFAULT: Self = Self::from_const(14); + /// Block device required + pub(crate) const ENOTBLK: Self = Self::from_const(15); + /// Device busy + pub(crate) const EBUSY: Self = Self::from_const(16); + /// File exists + pub(crate) const EEXIST: Self = Self::from_const(17); + /// Cross-device link + pub(crate) const EXDEV: Self = Self::from_const(18); + /// Operation not supported by device + pub(crate) const ENODEV: Self = Self::from_const(19); + /// Not a directory + pub(crate) const ENOTDIR: Self = Self::from_const(20); + /// Is a directory + pub(crate) const EISDIR: Self = Self::from_const(21); + /// Invalid argument + pub(crate) const EINVAL: Self = Self::from_const(22); + /// Too many open files in system + pub(crate) const ENFILE: Self = Self::from_const(23); + /// Too many open files + pub(crate) const EMFILE: Self = Self::from_const(24); + /// Inappropriate ioctl for device + pub(crate) const ENOTTY: Self = Self::from_const(25); + /// Text file busy + pub(crate) const ETXTBSY: Self = Self::from_const(26); + /// File too large + pub(crate) const EFBIG: Self = Self::from_const(27); + /// No space left on device + pub(crate) const ENOSPC: Self = Self::from_const(28); + /// Illegal seek + pub(crate) const ESPIPE: Self = Self::from_const(29); + /// Read-only file system + pub(crate) const EROFS: Self = Self::from_const(30); + /// Too many links + pub(crate) const EMLINK: Self = Self::from_const(31); + /// Broken pipe + pub(crate) const EPIPE: Self = Self::from_const(32); + /// Numerical argument out of domain + pub(crate) const EDOM: Self = Self::from_const(33); + /// Result too large + pub(crate) const ERANGE: Self = Self::from_const(34); + /// Resource temporarily unavailable + pub(crate) const EAGAIN: Self = Self::from_const(35); + /// Operation now in progress + pub(crate) const EINPROGRESS: Self = Self::from_const(36); + /// Operation already in progress + pub(crate) const EALREADY: Self = Self::from_const(37); + /// Socket operation on non-socket + pub(crate) const ENOTSOCK: Self = Self::from_const(38); + /// Destination address required + pub(crate) const EDESTADDRREQ: Self = Self::from_const(39); + /// Message too long + pub(crate) const EMSGSIZE: Self = Self::from_const(40); + /// Protocol wrong type for socket + pub(crate) const EPROTOTYPE: Self = Self::from_const(41); + /// Protocol not available + pub(crate) const ENOPROTOOPT: Self = Self::from_const(42); + /// Protocol not supported + pub(crate) const EPROTONOSUPPORT: Self = Self::from_const(43); + /// Socket type not supported + pub(crate) const ESOCKTNOSUPPORT: Self = Self::from_const(44); + /// Operation not supported + pub(crate) const EOPNOTSUPP: Self = Self::from_const(45); + /// Protocol family not supported + pub(crate) const EPFNOSUPPORT: Self = Self::from_const(46); + /// Address family not supported by protocol family + pub(crate) const EAFNOSUPPORT: Self = Self::from_const(47); + /// Address already in use + pub(crate) const EADDRINUSE: Self = Self::from_const(48); + /// Can't assign requested address + pub(crate) const EADDRNOTAVAIL: Self = Self::from_const(49); + /// Network is down + pub(crate) const ENETDOWN: Self = Self::from_const(50); + /// Network is unreachable + pub(crate) const ENETUNREACH: Self = Self::from_const(51); + /// Network dropped connection on reset + pub(crate) const ENETRESET: Self = Self::from_const(52); + /// Software caused connection abort + pub(crate) const ECONNABORTED: Self = Self::from_const(53); + /// Connection reset by peer + pub(crate) const ECONNRESET: Self = Self::from_const(54); + /// No buffer space available + pub(crate) const ENOBUFS: Self = Self::from_const(55); + /// Socket is already connected + pub(crate) const EISCONN: Self = Self::from_const(56); + /// Socket is not connected + pub(crate) const ENOTCONN: Self = Self::from_const(57); + /// Can't send after socket shutdown + pub(crate) const ESHUTDOWN: Self = Self::from_const(58); + /// Operation timed out + pub(crate) const ETIMEDOUT: Self = Self::from_const(60); + /// Connection refused + pub(crate) const ECONNREFUSED: Self = Self::from_const(61); + /// Too many levels of symbolic links + pub(crate) const ELOOP: Self = Self::from_const(62); + /// File name too long + pub(crate) const ENAMETOOLONG: Self = Self::from_const(63); + /// Host is down + pub(crate) const EHOSTDOWN: Self = Self::from_const(64); + /// No route to host + pub(crate) const EHOSTUNREACH: Self = Self::from_const(65); + /// Directory not empty + pub(crate) const ENOTEMPTY: Self = Self::from_const(66); + /// Too many processes + pub(crate) const EPROCLIM: Self = Self::from_const(67); + /// Too many users + pub(crate) const EUSERS: Self = Self::from_const(68); + /// Disc quota exceeded + pub(crate) const EDQUOT: Self = Self::from_const(69); + /// Stale NFS file handle + pub(crate) const ESTALE: Self = Self::from_const(70); + /// RPC struct is bad + pub(crate) const EBADRPC: Self = Self::from_const(72); + /// RPC version wrong + pub(crate) const ERPCMISMATCH: Self = Self::from_const(73); + /// RPC prog. not avail + pub(crate) const EPROGUNAVAIL: Self = Self::from_const(74); + /// Program version wrong + pub(crate) const EPROGMISMATCH: Self = Self::from_const(75); + /// Bad procedure for program + pub(crate) const EPROCUNAVAIL: Self = Self::from_const(76); + /// No locks available + pub(crate) const ENOLCK: Self = Self::from_const(77); + /// Function not implemented + pub(crate) const ENOSYS: Self = Self::from_const(78); + /// Inappropriate file type or format + pub(crate) const EFTYPE: Self = Self::from_const(79); + /// Authentication error + pub(crate) const EAUTH: Self = Self::from_const(80); + /// Need authenticator + pub(crate) const ENEEDAUTH: Self = Self::from_const(81); + /// Identifier removed + pub(crate) const EIDRM: Self = Self::from_const(82); + /// No message of desired type + pub(crate) const ENOMSG: Self = Self::from_const(83); + /// Value too large to be stored in data type + pub(crate) const EOVERFLOW: Self = Self::from_const(84); + /// Operation canceled + pub(crate) const ECANCELED: Self = Self::from_const(85); + /// Illegal byte sequence + pub(crate) const EILSEQ: Self = Self::from_const(86); + /// Attribute not found + pub(crate) const ENOATTR: Self = Self::from_const(87); + /// Programming error + pub(crate) const EDOOFUS: Self = Self::from_const(88); + /// Bad message + pub(crate) const EBADMSG: Self = Self::from_const(89); + /// Multihop attempted + pub(crate) const EMULTIHOP: Self = Self::from_const(90); + /// Link has been severed + pub(crate) const ENOLINK: Self = Self::from_const(91); + /// Protocol error + pub(crate) const EPROTO: Self = Self::from_const(92); + /// Capabilities insufficient + pub(crate) const ENOTCAPABLE: Self = Self::from_const(93); + /// Not permitted in capability mode + pub(crate) const ECAPMODE: Self = Self::from_const(94); + /// State not recoverable + pub(crate) const ENOTRECOVERABLE: Self = Self::from_const(95); + /// Previous owner died + pub(crate) const EOWNERDEAD: Self = Self::from_const(96); + /// Integrity check failed + pub(crate) const EINTEGRITY: Self = Self::from_const(97); + + /// Resource temporarily unavailable (alias for EAGAIN) + pub(crate) const EWOULDBLOCK: Self = Self::from_const(35); + /// Operation not supported (alias for EOPNOTSUPP) + pub(crate) const ENOTSUP: Self = Self::from_const(45); + + /// The maximum supported Errno + pub(crate) const MAX: Self = Self::from_const(97); +} diff --git a/litebox_platform_freebsd_userland/src/errno/mod.rs b/litebox_platform_freebsd_userland/src/errno/mod.rs new file mode 100644 index 000000000..57917d69d --- /dev/null +++ b/litebox_platform_freebsd_userland/src/errno/mod.rs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use thiserror::Error; + +mod generated; + +/// FreeBSD error numbers +/// +/// This is a transparent wrapper around FreeBSD error numbers (i.e., `i32`s) intended +/// to provide some type safety by expecting explicit conversions to/from `i32`s. +#[derive(PartialEq, Eq, Clone, Copy, Error)] +pub(crate) struct Errno { + value: core::num::NonZeroU8, +} + +impl From for i32 { + fn from(e: Errno) -> Self { + e.value.get().into() + } +} + +impl core::fmt::Display for Errno { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +impl core::fmt::Debug for Errno { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "Errno({} = {})", self.value.get(), self.as_str()) + } +} + +impl Errno { + /// Provide the negative integer representation of the error + /// + /// ```ignore + /// # use crate::errno::Errno; + /// assert_eq!(-1, Errno::EPERM.as_neg()); + /// // Direct conversion to i32 will give the positive variant + /// assert_eq!(1, Errno::EPERM.into()); + /// ``` + #[expect( + dead_code, + reason = "Unused in the current context, but useful for error handling later on." + )] + pub(crate) fn as_neg(self) -> i32 { + -i32::from(self) + } + + /// (Private-only) Helper function that makes the associated constants on [`Errno`] significantly more + /// readable. Not intended to be used outside this crate, or even this module. + const fn from_const(v: u8) -> Self { + Self { + value: core::num::NonZeroU8::new(v).unwrap(), + } + } +} + +/// Errors when converting to an [`Errno`] +#[derive(Error, Debug)] +pub(crate) enum ErrnoConversionError { + #[error("Expected positive error number")] + Negative, + #[error("Error number cannot be zero")] + Zero, + #[error("Error number is unexpectedly large")] + TooLarge, +} + +impl TryFrom for Errno { + type Error = ErrnoConversionError; + fn try_from(value: i32) -> Result { + let value: u32 = value.try_into().or(Err(ErrnoConversionError::Negative))?; + Self::try_from(value) + } +} +impl TryFrom for Errno { + type Error = ErrnoConversionError; + fn try_from(value: u32) -> Result { + let value: u8 = value.try_into().or(Err(ErrnoConversionError::TooLarge))?; + Self::try_from(value) + } +} +impl TryFrom for Errno { + type Error = ErrnoConversionError; + fn try_from(value: u8) -> Result { + let value = core::num::NonZeroU8::new(value).ok_or(ErrnoConversionError::Zero)?; + if value.get() <= Self::MAX.value.get() { + Ok(Self { value }) + } else { + Err(ErrnoConversionError::TooLarge) + } + } +} diff --git a/litebox_platform_freebsd_userland/src/freebsd_types.rs b/litebox_platform_freebsd_userland/src/freebsd_types.rs new file mode 100644 index 000000000..791e57533 --- /dev/null +++ b/litebox_platform_freebsd_userland/src/freebsd_types.rs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#![allow(non_camel_case_types)] + +pub(crate) const STDOUT_FILENO: i32 = 1; +pub(crate) const STDERR_FILENO: i32 = 2; + +bitflags::bitflags! { + /// Desired memory protection of a memory mapping. + #[derive(PartialEq, Debug)] + pub(crate) struct ProtFlags: core::ffi::c_int { + /// Pages cannot be accessed. + const PROT_NONE = 0; + /// Pages can be read. + const PROT_READ = 1 << 0; + /// Pages can be written. + const PROT_WRITE = 1 << 1; + /// Pages can be executed + const PROT_EXEC = 1 << 2; + /// + const _ = !0; + + const PROT_READ_EXEC = Self::PROT_READ.bits() | Self::PROT_EXEC.bits(); + const PROT_READ_WRITE = Self::PROT_READ.bits() | Self::PROT_WRITE.bits(); + } +} + +bitflags::bitflags! { + /// Additional parameters for [`mmap`] on FreeBSD. + #[derive(Debug)] + pub(crate) struct MapFlags: core::ffi::c_int { + /// Share this mapping. Mutually exclusive with `MAP_PRIVATE`. + const MAP_SHARED = 0x0001; + /// Changes are private + const MAP_PRIVATE = 0x0002; + /// Interpret addr exactly + const MAP_FIXED = 0x0010; + /// don't use a file (FreeBSD uses MAP_ANON) + const MAP_ANON = 0x1000; + /// Synonym for [`MAP_ANON`] (FreeBSD style) + const MAP_ANONYMOUS = Self::MAP_ANON.bits(); + /// Reserve address space without allocating memory + const MAP_GUARD = 0x2000; + /// For use with MAP_FIXED, don't replace existing mappings + const MAP_EXCL = 0x4000; + /// Do not include this mapping in core dumps + const MAP_NOCORE = 0x20000; + /// Prefault read pages (FreeBSD equivalent of MAP_POPULATE?) + const MAP_PREFAULT_READ = 0x40000; + /// Don't sync to backing store + const MAP_NOSYNC = 0x800; + /// Use 2MB super pages if possible + const MAP_ALIGNED_SUPER = 0x1000000; + /// Region grows down, like a stack + const MAP_STACK = 0x400; + /// + const _ = !0; + } +} + +/// Operations currently supported by the safer variants of the FreeBSD _umtx_op syscall +#[repr(i32)] +pub(crate) enum UmtxOpOperation { + UMTX_OP_WAIT_UINT = 11, + UMTX_OP_WAKE = 3, +} + +/// FreeBSD thread creation parameters structure. +/// Matches the C `struct thr_param` from +#[repr(C)] +#[derive(Clone, Debug)] +#[allow(dead_code)] +pub struct ThrParam { + pub start_func: u64, // void (*start_func)(void *) + pub arg: u64, // void *arg + pub stack_base: u64, // char *stack_base + pub stack_size: u64, // size_t stack_size + pub tls_base: u64, // char *tls_base + pub tls_size: u64, // size_t tls_size + pub child_tid: u64, // long *child_tid + pub parent_tid: u64, // long *parent_tid + pub flags: i32, // int flags + pub _pad: i32, // padding for 8-byte alignment + pub rtp: u64, // struct rtprio *rtp +} diff --git a/litebox_platform_freebsd_userland/src/lib.rs b/litebox_platform_freebsd_userland/src/lib.rs new file mode 100644 index 000000000..a090b0dca --- /dev/null +++ b/litebox_platform_freebsd_userland/src/lib.rs @@ -0,0 +1,1729 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! A [LiteBox platform](../litebox/platform/index.html) for running LiteBox on userland FreeBSD. + +// Restrict this crate to only work on FreeBSD. For now, we are restricting this to only x86-64 +// FreeBSD, but we _may_ allow for more in the future, if we find it useful to do so. +#![cfg(all(target_os = "freebsd", target_arch = "x86_64"))] + +use core::sync::atomic::AtomicU32; +use core::time::Duration; +use std::sync::atomic::{AtomicI32, Ordering}; + +use litebox::fs::OFlags; +use litebox::platform::page_mgmt::{FixedAddressBehavior, MemoryRegionPermissions}; +use litebox::platform::{ImmediatelyWokenUp, RawConstPointer as _, UnblockedOrTimedOut}; +use litebox::shim::ContinueOperation; +use litebox::utils::{ReinterpretUnsignedExt as _, TruncateExt as _}; +use litebox_common_linux::{ProtFlags, PunchthroughSyscall}; + +use zerocopy::{FromBytes, IntoBytes}; + +mod syscall_raw; +use syscall_raw::syscalls; + +mod errno; + +mod freebsd_types; + +extern crate alloc; + +/// The userland FreeBSD platform. +/// +/// This implements the main [`litebox::platform::Provider`] trait, i.e., implements all platform +/// traits. +pub struct FreeBSDUserland { + /// Reserved pages that are not available for guest programs to use. + reserved_pages: Vec>, +} + +impl core::fmt::Debug for FreeBSDUserland { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("FreeBSDUserland").finish_non_exhaustive() + } +} + +const SELFPROC_MAPS_PATH: &str = "/proc/curproc/map"; + +impl FreeBSDUserland { + /// Create a new userland-FreeBSD platform for use in `LiteBox`. + pub fn new() -> &'static Self { + register_exception_handlers(); + + let platform = Self { + reserved_pages: Self::read_proc_self_maps(), + }; + + Box::leak(Box::new(platform)) + } + + fn read_proc_self_maps() -> alloc::vec::Vec> { + let path = SELFPROC_MAPS_PATH; + + let Ok(c_path) = std::ffi::CString::new(path) else { + return alloc::vec::Vec::new(); + }; + + let fd = unsafe { + syscalls::syscall3( + syscalls::Sysno::Open, + c_path.as_ptr() as usize, + OFlags::RDONLY.bits() as usize, + 0, + ) + }; + + let Ok(fd) = fd else { + return alloc::vec::Vec::new(); + }; + + let mut buf = [0u8; 8192]; + let mut total_read = 0; + + loop { + if total_read >= buf.len() { + break; + } + + let remaining = buf.len() - total_read; + let n = unsafe { + syscalls::syscall3( + syscalls::Sysno::Read, + fd, + buf.as_mut_ptr() as usize + total_read, + remaining, + ) + }; + + match n { + Ok(0) => break, + Ok(bytes_read) => { + if bytes_read <= remaining { + total_read += bytes_read; + } else { + break; + } + } + Err(_) => { + // Close the file descriptor before returning + let _ = unsafe { syscalls::syscall1(syscalls::Sysno::Close, fd) }; + return alloc::vec::Vec::new(); + } + } + } + + // Close the file descriptor + let _ = unsafe { syscalls::syscall1(syscalls::Sysno::Close, fd) }; + + if total_read == 0 { + return alloc::vec::Vec::new(); + } + + let Ok(content) = core::str::from_utf8(&buf[..total_read]) else { + return alloc::vec::Vec::new(); + }; + + let mut reserved_pages = alloc::vec::Vec::new(); + + for line in content.lines() { + if line.trim().is_empty() { + continue; + } + + let parts: alloc::vec::Vec<&str> = line.split_whitespace().collect(); + + if parts.len() < 2 { + continue; + } + + // Parse FreeBSD format: start_addr end_addr ...other_fields... + let start_str = parts[0].strip_prefix("0x").unwrap_or(parts[0]); + let end_str = parts[1].strip_prefix("0x").unwrap_or(parts[1]); + + let Ok(start) = usize::from_str_radix(start_str, 16) else { + continue; + }; + + let Ok(end) = usize::from_str_radix(end_str, 16) else { + continue; + }; + + if start <= end { + reserved_pages.push(start..end); + } + } + + reserved_pages + } + + /// Returns parameters about the initial task for the shim. + #[expect( + clippy::missing_panics_doc, + reason = "panicking only on failures of documented FreeBSD contracts" + )] + #[expect( + clippy::similar_names, + reason = "pid and ppid are standard POSIX terms" + )] + pub fn init_task(&self) -> litebox_common_linux::TaskParams { + let mut tid: isize = 0; + unsafe { + syscalls::syscall1(syscalls::Sysno::ThrSelf, &raw mut tid as usize) + .expect("thr_self failed"); + } + let pid = i32::try_from(tid).expect("tid should fit in i32"); + let ppid = + unsafe { syscalls::syscall0(syscalls::Sysno::Getppid) }.expect("Failed to get PPID"); + let ppid: i32 = ppid.try_into().expect("ppid should fit in i32"); + + litebox_common_linux::TaskParams { + pid, + ppid, + uid: unsafe { syscalls::syscall0(syscalls::Sysno::Getuid) } + .expect("getuid failed") + .try_into() + .unwrap(), + euid: unsafe { syscalls::syscall0(syscalls::Sysno::Geteuid) } + .expect("geteuid failed") + .try_into() + .unwrap(), + gid: unsafe { syscalls::syscall0(syscalls::Sysno::Getgid) } + .expect("getgid failed") + .try_into() + .unwrap(), + egid: unsafe { syscalls::syscall0(syscalls::Sysno::Getegid) } + .expect("getegid failed") + .try_into() + .unwrap(), + } + } +} + +impl litebox::platform::Provider for FreeBSDUserland {} + +// --------------------------------------------------------------------------- +// TLS (`.tbss`) access helpers +// +// On FreeBSD x86_64, the ELF TLS model uses `@tpoff` with `fs` as the +// TLS segment register. This is the same as Linux x86_64. +// At guest-host transitions we swap `fs` and `gs`, so after the swap the +// host TLS base is in the normal segment register. +// --------------------------------------------------------------------------- + +/// TLS relocation suffix. +macro_rules! tls_suffix { + () => { + "@tpoff" + }; +} + +/// Segment register used for TLS in normal host context. +macro_rules! tls_seg { + () => { + "fs" + }; +} + +/// Segment register where the host TLS base is saved during guest execution. +macro_rules! saved_tls_seg { + () => { + "gs" + }; +} + +/// Full TLS memory operand for a `.tbss` variable in normal host context. +macro_rules! tls { + ($var:literal) => { + concat!(tls_seg!(), ":", $var, tls_suffix!()) + }; +} + +/// Full TLS memory operand for a `.tbss` variable accessed via the saved +/// segment register (before the fs/gs swap, e.g. from a signal handler). +macro_rules! saved_tls { + ($var:literal) => { + concat!(saved_tls_seg!(), ":", $var, tls_suffix!()) + }; +} + +impl litebox::platform::SignalProvider for FreeBSDUserland { + type Signal = litebox_common_linux::signal::Signal; + + fn take_pending_signals(&self, mut f: impl FnMut(Self::Signal)) { + // Atomically swap out pending signals and process them. + let signals = unsafe { + let mut ret: u32; + core::arch::asm!( + concat!("xchg ", tls!("pending_host_signals"), ", {val:e}"), + val = inout(reg) 0u32 => ret, + options(nostack), + ); + ret + }; + for bit in 0..32u32 { + if signals & (1 << bit) != 0 { + // Map bit positions to signals. Currently only SIGINT. + if bit == 0 { + f(litebox_common_linux::signal::Signal::SIGINT); + } + } + } + } +} + +// --------------------------------------------------------------------------- +// Thread context structure and shim handlers +// --------------------------------------------------------------------------- + +struct ThreadContext<'a> { + shim: &'a dyn litebox::shim::EnterShim, + ctx: &'a mut litebox_common_linux::PtRegs, +} + +impl ThreadContext<'_> { + fn call_shim( + &mut self, + f: impl FnOnce( + &dyn litebox::shim::EnterShim, + &mut litebox_common_linux::PtRegs, + ) -> ContinueOperation, + ) { + // Clear the interrupt flag before calling the shim. + unsafe { + core::arch::asm!( + concat!("mov BYTE PTR ", tls!("interrupt"), ", 0"), + options(nostack, preserves_flags) + ); + } + let op = f(self.shim, self.ctx); + match op { + ContinueOperation::Resume => unsafe { switch_to_guest(self.ctx) }, + ContinueOperation::Terminate => {} + } + } +} + +unsafe extern "C-unwind" fn init_handler(thread_ctx: &mut ThreadContext) { + thread_ctx.call_shim(|shim, ctx| shim.init(ctx)); +} + +unsafe extern "C-unwind" fn reenter_handler(thread_ctx: &mut ThreadContext) { + thread_ctx.call_shim(|shim, ctx| shim.reenter(ctx)); +} + +#[allow(clippy::cast_sign_loss)] +unsafe extern "C-unwind" fn syscall_handler(thread_ctx: &mut ThreadContext) { + thread_ctx.call_shim(|shim, ctx| shim.syscall(ctx)); +} + +extern "C-unwind" fn exception_handler( + thread_ctx: &mut ThreadContext, + trapno: usize, + error: usize, + cr2: usize, +) { + let info = litebox::shim::ExceptionInfo { + exception: litebox::shim::Exception(trapno.try_into().unwrap()), + error_code: error.try_into().unwrap(), + cr2, + kernel_mode: false, + }; + thread_ctx.call_shim(|shim, ctx| shim.exception(ctx, &info)); +} + +extern "C-unwind" fn interrupt_handler(thread_ctx: &mut ThreadContext) { + thread_ctx.call_shim(|shim, ctx| shim.interrupt(ctx)); +} + +// --------------------------------------------------------------------------- +// Guest-host context switching (x86_64) +// --------------------------------------------------------------------------- + +core::arch::global_asm!( + " + .section .tbss + .align 8 +scratch: + .quad 0 +host_sp: + .quad 0 +host_bp: + .quad 0 +guest_context_top: + .quad 0 +.globl guest_fsbase +guest_fsbase: + .quad 0 +in_guest: + .byte 0 +.globl interrupt +interrupt: + .byte 0 + .align 4 +.globl pending_host_signals +pending_host_signals: + .long 0 + .align 8 +.globl wait_waker_addr +wait_waker_addr: + .quad 0 + " +); + +// Ensure the linker retains the naked functions that define assembly-level +// labels (syscall_callback, exception_callback, interrupt_callback, +// switch_to_guest_start, switch_to_guest_end). +// Without this, `--gc-sections` may discard them in test builds. +#[used] +static _KEEP_ASM_FUNCTIONS: [unsafe extern "C-unwind" fn( + &mut ThreadContext, + *mut litebox_common_linux::PtRegs, + u8, +); 1] = [run_thread_arch]; +#[used] +static _KEEP_SWITCH_TO_GUEST: [unsafe extern "C" fn(&litebox_common_linux::PtRegs) -> !; 1] = + [switch_to_guest]; + +fn set_guest_fsbase(value: usize) { + unsafe { + core::arch::asm! { + "mov fs:guest_fsbase@tpoff, {}", + in(reg) value, + options(nostack, preserves_flags) + } + } +} + +fn get_guest_fsbase() -> usize { + let value: usize; + unsafe { + core::arch::asm! { + "mov {}, fs:guest_fsbase@tpoff", + out(reg) value, + options(nostack, preserves_flags) + } + } + value +} + +/// Runs the guest thread until it terminates. +/// +/// This saves all non-volatile register state then switches to the guest +/// context. When the guest makes a syscall, it jumps back into the middle of +/// this routine, at `syscall_callback`. This code then updates the guest +/// context structure, switches back to the host stack, and calls the syscall +/// handler. +/// +/// When the guest thread terminates, this function returns after restoring +/// non-volatile register state. +#[unsafe(naked)] +unsafe extern "C-unwind" fn run_thread_arch( + thread_ctx: &mut ThreadContext, + ctx: *mut litebox_common_linux::PtRegs, + reenter: u8, +) { + core::arch::naked_asm!( + " + .cfi_startproc + // Push all non-volatiles. + push rbp + mov rbp, rsp + .cfi_def_cfa rbp, 16 + push rbx + push r12 + push r13 + push r14 + push r15 + push rdi // save thread context + + // Save host rsp and rbp and guest context top in TLS. + mov fs:host_sp@tpoff, rsp + mov fs:host_bp@tpoff, rbp + lea r8, [rsi + {GUEST_CONTEXT_SIZE}] + mov fs:guest_context_top@tpoff, r8 + + // Save host fs base in gs base. This will stay set for the lifetime + // of this call stack. + rdfsbase r8 + wrgsbase r8 + + // Call init_handler or reenter_handler based on reenter flag (in dl). + test dl, dl + jnz 1f + call {init_handler} + jmp .Ldone +1: + call {reenter_handler} + jmp .Ldone + + // This entry point is called from the guest when it issues a syscall + // instruction. + // + // At entry, the register context is the guest context with the + // return address in rcx. r11 is an available scratch register (it would + // contain rflags if the syscall instruction had actually been issued). + .globl syscall_callback +syscall_callback: + // Clear in_guest flag. This must be the first instruction to match the + // expectations of `interrupt_signal_handler`. + mov BYTE PTR gs:in_guest@tpoff, 0 + + // Restore host fs base. + rdfsbase r11 + mov gs:guest_fsbase@tpoff, r11 + rdgsbase r11 + wrfsbase r11 + + // Switch to the top of the guest context. + mov r11, rsp + mov rsp, fs:guest_context_top@tpoff + + // TODO: save float and vector registers (xsave or fxsave) + // Save caller-saved registers + push 0x2b // pt_regs->ss = __USER_DS + push r11 // pt_regs->sp + pushfq // pt_regs->eflags + push 0x33 // pt_regs->cs = __USER_CS + push rcx // pt_regs->ip + push rax // pt_regs->orig_ax + + push rdi // pt_regs->di + push rsi // pt_regs->si + push rdx // pt_regs->dx + push rcx // pt_regs->cx + push -38 // pt_regs->ax = ENOSYS + push r8 // pt_regs->r8 + push r9 // pt_regs->r9 + push r10 // pt_regs->r10 + push [rsp + 88] // pt_regs->r11 = rflags + push rbx // pt_regs->bx + push rbp // pt_regs->bp + push r12 // pt_regs->r12 + push r13 // pt_regs->r13 + push r14 // pt_regs->r14 + push r15 // pt_regs->r15 + + // Restore the stack and frame pointer. + mov rsp, fs:host_sp@tpoff + mov rbp, fs:host_bp@tpoff + + // Handle the syscall. This will jump back to the guest but + // will return if the thread is exiting. + mov rdi, [rsp] // pass thread_ctx + call {syscall_handler} + // This thread is done. Return. + jmp .Ldone + + .globl exception_callback +exception_callback: + // Restore the stack and frame pointer. + mov rsp, fs:host_sp@tpoff + mov rbp, fs:host_bp@tpoff + + mov rdi, [rsp] // pass thread_ctx + call {exception_handler} + jmp .Ldone + + .globl interrupt_callback +interrupt_callback: + // Restore the stack and frame pointer. + mov rsp, fs:host_sp@tpoff + mov rbp, fs:host_bp@tpoff + + mov rdi, [rsp] // pass thread_ctx + call {interrupt_handler} + +.Ldone: + + lea rsp, [rbp - 5*8] + pop r15 + pop r14 + pop r13 + pop r12 + pop rbx + pop rbp + .cfi_def_cfa rsp, 8 + ret + .cfi_endproc +", + GUEST_CONTEXT_SIZE = const core::mem::size_of::(), + init_handler = sym init_handler, + reenter_handler = sym reenter_handler, + syscall_handler = sym syscall_handler, + exception_handler = sym exception_handler, + interrupt_handler = sym interrupt_handler, + ); +} + +/// Switches to the provided guest context. +/// +/// # Safety +/// The context must be valid guest context. This can only be called if +/// `run_thread_arch` is on the stack; after the guest exits, it will return to +/// the interior of `run_thread_arch`. +/// +/// Do not call this at a point where the stack needs to be unwound to run +/// destructors. +#[unsafe(naked)] +unsafe extern "C" fn switch_to_guest(ctx: &litebox_common_linux::PtRegs) -> ! { + core::arch::naked_asm!( + ".globl switch_to_guest_start", + "switch_to_guest_start:", + // Set `in_guest` now, then check if there is a pending interrupt. If + // so, jump to the interrupt handler. + // + // If an interrupt arrives after the check, then the signal handler will + // see that the IP is between `switch_to_guest_start` and + // `switch_to_guest_end` and will set the `interrupt` and jump to + // `interrupt_callback`. + "mov BYTE PTR fs:in_guest@tpoff, 1", + "cmp BYTE PTR fs:interrupt@tpoff, 0", + "jne interrupt_callback", + // Restore guest context from ctx. + "mov rsp, rdi", + // Switch to the guest fsbase + "mov rdx, fs:guest_fsbase@tpoff", + "wrfsbase rdx", + "pop r15", + "pop r14", + "pop r13", + "pop r12", + "pop rbp", + "pop rbx", + "pop r11", + "pop r10", + "pop r9", + "pop r8", + "pop rax", + "pop rcx", + "pop rdx", + "pop rsi", + "pop rdi", + "add rsp, 8", // skip orig_rax + "pop gs:scratch@tpoff", // read rip into scratch + "add rsp, 8", // skip cs + "popfq", + "pop rsp", + "jmp gs:scratch@tpoff", // jump to the guest + ".globl switch_to_guest_end", + "switch_to_guest_end:", + ); +} + +unsafe extern "C" { + // Defined in asm blocks above + fn syscall_callback() -> isize; + fn exception_callback(); + fn interrupt_callback(); + fn switch_to_guest_start(); + fn switch_to_guest_end(); +} + +// --------------------------------------------------------------------------- +// Thread management +// --------------------------------------------------------------------------- + +fn run_thread_inner( + shim: &dyn litebox::shim::EnterShim, + ctx: &mut litebox_common_linux::PtRegs, + reenter: bool, +) { + let ctx_ptr = core::ptr::from_mut(ctx); + let mut thread_ctx = ThreadContext { shim, ctx }; + ThreadHandle::run_with_handle(|| { + with_signal_alt_stack(|| unsafe { + run_thread_arch(&mut thread_ctx, ctx_ptr, u8::from(reenter)); + }); + }); +} + +fn thread_start( + init_thread: Box< + dyn litebox::shim::InitThread, + >, + mut ctx: litebox_common_linux::PtRegs, +) { + let shim = init_thread.init(); + run_thread_inner(shim.as_ref(), &mut ctx, false); +} + +/// A handle to a platform thread. +#[derive(Clone)] +pub struct ThreadHandle(std::sync::Arc>>); + +thread_local! { + static CURRENT_THREAD: std::cell::RefCell> = const { std::cell::RefCell::new(None) }; +} + +impl ThreadHandle { + fn run_with_handle(f: impl FnOnce() -> R) -> R { + let handle = ThreadHandle(std::sync::Arc::new(std::sync::Mutex::new(Some(unsafe { + libc::pthread_self() + })))); + CURRENT_THREAD.with_borrow_mut(|current| { + assert!( + current.is_none(), + "nested run_with_handle calls are not supported" + ); + *current = Some(handle); + }); + let _guard = litebox::utils::defer(|| { + let current = CURRENT_THREAD.take().unwrap(); + *current.0.lock().unwrap() = None; + }); + f() + } + + fn current() -> Self { + CURRENT_THREAD.with_borrow(|thread| { + thread + .clone() + .expect("current_thread called outside of a LiteBox thread") + }) + } + + fn interrupt(&self) { + let thread = self.0.lock().unwrap(); + if let Some(&thread) = thread.as_ref() { + unsafe { + libc::pthread_kill(thread, INTERRUPT_SIGNAL_NUMBER.load(Ordering::Relaxed)); + } + } + } +} + +fn block_guest_signals() { + unsafe { + let mut set: libc::sigset_t = std::mem::zeroed(); + libc::sigemptyset(&raw mut set); + libc::sigaddset(&raw mut set, libc::SIGALRM); + libc::sigaddset(&raw mut set, libc::SIGINT); + libc::pthread_sigmask(libc::SIG_BLOCK, &raw const set, std::ptr::null_mut()); + } +} + +/// Spawn a non-guest ("host") thread that automatically blocks guest interrupt +/// signals before running `f`. +pub fn spawn_host_thread(f: F) -> std::thread::JoinHandle +where + F: FnOnce() -> T + Send + 'static, + T: Send + 'static, +{ + std::thread::spawn(move || { + block_guest_signals(); + f() + }) +} + +impl litebox::platform::ThreadProvider for FreeBSDUserland { + type ExecutionContext = litebox_common_linux::PtRegs; + type ThreadSpawnError = std::io::Error; + type ThreadHandle = ThreadHandle; + + unsafe fn spawn_thread( + &self, + ctx: &litebox_common_linux::PtRegs, + init_thread: Box< + dyn litebox::shim::InitThread, + >, + ) -> Result<(), Self::ThreadSpawnError> { + let ctx = ctx.clone(); + let _handle = std::thread::Builder::new().spawn(move || thread_start(init_thread, ctx))?; + Ok(()) + } + + fn current_thread(&self) -> Self::ThreadHandle { + ThreadHandle::current() + } + + fn interrupt_thread(&self, thread: &Self::ThreadHandle) { + thread.interrupt(); + } + + #[cfg(debug_assertions)] + fn run_test_thread(f: impl FnOnce() -> R) -> R { + unsafe { + core::arch::asm!( + "rdfsbase {tmp}", + "wrgsbase {tmp}", + tmp = out(reg) _, + options(nostack, preserves_flags), + ); + } + ThreadHandle::run_with_handle(f) + } +} + +// --------------------------------------------------------------------------- +// RawMutex (using FreeBSD umtx_op) +// --------------------------------------------------------------------------- + +impl litebox::platform::RawMutexProvider for FreeBSDUserland { + type RawMutex = RawMutex; +} + +/// Raw mutex for FreeBSD using `_umtx_op`. +pub struct RawMutex { + inner: AtomicU32, +} + +impl RawMutex { + const fn new() -> Self { + Self { + inner: AtomicU32::new(0), + } + } + + fn block_or_maybe_timeout( + &self, + val: u32, + timeout: Option, + ) -> Result { + use core::sync::atomic::Ordering::SeqCst; + + // Immediately wake up if the value is different. + if self.inner.load(SeqCst) != val { + return Err(ImmediatelyWokenUp); + } + + match umtx_op_operation_timeout( + &self.inner, + freebsd_types::UmtxOpOperation::UMTX_OP_WAIT_UINT, + val as usize, + timeout, + ) { + Ok(0) => Ok(UnblockedOrTimedOut::Unblocked), + Err(e) if e == i32::from(crate::errno::Errno::EINTR) as isize => { + Ok(UnblockedOrTimedOut::Unblocked) + } + Err(e) if e == i32::from(crate::errno::Errno::EAGAIN) as isize => { + Err(ImmediatelyWokenUp) + } + Err(e) if e == i32::from(crate::errno::Errno::ETIMEDOUT) as isize => { + Ok(UnblockedOrTimedOut::TimedOut) + } + Err(e) => { + panic!("Unexpected errno={e} for UMTX_OP_WAIT") + } + _ => unreachable!(), + } + } +} + +impl litebox::platform::RawMutex for RawMutex { + const INIT: Self = Self::new(); + + fn underlying_atomic(&self) -> &AtomicU32 { + &self.inner + } + + fn wake_many(&self, n: usize) -> usize { + assert!(n > 0); + let n: u32 = n.try_into().unwrap(); + + // FreeBSD umtx_op WAKE always returns 0 on success and we cannot + // determine how many threads were actually woken up. + match umtx_op_operation_timeout( + &self.inner, + freebsd_types::UmtxOpOperation::UMTX_OP_WAKE, + n as usize, + None, + ) { + Err(_) => 0, + Ok(_) => n as usize, + } + } + + fn block(&self, val: u32) -> Result<(), ImmediatelyWokenUp> { + match self.block_or_maybe_timeout(val, None) { + Ok(UnblockedOrTimedOut::Unblocked) => Ok(()), + Ok(UnblockedOrTimedOut::TimedOut) => unreachable!(), + Err(ImmediatelyWokenUp) => Err(ImmediatelyWokenUp), + } + } + + fn block_or_timeout( + &self, + val: u32, + timeout: Duration, + ) -> Result { + self.block_or_maybe_timeout(val, Some(timeout)) + } +} + +// --------------------------------------------------------------------------- +// IP Interface (stub) +// --------------------------------------------------------------------------- + +impl litebox::platform::IPInterfaceProvider for FreeBSDUserland { + fn send_ip_packet(&self, packet: &[u8]) -> Result<(), litebox::platform::SendError> { + unimplemented!( + "send_ip_packet is not implemented for FreeBSD yet. packet length: {}", + packet.len() + ); + } + + fn receive_ip_packet( + &self, + packet: &mut [u8], + ) -> Result { + unimplemented!( + "receive_ip_packet is not implemented for FreeBSD yet. packet length: {}", + packet.len() + ); + } +} + +// --------------------------------------------------------------------------- +// Time +// --------------------------------------------------------------------------- + +impl litebox::platform::TimeProvider for FreeBSDUserland { + type Instant = Instant; + type SystemTime = SystemTime; + + fn now(&self) -> Self::Instant { + let mut t = core::mem::MaybeUninit::::uninit(); + unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, t.as_mut_ptr()) }; + let t = unsafe { t.assume_init() }; + Instant { + inner: Duration::new( + t.tv_sec.reinterpret_as_unsigned(), + t.tv_nsec.reinterpret_as_unsigned().truncate(), + ), + } + } + + fn current_time(&self) -> Self::SystemTime { + let mut t = core::mem::MaybeUninit::::uninit(); + unsafe { libc::clock_gettime(libc::CLOCK_REALTIME, t.as_mut_ptr()) }; + let t = unsafe { t.assume_init() }; + SystemTime { + inner: Duration::new( + t.tv_sec.reinterpret_as_unsigned(), + t.tv_nsec.reinterpret_as_unsigned().truncate(), + ), + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct Instant { + inner: Duration, +} + +impl litebox::platform::Instant for Instant { + fn checked_duration_since(&self, earlier: &Self) -> Option { + self.inner.checked_sub(earlier.inner) + } + fn checked_add(&self, duration: Duration) -> Option { + Some(Self { + inner: self.inner.checked_add(duration)?, + }) + } +} + +pub struct SystemTime { + inner: Duration, +} + +impl litebox::platform::SystemTime for SystemTime { + const UNIX_EPOCH: Self = SystemTime { + inner: Duration::ZERO, + }; + + fn duration_since(&self, earlier: &Self) -> Result { + self.inner + .checked_sub(earlier.inner) + .ok_or_else(|| earlier.inner.checked_sub(self.inner).unwrap()) + } +} + +// --------------------------------------------------------------------------- +// Punchthrough +// --------------------------------------------------------------------------- + +pub struct PunchthroughToken<'a> { + punchthrough: PunchthroughSyscall<'a, FreeBSDUserland>, +} + +impl<'a> litebox::platform::PunchthroughToken for PunchthroughToken<'a> { + type Punchthrough = PunchthroughSyscall<'a, FreeBSDUserland>; + fn execute( + self, + ) -> Result< + ::ReturnSuccess, + litebox::platform::PunchthroughError< + ::ReturnFailure, + >, + > { + match self.punchthrough { + PunchthroughSyscall::SetFsBase { addr } => { + set_guest_fsbase(addr); + Ok(0) + } + PunchthroughSyscall::GetFsBase => Ok(get_guest_fsbase()), + _ => Err(litebox::platform::PunchthroughError::Unimplemented( + "PunchthroughToken for FreeBSDUserland", + )), + } + } +} + +impl litebox::platform::PunchthroughProvider for FreeBSDUserland { + type PunchthroughToken<'a> = PunchthroughToken<'a>; + fn get_punchthrough_token_for<'a>( + &self, + punchthrough: as litebox::platform::PunchthroughToken>::Punchthrough, + ) -> Option> { + Some(PunchthroughToken { punchthrough }) + } +} + +// --------------------------------------------------------------------------- +// Debug log +// --------------------------------------------------------------------------- + +impl litebox::platform::DebugLogProvider for FreeBSDUserland { + fn debug_log_print(&self, msg: &str) { + let _ = unsafe { + syscalls::syscall3( + syscalls::Sysno::Write, + freebsd_types::STDERR_FILENO as usize, + msg.as_ptr() as usize, + msg.len(), + ) + }; + } +} + +// --------------------------------------------------------------------------- +// Raw pointers +// --------------------------------------------------------------------------- + +type UserMutPtr = litebox::platform::common_providers::userspace_pointers::UserMutPtr< + litebox::platform::common_providers::userspace_pointers::NoValidation, + T, +>; +type UserConstPtr = litebox::platform::common_providers::userspace_pointers::UserConstPtr< + litebox::platform::common_providers::userspace_pointers::NoValidation, + T, +>; + +impl litebox::platform::RawPointerProvider for FreeBSDUserland { + type RawConstPointer = UserConstPtr; + type RawMutPointer = UserMutPtr; +} + +// --------------------------------------------------------------------------- +// umtx_op helper +// --------------------------------------------------------------------------- + +#[expect( + clippy::similar_names, + reason = "tv_sec/tv_nsec and pid/ppid are standard POSIX names" +)] +fn umtx_op_operation_timeout( + obj: &AtomicU32, + op: freebsd_types::UmtxOpOperation, + val: usize, + timeout: Option, +) -> Result { + let obj_ptr = core::ptr::from_ref(obj) as usize; + let op: i32 = op as _; + let timeout_spec = timeout.map(|t| { + let tv_sec = t.as_secs().cast_signed(); + let tv_nsec = i64::from(t.subsec_nanos()); + libc::timespec { tv_sec, tv_nsec } + }); + + let (uaddr, uaddr2) = if let Some(ref ts) = timeout_spec { + ( + core::mem::size_of::(), + core::ptr::from_ref(ts) as usize, + ) + } else { + (obj_ptr, 0) + }; + + #[expect( + clippy::cast_sign_loss, + reason = "umtx op codes are small positive values" + )] + let op = op as usize; + + unsafe { syscalls::syscall5(syscalls::Sysno::UmtxOp, obj_ptr, op, val, uaddr, uaddr2) } + .map_err(|err| i32::from(err) as isize) +} + +// --------------------------------------------------------------------------- +// Page management +// --------------------------------------------------------------------------- + +fn prot_flags(flags: MemoryRegionPermissions) -> ProtFlags { + let mut res = ProtFlags::PROT_NONE; + res.set( + ProtFlags::PROT_READ, + flags.contains(MemoryRegionPermissions::READ), + ); + res.set( + ProtFlags::PROT_WRITE, + flags.contains(MemoryRegionPermissions::WRITE), + ); + res.set( + ProtFlags::PROT_EXEC, + flags.contains(MemoryRegionPermissions::EXEC), + ); + if flags.contains(MemoryRegionPermissions::SHARED) { + unimplemented!() + } + res +} + +impl litebox::platform::PageManagementProvider for FreeBSDUserland { + const TASK_ADDR_MIN: usize = 0x1000; + const TASK_ADDR_MAX: usize = 0x7FFF_FFFF_F000; // (1 << 47) - PAGE_SIZE + + fn allocate_pages( + &self, + suggested_range: core::ops::Range, + initial_permissions: MemoryRegionPermissions, + can_grow_down: bool, + populate_pages_immediately: bool, + fixed_address_behavior: FixedAddressBehavior, + ) -> Result, litebox::platform::page_mgmt::AllocationError> { + let map_flags = freebsd_types::MapFlags::MAP_PRIVATE + | freebsd_types::MapFlags::MAP_ANONYMOUS + | (match fixed_address_behavior { + FixedAddressBehavior::Replace => freebsd_types::MapFlags::MAP_FIXED, + FixedAddressBehavior::NoReplace => { + freebsd_types::MapFlags::MAP_FIXED | freebsd_types::MapFlags::MAP_EXCL + } + FixedAddressBehavior::Hint => freebsd_types::MapFlags::empty(), + } | if can_grow_down { + freebsd_types::MapFlags::MAP_STACK + } else { + freebsd_types::MapFlags::empty() + } | if populate_pages_immediately { + freebsd_types::MapFlags::MAP_PREFAULT_READ + } else { + freebsd_types::MapFlags::empty() + }); + + let ptr = unsafe { + syscalls::syscall6( + syscalls::Sysno::Mmap, + suggested_range.start, + suggested_range.len(), + prot_flags(initial_permissions) + .bits() + .reinterpret_as_unsigned() as usize, + map_flags.bits().reinterpret_as_unsigned() as usize, + (-1isize).cast_unsigned(), + 0, + ) + } + .expect("mmap failed"); + + Ok(UserMutPtr::from_usize(ptr)) + } + + unsafe fn deallocate_pages( + &self, + range: core::ops::Range, + ) -> Result<(), litebox::platform::page_mgmt::DeallocationError> { + let _ = unsafe { syscalls::syscall2(syscalls::Sysno::Munmap, range.start, range.len()) } + .expect("munmap failed"); + Ok(()) + } + + // remap_pages: FreeBSD lacks mremap(), so we use the default trait + // implementation which does allocate_pages + memcpy + deallocate_pages. + + unsafe fn update_permissions( + &self, + range: core::ops::Range, + new_permissions: MemoryRegionPermissions, + ) -> Result<(), litebox::platform::page_mgmt::PermissionUpdateError> { + unsafe { + syscalls::syscall3( + syscalls::Sysno::Mprotect, + range.start, + range.len(), + prot_flags(new_permissions).bits().reinterpret_as_unsigned() as usize, + ) + } + .expect("mprotect failed"); + Ok(()) + } + + fn reserved_pages(&self) -> impl Iterator> { + self.reserved_pages.iter() + } +} + +// --------------------------------------------------------------------------- +// VmemPageFaultHandler (dummy - host kernel handles page faults) +// --------------------------------------------------------------------------- + +/// Dummy `VmemPageFaultHandler`. +/// +/// Page faults are handled transparently by the host FreeBSD kernel. +/// Provided to satisfy trait bounds for `PageManager::handle_page_fault`. +impl litebox::mm::linux::VmemPageFaultHandler for FreeBSDUserland { + unsafe fn handle_page_fault( + &self, + _fault_addr: usize, + _flags: litebox::mm::linux::VmFlags, + _error_code: u64, + ) -> Result<(), litebox::mm::linux::PageFaultError> { + unreachable!("host kernel handles page faults for FreeBSD userland") + } + + fn access_error(_error_code: u64, _flags: litebox::mm::linux::VmFlags) -> bool { + unreachable!("host kernel handles page faults for FreeBSD userland") + } +} + +// --------------------------------------------------------------------------- +// Stdio +// --------------------------------------------------------------------------- + +impl litebox::platform::StdioProvider for FreeBSDUserland { + fn read_from_stdin(&self, buf: &mut [u8]) -> Result { + use std::io::Read as _; + std::io::stdin().read(buf).map_err(|err| { + if err.kind() == std::io::ErrorKind::BrokenPipe { + litebox::platform::StdioReadError::Closed + } else { + panic!("unhandled error {err}") + } + }) + } + + fn write_to( + &self, + stream: litebox::platform::StdioOutStream, + buf: &[u8], + ) -> Result { + match unsafe { + syscalls::syscall3( + syscalls::Sysno::Write, + usize::try_from(match stream { + litebox::platform::StdioOutStream::Stdout => freebsd_types::STDOUT_FILENO, + litebox::platform::StdioOutStream::Stderr => freebsd_types::STDERR_FILENO, + }) + .unwrap(), + buf.as_ptr() as usize, + buf.len(), + ) + } { + Ok(n) => Ok(n), + Err(err) => panic!("unhandled error {err}"), + } + } + + fn is_a_tty(&self, stream: litebox::platform::StdioStream) -> bool { + use litebox::platform::StdioStream; + use std::io::IsTerminal as _; + match stream { + StdioStream::Stdin => std::io::stdin().is_terminal(), + StdioStream::Stdout => std::io::stdout().is_terminal(), + StdioStream::Stderr => std::io::stderr().is_terminal(), + } + } +} + +// --------------------------------------------------------------------------- +// Allocator +// --------------------------------------------------------------------------- + +#[global_allocator] +static SLAB_ALLOC: litebox::mm::allocator::SafeZoneAllocator<'static, 28, FreeBSDUserland> = + litebox::mm::allocator::SafeZoneAllocator::new(); + +impl litebox::mm::allocator::MemoryProvider for FreeBSDUserland { + fn alloc(layout: &std::alloc::Layout) -> Option<(usize, usize)> { + let size = core::cmp::max( + layout.size().next_power_of_two(), + core::cmp::max(layout.align(), 0x1000) << 1, + ); + unsafe { + syscalls::syscall6( + syscalls::Sysno::Mmap, + 0, + size, + ProtFlags::PROT_READ_WRITE.bits().reinterpret_as_unsigned() as usize, + ((freebsd_types::MapFlags::MAP_PRIVATE | freebsd_types::MapFlags::MAP_ANON) + .bits() + .reinterpret_as_unsigned()) as usize, + usize::MAX, + 0, + ) + } + .map(|addr| (addr, size)) + .ok() + } + + unsafe fn free(_addr: usize) { + todo!(); + } +} + +// --------------------------------------------------------------------------- +// System info +// --------------------------------------------------------------------------- + +impl litebox::platform::SystemInfoProvider for FreeBSDUserland { + fn get_syscall_entry_point(&self) -> usize { + syscall_callback as *const () as usize + } + + fn get_vdso_address(&self) -> Option { + None + } +} + +// --------------------------------------------------------------------------- +// Thread-local storage +// --------------------------------------------------------------------------- + +thread_local! { + static PLATFORM_TLS: std::cell::Cell<*mut ()> = const { std::cell::Cell::new(core::ptr::null_mut()) }; +} + +unsafe impl litebox::platform::ThreadLocalStorageProvider for FreeBSDUserland { + fn get_thread_local_storage() -> *mut () { + PLATFORM_TLS.get() + } + + unsafe fn replace_thread_local_storage(value: *mut ()) -> *mut () { + PLATFORM_TLS.replace(value) + } + + fn clear_guest_thread_local_storage() { + set_guest_fsbase(0); + } +} + +// --------------------------------------------------------------------------- +// CRNG +// --------------------------------------------------------------------------- + +impl litebox::platform::CrngProvider for FreeBSDUserland { + fn fill_bytes_crng(&self, buf: &mut [u8]) { + getrandom::fill(buf).expect("getrandom failed"); + } +} + +// --------------------------------------------------------------------------- +// Signal handling +// --------------------------------------------------------------------------- + +static INTERRUPT_SIGNAL_NUMBER: AtomicI32 = AtomicI32::new(0); + +fn with_signal_alt_stack(f: impl FnOnce() -> R) -> R { + let alt_stack_size = libc::SIGSTKSZ * 2; + let guard_page_size = 0x1000; + let stack_base = unsafe { + libc::mmap( + std::ptr::null_mut(), + guard_page_size + alt_stack_size, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_PRIVATE | libc::MAP_ANONYMOUS, + -1, + 0, + ) + }; + assert!( + stack_base != libc::MAP_FAILED, + "failed to allocate memory for alternate signal stack: {}", + std::io::Error::last_os_error() + ); + let _unmap_guard = litebox::utils::defer(|| { + let r = unsafe { libc::munmap(stack_base, guard_page_size + alt_stack_size) }; + assert!( + r == 0, + "failed to free memory for alternate signal stack: {}", + std::io::Error::last_os_error() + ); + }); + + // Set up a guard page to catch stack overflows. + let r = unsafe { libc::mprotect(stack_base, guard_page_size, libc::PROT_NONE) }; + assert!( + r == 0, + "failed to set guard page for alternate signal stack: {}", + std::io::Error::last_os_error() + ); + + let alt_stack = libc::stack_t { + ss_sp: stack_base.cast(), + ss_flags: 0, + ss_size: alt_stack_size, + }; + let mut oss = libc::stack_t { + ss_sp: std::ptr::null_mut(), + ss_flags: 0, + ss_size: 0, + }; + unsafe { + let r = libc::sigaltstack(&raw const alt_stack, &raw mut oss); + assert!( + r >= 0, + "failed to set up alternate signal stack: {}", + std::io::Error::last_os_error(), + ); + } + let _restore_guard = litebox::utils::defer(|| unsafe { + let r = libc::sigaltstack(&raw const oss, std::ptr::null_mut()); + assert!(r >= 0); + }); + + f() +} + +/// Copy signal context into the guest context structure. +#[expect( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + reason = "register values are reinterpreted between i64 and usize on x86_64" +)] +fn copy_signal_context(regs: &mut litebox_common_linux::PtRegs, context: *mut libc::c_void) { + // SAFETY: The context pointer is guaranteed to be a valid ucontext_t by the + // signal handler calling convention. + let uc = unsafe { &*context.cast::() }; + let mc = &uc.uc_mcontext; + regs.r15 = mc.mc_r15 as usize; + regs.r14 = mc.mc_r14 as usize; + regs.r13 = mc.mc_r13 as usize; + regs.r12 = mc.mc_r12 as usize; + regs.rbp = mc.mc_rbp as usize; + regs.rbx = mc.mc_rbx as usize; + regs.r11 = mc.mc_r11 as usize; + regs.r10 = mc.mc_r10 as usize; + regs.r9 = mc.mc_r9 as usize; + regs.r8 = mc.mc_r8 as usize; + regs.rax = mc.mc_rax as usize; + regs.rcx = mc.mc_rcx as usize; + regs.rdx = mc.mc_rdx as usize; + regs.rsi = mc.mc_rsi as usize; + regs.rdi = mc.mc_rdi as usize; + regs.orig_rax = mc.mc_rax as usize; + regs.rip = mc.mc_rip as usize; + regs.cs = mc.mc_cs as usize; + regs.eflags = mc.mc_rflags as usize; + regs.rsp = mc.mc_rsp as usize; + regs.ss = mc.mc_ss as usize; +} + +/// Modify the signal context to jump to a different instruction address when +/// the signal handler returns. +#[expect( + clippy::cast_possible_wrap, + clippy::ptr_as_ptr, + reason = "register values are reinterpreted between usize and i64 on x86_64" +)] +fn set_signal_return( + context: *mut libc::c_void, + target: unsafe extern "C" fn(), + rdi: usize, + rsi: usize, + rdx: usize, + rcx: usize, +) { + // SAFETY: The context pointer is guaranteed to be a valid ucontext_t. + let uc = unsafe { &mut *(context as *mut libc::ucontext_t) }; + let mc = &mut uc.uc_mcontext; + mc.mc_rip = target as usize as i64; + mc.mc_rdi = rdi as i64; + mc.mc_rsi = rsi as i64; + mc.mc_rdx = rdx as i64; + mc.mc_rcx = rcx as i64; +} + +unsafe extern "C" fn record_pending_signal(signal: litebox_common_linux::signal::Signal) { + let bit = match signal { + litebox_common_linux::signal::Signal::SIGINT => 0, + litebox_common_linux::signal::Signal::SIGALRM => 1, + _ => return, + }; + unsafe { + core::arch::asm!( + concat!("lock or DWORD PTR ", saved_tls!("pending_host_signals"), ", {bit:e}"), + bit = in(reg) 1u32 << bit, + options(nostack), + ); + } +} + +/// Signal handler for exceptions (SIGSEGV, SIGBUS, SIGFPE, SIGILL, SIGTRAP). +#[expect( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + clippy::cast_possible_wrap, + reason = "register values are reinterpreted between i64 and usize on x86_64" +)] +unsafe extern "C" fn exception_signal_handler( + sig: i32, + info: *mut libc::siginfo_t, + context: *mut libc::c_void, +) { + // SAFETY: The context pointer is guaranteed to be a valid ucontext_t by the + // signal handler calling convention. + let uc = unsafe { &*context.cast::() }; + let rip = uc.uc_mcontext.mc_rip as usize; + + // Before checking TLS state (which may not be initialized), check the + // exception table for fallible memory access recovery. This handles the + // case where host code (e.g. read_u8_fallible) triggers a SIGSEGV. + if sig == libc::SIGSEGV + && let Some(fixup) = litebox::mm::exception_table::search_exception_tables(rip) + { + let uc_mut = unsafe { &mut *context.cast::() }; + uc_mut.uc_mcontext.mc_rip = fixup as i64; + return; + } + + // If we are in host code (not in guest), just record the signal and return. + let in_guest: u8; + unsafe { + core::arch::asm!( + concat!("mov {out}, BYTE PTR ", saved_tls!("in_guest")), + out = out(reg_byte) in_guest, + options(nostack, preserves_flags, readonly), + ); + } + + if in_guest == 0 { + // Host-side exception. Check if this is in the switch_to_guest code. + let start = switch_to_guest_start as *const () as usize; + let end = switch_to_guest_end as *const () as usize; + if rip >= start && rip < end { + // The signal arrived in switch_to_guest. Redirect to interrupt handler. + unsafe { + core::arch::asm!( + concat!("mov BYTE PTR ", saved_tls!("interrupt"), ", 1"), + options(nostack, preserves_flags), + ); + } + set_signal_return(context, interrupt_callback, 0, 0, 0, 0); + return; + } + // True host exception - this is a bug. Let the default handler deal with it. + return; + } + + // Guest exception. Save context and redirect. + let trapno: usize = unsafe { (*info).si_signo } as usize; + let error: usize = uc.uc_mcontext.mc_err as usize; + let cr2: usize = unsafe { (*info).si_addr as usize }; + + // Save guest context + let guest_ctx_top: usize; + unsafe { + core::arch::asm!( + concat!("mov {}, ", saved_tls!("guest_context_top")), + out(reg) guest_ctx_top, + options(nostack, preserves_flags, readonly), + ); + } + let regs = unsafe { + &mut *((guest_ctx_top - size_of::()) + as *mut litebox_common_linux::PtRegs) + }; + copy_signal_context(regs, context); + + // Restore host fsbase + unsafe { + core::arch::asm!( + concat!("mov BYTE PTR ", saved_tls!("in_guest"), ", 0"), + options(nostack, preserves_flags), + ); + } + set_signal_return(context, exception_callback, 0, trapno, error, cr2); +} + +/// Signal handler for interrupt signals (SIGINT, SIGALRM, and the RT interrupt signal). +#[expect( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + reason = "register values are reinterpreted between i64 and usize on x86_64" +)] +unsafe extern "C" fn interrupt_signal_handler( + sig: i32, + _info: *mut libc::siginfo_t, + context: *mut libc::c_void, +) { + let uc = unsafe { &*context.cast::() }; + let rip = uc.uc_mcontext.mc_rip as usize; + + if sig == libc::SIGINT { + unsafe { record_pending_signal(litebox_common_linux::signal::Signal::SIGINT) }; + } else if sig == libc::SIGALRM { + unsafe { record_pending_signal(litebox_common_linux::signal::Signal::SIGALRM) }; + } + + let in_guest: u8; + unsafe { + core::arch::asm!( + concat!("mov {out}, BYTE PTR ", saved_tls!("in_guest")), + out = out(reg_byte) in_guest, + options(nostack, preserves_flags, readonly), + ); + } + + if in_guest == 0 { + // Case 1: signal is the RT signal and we're in switch_to_guest. + let start = switch_to_guest_start as *const () as usize; + let end = switch_to_guest_end as *const () as usize; + if rip >= start && rip < end { + unsafe { + core::arch::asm!( + concat!("mov BYTE PTR ", saved_tls!("interrupt"), ", 1"), + options(nostack, preserves_flags), + ); + } + set_signal_return(context, interrupt_callback, 0, 0, 0, 0); + } + // Case 2: signal in host code but interrupting a wait or other host operation. + // This is fine; the wait will see the pending_host_signals flag. + return; + } + + // Case 3 & 4: in guest. Copy out the context and jump to interrupt handler. + let guest_ctx_top: usize; + unsafe { + core::arch::asm!( + concat!("mov {}, ", saved_tls!("guest_context_top")), + out(reg) guest_ctx_top, + options(nostack, preserves_flags, readonly), + ); + } + let regs = unsafe { + &mut *((guest_ctx_top - size_of::()) + as *mut litebox_common_linux::PtRegs) + }; + copy_signal_context(regs, context); + + unsafe { + core::arch::asm!( + concat!("mov BYTE PTR ", saved_tls!("in_guest"), ", 0"), + options(nostack, preserves_flags), + ); + } + set_signal_return(context, interrupt_callback, 0, 0, 0, 0); +} + +fn register_exception_handlers() { + static ONCE: std::sync::Once = std::sync::Once::new(); + ONCE.call_once(|| { + fn sigaction_fn(sig: i32, sa: Option<&libc::sigaction>, old_sa: &mut libc::sigaction) { + unsafe { + let r = libc::sigaction( + sig, + sa.map_or(std::ptr::null(), |sa| &raw const *sa), + &raw mut *old_sa, + ); + assert!( + r >= 0, + "failed to query existing signal handler for signal {}: {}", + sig, + std::io::Error::last_os_error() + ); + } + } + + // Find an available signal for thread interrupts + let interrupt_signal = { + // On FreeBSD, use SIGUSR1 as the interrupt signal since there + // are no RT signals in the Linux sense. + let sig = libc::SIGUSR1; + let mut sa: libc::sigaction = unsafe { core::mem::zeroed() }; + sa.sa_flags = libc::SA_SIGINFO | libc::SA_ONSTACK; + sa.sa_sigaction = interrupt_signal_handler as *const () as usize; + let mut old_sa = unsafe { core::mem::zeroed() }; + sigaction_fn(sig, Some(&sa), &mut old_sa); + INTERRUPT_SIGNAL_NUMBER.store(sig, Ordering::Relaxed); + sig + }; + + // Register exception signal handlers + let exception_signals = &[ + libc::SIGSEGV, + libc::SIGBUS, + libc::SIGFPE, + libc::SIGILL, + libc::SIGTRAP, + ]; + for &sig in exception_signals { + let mut sa: libc::sigaction = unsafe { core::mem::zeroed() }; + sa.sa_flags = libc::SA_SIGINFO | libc::SA_ONSTACK; + sa.sa_sigaction = exception_signal_handler as *const () as usize; + let mut old_sa = unsafe { core::mem::zeroed() }; + sigaction_fn(sig, Some(&sa), &mut old_sa); + } + + // Register SIGINT and SIGALRM handlers to record pending signals + for &sig in &[libc::SIGINT, libc::SIGALRM] { + if sig == interrupt_signal { + continue; // already registered + } + let mut sa: libc::sigaction = unsafe { core::mem::zeroed() }; + sa.sa_flags = libc::SA_SIGINFO | libc::SA_ONSTACK; + sa.sa_sigaction = interrupt_signal_handler as *const () as usize; + let mut old_sa = unsafe { core::mem::zeroed() }; + sigaction_fn(sig, Some(&sa), &mut old_sa); + } + }); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use core::sync::atomic::AtomicU32; + use litebox::platform::RawMutex; + use std::thread::sleep; + + use crate::FreeBSDUserland; + use litebox::platform::{DebugLogProvider, PageManagementProvider}; + + extern crate std; + + #[test] + fn test_raw_mutex() { + let mutex = std::sync::Arc::new(super::RawMutex { + inner: AtomicU32::new(0), + }); + + let copied_mutex = mutex.clone(); + std::thread::spawn(move || { + sleep(core::time::Duration::from_millis(500)); + copied_mutex.wake_many(10); + }); + + assert!(mutex.block(0).is_ok()); + } + + #[test] + fn test_reserved_pages() { + let platform = FreeBSDUserland::new(); + + platform.debug_log_print("msg from FreeBSDUserland test_reserved_pages\n"); + + let reserved_pages: Vec<_> = + >::reserved_pages(platform).collect(); + + // Check that the reserved pages are in order and non-overlapping + let mut prev = 0; + for page in reserved_pages { + assert!(page.start >= prev); + assert!(page.end > page.start); + prev = page.end; + } + } +} diff --git a/litebox_platform_freebsd_userland/src/syscall_raw.rs b/litebox_platform_freebsd_userland/src/syscall_raw.rs new file mode 100644 index 000000000..4a9c45d4a --- /dev/null +++ b/litebox_platform_freebsd_userland/src/syscall_raw.rs @@ -0,0 +1,301 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! FreeBSD host system call support. + +#[repr(i32)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[expect( + dead_code, + reason = "To be comprehensive, we added an over-approximated syscall list. To be removed." +)] +pub(crate) enum SyscallTable { + Exit = 1, + Read = 3, + Write = 4, + Open = 5, + Close = 6, + Getpid = 20, + Mount = 21, + Unmount = 22, + Setuid = 23, + Getuid = 24, + Geteuid = 25, + Getppid = 39, + Getegid = 43, + Getgid = 47, + Munmap = 73, + Mprotect = 74, + Sysctl = 202, + ThrExit = 431, + ThrSelf = 432, + UmtxOp = 454, + ThrNew = 455, + Mmap = 477, +} + +/// Direct syscall wrappers for FreeBSD x86_64 +#[cfg(target_arch = "x86_64")] +pub(crate) mod syscalls { + use super::SyscallTable; + + /// Syscall number alias for compatibility + pub(crate) type Sysno = SyscallTable; + + /// Result type for syscalls + pub(crate) type SyscallResult = Result; + + /// Perform a syscall with no arguments + #[inline] + pub(crate) unsafe fn syscall0(num: SyscallTable) -> SyscallResult { + let ret: usize; + let carry: u8; + unsafe { + core::arch::asm!( + "syscall", + "setc {}", + out(reg_byte) carry, + in("rax") num as i32, + out("rcx") _, + out("r11") _, + lateout("rax") ret, + ); + } + if carry != 0 { + #[expect( + clippy::cast_possible_truncation, + clippy::cast_possible_wrap, + reason = "errno values are small positive integers that fit in i32" + )] + let errno = ret as i32; + Err(crate::errno::Errno::try_from(errno).unwrap_or(crate::errno::Errno::EINVAL)) + } else { + Ok(ret) + } + } + + /// Perform a syscall with one argument + #[inline] + pub(crate) unsafe fn syscall1(num: SyscallTable, arg1: usize) -> SyscallResult { + let ret: usize; + let carry: u8; + unsafe { + core::arch::asm!( + "syscall", + "setc {}", + out(reg_byte) carry, + in("rax") num as i32, + in("rdi") arg1, + out("rcx") _, + out("r11") _, + lateout("rax") ret, + ); + } + if carry != 0 { + #[expect( + clippy::cast_possible_truncation, + clippy::cast_possible_wrap, + reason = "errno values are small positive integers that fit in i32" + )] + let errno = ret as i32; + Err(crate::errno::Errno::try_from(errno).unwrap_or(crate::errno::Errno::EINVAL)) + } else { + Ok(ret) + } + } + + /// Perform a syscall with two arguments + #[inline] + pub(crate) unsafe fn syscall2(num: SyscallTable, arg1: usize, arg2: usize) -> SyscallResult { + let ret: usize; + let carry: u8; + unsafe { + core::arch::asm!( + "syscall", + "setc {}", + out(reg_byte) carry, + in("rax") num as i32, + in("rdi") arg1, + in("rsi") arg2, + out("rcx") _, + out("r11") _, + lateout("rax") ret, + ); + } + if carry != 0 { + #[expect( + clippy::cast_possible_truncation, + clippy::cast_possible_wrap, + reason = "errno values are small positive integers that fit in i32" + )] + let errno = ret as i32; + Err(crate::errno::Errno::try_from(errno).unwrap_or(crate::errno::Errno::EINVAL)) + } else { + Ok(ret) + } + } + + /// Perform a syscall with three arguments + #[inline] + pub(crate) unsafe fn syscall3( + num: SyscallTable, + arg1: usize, + arg2: usize, + arg3: usize, + ) -> SyscallResult { + let ret: usize; + let carry: u8; + unsafe { + core::arch::asm!( + "syscall", + "setc {}", + out(reg_byte) carry, + in("rax") num as i32, + in("rdi") arg1, + in("rsi") arg2, + in("rdx") arg3, + out("rcx") _, + out("r11") _, + lateout("rax") ret, + ); + } + if carry != 0 { + #[expect( + clippy::cast_possible_truncation, + clippy::cast_possible_wrap, + reason = "errno values are small positive integers that fit in i32" + )] + let errno = ret as i32; + Err(crate::errno::Errno::try_from(errno).unwrap_or(crate::errno::Errno::EINVAL)) + } else { + Ok(ret) + } + } + + /// Perform a syscall with four arguments + #[expect( + dead_code, + reason = "To be comprehensive, we added all syscall interfaces for now. To be removed later on." + )] + #[inline] + pub(crate) unsafe fn syscall4( + num: SyscallTable, + arg1: usize, + arg2: usize, + arg3: usize, + arg4: usize, + ) -> SyscallResult { + let ret: usize; + let carry: u8; + unsafe { + core::arch::asm!( + "syscall", + "setc {}", + out(reg_byte) carry, + in("rax") num as i32, + in("rdi") arg1, + in("rsi") arg2, + in("rdx") arg3, + in("r10") arg4, + out("rcx") _, + out("r11") _, + lateout("rax") ret, + ); + } + if carry != 0 { + #[expect( + clippy::cast_possible_truncation, + clippy::cast_possible_wrap, + reason = "errno values are small positive integers that fit in i32" + )] + let errno = ret as i32; + Err(crate::errno::Errno::try_from(errno).unwrap_or(crate::errno::Errno::EINVAL)) + } else { + Ok(ret) + } + } + + /// Perform a syscall with five arguments + #[inline] + pub(crate) unsafe fn syscall5( + num: SyscallTable, + arg1: usize, + arg2: usize, + arg3: usize, + arg4: usize, + arg5: usize, + ) -> SyscallResult { + let ret: usize; + let carry: u8; + unsafe { + core::arch::asm!( + "syscall", + "setc {}", + out(reg_byte) carry, + in("rax") num as i32, + in("rdi") arg1, + in("rsi") arg2, + in("rdx") arg3, + in("r10") arg4, + in("r8") arg5, + out("rcx") _, + out("r11") _, + lateout("rax") ret, + ); + } + if carry != 0 { + #[expect( + clippy::cast_possible_truncation, + clippy::cast_possible_wrap, + reason = "errno values are small positive integers that fit in i32" + )] + let errno = ret as i32; + Err(crate::errno::Errno::try_from(errno).unwrap_or(crate::errno::Errno::EINVAL)) + } else { + Ok(ret) + } + } + + /// Perform a syscall with six arguments + #[inline] + pub(crate) unsafe fn syscall6( + num: SyscallTable, + arg1: usize, + arg2: usize, + arg3: usize, + arg4: usize, + arg5: usize, + arg6: usize, + ) -> SyscallResult { + let ret: usize; + let carry: u8; + unsafe { + core::arch::asm!( + "syscall", + "setc {}", + out(reg_byte) carry, + in("rax") num as i32, + in("rdi") arg1, + in("rsi") arg2, + in("rdx") arg3, + in("r10") arg4, + in("r8") arg5, + in("r9") arg6, + out("rcx") _, + out("r11") _, + lateout("rax") ret, + ); + } + if carry != 0 { + #[expect( + clippy::cast_possible_truncation, + clippy::cast_possible_wrap, + reason = "errno values are small positive integers that fit in i32" + )] + let errno = ret as i32; + Err(crate::errno::Errno::try_from(errno).unwrap_or(crate::errno::Errno::EINVAL)) + } else { + Ok(ret) + } + } +} diff --git a/litebox_platform_multiplex/Cargo.toml b/litebox_platform_multiplex/Cargo.toml index 53abe0148..e747efa26 100644 --- a/litebox_platform_multiplex/Cargo.toml +++ b/litebox_platform_multiplex/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" litebox = { path = "../litebox/", version = "0.1.0" } litebox_platform_linux_userland = { path = "../litebox_platform_linux_userland/", version = "0.1.0", default-features = false, optional = true } litebox_platform_linux_kernel = { path = "../litebox_platform_linux_kernel/", version = "0.1.0", default-features = false, optional = true } +litebox_platform_freebsd_userland = { path = "../litebox_platform_freebsd_userland/", version = "0.1.0", default-features = false, optional = true } litebox_platform_windows_userland = { path = "../litebox_platform_windows_userland/", version = "0.1.0", default-features = false, optional = true } litebox_platform_lvbs = { path = "../litebox_platform_lvbs/", version = "0.1.0", default-features = false, optional = true } once_cell = { version = "1.20.2", default-features = false, features = ["alloc", "race"] } @@ -14,6 +15,7 @@ cfg-if = "1.0.0" [features] default = ["platform_linux_userland_with_linux_syscall"] +platform_freebsd_userland = ["dep:litebox_platform_freebsd_userland"] platform_linux_userland = ["dep:litebox_platform_linux_userland"] platform_windows_userland = ["dep:litebox_platform_windows_userland"] platform_lvbs = ["dep:litebox_platform_lvbs"] diff --git a/litebox_platform_multiplex/src/lib.rs b/litebox_platform_multiplex/src/lib.rs index 57edcab06..419e28902 100644 --- a/litebox_platform_multiplex/src/lib.rs +++ b/litebox_platform_multiplex/src/lib.rs @@ -27,7 +27,9 @@ extern crate alloc; // NOTE: Currently, we only support one platform, thus this is a trivial no-op. However, once we // have more, we must account for each of the possible pairs. cfg_if::cfg_if! { - if #[cfg(all(feature = "platform_linux_userland", target_os = "linux"))] { + if #[cfg(all(feature = "platform_freebsd_userland", target_os = "freebsd"))] { + pub type Platform = litebox_platform_freebsd_userland::FreeBSDUserland; + } else if #[cfg(all(feature = "platform_linux_userland", target_os = "linux"))] { pub type Platform = litebox_platform_linux_userland::LinuxUserland; } else if #[cfg(all(feature = "platform_windows_userland", target_os = "windows"))] { pub type Platform = litebox_platform_windows_userland::WindowsUserland; diff --git a/litebox_shim_linux/Cargo.toml b/litebox_shim_linux/Cargo.toml index 94d889a7f..ec2ac4403 100644 --- a/litebox_shim_linux/Cargo.toml +++ b/litebox_shim_linux/Cargo.toml @@ -19,6 +19,7 @@ zerocopy = { version = "0.8", default-features = false, features = ["derive"] } [features] default = ["platform_linux_userland"] +platform_freebsd_userland = ["litebox_platform_multiplex/platform_freebsd_userland"] platform_linux_userland = ["litebox_platform_multiplex/platform_linux_userland_with_linux_syscall"] platform_windows_userland = ["litebox_platform_multiplex/platform_windows_userland"] platform_linux_snp = ["litebox_platform_multiplex/platform_linux_snp"] diff --git a/litebox_shim_linux/src/syscalls/misc.rs b/litebox_shim_linux/src/syscalls/misc.rs index 4e82dacd8..657fbd20b 100644 --- a/litebox_shim_linux/src/syscalls/misc.rs +++ b/litebox_shim_linux/src/syscalls/misc.rs @@ -168,6 +168,8 @@ impl Task { mod tests { use core::mem::MaybeUninit; + use litebox::platform::RawConstPointer as _; + use crate::syscalls::tests::init_platform; #[test] @@ -177,7 +179,7 @@ mod tests { let task = init_platform(None); let mut buf = [0u8; 16]; - let ptr = crate::MutPtr::from_ptr(buf.as_mut_ptr()); + let ptr = crate::MutPtr::from_usize(buf.as_mut_ptr() as usize); let count = task .sys_getrandom(ptr, buf.len() - 1, RngFlags::empty()) .expect("getrandom failed"); @@ -194,7 +196,7 @@ mod tests { let task = init_platform(None); let mut utsname = MaybeUninit::::uninit(); - let ptr = crate::MutPtr::from_ptr(utsname.as_mut_ptr()); + let ptr = crate::MutPtr::from_usize(utsname.as_mut_ptr() as usize); task.sys_uname(ptr).expect("uname failed"); let utsname = unsafe { utsname.assume_init() }; diff --git a/litebox_shim_linux/src/syscalls/mm.rs b/litebox_shim_linux/src/syscalls/mm.rs index 30c429b62..6df800d60 100644 --- a/litebox_shim_linux/src/syscalls/mm.rs +++ b/litebox_shim_linux/src/syscalls/mm.rs @@ -356,10 +356,12 @@ impl Task { #[cfg(test)] mod tests { + #[allow(unused_imports)] use litebox::{ fs::{Mode, OFlags}, platform::{PageManagementProvider, RawConstPointer, RawMutPointer}, }; + #[allow(unused_imports)] use litebox_common_linux::{MRemapFlags, MapFlags, ProtFlags, errno::Errno}; use crate::syscalls::tests::init_platform; diff --git a/litebox_shim_linux/src/syscalls/process.rs b/litebox_shim_linux/src/syscalls/process.rs index aea95fd6a..08a39b9b0 100644 --- a/litebox_shim_linux/src/syscalls/process.rs +++ b/litebox_shim_linux/src/syscalls/process.rs @@ -1522,20 +1522,20 @@ mod tests { // Save old FS base let mut old_fs_base = MaybeUninit::::uninit(); - let ptr = MutPtr::from_ptr(old_fs_base.as_mut_ptr()); + let ptr = MutPtr::from_usize(old_fs_base.as_mut_ptr() as usize); task.sys_arch_prctl(ArchPrctlArg::GetFs(ptr)) .expect("Failed to get FS base"); let old_fs_base = unsafe { old_fs_base.assume_init() }; // Set new FS base let mut new_fs_base: [u8; 16] = [0; 16]; - let ptr = MutPtr::from_ptr(new_fs_base.as_mut_ptr()); + let ptr: MutPtr = MutPtr::from_usize(new_fs_base.as_mut_ptr() as usize); task.sys_arch_prctl(ArchPrctlArg::SetFs(ptr.as_usize())) .expect("Failed to set FS base"); // Verify new FS base let mut current_fs_base = MaybeUninit::::uninit(); - let ptr = MutPtr::from_ptr(current_fs_base.as_mut_ptr()); + let ptr = MutPtr::from_usize(current_fs_base.as_mut_ptr() as usize); task.sys_arch_prctl(ArchPrctlArg::GetFs(ptr)) .expect("Failed to get FS base"); let current_fs_base = unsafe { current_fs_base.assume_init() }; @@ -1564,19 +1564,20 @@ mod tests { #[test] fn test_prctl_set_get_name() { + use litebox::platform::RawConstPointer as _; let task = crate::syscalls::tests::init_platform(None); // Prepare a null-terminated name to set let name: &[u8] = b"litebox-test\0"; // Call prctl(PR_SET_NAME, set_buf) - let set_ptr = crate::ConstPtr::from_ptr(name.as_ptr()); + let set_ptr = crate::ConstPtr::from_usize(name.as_ptr() as usize); task.sys_prctl(litebox_common_linux::PrctlArg::SetName(set_ptr)) .expect("sys_prctl SetName failed"); // Prepare buffer for prctl(PR_GET_NAME, get_buf) let mut get_buf = [0u8; litebox_common_linux::TASK_COMM_LEN]; - let get_ptr = crate::MutPtr::from_ptr(get_buf.as_mut_ptr()); + let get_ptr = crate::MutPtr::from_usize(get_buf.as_mut_ptr() as usize); task.sys_prctl(litebox_common_linux::PrctlArg::GetName(get_ptr)) .expect("sys_prctl GetName failed"); @@ -1588,13 +1589,13 @@ mod tests { // Test too long name let long_name = [b'a'; litebox_common_linux::TASK_COMM_LEN + 10]; - let long_name_ptr = crate::ConstPtr::from_ptr(long_name.as_ptr()); + let long_name_ptr = crate::ConstPtr::from_usize(long_name.as_ptr() as usize); task.sys_prctl(litebox_common_linux::PrctlArg::SetName(long_name_ptr)) .expect("sys_prctl SetName failed"); // Get the name again let mut get_buf = [0u8; litebox_common_linux::TASK_COMM_LEN]; - let get_ptr = crate::MutPtr::from_ptr(get_buf.as_mut_ptr()); + let get_ptr = crate::MutPtr::from_usize(get_buf.as_mut_ptr() as usize); task.sys_prctl(litebox_common_linux::PrctlArg::GetName(get_ptr)) .expect("sys_prctl GetName failed"); assert_eq!( @@ -1612,10 +1613,10 @@ mod tests { /// Installing a custom handler for SIGINT: a background OS thread sends /// a real SIGINT via `libc::kill`, which should interrupt a blocking sleep /// with `EINTR`. - /// Target Linux only because it use tgkill syscall to send signal to specific thread. - #[cfg(all(target_os = "linux", debug_assertions))] + #[cfg(all(any(target_os = "linux", target_os = "freebsd"), debug_assertions))] #[test] fn test_sigint_with_custom_handler() { + use litebox::platform::RawConstPointer as _; use litebox_common_linux::signal::{SaFlags, SigAction, SigSet, Signal}; use litebox_common_linux::{ClockId, TimerFlags, Timespec}; @@ -1630,7 +1631,7 @@ mod tests { restorer: 0, mask: SigSet::empty(), }; - let act_ptr = crate::ConstPtr::from_ptr(&raw const act); + let act_ptr = crate::ConstPtr::from_usize(&raw const act as usize); task.sys_rt_sigaction( Signal::SIGINT, Some(act_ptr), @@ -1641,13 +1642,12 @@ mod tests { // Spawn a plain OS thread that sends a real SIGINT to this // specific thread after a short delay, giving it time to enter nanosleep. - let pid = unsafe { libc::getpid() }; - let tid = unsafe { libc::syscall(libc::SYS_gettid) }; + let thread = unsafe { libc::pthread_self() }; let handle = std::thread::spawn(move || { std::thread::sleep(std::time::Duration::from_millis(200)); // Safety: sending a signal to a thread in our own process is always valid. - let ret = unsafe { libc::syscall(libc::SYS_tgkill, pid, tid, libc::SIGINT) }; - assert_eq!(ret, 0, "tgkill failed"); + let ret = unsafe { libc::pthread_kill(thread, libc::SIGINT) }; + assert_eq!(ret, 0, "pthread_kill failed"); }); let mut request = Timespec { @@ -1657,8 +1657,8 @@ mod tests { let result = task.sys_clock_nanosleep( ClockId::Monotonic, TimerFlags::empty(), - litebox_common_linux::TimeParam::Timespec64(crate::MutPtr::from_ptr( - &raw mut request, + litebox_common_linux::TimeParam::Timespec64(crate::MutPtr::from_usize( + &raw mut request as usize, )), litebox_common_linux::TimeParam::None, );