diff --git a/Cargo.lock b/Cargo.lock index 542c2a4ad..d338c4dc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -445,17 +445,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "delegate" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "der" version = "0.7.10" @@ -519,30 +508,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -[[package]] -name = "elf" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4445909572dbd556c457c849c4ca58623d84b27c8fff1e74b0b4227d8b90d17b" - [[package]] name = "elf" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55dd888a213fc57e957abf2aa305ee3e8a28dbe05687a251f33b637cd46b0070" -[[package]] -name = "elf_loader" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a0da8db95cff71e500b3d7015c2441a4eb628e0df788b23d1b8d1243314342" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "delegate", - "elf 0.7.4", -] - [[package]] name = "encode_unicode" version = "1.0.0" @@ -840,7 +811,7 @@ dependencies = [ "bitfield", "bitflags 2.9.4", "cfg-if", - "elf 0.8.0", + "elf", "int-enum", "litebox", "syscalls", @@ -853,10 +824,9 @@ name = "litebox_common_optee" version = "0.1.0" dependencies = [ "bitflags 2.9.4", - "elf 0.8.0", + "elf", "litebox", "litebox_common_linux", - "modular-bitfield", "num_enum", "thiserror", "zerocopy", @@ -909,7 +879,7 @@ dependencies = [ "cms", "const-oid", "digest", - "elf 0.8.0", + "elf", "hashbrown", "libc", "litebox", @@ -1005,6 +975,7 @@ dependencies = [ "once_cell", "spin 0.10.0", "x86_64", + "zerocopy", ] [[package]] @@ -1069,8 +1040,7 @@ dependencies = [ "bitvec", "cfg-if", "ctr", - "elf 0.8.0", - "elf_loader", + "elf", "hashbrown", "litebox", "litebox_common_linux", diff --git a/dev_tests/src/ratchet.rs b/dev_tests/src/ratchet.rs index 58ea1b154..bbc7c35b3 100644 --- a/dev_tests/src/ratchet.rs +++ b/dev_tests/src/ratchet.rs @@ -73,7 +73,6 @@ fn ratchet_maybe_uninit() -> Result<()> { ("litebox/", 1), ("litebox_platform_linux_userland/", 3), ("litebox_shim_linux/", 5), - ("litebox_shim_optee/", 1), ], |file| { Ok(file diff --git a/litebox_common_optee/Cargo.toml b/litebox_common_optee/Cargo.toml index 8476afcd8..108280d16 100644 --- a/litebox_common_optee/Cargo.toml +++ b/litebox_common_optee/Cargo.toml @@ -8,7 +8,6 @@ bitflags = "2.9.0" elf = { version = "0.8.0", default-features = false } litebox = { path = "../litebox/", version = "0.1.0" } litebox_common_linux = { path = "../litebox_common_linux/", version = "0.1.0" } -modular-bitfield = { version = "0.12.0", default-features = false } num_enum = { version = "0.7.3", default-features = false } zerocopy = { version = "0.8", features = ["derive"] } thiserror = { version = "2.0.6", default-features = false } diff --git a/litebox_common_optee/src/lib.rs b/litebox_common_optee/src/lib.rs index 8659fe00a..66a24be35 100644 --- a/litebox_common_optee/src/lib.rs +++ b/litebox_common_optee/src/lib.rs @@ -9,14 +9,13 @@ extern crate alloc; use alloc::boxed::Box; +use core::mem::size_of; use litebox::platform::RawConstPointer as _; use litebox::utils::TruncateExt; use litebox_common_linux::{PtRegs, errno::Errno}; -use modular_bitfield::prelude::*; -use modular_bitfield::specifiers::{B8, B54}; use num_enum::TryFromPrimitive; use syscall_nr::{LdelfSyscallNr, TeeSyscallNr}; -use zerocopy::{FromBytes, Immutable, IntoBytes}; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; pub mod syscall_nr; @@ -401,29 +400,67 @@ pub struct UteeParams { pub types: UteeParamsTypes, pub vals: [u64; TEE_NUM_PARAMS * 2], } + +/// Number of TEE parameters to be passed to TAs. const TEE_NUM_PARAMS: usize = 4; -#[expect( - clippy::identity_op, - reason = "the macro auto-generates this, but some issue causes it to still bubble up; this suppresses it the hard way" -)] -mod workaround_identity_op_suppression { - use modular_bitfield::prelude::*; - use modular_bitfield::specifiers::{B4, B48}; - use zerocopy::{FromBytes, Immutable, IntoBytes}; - #[bitfield] - #[derive(Clone, Copy, Default, FromBytes, Immutable, IntoBytes)] - #[repr(C)] - pub struct UteeParamsTypes { - pub type_0: B4, - pub type_1: B4, - pub type_2: B4, - pub type_3: B4, - #[skip] - __: B48, - } -} -pub use workaround_identity_op_suppression::UteeParamsTypes; +/// Number of RPC parameters that the OP-TEE Shim defined and reported to the normal-world +/// Linux kernel driver during `EXCHANGE_CAPABILITIES`. The Linux kernel driver is +/// expected to allocate a shared buffer for this number of parameters. +const NUM_RPC_PARAMS: usize = 4; + +/// Packed parameter types for [`UteeParams`]. +/// +/// Wire layout (little-endian u64): +/// - bits \[3:0\] – type_0 +/// - bits \[7:4\] – type_1 +/// - bits \[11:8\] – type_2 +/// - bits \[15:12\] – type_3 +/// - bits \[63:16\] – reserved (zero) +#[derive(Clone, Copy, Default, FromBytes, Immutable, IntoBytes, KnownLayout)] +#[repr(transparent)] +pub struct UteeParamsTypes(u64); + +impl UteeParamsTypes { + const NIBBLE_MASK: u64 = 0xF; + + /// Get the 4-bit type at the given `index` (0–3). + #[allow(clippy::cast_possible_truncation)] + fn get(self, index: usize) -> u8 { + ((self.0 >> (index * 4)) & Self::NIBBLE_MASK) as u8 + } + + /// Set the 4-bit type at the given `index` (0–3). + fn set(&mut self, index: usize, value: u8) { + let shift = index * 4; + self.0 = (self.0 & !(Self::NIBBLE_MASK << shift)) | (u64::from(value & 0xF) << shift); + } + + pub fn type_0(&self) -> u8 { + self.get(0) + } + pub fn type_1(&self) -> u8 { + self.get(1) + } + pub fn type_2(&self) -> u8 { + self.get(2) + } + pub fn type_3(&self) -> u8 { + self.get(3) + } + pub fn set_type_0(&mut self, v: u8) { + self.set(0, v); + } + pub fn set_type_1(&mut self, v: u8) { + self.set(1, v); + } + pub fn set_type_2(&mut self, v: u8) { + self.set(2, v); + } + pub fn set_type_3(&mut self, v: u8) { + self.set(3, v); + } +} const TEE_PARAM_TYPE_NONE: u8 = 0; const TEE_PARAM_TYPE_VALUE_INPUT: u8 = 1; @@ -1246,10 +1283,70 @@ impl TryFrom for UteeEntryFunc { } } +const OPTEE_MSG_RPC_CMD_LOAD_TA: u32 = 0; +const OPTEE_MSG_RPC_CMD_RPMB: u32 = 1; +const OPTEE_MSG_RPC_CMD_FS: u32 = 2; +const OPTEE_MSG_RPC_CMD_GET_TIME: u32 = 3; +const OPTEE_MSG_RPC_CMD_NOTIFICATION: u32 = 4; +const OPTEE_MSG_RPC_CMD_SUSPEND: u32 = 5; +const OPTEE_MSG_RPC_CMD_SHM_ALLOC: u32 = 6; +const OPTEE_MSG_RPC_CMD_SHM_FREE: u32 = 7; +const OPTEE_MSG_RPC_CMD_GPROF: u32 = 9; +const OPTEE_MSG_RPC_CMD_SOCKET: u32 = 10; +const OPTEE_MSG_RPC_CMD_FTRACE: u32 = 11; +const OPTEE_MSG_RPC_CMD_PLUGIN: u32 = 12; +const OPTEE_MSG_RPC_CMD_I2C_TRANSFER: u32 = 21; +const OPTEE_MSG_RPC_CMD_RPMB_PROBE_RESET: u32 = 22; +const OPTEE_MSG_RPC_CMD_RPMB_PROBE_NEXT: u32 = 23; +const OPTEE_MSG_RPC_CMD_RPMB_PROBE_FRAMES: u32 = 24; + +/// RPC command IDs from `optee_os/core/include/optee_msg.h` +/// +/// These are the command IDs used in the `cmd` field of the RPC `optee_msg_arg`. +/// They live in a separate namespace from [`OpteeMessageCommand`] (which is for main +/// messaging between the normal-world driver and OP-TEE OS). +#[non_exhaustive] +#[derive(Clone, Copy, Debug, PartialEq, TryFromPrimitive)] +#[repr(u32)] +pub enum OpteeRpcCommand { + /// Load a TA into memory, defined in tee-supplicant. + LoadTa = OPTEE_MSG_RPC_CMD_LOAD_TA, + /// Reserved + Rpmb = OPTEE_MSG_RPC_CMD_RPMB, + /// REE file-system access, defined in tee-supplicant. + Fs = OPTEE_MSG_RPC_CMD_FS, + /// Get time. + GetTime = OPTEE_MSG_RPC_CMD_GET_TIME, + /// Notification from/to secure world. + Notification = OPTEE_MSG_RPC_CMD_NOTIFICATION, + /// Suspend execution. + Suspend = OPTEE_MSG_RPC_CMD_SUSPEND, + /// Allocate a piece of shared memory. + ShmAlloc = OPTEE_MSG_RPC_CMD_SHM_ALLOC, + /// Free previously allocated shared memory. + ShmFree = OPTEE_MSG_RPC_CMD_SHM_FREE, + /// GProf support management commands. + Gprof = OPTEE_MSG_RPC_CMD_GPROF, + /// Socket commands. + Socket = OPTEE_MSG_RPC_CMD_SOCKET, + /// Ftrace support management commands. + Ftrace = OPTEE_MSG_RPC_CMD_FTRACE, + /// Plugin commands. + Plugin = OPTEE_MSG_RPC_CMD_PLUGIN, + /// I2C transfer commands. + I2cTransfer = OPTEE_MSG_RPC_CMD_I2C_TRANSFER, + /// Reset RPMB probing + RpmbProbeReset = OPTEE_MSG_RPC_CMD_RPMB_PROBE_RESET, + /// Probe next RPMB device + RpmbProbeNext = OPTEE_MSG_RPC_CMD_RPMB_PROBE_NEXT, + /// RPBM access + RpmbProbeFrames = OPTEE_MSG_RPC_CMD_RPMB_PROBE_FRAMES, +} + /// Temporary memory reference parameter /// /// `optee_msg_param_tmem` from `optee_os/core/include/optee_msg.h` -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, FromBytes, IntoBytes, Immutable, KnownLayout)] #[repr(C)] pub struct OpteeMsgParamTmem { /// Physical address of the buffer @@ -1263,7 +1360,7 @@ pub struct OpteeMsgParamTmem { /// Registered memory reference parameter /// /// `optee_msg_param_rmem` from `optee_os/core/include/optee_msg.h` -#[derive(Clone, Copy)] +#[derive(Clone, Copy, FromBytes, IntoBytes, Immutable, KnownLayout)] #[repr(C)] pub struct OpteeMsgParamRmem { /// Offset into shared memory reference @@ -1280,7 +1377,7 @@ pub struct OpteeMsgParamRmem { /// /// Note: LiteBox doesn't currently support FF-A shared memory, so this struct is /// provided for completeness but is not used. -#[derive(Clone, Copy)] +#[derive(Clone, Copy, FromBytes, IntoBytes, Immutable, KnownLayout)] #[repr(C)] pub struct OpteeMsgParamFmem { /// Lower bits of offset into shared memory reference @@ -1297,7 +1394,7 @@ pub struct OpteeMsgParamFmem { /// Opaque value parameter /// Value parameters are passed unchecked between normal and secure world. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, Immutable, KnownLayout)] #[repr(C)] pub struct OpteeMsgParamValue { pub a: u64, @@ -1305,16 +1402,12 @@ pub struct OpteeMsgParamValue { pub c: u64, } -/// Parameter used together with `OpteeMsgArgs` -#[derive(Clone, Copy)] -#[repr(C)] -pub union OpteeMsgParamUnion { - tmem: OpteeMsgParamTmem, - rmem: OpteeMsgParamRmem, - fmem: OpteeMsgParamFmem, - value: OpteeMsgParamValue, - octets: [u8; 24], -} +/// Parameter used together with `OpteeMsgArgs`. +/// +/// The 24-byte `data` field is the on-wire union of [`OpteeMsgParamTmem`], +/// [`OpteeMsgParamRmem`], [`OpteeMsgParamFmem`], and [`OpteeMsgParamValue`]. +/// Use the typed accessor methods to interpret it. +const OPTEE_MSG_PARAM_DATA_SIZE: usize = 24; const OPTEE_MSG_ATTR_TYPE_NONE: u8 = 0x0; const OPTEE_MSG_ATTR_TYPE_VALUE_INPUT: u8 = 0x1; @@ -1345,79 +1438,192 @@ pub enum OpteeMsgAttrType { TmemInout = OPTEE_MSG_ATTR_TYPE_TMEM_INOUT, } -#[non_exhaustive] -#[bitfield] -#[derive(Clone, Copy, Default)] -#[repr(C)] -pub struct OpteeMsgAttr { - pub typ: B8, - pub meta: bool, - pub noncontig: bool, - #[skip] - __: B54, +/// Attribute field of [`OpteeMsgParam`]. +/// +/// Wire layout (little-endian u64): +/// - bits \[7:0\] – type (`OPTEE_MSG_ATTR_TYPE_*`) +/// - bit 8 – meta +/// - bit 9 – noncontig +/// - bits \[63:10\] – reserved (zero) +#[derive(Clone, Copy, Default, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(transparent)] +pub struct OpteeMsgAttr(u64); + +impl OpteeMsgAttr { + /// Returns the attribute type (bits 0–7). + #[allow(clippy::cast_possible_truncation)] + pub fn attr_type(&self) -> u8 { + self.0 as u8 + } + + /// Returns `true` when the meta bit (bit 8) is set. + pub fn meta(&self) -> bool { + self.0 & (1 << 8) != 0 + } + + /// Returns `true` when the noncontig bit (bit 9) is set. + pub fn noncontig(&self) -> bool { + self.0 & (1 << 9) != 0 + } } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, FromBytes, IntoBytes, Immutable, KnownLayout)] #[repr(C)] pub struct OpteeMsgParam { attr: OpteeMsgAttr, - u: OpteeMsgParamUnion, + data: [u8; OPTEE_MSG_PARAM_DATA_SIZE], } impl OpteeMsgParam { pub fn attr_type(&self) -> OpteeMsgAttrType { - OpteeMsgAttrType::try_from(self.attr.typ()).unwrap_or(OpteeMsgAttrType::None) + OpteeMsgAttrType::try_from(self.attr.attr_type()).unwrap_or(OpteeMsgAttrType::None) } pub fn get_param_tmem(&self) -> Option { if matches!( - self.attr.typ(), + self.attr.attr_type(), OPTEE_MSG_ATTR_TYPE_TMEM_INPUT | OPTEE_MSG_ATTR_TYPE_TMEM_OUTPUT | OPTEE_MSG_ATTR_TYPE_TMEM_INOUT ) { - Some(unsafe { self.u.tmem }) + OpteeMsgParamTmem::read_from_bytes(&self.data).ok() } else { None } } pub fn get_param_rmem(&self) -> Option { if matches!( - self.attr.typ(), + self.attr.attr_type(), OPTEE_MSG_ATTR_TYPE_RMEM_INPUT | OPTEE_MSG_ATTR_TYPE_RMEM_OUTPUT | OPTEE_MSG_ATTR_TYPE_RMEM_INOUT ) { - Some(unsafe { self.u.rmem }) + OpteeMsgParamRmem::read_from_bytes(&self.data).ok() } else { None } } pub fn get_param_fmem(&self) -> Option { if matches!( - self.attr.typ(), + self.attr.attr_type(), OPTEE_MSG_ATTR_TYPE_RMEM_INPUT | OPTEE_MSG_ATTR_TYPE_RMEM_OUTPUT | OPTEE_MSG_ATTR_TYPE_RMEM_INOUT ) { - Some(unsafe { self.u.fmem }) + OpteeMsgParamFmem::read_from_bytes(&self.data).ok() } else { None } } pub fn get_param_value(&self) -> Option { if matches!( - self.attr.typ(), + self.attr.attr_type(), OPTEE_MSG_ATTR_TYPE_VALUE_INPUT | OPTEE_MSG_ATTR_TYPE_VALUE_OUTPUT | OPTEE_MSG_ATTR_TYPE_VALUE_INOUT ) { - Some(unsafe { self.u.value }) + OpteeMsgParamValue::read_from_bytes(&self.data).ok() } else { None } } } +/// Compute the total byte size of an `optee_msg_arg` with `num_params` parameters. +/// Equivalent to the C macro `OPTEE_MSG_GET_ARG_SIZE(num_params)`. +/// +/// Returns `size_of::() + num_params * size_of::()` +/// (i.e. the 32-byte header plus N × 32-byte parameter slots). +/// +/// `num_params` is the total count of entries in `params[]`, which includes both meta +/// parameters and client parameters. For example, `OpenSession` uses `TEE_NUM_PARAMS + 2` +/// (4 client + 2 meta) and `InvokeCommand` uses up to `TEE_NUM_PARAMS` (4 client). +/// +/// # Safety invariant +/// +/// Callers must ensure `num_params` has been validated against `OpteeMsgArgs::MAX_ARG_PARAM_COUNT`. +/// An unvalidated `num_params` from normal world memory could produce an oversized result, +/// leading to out-of-bounds access on fixed-size arrays. +/// See CVE-2022-46152 (OP-TEE OOB via unvalidated `num_params`). +#[inline] +pub const fn optee_msg_args_total_size(num_params: u32) -> usize { + debug_assert!( + num_params as usize <= OpteeMsgArgs::MAX_ARG_PARAM_COUNT, + "optee_msg_args_total_size: num_params exceeds MAX_ARG_PARAM_COUNT" + ); + core::mem::size_of::() + + core::mem::size_of::() * num_params as usize +} + +/// Zerocopy-friendly header of `optee_msg_arg`. +/// +/// This struct represents the fixed 32-byte header of the C `struct optee_msg_arg`. +/// Unlike `OpteeMsgArgs`, all fields are plain `u32` so it can derive `FromBytes`/`IntoBytes`. +/// +/// A single `optee_msg_arg` on the wire: +/// +/// ```text +/// byte offset +/// 0 cmd (u32) +/// 4 func (u32) +/// 8 session (u32) +/// 12 cancel_id (u32) +/// 16 pad (u32) +/// 20 ret (u32) +/// 24 ret_origin (u32) +/// 28 num_params (u32) N = num_params +/// ---- header: 32 bytes (OpteeMsgArgsHeader) ---- +/// 32 params[0] (32 bytes each, OpteeMsgParam) +/// 64 params[1] +/// ... +/// 32 + N*32 (end) +/// ``` +/// +/// Total size = `size_of::() + N * size_of::()` +#[derive(Clone, Copy, Debug, FromBytes, IntoBytes, Immutable)] +#[repr(C)] +pub struct OpteeMsgArgsHeader { + pub cmd: u32, + pub func: u32, + pub session: u32, + pub cancel_id: u32, + pub pad: u32, + pub ret: u32, + pub ret_origin: u32, + pub num_params: u32, +} + +/// Convert the header portion of this `OpteeMsgArgs` to an `OpteeMsgArgsHeader`. +impl From for OpteeMsgArgsHeader { + fn from(args: OpteeMsgArgs) -> Self { + Self { + cmd: args.cmd as u32, + func: args.func, + session: args.session, + cancel_id: args.cancel_id, + pad: 0, + ret: args.ret.into(), + ret_origin: *args.ret_origin.value(), + num_params: args.num_params, + } + } +} + +/// Convert the header portion of this `OpteeRpcArgs` to an `OpteeMsgArgsHeader`. +impl From for OpteeMsgArgsHeader { + fn from(args: OpteeRpcArgs) -> Self { + Self { + cmd: args.cmd as u32, + func: 0, + session: 0, + cancel_id: 0, + pad: 0, + ret: args.ret.into(), + ret_origin: *args.ret_origin.value(), + num_params: args.num_params, + } + } +} + /// `optee_msg_arg` from `optee_os/core/include/optee_msg.h` /// OP-TEE message argument structure that the normal world (or VTL0) OP-TEE driver and OP-TEE OS use to /// exchange messages. @@ -1439,15 +1645,18 @@ pub struct OpteeMsgArgs { pub ret: TeeResult, /// Origin of the return value pub ret_origin: TeeOrigin, - /// Number of parameters contained in `params` + /// Number of parameters contained in `params`. It includes both meta parameters (e.g. TA UUID, + /// client identity for `OpenSession`) and client parameters. Typical values: 0 (close/cancel), + /// `TEE_NUM_PARAMS` (invoke), `TEE_NUM_PARAMS + 2` (open_session). pub num_params: u32, - /// Parameters to be passed to the secure world. If `cmd == OpenSession`, the first two params contain - /// a TA UUID and they are not delivered to the TA. - /// Note that, originally, the length of this array is variable. We fix it to `TEE_NUM_PARAMS + 2` to - /// simplify the implementation (our OP-TEE Shim supports up to four parameters as well). + /// Parameters to be passed to/from the secure world. If `cmd == OpenSession`, the first + /// two params are meta parameters (TA UUID and client identity, marked with + /// `OPTEE_MSG_ATTR_META`) and are not delivered to the TA. /// - /// TODO: To support OP-TEE RPC, we should make this array length dynamic. Consider to use - /// a trailing unsized slice (DST) or other mechanisms. + /// The C `struct optee_msg_arg` uses a flexible array member `params[]` whose length + /// is determined by `num_params`. We fix it to `TEE_NUM_PARAMS + 2` (= `MAX_ARG_PARAM_COUNT`) + /// to match the Linux driver's `MAX_ARG_PARAM_COUNT`. The variable-length wire format + /// is handled by the read/write proxy and `write_msg_args_to_normal_world`. pub params: [OpteeMsgParam; TEE_NUM_PARAMS + 2], } @@ -1509,7 +1718,7 @@ impl OpteeMsgArgs { if index >= self.num_params as usize { Err(OpteeSmcReturnCode::ENotAvail) } else { - self.params[index].u.value = value; + self.params[index].data.copy_from_slice(value.as_bytes()); Ok(()) } } @@ -1524,11 +1733,263 @@ impl OpteeMsgArgs { if index >= self.num_params as usize { Err(OpteeSmcReturnCode::ENotAvail) } else { - // rmem.size and tmem.size are at the same offset as value.b in the union - self.params[index].u.rmem.size = size; + // rmem.size and tmem.size are at byte offset 8 in the 24-byte data, + // the same position as value.b in the original union. + self.params[index].data[8..16].copy_from_slice(&size.to_le_bytes()); Ok(()) } } + + /// Maximum number of parameters that `OpteeMsgArgs` can hold. + /// + /// This is `TEE_NUM_PARAMS + 2` = 6, matching the Linux driver's `MAX_ARG_PARAM_COUNT`. + pub const MAX_ARG_PARAM_COUNT: usize = TEE_NUM_PARAMS + 2; + + /// Construct an `OpteeMsgArgs` from an `OpteeMsgArgsHeader` and a raw parameter byte slice. + /// + /// `raw_params` must contain at least `header.num_params * size_of::()` bytes. + /// `header.num_params` must not exceed `MAX_ARG_PARAM_COUNT` (6). + pub fn from_header_and_raw_params( + header: &OpteeMsgArgsHeader, + raw_params: &[u8], + ) -> Result { + let num = header.num_params as usize; + if num > Self::MAX_ARG_PARAM_COUNT { + return Err(OpteeSmcReturnCode::EBadCmd); + } + if raw_params.len() < num * size_of::() { + return Err(OpteeSmcReturnCode::EBadAddr); + } + + let cmd = + OpteeMessageCommand::try_from(header.cmd).map_err(|_| OpteeSmcReturnCode::EBadCmd)?; + + let ret = TeeResult::try_from(header.ret).map_err(|_| OpteeSmcReturnCode::EBadCmd)?; + + let ret_origin = TeeOrigin::read_from_bytes(header.ret_origin.as_bytes()) + .map_err(|_| OpteeSmcReturnCode::EBadCmd)?; + + let mut params = [OpteeMsgParam { + attr: OpteeMsgAttr::default(), + data: [0u8; OPTEE_MSG_PARAM_DATA_SIZE], + }; Self::MAX_ARG_PARAM_COUNT]; + + for (i, param) in params.iter_mut().enumerate().take(num) { + let offset = i * size_of::(); + let param_bytes = &raw_params[offset..offset + size_of::()]; + *param = OpteeMsgParam::read_from_bytes(param_bytes) + .map_err(|_| OpteeSmcReturnCode::EBadAddr)?; + } + + Ok(Self { + cmd, + func: header.func, + session: header.session, + cancel_id: header.cancel_id, + pad: 0, + ret, + ret_origin, + num_params: header.num_params, + params, + }) + } + + /// Serialize this `OpteeMsgArgs` into a raw byte buffer. + /// + /// The buffer should be large enough to hold the header plus `num_params` parameters + /// (as returned by [`optee_msg_args_total_size`]). + pub fn serialize(&self, buf: &mut [u8]) -> Result<(), OpteeSmcReturnCode> { + if buf.len() < optee_msg_args_total_size(self.num_params) + || self.num_params as usize > Self::MAX_ARG_PARAM_COUNT + { + return Err(OpteeSmcReturnCode::EBadAddr); + } + let header = OpteeMsgArgsHeader::from(*self); + let header_bytes = header.as_bytes(); + buf[..header_bytes.len()].copy_from_slice(header_bytes); + write_optee_msg_params_to_buf( + &self.params[..self.num_params as usize], + &mut buf[header_bytes.len()..], + ); + Ok(()) + } +} + +/// OP-TEE RPC argument structure. +/// +/// This is the RPC counterpart of [`OpteeMsgArgs`]. On the wire it shares the same +/// 32-byte header layout (`optee_msg_arg`), but only a subset of the header fields +/// are meaningful for RPC: +/// +/// | Field | RPC meaning | +/// |----------------------|------------------------------------------------------------| +/// | `cmd` | RPC command ([`OpteeRpcCommand`]) | +/// | `ret` / `ret_origin` | Return value written back by the normal-world RPC handler. | +/// | `num_params` | Number of RPC parameters (`EXCHANGE_CAPABILITIES`). | +/// | `params[]` | RPC payload (e.g. TA load request). | +/// +/// The remaining header fields (`func`, `session`, `cancel_id`) are **unused** for RPC +/// and always zero on the wire. They are not exposed in this struct. +#[derive(Clone, Copy)] +#[repr(C)] +pub struct OpteeRpcArgs { + /// RPC command ID. Unlike main args, this uses [`OpteeRpcCommand`] (e.g. `LoadTa`, + /// `ShmAlloc`) rather than [`OpteeMessageCommand`]. + pub cmd: OpteeRpcCommand, + /// unused for RPC. Corresponds to `func` in the main `optee_msg_arg`. + _reserved_func: u32, + /// unused for RPC. Corresponds to `session`. + _reserved_session: u32, + /// unused for RPC. Corresponds to `cancel_id`. + _reserved_cancel_id: u32, + /// Padding (matches `pad` in the wire format). + _pad: u32, + /// Return value from the normal-world RPC handler. + pub ret: TeeResult, + /// Origin of the return value. + pub ret_origin: TeeOrigin, + /// Number of parameters in `params`, negotiated during `EXCHANGE_CAPABILITIES`. + pub num_params: u32, + /// RPC parameters. Fixed to `NUM_RPC_PARAMS` entries. + pub params: [OpteeMsgParam; Self::MAX_RPC_ARG_PARAM_COUNT], +} + +impl OpteeRpcArgs { + /// Maximum number of RPC parameters this struct can hold. + /// + /// This is `NUM_RPC_PARAMS`, the count negotiated during `EXCHANGE_CAPABILITIES`. + pub const MAX_RPC_ARG_PARAM_COUNT: usize = NUM_RPC_PARAMS; + + /// Construct an `OpteeRpcArgs` from an `OpteeMsgArgsHeader` and a raw parameter byte slice. + /// + /// Unlike [`OpteeMsgArgs::from_header_and_raw_params`], `cmd` is parsed as [`OpteeRpcCommand`] and + /// `func`, `session`, and `cancel_id` are stored as zeros — they carry no meaning for RPC. + pub fn from_header_and_raw_params( + header: &OpteeMsgArgsHeader, + raw_params: &[u8], + ) -> Result { + let num = header.num_params as usize; + if num > Self::MAX_RPC_ARG_PARAM_COUNT { + return Err(OpteeSmcReturnCode::EBadCmd); + } + if raw_params.len() < num * size_of::() { + return Err(OpteeSmcReturnCode::EBadAddr); + } + + let cmd = OpteeRpcCommand::try_from(header.cmd).map_err(|_| OpteeSmcReturnCode::EBadCmd)?; + + let ret = TeeResult::try_from(header.ret).map_err(|_| OpteeSmcReturnCode::EBadCmd)?; + + let ret_origin = TeeOrigin::read_from_bytes(header.ret_origin.as_bytes()) + .map_err(|_| OpteeSmcReturnCode::EBadCmd)?; + + let mut params = [OpteeMsgParam { + attr: OpteeMsgAttr::default(), + data: [0u8; OPTEE_MSG_PARAM_DATA_SIZE], + }; Self::MAX_RPC_ARG_PARAM_COUNT]; + + for (i, param) in params.iter_mut().enumerate().take(num) { + let offset = i * size_of::(); + let param_bytes = &raw_params[offset..offset + size_of::()]; + *param = OpteeMsgParam::read_from_bytes(param_bytes) + .map_err(|_| OpteeSmcReturnCode::EBadAddr)?; + } + + Ok(Self { + cmd, + _reserved_func: 0, + _reserved_session: 0, + _reserved_cancel_id: 0, + _pad: 0, + ret, + ret_origin, + num_params: header.num_params, + params, + }) + } + + /// Serialize this `OpteeRpcArgs` into a raw byte buffer. + /// + /// The buffer should be large enough to hold the header plus `num_params` parameters + /// (as returned by [`optee_msg_args_total_size`]). + pub fn serialize(&self, buf: &mut [u8]) -> Result<(), OpteeSmcReturnCode> { + if buf.len() < optee_msg_args_total_size(self.num_params) + || self.num_params as usize > Self::MAX_RPC_ARG_PARAM_COUNT + { + return Err(OpteeSmcReturnCode::EBadAddr); + } + let header = OpteeMsgArgsHeader::from(*self); + let header_bytes = header.as_bytes(); + buf[..header_bytes.len()].copy_from_slice(header_bytes); + write_optee_msg_params_to_buf( + &self.params[..self.num_params as usize], + &mut buf[header_bytes.len()..], + ); + Ok(()) + } + + /// Access a parameter by index with bounds checking against `num_params`. + pub fn get_param_value(&self, index: usize) -> Result { + if index >= self.num_params as usize { + Err(OpteeSmcReturnCode::ENotAvail) + } else { + self.params[index] + .get_param_value() + .ok_or(OpteeSmcReturnCode::EBadCmd) + } + } + + /// Access a tmem parameter by index with bounds checking against `num_params`. + pub fn get_param_tmem(&self, index: usize) -> Result { + if index >= self.num_params as usize { + Err(OpteeSmcReturnCode::ENotAvail) + } else { + self.params[index] + .get_param_tmem() + .ok_or(OpteeSmcReturnCode::EBadCmd) + } + } + + /// Set a value parameter by index with bounds checking against `num_params`. + pub fn set_param_value( + &mut self, + index: usize, + value: OpteeMsgParamValue, + ) -> Result<(), OpteeSmcReturnCode> { + if index >= self.num_params as usize { + Err(OpteeSmcReturnCode::ENotAvail) + } else { + self.params[index].data.copy_from_slice(value.as_bytes()); + Ok(()) + } + } + + /// Set a tmem parameter by index with bounds checking against `num_params`. + pub fn set_param_tmem( + &mut self, + index: usize, + tmem: OpteeMsgParamTmem, + ) -> Result<(), OpteeSmcReturnCode> { + if index >= self.num_params as usize { + Err(OpteeSmcReturnCode::ENotAvail) + } else { + self.params[index].data.copy_from_slice(tmem.as_bytes()); + Ok(()) + } + } + + // Note: RPC does not use rmem params. Rmem requires pre-registered shared memory + // references from the normal-world driver, which is a main-messaging-path concept. + // RPC uses tmem for buffer references since OP-TEE provides physical addresses directly. +} + +/// Serialize the params portion as raw bytes into `buf`. +#[inline] +fn write_optee_msg_params_to_buf(params: &[OpteeMsgParam], buf: &mut [u8]) { + for (i, param) in params.iter().enumerate() { + let offset = i * size_of::(); + buf[offset..offset + size_of::()].copy_from_slice(param.as_bytes()); + } } /// A memory page to exchange OP-TEE SMC call arguments. @@ -1562,7 +2023,7 @@ impl From<&OpteeSmcArgsPage> for OpteeSmcArgs { } /// OP-TEE SMC call arguments. -#[derive(Clone, Copy, Default)] +#[derive(Clone, Copy, Default, FromBytes)] pub struct OpteeSmcArgs { args: [usize; Self::NUM_OPTEE_SMC_ARGS], } @@ -1659,6 +2120,7 @@ pub enum OpteeSmcResult<'a> { }, CallWithArg { msg_args: Box, + rpc_args: Option>, }, } @@ -1750,8 +2212,14 @@ const OPTEE_SMC_RETURN_ENOMEM: usize = 0x6; const OPTEE_SMC_RETURN_ENOTAVAIL: usize = 0x7; const OPTEE_SMC_RETURN_UNKNOWN_FUNCTION: usize = 0xffff_ffff; +const OPTEE_SMC_RETURN_RPC_PREFIX: usize = 0xffff_0000; +const OPTEE_SMC_RETURN_RPC_ALLOC: usize = OPTEE_SMC_RETURN_RPC_PREFIX; +const OPTEE_SMC_RETURN_RPC_FREE: usize = OPTEE_SMC_RETURN_RPC_PREFIX | 0x2; +const OPTEE_SMC_RETURN_RPC_FOREIGN_INTR: usize = OPTEE_SMC_RETURN_RPC_PREFIX | 0x4; +const OPTEE_SMC_RETURN_RPC_CMD: usize = OPTEE_SMC_RETURN_RPC_PREFIX | 0x5; + #[non_exhaustive] -#[derive(Copy, Clone, PartialEq, TryFromPrimitive)] +#[derive(Copy, Clone, Debug, PartialEq, TryFromPrimitive)] #[repr(usize)] pub enum OpteeSmcReturnCode { Ok = OPTEE_SMC_RETURN_OK, @@ -1763,6 +2231,10 @@ pub enum OpteeSmcReturnCode { ENomem = OPTEE_SMC_RETURN_ENOMEM, ENotAvail = OPTEE_SMC_RETURN_ENOTAVAIL, UnknownFunction = OPTEE_SMC_RETURN_UNKNOWN_FUNCTION, + RpcAlloc = OPTEE_SMC_RETURN_RPC_ALLOC, + RpcFree = OPTEE_SMC_RETURN_RPC_FREE, + RpcForeignIntr = OPTEE_SMC_RETURN_RPC_FOREIGN_INTR, + RpcCmd = OPTEE_SMC_RETURN_RPC_CMD, } impl From for OpteeSmcReturnCode { @@ -1827,6 +2299,20 @@ pub fn parse_ta_head(elf_data: &[u8]) -> Option { mod tests { use super::*; + #[test] + fn test_optee_msg_args_header_size_and_layout() { + use core::mem::{offset_of, size_of}; + assert_eq!(size_of::(), 32); + assert_eq!(offset_of!(OpteeMsgArgsHeader, cmd), 0); + assert_eq!(offset_of!(OpteeMsgArgsHeader, func), 4); + assert_eq!(offset_of!(OpteeMsgArgsHeader, session), 8); + assert_eq!(offset_of!(OpteeMsgArgsHeader, cancel_id), 12); + assert_eq!(offset_of!(OpteeMsgArgsHeader, pad), 16); + assert_eq!(offset_of!(OpteeMsgArgsHeader, ret), 20); + assert_eq!(offset_of!(OpteeMsgArgsHeader, ret_origin), 24); + assert_eq!(offset_of!(OpteeMsgArgsHeader, num_params), 28); + } + #[test] fn test_tee_uuid_from_u64_array() { // Test with OP-TEE's well-known UUID: 384fb3e0-e7f8-11e3-af63-0002a5d5c51b @@ -1845,4 +2331,118 @@ mod tests { [0xaf, 0x63, 0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b] ); } + + #[test] + fn test_header_to_msg_args_too_many_params() { + let header = OpteeMsgArgsHeader { + cmd: 0, + func: 0, + session: 0, + cancel_id: 0, + pad: 0, + ret: 0, + ret_origin: 0, + num_params: 7, // exceeds MAX_ARG_PARAM_COUNT = 6 + }; + let result = OpteeMsgArgs::from_header_and_raw_params(&header, &[0u8; 224]); + assert!(result.is_err()); + } + + #[test] + fn test_header_to_msg_args_raw_params_too_short() { + let header = OpteeMsgArgsHeader { + cmd: 0, + func: 0, + session: 0, + cancel_id: 0, + pad: 0, + ret: 0, + ret_origin: 0, + num_params: 4, + }; + let result = OpteeMsgArgs::from_header_and_raw_params(&header, &[0u8; 64]); + assert!(result.is_err()); + } + + #[test] + fn test_roundtrip_header_params_write_read() { + use alloc::vec; + + let header = OpteeMsgArgsHeader { + cmd: 1, // InvokeCommand + func: 0x1234, + session: 0xABCD, + cancel_id: 0, + pad: 0, + ret: 0, + ret_origin: 0, + num_params: 3, + }; + let mut params_in = vec![0u8; 3 * size_of::()]; + for (i, byte) in params_in.iter_mut().enumerate() { + *byte = u8::try_from(i % 256).unwrap(); + } + + let msg_args = + OpteeMsgArgs::from_header_and_raw_params(&header, ¶ms_in).expect("expected Ok"); + assert_eq!(msg_args.func, 0x1234); + assert_eq!(msg_args.session, 0xABCD); + assert_eq!(msg_args.num_params, 3); + + let header_out = OpteeMsgArgsHeader::from(msg_args); + assert_eq!(header_out.cmd, 1); + assert_eq!(header_out.func, 0x1234); + assert_eq!(header_out.session, 0xABCD); + assert_eq!(header_out.num_params, 3); + } + + #[test] + fn test_optee_rpc_args_roundtrip() { + use alloc::vec; + + // ShmAlloc = 6 + let header = OpteeMsgArgsHeader { + cmd: 6, + func: 0, + session: 0, + cancel_id: 0, + pad: 0, + ret: 0, + ret_origin: 0, + num_params: 2, + }; + let mut params_in = vec![0u8; 2 * size_of::()]; + for (i, byte) in params_in.iter_mut().enumerate() { + *byte = u8::try_from(i % 256).unwrap(); + } + + let rpc_args = OpteeRpcArgs::from_header_and_raw_params(&header, ¶ms_in) + .expect("should parse RPC args"); + assert_eq!(rpc_args.cmd, OpteeRpcCommand::ShmAlloc); + assert_eq!(rpc_args.num_params, 2); + + let header_out = OpteeMsgArgsHeader::from(rpc_args); + assert_eq!(header_out.cmd, 6); + assert_eq!(header_out.func, 0); + assert_eq!(header_out.session, 0); + assert_eq!(header_out.cancel_id, 0); + assert_eq!(header_out.num_params, 2); + } + + #[test] + fn test_rpc_args_rejects_main_cmd() { + // Pick a cmd value that lies in the gap between Plugin (12) and I2C Transfer (21), + // so it is not a valid OpteeRpcCommand variant. + let header = OpteeMsgArgsHeader { + cmd: 14, // not a valid OpteeRpcCommand + func: 0, + session: 0, + cancel_id: 0, + pad: 0, + ret: 0, + ret_origin: 0, + num_params: 0, + }; + assert!(OpteeRpcArgs::from_header_and_raw_params(&header, &[]).is_err()); + } } diff --git a/litebox_platform_lvbs/Cargo.toml b/litebox_platform_lvbs/Cargo.toml index 190f28b13..11d0cdb20 100644 --- a/litebox_platform_lvbs/Cargo.toml +++ b/litebox_platform_lvbs/Cargo.toml @@ -17,7 +17,6 @@ spin = { version = "0.10.0", default-features = false, features = [ "once", "rwlock", ] } -libc = "0.2.169" arrayvec = { version = "0.7.6", default-features = false } rangemap = { version = "1.5.1", features = ["const_fn"] } thiserror = { version = "2.0.6", default-features = false } @@ -42,6 +41,9 @@ zerocopy = { version = "0.8", default-features = false, features = ["derive"] } [target.'cfg(target_arch = "x86_64")'.dependencies] x86_64 = { version = "0.15.2", default-features = false, features = ["instructions"] } +[dev-dependencies] +libc = "0.2.177" + [features] default = ["optee_syscall"] optee_syscall = [] diff --git a/litebox_runner_lvbs/Cargo.toml b/litebox_runner_lvbs/Cargo.toml index 0c22037a5..b2f4cf88e 100644 --- a/litebox_runner_lvbs/Cargo.toml +++ b/litebox_runner_lvbs/Cargo.toml @@ -14,7 +14,7 @@ spin = { version = "0.10.0", default-features = false, features = ["spin_mutex"] hashbrown = "0.15.2" once_cell = { version = "1.21.3", default-features = false, features = ["race", "alloc"] } num_enum = { version = "0.7.4", default-features = false } - +zerocopy = { version = "0.8", default-features = false } [target.'cfg(target_arch = "x86_64")'.dependencies] x86_64 = { version = "0.15.2", default-features = false, features = ["instructions"] } diff --git a/litebox_runner_lvbs/src/lib.rs b/litebox_runner_lvbs/src/lib.rs index 1ca9e28fd..d72d35506 100644 --- a/litebox_runner_lvbs/src/lib.rs +++ b/litebox_runner_lvbs/src/lib.rs @@ -7,6 +7,7 @@ extern crate alloc; use alloc::boxed::Box; use alloc::sync::Arc; +use alloc::vec; use core::{ops::Neg, panic::PanicInfo}; use litebox::{ mm::linux::PAGE_SIZE, @@ -15,8 +16,8 @@ use litebox::{ }; use litebox_common_linux::errno::Errno; use litebox_common_optee::{ - OpteeMessageCommand, OpteeMsgArgs, OpteeSmcArgs, OpteeSmcResult, OpteeSmcReturnCode, TeeOrigin, - TeeResult, UteeEntryFunc, UteeParams, + OpteeMessageCommand, OpteeMsgArgs, OpteeRpcArgs, OpteeSmcArgs, OpteeSmcResult, + OpteeSmcReturnCode, TeeOrigin, TeeResult, UteeEntryFunc, UteeParams, optee_msg_args_total_size, }; use litebox_platform_lvbs::{ arch::{gdt, get_core_id, instrs::hlt_loop, interrupts}, @@ -310,28 +311,42 @@ fn optee_smc_handler(smc_args_addr: usize) -> OpteeSmcArgs { smc_args.set_return_code(OpteeSmcReturnCode::EBadCmd); return *smc_args; }; - match smc_result { - OpteeSmcResult::CallWithArg { msg_args } => { - let mut msg_args = *msg_args; - debug_serial_println!("OP-TEE SMC with MsgArgs Command: {:?}", msg_args.cmd); - let result = match msg_args.cmd { - OpenSession => handle_open_session(&mut msg_args, msg_args_phys_addr), - InvokeCommand => handle_invoke_command(&mut msg_args, msg_args_phys_addr), - CloseSession => handle_close_session(&mut msg_args, msg_args_phys_addr), - _ => handle_optee_msg_args(&msg_args), - }; - - // Always switch back to base page table before returning to VTL0 - // Safety: No user-space memory references are held after this point - unsafe { switch_to_base_page_table() }; - - match result { - Ok(()) => smc_args.set_return_code(OpteeSmcReturnCode::Ok), - Err(e) => smc_args.set_return_code(e), + if let OpteeSmcResult::CallWithArg { + msg_args, + rpc_args: _, + } = smc_result + { + let mut msg_args = *msg_args; + debug_serial_println!("OP-TEE SMC with MsgArgs Command: {:?}", msg_args.cmd); + let result = match msg_args.cmd { + OpenSession => handle_open_session(&mut msg_args, msg_args_phys_addr), + InvokeCommand => handle_invoke_command(&mut msg_args, msg_args_phys_addr), + CloseSession => handle_close_session(&mut msg_args, msg_args_phys_addr), + _ => { + let r = handle_optee_msg_args(&msg_args); + if r.is_ok() { + msg_args.ret = TeeResult::Success; + } else { + msg_args.ret = TeeResult::BadParameters; + } + msg_args.ret_origin = TeeOrigin::Tee; + let _ = write_non_ta_msg_args_to_normal_world(&msg_args, msg_args_phys_addr); + r } - *smc_args + }; + + // Always switch back to base page table before returning to VTL0 + // Safety: No user-space memory references are held after this point + unsafe { switch_to_base_page_table() }; + + if let Err(e) = result { + smc_args.set_return_code(e); + } else { + smc_args.set_return_code(OpteeSmcReturnCode::Ok); } - _ => smc_result.into(), + *smc_args + } else { + smc_result.into() } } @@ -1022,6 +1037,9 @@ fn handle_close_session( /// Update msg_args with return values and write back to normal world memory. /// +/// Serializes `OpteeMsgArgs` into a contiguous byte blob and writes it to +/// the VTL0 physical address. +/// /// Per OP-TEE OS semantics: /// - `TeeOrigin::Tee` is used when the error comes from TEE itself (panic/TARGET_DEAD) /// - `TeeOrigin::TrustedApp` is used when the error comes from the TA @@ -1067,10 +1085,72 @@ fn write_msg_args_to_normal_world( ta_req_info, msg_args, )?; - let mut ptr = - NormalWorldMutPtr::::with_usize(msg_args_phys_addr.truncate())?; - // SAFETY: Writing msg_args back to normal world memory at a valid address. - unsafe { ptr.write_at_offset(0, *msg_args) }?; + + let msg_args_size = optee_msg_args_total_size(msg_args.num_params); + let mut blob = vec![0u8; msg_args_size]; + msg_args.serialize(&mut blob)?; + + let mut ptr = NormalWorldMutPtr::::with_contiguous_pages( + msg_args_phys_addr.truncate(), + msg_args_size, + )?; + // SAFETY: Writing msg_args back to normal world memory at a valid physical address. + // The blob contains the serialized variable-length optee_msg_arg structure(s). + unsafe { ptr.write_slice_at_offset(0, &blob) }?; + Ok(()) +} + +/// Write back `OpteeMsgArgs` for non-TA commands (e.g., RegisterShm, UnregisterShm) that +/// don't require TA userspace memory access. +/// +/// Unlike [`write_msg_args_to_normal_world`], this function does not access TA userspace +/// memory and can be called from the base page table context. It simply serializes the +/// msg_args (which should already have `ret` / `ret_origin` set by the caller) back to +/// the normal world physical address. +#[inline] +fn write_non_ta_msg_args_to_normal_world( + msg_args: &OpteeMsgArgs, + msg_args_phys_addr: u64, +) -> Result<(), OpteeSmcReturnCode> { + let msg_args_size = optee_msg_args_total_size(msg_args.num_params); + let mut blob = vec![0u8; msg_args_size]; + msg_args.serialize(&mut blob)?; + + let mut ptr = NormalWorldMutPtr::::with_contiguous_pages( + msg_args_phys_addr.truncate(), + msg_args_size, + )?; + // SAFETY: Writing msg_args back to normal world memory at a valid physical address. + // The blob contains the serialized variable-length optee_msg_arg structure(s). + unsafe { ptr.write_slice_at_offset(0, &blob) }?; + Ok(()) +} + +/// Write `OpteeRpcArgs` to the normal world. Its write address is determined by +/// `msg_args_phys_addr` and the size of `OpteeMsgArgs`. +/// +/// Unlike [`write_msg_args_to_normal_world`], this function does not access TA userspace +/// memory and can be called from the base page table context. It simply serializes the +/// rpc_args and writes it to the normal world physical address. +#[expect(dead_code)] +#[inline] +fn write_rpc_args_to_normal_world( + msg_args: &OpteeMsgArgs, + msg_args_phys_addr: u64, + rpc_args: &OpteeRpcArgs, +) -> Result<(), OpteeSmcReturnCode> { + let msg_args_size = optee_msg_args_total_size(msg_args.num_params); + + let rpc_args_size = optee_msg_args_total_size(rpc_args.num_params); + let mut blob = vec![0u8; rpc_args_size]; + rpc_args.serialize(&mut blob)?; + + let rpc_pa: usize = + >::truncate(msg_args_phys_addr) + msg_args_size; // RPC args are placed right after the main msg_args blob + let mut ptr = NormalWorldMutPtr::::with_contiguous_pages(rpc_pa, rpc_args_size)?; + // SAFETY: Writing rpc_args back to normal world memory at a valid physical address. + // The blob contains the serialized variable-length optee_msg_arg structure(s). + unsafe { ptr.write_slice_at_offset(0, &blob) }?; Ok(()) } diff --git a/litebox_shim_optee/Cargo.toml b/litebox_shim_optee/Cargo.toml index 5ba2ff13c..e277e60cc 100644 --- a/litebox_shim_optee/Cargo.toml +++ b/litebox_shim_optee/Cargo.toml @@ -20,15 +20,7 @@ num_enum = { version = "0.7.3", default-features = false } once_cell = { version = "1.20.2", default-features = false, features = ["alloc", "race"] } spin = { version = "0.10.0", default-features = false, features = ["spin_mutex", "once"] } thiserror = { version = "2.0.6", default-features = false } -zerocopy = { version = "0.8", default-features = false } - -[target.'cfg(target_arch = "x86")'.dependencies] -# ELF loader requires us to enable the `rel` dependency on 32-bit to work. -# Enabling it on 64-bit on the other hand is incorrect and would not work. Thus, -# we need to do it as a target-specific dependency. -# -# NOTE: We must keep this in sync with the version above. -elf_loader = { version = "0.12.0", default-features = false, features = ["rel"] } +zerocopy = { version = "0.8", default-features = false, features = ["derive"] } [features] default = ["platform_lvbs"] diff --git a/litebox_shim_optee/src/msg_handler.rs b/litebox_shim_optee/src/msg_handler.rs index 730b1b794..f0c70acf7 100644 --- a/litebox_shim_optee/src/msg_handler.rs +++ b/litebox_shim_optee/src/msg_handler.rs @@ -17,16 +17,19 @@ //! shared memory). use crate::{NormalWorldConstPtr, NormalWorldMutPtr}; use alloc::{boxed::Box, vec::Vec}; +use core::mem::size_of; use hashbrown::HashMap; use litebox::{mm::linux::PAGE_SIZE, utils::TruncateExt}; use litebox_common_linux::vmap::{PhysPageAddr, PhysPointerError}; use litebox_common_optee::{ - OpteeMessageCommand, OpteeMsgArgs, OpteeMsgAttrType, OpteeMsgParamRmem, OpteeMsgParamTmem, - OpteeMsgParamValue, OpteeSecureWorldCapabilities, OpteeSmcArgs, OpteeSmcFunction, - OpteeSmcResult, OpteeSmcReturnCode, TeeIdentity, TeeLogin, TeeOrigin, TeeParamType, TeeResult, - TeeUuid, UteeEntryFunc, UteeParamOwned, UteeParams, + OpteeMessageCommand, OpteeMsgArgs, OpteeMsgArgsHeader, OpteeMsgAttrType, OpteeMsgParamRmem, + OpteeMsgParamTmem, OpteeMsgParamValue, OpteeRpcArgs, OpteeSecureWorldCapabilities, + OpteeSmcArgs, OpteeSmcFunction, OpteeSmcResult, OpteeSmcReturnCode, TeeIdentity, TeeLogin, + TeeOrigin, TeeParamType, TeeResult, TeeUuid, UteeEntryFunc, UteeParamOwned, UteeParams, + optee_msg_args_total_size, }; use once_cell::race::OnceBox; +use zerocopy::{FromBytes, Immutable}; // OP-TEE version and build info (2.0) // TODO: Consider replacing it with our own version info @@ -50,7 +53,6 @@ const OPTEE_MSG_OS_OPTEE_UUID_3: u32 = 0xa5d5_c51b; // We do not support notification for now const MAX_NOTIF_VALUE: usize = 0; -const NUM_RPC_PARMS: usize = 4; #[inline] fn page_align_down(address: u64) -> u64 { @@ -62,6 +64,105 @@ fn page_align_up(len: u64) -> u64 { len.next_multiple_of(PAGE_SIZE as u64) } +/// Read `OpteeMsgArgs` (and optional `OpteeRpcArgs`) from a VTL0 physical address. +/// +/// Copies the maximum possible size in one shot into a private VTL1 buffer, then +/// parses entirely from that buffer. This eliminates TOCTOU (double-fetch) issues: +/// VTL0 cannot mutate the data between validation and use because we only touch +/// our private copy after the single read. +/// +/// The copy size is determined by known-good upper bounds, not by untrusted data: +/// - Main args: `optee_msg_args_total_size(MAX_ARG_PARAM_COUNT = 6)` = 224 bytes (the Linux +/// driver always allocates at least this much for the main arg). +/// - RPC args (when present): `optee_msg_args_total_size(rpc_num_params)`, where +/// `rpc_num_params` is our own negotiated value from `EXCHANGE_CAPABILITIES`. +/// +/// If `has_rpc_arg` is true, expects an appended RPC `optee_msg_arg` immediately after +/// the main one at offset `optee_msg_args_total_size(num_params)` (the *actual* `num_params`, +/// not `MAX_ARG_PARAM_COUNT`). This matches the Linux driver's layout. +/// +/// VTL0 physical memory layout at `phys_addr`: +/// +/// ```text +/// phys_addr +/// | +/// v +/// +--------+------+--------+--------+------+----------~-+ +/// | header |par[0]|par[N-1]| header |par[0]|par[R-1]|xxx| +/// | 32B | ... | 32B | 32B | ... | 32B |xxx| +/// +--------+------+--------+--------+------+----------~-+ +/// |<--- main_size -------->|<--- rpc_size --------->| | +/// | ^ ^ | +/// | RPC starts here main_max | +/// |<----- copy_size = main_max + rpc_max -------------~>| +/// +/// N = actual num_params from header (validated <= MAX_ARG_PARAM_COUNT after copy) +/// R = rpc_num_params (negotiated during OPTEE_SMC_EXCHANGE_CAPABILITIES) +/// +/// We copy main_max + rpc_max bytes (the upper bound), which covers the +/// actual data (main_size + rpc_size) plus some unused area between +/// main_size and main_max. We parse using main_size from our private +/// snapshot, so the RPC arg is found at its correct offset. +/// ``` +/// +/// Returns `(main_args, Option)`. +pub fn read_optee_msg_args_from_phys( + phys_addr: usize, + has_rpc_arg: bool, +) -> Result<(Box, Option>), OpteeSmcReturnCode> { + // Compute copy size from known-good upper bounds — no untrusted data involved. + let main_max = optee_msg_args_total_size(OpteeMsgArgs::MAX_ARG_PARAM_COUNT.truncate()); + let copy_size = if has_rpc_arg { + main_max + optee_msg_args_total_size(OpteeRpcArgs::MAX_RPC_ARG_PARAM_COUNT.truncate()) + } else { + main_max + }; + + let mut blob = alloc::vec![0u8; copy_size]; + + let mut blob_ptr = + NormalWorldConstPtr::::with_contiguous_pages(phys_addr, copy_size) + .map_err(|_| OpteeSmcReturnCode::EBadAddr)?; + unsafe { blob_ptr.read_slice_at_offset(0, &mut blob) } + .map_err(|_| OpteeSmcReturnCode::EBadAddr)?; + + // Parse main header from the private buffer. + let main_header = OpteeMsgArgsHeader::read_from_prefix(&blob) + .map_err(|_| OpteeSmcReturnCode::EBadAddr)? + .0; + + // Validate num_params from the snapshot. + if main_header.num_params as usize > OpteeMsgArgs::MAX_ARG_PARAM_COUNT { + return Err(OpteeSmcReturnCode::EBadCmd); + } + + let main_size = optee_msg_args_total_size(main_header.num_params); + let main_params = &blob[size_of::()..main_size]; + let main_args = OpteeMsgArgs::from_header_and_raw_params(&main_header, main_params)?; + + // Parse RPC args if present. + // The Linux kernel driver places the RPC arg at offset main_size (based on the actual + // num_params, not MAX_ARG_PARAM_COUNT). Since we copied main_max bytes which is >= main_size, + // and main_size is computed from our own validated snapshot, this is safe. + let rpc_args = if has_rpc_arg { + let rpc_blob = &blob[main_size..]; + let rpc_header = OpteeMsgArgsHeader::read_from_prefix(rpc_blob) + .map_err(|_| OpteeSmcReturnCode::EBadAddr)? + .0; + // Re-validate RPC num_params from the snapshot against our negotiated limit. + if rpc_header.num_params as usize > OpteeRpcArgs::MAX_RPC_ARG_PARAM_COUNT { + return Err(OpteeSmcReturnCode::EBadCmd); + } + let rpc_params = &rpc_blob[size_of::()..]; + let rpc = OpteeRpcArgs::from_header_and_raw_params(&rpc_header, rpc_params)?; + Some(Box::new(rpc)) + } else { + None + }; + + Ok((Box::new(main_args), rpc_args)) +} + /// This function handles `OpteeSmcArgs` passed from the normal world (VTL0) via an OP-TEE SMC call. /// It returns an `OpteeSmcResult` representing the result of the SMC call or `OpteeMsgArgs` it contains /// if the SMC call involves with an OP-TEE message which should be handled by @@ -77,19 +178,21 @@ pub fn handle_optee_smc_args( func_id ); match func_id { - OpteeSmcFunction::CallWithArg - | OpteeSmcFunction::CallWithRpcArg - | OpteeSmcFunction::CallWithRegdArg => { + OpteeSmcFunction::CallWithArg => { let msg_args_addr = smc.optee_msg_args_phys_addr()?; let msg_args_addr: usize = msg_args_addr.truncate(); - let mut ptr = NormalWorldConstPtr::::with_usize(msg_args_addr) - .map_err(|_| OpteeSmcReturnCode::EBadAddr)?; - let msg_args = - unsafe { ptr.read_at_offset(0) }.map_err(|_| OpteeSmcReturnCode::EBadAddr)?; + let (msg_args, _) = read_optee_msg_args_from_phys(msg_args_addr, false)?; Ok(OpteeSmcResult::CallWithArg { - msg_args: Box::new(*msg_args), + msg_args, + rpc_args: None, }) } + OpteeSmcFunction::CallWithRpcArg | OpteeSmcFunction::CallWithRegdArg => { + let msg_args_addr = smc.optee_msg_args_phys_addr()?; + let msg_args_addr: usize = msg_args_addr.truncate(); + let (msg_args, rpc_args) = read_optee_msg_args_from_phys(msg_args_addr, true)?; + Ok(OpteeSmcResult::CallWithArg { msg_args, rpc_args }) + } OpteeSmcFunction::ExchangeCapabilities => { // TODO: update the below when we support more features let default_cap = OpteeSecureWorldCapabilities::DYNAMIC_SHM @@ -99,7 +202,7 @@ pub fn handle_optee_smc_args( status: OpteeSmcReturnCode::Ok, capabilities: default_cap, max_notif_value: MAX_NOTIF_VALUE, - data: NUM_RPC_PARMS, + data: OpteeRpcArgs::MAX_RPC_ARG_PARAM_COUNT, }) } OpteeSmcFunction::DisableShmCache => { @@ -432,7 +535,7 @@ pub fn update_optee_msg_args( /// virtually contiguous in the normal world. All these address values must be page aligned. /// /// `pages_data` from [Linux](https://elixir.bootlin.com/linux/v6.18.2/source/drivers/tee/optee/smc_abi.c#L409) -#[derive(Clone, Copy)] +#[derive(Clone, Copy, FromBytes, Immutable)] #[repr(C)] struct ShmRefPagesData { pub pages_list: [u64; Self::PAGELIST_ENTRIES_PER_PAGE], diff --git a/litebox_shim_optee/src/ptr.rs b/litebox_shim_optee/src/ptr.rs index a27105caa..2a54bac25 100644 --- a/litebox_shim_optee/src/ptr.rs +++ b/litebox_shim_optee/src/ptr.rs @@ -65,6 +65,29 @@ use litebox_common_linux::vmap::{ PhysPageAddr, PhysPageMapInfo, PhysPageMapPermissions, PhysPointerError, VmapManager, }; use litebox_platform_multiplex::platform; +use zerocopy::FromBytes; + +/// Allocate a zeroed `Box` on the heap. +/// +/// # Panics +/// +/// Panics if `T` is a zero-sized type, since `alloc_zeroed` with a zero-sized +/// layout is undefined behavior. +fn box_new_zeroed() -> alloc::boxed::Box { + assert!( + core::mem::size_of::() > 0, + "box_new_zeroed does not support zero-sized types" + ); + let layout = core::alloc::Layout::new::(); + // Safety: layout has a non-zero size and correct alignment for T. + let ptr = unsafe { alloc::alloc::alloc_zeroed(layout) }.cast::(); + if ptr.is_null() { + alloc::alloc::handle_alloc_error(layout); + } + // Safety: ptr is a valid, zeroed, properly aligned heap allocation for T. + // T: FromBytes guarantees all-zero is a valid bit pattern. + unsafe { alloc::boxed::Box::from_raw(ptr) } +} #[inline] fn align_down(address: usize, align: usize) -> usize { @@ -176,10 +199,14 @@ impl PhysMutPtr { /// The caller should be aware that the given physical address might be concurrently written by /// other entities (e.g., the normal world kernel) if there is no extra security mechanism /// in place (e.g., by the hypervisor or hardware). That is, it might read corrupt data. + /// `FromBytes` is required to ensure T is valid for any bit pattern from untrusted physical memory. pub unsafe fn read_at_offset( &mut self, count: usize, - ) -> Result, PhysPointerError> { + ) -> Result, PhysPointerError> + where + T: FromBytes, + { if count >= self.count { return Err(PhysPointerError::IndexOutOfBounds(count, self.count)); } @@ -190,19 +217,18 @@ impl PhysMutPtr { PhysPageMapPermissions::READ, )? }; - let mut buffer = core::mem::MaybeUninit::::uninit(); + let mut boxed = box_new_zeroed::(); // Fallible: another core may unmap this page concurrently. let result = unsafe { litebox::mm::exception_table::memcpy_fallible( - buffer.as_mut_ptr().cast::(), + core::ptr::from_mut::(boxed.as_mut()).cast::(), guard.ptr.cast::(), guard.size, ) }; debug_assert!(result.is_ok(), "fault reading from mapped physical page"); result.map_err(|_| PhysPointerError::CopyFailed)?; - // Safety: memcpy_fallible fully initialized the buffer on success. - Ok(alloc::boxed::Box::new(unsafe { buffer.assume_init() })) + Ok(boxed) } /// Read a slice of values at the given offset from the physical pointer. @@ -212,11 +238,15 @@ impl PhysMutPtr { /// The caller should be aware that the given physical address might be concurrently written by /// other entities (e.g., the normal world kernel) if there is no extra security mechanism /// in place (e.g., by the hypervisor or hardware). That is, it might read corrupt data. + /// `FromBytes` is required to ensure T is valid for any bit pattern from untrusted physical memory. pub unsafe fn read_slice_at_offset( &mut self, count: usize, values: &mut [T], - ) -> Result<(), PhysPointerError> { + ) -> Result<(), PhysPointerError> + where + T: FromBytes, + { if count .checked_add(values.len()) .is_none_or(|end| end > self.count) @@ -467,7 +497,7 @@ pub struct PhysConstPtr { inner: PhysMutPtr, } -impl PhysConstPtr { +impl PhysConstPtr { /// Create a new `PhysConstPtr` from the given physical page array and offset. /// /// All addresses in `pages` should be valid and aligned to `ALIGN`, and `offset` should be smaller @@ -513,7 +543,10 @@ impl PhysConstPtr { pub unsafe fn read_at_offset( &mut self, count: usize, - ) -> Result, PhysPointerError> { + ) -> Result, PhysPointerError> + where + T: FromBytes, + { unsafe { self.inner.read_at_offset(count) } } @@ -528,7 +561,10 @@ impl PhysConstPtr { &mut self, count: usize, values: &mut [T], - ) -> Result<(), PhysPointerError> { + ) -> Result<(), PhysPointerError> + where + T: FromBytes, + { unsafe { self.inner.read_slice_at_offset(count, values) } } }