diff --git a/drivers/soc/apple/bio.rs b/drivers/soc/apple/bio.rs index 6b4e1edbe29664..46af2ad60beb62 100644 --- a/drivers/soc/apple/bio.rs +++ b/drivers/soc/apple/bio.rs @@ -279,22 +279,19 @@ impl IdentityIndex { let Some(raw) = store.read(&identity_key())? else { return Ok(index); }; - if raw.len() < 4 { - return Ok(index); + if raw.len() < 8 { + return Err(EINVAL); } if u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]) != INDEX_FORMAT { return Err(EINVAL); } - if raw.len() < 8 { - return Ok(index); - } let count = u32::from_le_bytes([raw[4], raw[5], raw[6], raw[7]]) as usize; let stride = UUID_LEN + LABEL_LEN; - for i in 0..count.min(MAX_IDENTITIES) { + if count > MAX_IDENTITIES || raw.len() != 8 + count * stride { + return Err(EINVAL); + } + for i in 0..count { let at = 8 + i * stride; - if at + stride > raw.len() { - break; - } let mut entry = Identity::ZERO; entry.uuid.copy_from_slice(&raw[at..at + UUID_LEN]); entry diff --git a/drivers/soc/apple/catacomb.rs b/drivers/soc/apple/catacomb.rs index 8b18efb4ead770..fd0a133022afbf 100644 --- a/drivers/soc/apple/catacomb.rs +++ b/drivers/soc/apple/catacomb.rs @@ -46,28 +46,45 @@ pub(crate) fn write(kind: u8, blob: &[u8]) -> Result<()> { file.sync() } -pub(crate) fn read(kind: u8) -> Option> { - let path = path_for(kind)?; - let file = shim::StoreFile::open_readonly(path).ok()?; - let size = file.size().ok()?; +/// `Ok(None)` only when the file is missing. A file that exists but cannot be +/// read or fails validation is an error: "cannot tell" must never read as +/// "nothing saved", or a caller may select a fresh context over a saved one. +pub(crate) fn read(kind: u8) -> Result>> { + let path = path_for(kind).ok_or(EINVAL)?; + let file = match shim::StoreFile::open_readonly(path) { + Ok(file) => file, + Err(e) if e == ENOENT => return Ok(None), + Err(e) => return Err(e), + }; + let size = file.size()?; if size < HEADER as u64 { - return None; + return Err(EINVAL); } let mut head = [0u8; HEADER]; - file.read_exact(0, &mut head).ok()?; + file.read_exact(0, &mut head)?; if head[..MAGIC.len()] != MAGIC { - return None; + return Err(EINVAL); } let len = u32::from_le_bytes([head[8], head[9], head[10], head[11]]) as usize; let crc = u16::from_le_bytes([head[12], head[13]]); if len == 0 || len > MAX_CATACOMB || size != (HEADER + len) as u64 { - return None; + return Err(EINVAL); } - let mut blob: KVec = KVec::with_capacity(len, GFP_KERNEL).ok()?; - blob.resize(len, 0, GFP_KERNEL).ok()?; - file.read_exact(HEADER as u64, &mut blob).ok()?; + let mut blob: KVec = KVec::with_capacity(len, GFP_KERNEL)?; + blob.resize(len, 0, GFP_KERNEL)?; + file.read_exact(HEADER as u64, &mut blob)?; if crc16_ccitt_false(&blob) != crc { - return None; + return Err(EINVAL); } - Some(blob) + Ok(Some(blob)) +} + +/// True only for a present, valid file. +pub(crate) fn is_saved(kind: u8) -> bool { + matches!(read(kind), Ok(Some(_))) +} + +/// True only when the file is provably missing. +pub(crate) fn is_absent(kind: u8) -> bool { + matches!(read(kind), Ok(None)) } diff --git a/drivers/soc/apple/keybag.rs b/drivers/soc/apple/keybag.rs index ef629ae4fd484b..8de3c5478a846b 100644 --- a/drivers/soc/apple/keybag.rs +++ b/drivers/soc/apple/keybag.rs @@ -177,6 +177,15 @@ pub(crate) fn read(slot: Slot) -> Result { head[OFF_STATE + 3], ]); if state == STATE_REFUSED { + let expected_crc = crc16_ccitt_false(&checksum_input(state, &[], &[0; UUID_LEN], &[])?); + let crc = u16::from_le_bytes([head[OFF_CRC], head[OFF_CRC + 1]]); + if size != HEADER as u64 + || crc != expected_crc + || head[OFF_LEN..OFF_CRC].iter().any(|byte| *byte != 0) + || head[OFF_CRC + 2..].iter().any(|byte| *byte != 0) + { + return Err(EINVAL); + } return Ok(State::Absent(NoStoredKeyBag { slot })); } let Some(provenance) = UuidProvenance::from_state(state) else { diff --git a/drivers/soc/apple/refkey.rs b/drivers/soc/apple/refkey.rs index e380dfd9ecfb05..533784b8671858 100644 --- a/drivers/soc/apple/refkey.rs +++ b/drivers/soc/apple/refkey.rs @@ -177,7 +177,19 @@ impl SepData { return; } - if let Ok(f) = shim::StoreFile::open_readonly(path) { + let existing = match shim::StoreFile::open_readonly(path) { + Ok(file) => Some(file), + Err(ENOENT) => None, + Err(error) => { + dev_err!( + self.dev, + "sks: cannot read existing machine ref-key: {:?}; refusing replacement\n", + error + ); + return; + } + }; + if let Some(f) = existing { let sz = f.size().unwrap_or(0); if (1..=8192).contains(&sz) { let mut blob: KVec = KVec::new(); diff --git a/drivers/soc/apple/sbio.rs b/drivers/soc/apple/sbio.rs index 9c69ca184c6912..e461f45c162292 100644 --- a/drivers/soc/apple/sbio.rs +++ b/drivers/soc/apple/sbio.rs @@ -424,7 +424,7 @@ impl SepData { dev_err!(self.dev, "enrol: opt-in fresh-context owner export failed\n"); return false; } - if crate::catacomb::read(PRIVATE_TYPE_CATACOMB_OWNER).is_none() { + if !crate::catacomb::is_saved(PRIVATE_TYPE_CATACOMB_OWNER) { dev_err!(self.dev, "enrol: opt-in fresh context has no owner file\n"); return false; } @@ -510,9 +510,9 @@ impl SepData { return; } if self.templates_restored.load(Relaxed) - || crate::catacomb::read(PRIVATE_TYPE_CATACOMB_OWNER).is_some() - || crate::catacomb::read(PRIVATE_TYPE_CATACOMB_MASTER).is_none() - || crate::catacomb::read(PRIVATE_TYPE_CATACOMB_USER).is_none() + || !crate::catacomb::is_absent(PRIVATE_TYPE_CATACOMB_OWNER) + || !crate::catacomb::is_saved(PRIVATE_TYPE_CATACOMB_MASTER) + || !crate::catacomb::is_saved(PRIVATE_TYPE_CATACOMB_USER) { dev_warn!(self.dev, "owner-probe: preconditions not met; no context selected\n"); return; @@ -648,8 +648,10 @@ impl SepData { crate::sbio::ComponentAction::AlreadyActive => RestoreOutcome::AlreadyActive, crate::sbio::ComponentAction::Unsupported => RestoreOutcome::Failed, crate::sbio::ComponentAction::Load => { - let Some(blob) = self.read_stored(kind, what) else { - return RestoreOutcome::NoStoredFile; + let blob = match self.read_stored(kind, what) { + Ok(Some(blob)) => blob, + Ok(None) => return RestoreOutcome::NoStoredFile, + Err(_) => return RestoreOutcome::Failed, }; let at = crate::sbio::SBIO_SAVED_USER_ID_AT; @@ -766,8 +768,10 @@ impl SepData { } fn restore_lockout(&self) -> RestoreOutcome { - let Some(blob) = self.read_stored(PRIVATE_TYPE_LOCKOUT, c"bio lockout record") else { - return RestoreOutcome::NoStoredFile; + let blob = match self.read_stored(PRIVATE_TYPE_LOCKOUT, c"bio lockout record") { + Ok(Some(blob)) => blob, + Ok(None) => return RestoreOutcome::NoStoredFile, + Err(_) => return RestoreOutcome::Failed, }; let Some(request) = crate::sbio::SbioLoadLockout::new(&blob) else { return RestoreOutcome::Failed; @@ -787,17 +791,26 @@ impl SepData { } } - fn read_stored(&self, kind: u8, _what: &CStr) -> Option> { + /// `Ok(None)` means provably not stored; any read failure is an error so + /// it cannot be mistaken for a missing file. + fn read_stored(&self, kind: u8, what: &CStr) -> Result>> { if crate::catacomb::is_kind(kind) { - if let Some(blob) = crate::catacomb::read(kind) { - return Some(blob); + match crate::catacomb::read(kind) { + Ok(Some(blob)) => return Ok(Some(blob)), + Ok(None) => {} + Err(e) => { + dev_err!(self.dev, "restore: cannot read {}: {:?}\n", what, e); + return Err(e); + } } } match self.with_host_store(|store| store.read(&store::Key::root(kind))) { - Some(Ok(Some(blob))) => Some(blob), - Some(Ok(None)) => None, - Some(Err(_)) => None, - None => None, + Some(Ok(found)) => Ok(found), + Some(Err(e)) => { + dev_err!(self.dev, "restore: cannot read {}: {:?}\n", what, e); + Err(e) + } + None => Ok(None), } } @@ -851,7 +864,7 @@ impl SepData { } if (who.value() == user.value() || who.value() == crate::sbio::CatacombUser::MASTER.value()) - && crate::catacomb::read(kind).is_none() + && !crate::catacomb::is_saved(kind) { dev_err!(self.dev, "enrol: {} has no durable host file\n", what); return false; @@ -865,7 +878,7 @@ impl SepData { } fn save_all_components_user_first(&self, user: crate::sbio::UserId) -> bool { - if crate::catacomb::read(PRIVATE_TYPE_CATACOMB_OWNER).is_none() { + if !crate::catacomb::is_saved(PRIVATE_TYPE_CATACOMB_OWNER) { dev_err!(self.dev, "enrol: opt-in completion has no owner Catacomb file\n"); return false; } @@ -904,7 +917,7 @@ impl SepData { if !self.save_catacomb(who, kind, what) { return false; } - if crate::catacomb::read(kind).is_none() { + if !crate::catacomb::is_saved(kind) { dev_err!(self.dev, "enrol: opt-in {} has no durable host file\n", what); return false; } @@ -960,7 +973,7 @@ impl SepData { dev_err!(self.dev, "verify: {} persistence failed\n", what); return false; } - if crate::catacomb::read(kind).is_none() { + if !crate::catacomb::is_saved(kind) { dev_err!(self.dev, "verify: {} has no durable host file\n", what); return false; } @@ -3777,7 +3790,7 @@ impl SepData { } // 0x57 cannot be undone. Check first that its deletion can be saved. - if crate::catacomb::read(PRIVATE_TYPE_CATACOMB_OWNER).is_none() + if !crate::catacomb::is_saved(PRIVATE_TYPE_CATACOMB_OWNER) || !self .enrol_material .lock() @@ -3855,7 +3868,7 @@ impl SepData { ); return Err(EIO); } - if crate::catacomb::read(kind).is_none() { + if !crate::catacomb::is_saved(kind) { dev_err!(self.dev, "bio: the {} has no durable host file\n", what); return Err(EIO); } diff --git a/drivers/soc/apple/shim.rs b/drivers/soc/apple/shim.rs index 8e30c3f54b8177..2d431f7c441008 100644 --- a/drivers/soc/apple/shim.rs +++ b/drivers/soc/apple/shim.rs @@ -50,11 +50,9 @@ fn result_of(ret: c_long) -> Result { impl StoreFile { pub(crate) fn open(path: &CStr) -> Result { - // SAFETY: `path` is NUL-terminated; the shim returns NULL on failure. - let handle = unsafe { sep_store_open(path.as_char_ptr()) }; - if handle.is_null() { - return Err(ENOENT); - } + // SAFETY: `path` is NUL-terminated; the shim returns a live file or + // an error pointer, as required by `from_err_ptr`. + let handle = unsafe { kernel::error::from_err_ptr(sep_store_open(path.as_char_ptr()))? }; Ok(StoreFile { handle }) } @@ -72,20 +70,17 @@ impl StoreFile { } pub(crate) fn open_trunc(path: &CStr) -> Result { - // SAFETY: `path` is NUL-terminated; the shim returns NULL on failure. - let handle = unsafe { sep_store_open_trunc(path.as_char_ptr()) }; - if handle.is_null() { - return Err(ENOENT); - } + // SAFETY: `path` is NUL-terminated; the shim returns a live file or + // an error pointer, as required by `from_err_ptr`. + let handle = + unsafe { kernel::error::from_err_ptr(sep_store_open_trunc(path.as_char_ptr()))? }; Ok(StoreFile { handle }) } pub(crate) fn open_readonly(path: &CStr) -> Result { - // SAFETY: `path` is NUL-terminated; the shim returns NULL on failure. - let handle = unsafe { sep_store_open_ro(path.as_char_ptr()) }; - if handle.is_null() { - return Err(ENOENT); - } + // SAFETY: `path` is NUL-terminated; the shim returns a live file or + // an error pointer, as required by `from_err_ptr`. + let handle = unsafe { kernel::error::from_err_ptr(sep_store_open_ro(path.as_char_ptr()))? }; Ok(StoreFile { handle }) } diff --git a/drivers/soc/apple/store_shim.c b/drivers/soc/apple/store_shim.c index 0eabe7305b0497..c1a1a56b201c77 100644 --- a/drivers/soc/apple/store_shim.c +++ b/drivers/soc/apple/store_shim.c @@ -47,9 +47,7 @@ static struct file *open_as_kernel(const char *path, int flags, umode_t mode) /* Opens the store, creating if absent. Mode 0600: the file is the driver's own. */ void *sep_store_open(const char *path) { - struct file *f = open_as_kernel(path, O_RDWR | O_CREAT | O_LARGEFILE, 0600); - - return IS_ERR(f) ? NULL : f; + return open_as_kernel(path, O_RDWR | O_CREAT | O_LARGEFILE, 0600); } /* @@ -75,20 +73,16 @@ void *sep_store_open_host(const char *path, bool *created) */ void *sep_store_open_trunc(const char *path) { - struct file *f = open_as_kernel(path, O_RDWR | O_CREAT | O_TRUNC | O_LARGEFILE, 0600); - - return IS_ERR(f) ? NULL : f; + return open_as_kernel(path, O_RDWR | O_CREAT | O_TRUNC | O_LARGEFILE, 0600); } /* - * Opens an existing file read-only, NULL if absent. O_RDONLY and no O_CREAT so - * the seed cannot be created, truncated or written by mistake. + * Opens an existing file read-only, returning an error pointer on failure. + * O_RDONLY and no O_CREAT: the seed cannot be created, truncated or written. */ void *sep_store_open_ro(const char *path) { - struct file *f = open_as_kernel(path, O_RDONLY | O_LARGEFILE, 0); - - return IS_ERR(f) ? NULL : f; + return open_as_kernel(path, O_RDONLY | O_LARGEFILE, 0); } void *sep_store_open_block(const char *path, int writable) diff --git a/tools/aurora-sep/tests/test_identity_preservation.py b/tools/aurora-sep/tests/test_identity_preservation.py new file mode 100644 index 00000000000000..bb2c29b2266274 --- /dev/null +++ b/tools/aurora-sep/tests/test_identity_preservation.py @@ -0,0 +1,362 @@ +# SPDX-License-Identifier: MIT +"""Compile production identity readers against in-memory, device-free seams.""" +from pathlib import Path +import re +import shutil +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[3] / 'drivers/soc/apple' + + +def body(source, signature): + start = source.index(signature) + at = source.index('{', start) + depth, end = 1, at + 1 + while depth: + depth += (source[end] == '{') - (source[end] == '}') + end += 1 + return source[start:end] + + +COMMON = r''' +#![allow(dead_code, unused_variables, unused_imports, unused_macros, non_upper_case_globals)] +use std::cell::{Cell, RefCell}; +use std::ops::{Deref,DerefMut}; +use std::ffi::CStr; +type Result = std::result::Result; +const ENOENT:i32=2; const EIO:i32=5; const EINVAL:i32=22; +const ENOTSYNC:i32=52; const EEXIST:i32=17; const GFP_KERNEL:u32=0; +#[derive(Clone)] struct KVec(Vec); +impl KVec { + fn new()->Self {Self(Vec::new())} + fn with_capacity(n:usize,_:u32)->Result {Ok(Self(Vec::with_capacity(n)))} + fn push(&mut self,v:T,_:u32)->Result<()> {self.0.push(v);Ok(())} +} +impl KVec { + fn resize(&mut self,n:usize,v:T,_:u32)->Result<()> {self.0.resize(n,v);Ok(())} + fn extend_from_slice(&mut self,v:&[T],_:u32)->Result<()> {self.0.extend_from_slice(v);Ok(())} +} +impl Deref for KVec {type Target=Vec; fn deref(&self)->&Vec{&self.0}} +impl DerefMut for KVec {fn deref_mut(&mut self)->&mut Vec{&mut self.0}} +struct Secret(KVec); +impl Deref for Secret {type Target=[u8];fn deref(&self)->&[u8]{&self.0.0}} +macro_rules! dev_err {($($x:tt)*)=>{()};} +macro_rules! dev_warn {($($x:tt)*)=>{()};} +''' + +REFKEY = r''' +thread_local! {static OPEN_ERROR:Cell=const{Cell::new(0)};} +mod sks {pub type KeyBagHandle=u32;} +mod module_parameters { + pub struct Param; pub static refkey_v2:Param=Param; + impl Param {pub fn value(&self)->&u8{&0}} +} +mod shim { + use super::*; + pub struct StoreFile; + impl StoreFile { + pub fn open_readonly(_: &CStr)->Result{OPEN_ERROR.with(|e|if e.get()==0{Ok(Self)}else{Err(e.get())})} + pub fn open(_: &CStr)->Result{panic!("fixture never permits persistence")} + pub fn size(&self)->Result{Ok(1)} + pub fn read_exact(&self,_:u64,_:&mut[u8])->Result<()>{Ok(())} + pub fn write_all(&self,_:u64,_:&[u8])->Result<()>{panic!("write")} + pub fn sync(&self)->Result<()>{panic!("sync")} + } +} +struct Cache; +impl Cache {fn lock(&self)->Option<()>{None}} +struct SepData {machine_refkey:Cache,creates:Cell} +impl SepData { + fn sks_create_refkey(&self,_:u32,_:&[u8])->Option>{self.creates.set(self.creates.get()+1);None} + fn cache_machine_refkey(&self,_:KVec)->bool{true} + METHOD +} +fn main(){ + for error in [0,ENOENT,EIO,13,12,30,40] { + OPEN_ERROR.with(|e|e.set(error)); + let s=SepData{machine_refkey:Cache,creates:Cell::new(0)}; + s.sks_machine_refkey(0,&[]); + assert_eq!(s.creates.get(),usize::from(error==ENOENT),"only absence permits ref-key creation"); + } + println!("refkey cases passed"); +} +''' + +INDEX = r''' +const UUID_LEN:usize=16; const LABEL_LEN:usize=128;const MAX_IDENTITIES:usize=32; +const INDEX_FORMAT:u32=2; +#[derive(Clone)] struct Identity{uuid:[u8;UUID_LEN],label:[u8;LABEL_LEN]} +impl Identity {const ZERO:Self=Self{uuid:[0;UUID_LEN],label:[0;LABEL_LEN]};} +struct Store(Option>); +impl Store {fn read(&mut self,_:&())->Result>>{Ok(self.0.clone().map(KVec))}} +fn identity_key(){} +struct IdentityIndex{entries:KVec} +impl IdentityIndex{ + fn new()->Self{Self{entries:KVec::new()}} + METHOD +} +fn image(count:u32,n:usize)->Vec{ + let mut v=Vec::from(INDEX_FORMAT.to_le_bytes());v.extend(count.to_le_bytes()); + v.resize(8+n*(UUID_LEN+LABEL_LEN),0);v +} +fn main(){ + assert!(IdentityIndex::load(&mut Store(None)).unwrap().entries.is_empty()); + for n in [0,1,MAX_IDENTITIES]{assert_eq!(IdentityIndex::load(&mut Store(Some(image(n as u32,n)))).unwrap().entries.len(),n);} + for len in 0..8 {assert!(IdentityIndex::load(&mut Store(Some(vec![0;len]))).is_err(),"short index admitted");} + let valid=image(1,1); + for len in 8..valid.len(){assert!(IdentityIndex::load(&mut Store(Some(valid[..len].to_vec()))).is_err(),"truncated index admitted");} + for v in [image(2,1),image((MAX_IDENTITIES+1) as u32,MAX_IDENTITIES+1)] { + assert!(IdentityIndex::load(&mut Store(Some(v))).is_err(),"count mismatch admitted"); + } + let mut trailing=image(0,0);trailing.push(0); + assert!(IdentityIndex::load(&mut Store(Some(trailing))).is_err(),"trailing index bytes admitted"); + println!("index cases passed"); +} +''' + +KEYBAG = r''' +const MAGIC:[u8;16]=*b"APPLE-SEP-KBAG01"; +const VERSION:u32=2;const STATE_REFUSED:u32=2;const UUID_LEN:usize=16; +const OFF_VERSION:usize=16;const OFF_STATE:usize=20;const OFF_LEN:usize=24; +const OFF_CRC:usize=28;const OFF_UUID:usize=32;const OFF_SECRET_LEN:usize=48; +const HEADER:usize=52;const MAX_WRAPPED:usize=65536; +#[derive(Clone,Copy)] enum Slot{Identity} +impl Slot{fn path(self)->&'static CStr{c"unused"}} +enum UuidProvenance{Generated,Readback} +impl UuidProvenance{fn from_state(n:u32)->Option{match n{1=>Some(Self::Generated),3=>Some(Self::Readback),_=>None}}} +struct NoStoredKeyBag{slot:Slot} +struct StoredKeyBag{wrapped:KVec,uuid:[u8;16],secret:Secret,provenance:UuidProvenance} +enum State{Present(StoredKeyBag),Absent(NoStoredKeyBag)} +thread_local!{static IMAGE:RefCell>=const{RefCell::new(Vec::new())};} +mod shim{ + use super::*;pub struct StoreFile; + impl StoreFile{ + pub fn open_readonly(_: &CStr)->Result{Ok(Self)} + pub fn size(&self)->Result{Ok(IMAGE.with(|v|v.borrow().len() as u64))} + pub fn read_exact(&self,at:u64,out:&mut[u8])->Result<()>{ + IMAGE.with(|v|{let v=v.borrow();let Some(s)=v.get(at as usize..at as usize+out.len())else{return Err(EIO)};out.copy_from_slice(s);Ok(())}) + } + } +} +@@CRC@@ +CHECKSUM +METHOD +fn refused()->Vec{ + let mut v=vec![0;HEADER];v[..16].copy_from_slice(&MAGIC); + v[OFF_VERSION..OFF_VERSION+4].copy_from_slice(&VERSION.to_le_bytes()); + v[OFF_STATE..OFF_STATE+4].copy_from_slice(&STATE_REFUSED.to_le_bytes()); + let crc=crc16_ccitt_false(&checksum_input(STATE_REFUSED,&[],&[0;16],&[]).unwrap()); + v[OFF_CRC..OFF_CRC+2].copy_from_slice(&crc.to_le_bytes());v +} +fn main(){ + let good=refused();IMAGE.with(|v|*v.borrow_mut()=good.clone()); + assert!(matches!(read(Slot::Identity),Ok(State::Absent(_)))); + for at in OFF_LEN..HEADER { + let mut bad=good.clone();bad[at]^=1;IMAGE.with(|v|*v.borrow_mut()=bad); + assert!(read(Slot::Identity).is_err(),"noncanonical refusal admitted at {}",at); + } + let mut long=good;long.push(0);IMAGE.with(|v|*v.borrow_mut()=long); + assert!(read(Slot::Identity).is_err(),"trailing refusal data admitted"); + let wrapped=[1,2,3];let secret=[4,5];let uuid=[6;16]; + let mut committed=refused();committed[OFF_STATE..OFF_STATE+4].copy_from_slice(&3u32.to_le_bytes()); + committed[OFF_LEN..OFF_LEN+4].copy_from_slice(&3u32.to_le_bytes()); + committed[OFF_SECRET_LEN..OFF_SECRET_LEN+4].copy_from_slice(&2u32.to_le_bytes()); + committed[OFF_UUID..OFF_UUID+16].copy_from_slice(&uuid); + let crc=crc16_ccitt_false(&checksum_input(3,&wrapped,&uuid,&secret).unwrap()); + committed[OFF_CRC..OFF_CRC+2].copy_from_slice(&crc.to_le_bytes()); + committed.extend(wrapped);committed.extend(secret);IMAGE.with(|v|*v.borrow_mut()=committed); + assert!(matches!(read(Slot::Identity),Ok(State::Present(_))),"valid committed bag refused"); + println!("keybag cases passed"); +} +''' + +CATACOMB = r''' +const MAGIC:[u8;8]=*b"SEPCTMB1";const HEADER:usize=16;const MAX_CATACOMB:usize=1<<20; +const PRIVATE_TYPE_CATACOMB_MASTER:u8=1;const PRIVATE_TYPE_CATACOMB_OWNER:u8=2;const PRIVATE_TYPE_CATACOMB_USER:u8=3; +thread_local!{static OPEN:Cell=const{Cell::new(0)};static IMAGE:RefCell>=const{RefCell::new(Vec::new())};} +mod shim{ + use super::*;pub struct StoreFile; + impl StoreFile{ + pub fn open_readonly(_: &CStr)->Result{OPEN.with(|e|if e.get()==0{Ok(Self)}else{Err(e.get())})} + pub fn size(&self)->Result{Ok(IMAGE.with(|v|v.borrow().len() as u64))} + pub fn read_exact(&self,at:u64,out:&mut[u8])->Result<()>{ + IMAGE.with(|v|{let v=v.borrow();let Some(s)=v.get(at as usize..at as usize+out.len())else{return Err(EIO)};out.copy_from_slice(s);Ok(())}) + } + } +} +@@CRC@@ +PATH +METHOD +fn image(blob:&[u8])->Vec{ + let mut v=Vec::from(MAGIC);v.extend((blob.len() as u32).to_le_bytes()); + v.extend(crc16_ccitt_false(blob).to_le_bytes());v.extend([0,0]);v.extend(blob);v +} +fn main(){ + let k=PRIVATE_TYPE_CATACOMB_MASTER; + OPEN.with(|e|e.set(ENOENT));assert!(matches!(read(k),Ok(None)),"missing file must be absence"); + for err in [EIO,13,12,30,40]{OPEN.with(|e|e.set(err));assert!(read(k).is_err(),"open error read as absence");} + OPEN.with(|e|e.set(0)); + let good=image(&[1,2,3]);IMAGE.with(|v|*v.borrow_mut()=good.clone()); + assert_eq!(read(k).unwrap().unwrap().0,vec![1,2,3]); + for len in 0..good.len(){IMAGE.with(|v|*v.borrow_mut()=good[..len].to_vec());assert!(read(k).is_err(),"truncated file read as absence or data");} + let mut bad=good.clone();bad[HEADER]^=1;IMAGE.with(|v|*v.borrow_mut()=bad);assert!(read(k).is_err(),"bad checksum admitted"); + let mut bad=good;bad[0]^=1;IMAGE.with(|v|*v.borrow_mut()=bad);assert!(read(k).is_err(),"bad magic admitted"); + println!("catacomb cases passed"); +} +''' + + +CALLERS = r''' +use std::sync::atomic::{AtomicBool, Ordering::Relaxed}; +macro_rules! dev_info {($($x:tt)*)=>{()};} +const PRIVATE_TYPE_CATACOMB_MASTER:u8=0xF2;const PRIVATE_TYPE_CATACOMB_OWNER:u8=0xF3;const PRIVATE_TYPE_CATACOMB_USER:u8=0xF4; +const SBIO_PROBE_USER_ID:i32=1; +#[derive(Debug,PartialEq)] enum RestoreOutcome{Restored,AlreadyActive,NoStoredFile,Failed,EmptyTolerated} +enum LoadAnswer{StatusZero,ColdTransition,AlreadyActive} +// 0 = valid file, ENOENT = missing, anything else = read error. +thread_local!{ + static FILE:RefCell<[i32;3]>=const{RefCell::new([0;3])}; + static HOST:Cell=const{Cell::new(-1)}; + static ASKED_ENCLAVE:Cell=const{Cell::new(false)}; + static TRANSFERRED:Cell=const{Cell::new(false)}; +} +mod catacomb{ + use super::*; + pub fn is_kind(_:u8)->bool{true} + pub fn read(kind:u8)->Result>>{ + match FILE.with(|f|f.borrow()[(kind-PRIVATE_TYPE_CATACOMB_MASTER) as usize]){ + 0=>Ok(Some(KVec(vec![1,0,0,0]))),ENOENT=>Ok(None),e=>Err(e)} + } + @@IS_SAVED@@ + @@IS_ABSENT@@ +} +mod store{pub struct Key;impl Key{pub fn root(_:u8)->Self{Key}}} +struct Store; +impl Store{fn read(&self,_:&store::Key)->Result>>{match HOST.with(|h|h.get()){0=>Ok(Some(KVec(vec![1,0,0,0]))),ENOENT=>Ok(None),e=>Err(e)}}} +mod module_parameters{pub struct Param;pub static probe_owner_export:Param=Param;impl Param{pub fn value(&self)->&u8{&1}}} +mod sbio{ + pub enum ComponentAction{AlreadyActive,Unsupported,Load} + pub fn component_action(_:u32)->ComponentAction{ComponentAction::Load} + pub const SBIO_SAVED_USER_ID_AT:usize=0;pub const SBIO_STATUS_COLD_TRANSITION:u32=0x8002;pub const SBIO_STATUS_ALREADY_ACTIVE:u32=0x101; + pub const COMPONENT_STATE_ACTIVE:u32=1; + pub struct SbioLoadCatacomb; + impl SbioLoadCatacomb{pub fn new(_:&[u8])->Option{Some(Self)}pub fn opcode(&self)->u8{0}pub fn name(&self)->u8{0}pub fn payload(&self)->&[u8]{&[]}} + pub struct NoExistingCatacomb;impl NoExistingCatacomb{pub fn from_zero_identities(_:usize)->Option{None}} + pub enum ContextScope{SYSTEM}pub fn sbio_select_context(_:ContextScope,_:&NoExistingCatacomb){} + pub struct ComponentStates;impl ComponentStates{pub fn new(_:&[u8])->Self{Self}pub fn state_for(&self,_:i32)->Option{None}} + pub struct CatacombUser;impl CatacombUser{pub const OWNER:Self=Self;pub fn value(&self)->i32{0}} +} +struct Status;impl Status{fn is_ok(&self)->bool{true}fn answered(&self)->Option{None}} +impl std::fmt::Display for Status{fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{Ok(())}} +struct Done{status:Status} +struct Sbio{dev:(),templates_restored:AtomicBool} +impl Sbio{ + fn with_host_store(&self,f:impl FnOnce(&Store)->R)->Option{if HOST.with(|h|h.get())<0{None}else{Some(f(&Store))}} + fn sbio_transfer_raw(&self,_:u8,_:u8,_:&[u8])->Result{TRANSFERRED.with(|t|t.set(true));Ok(Done{status:Status})} + fn confirm_active(&self,_:i32,_:&CStr,_:LoadAnswer)->RestoreOutcome{RestoreOutcome::Restored} + fn log_identity_count(&self,_:&CStr)->Option{Some(1)} + fn enclave_identities_for(&self,_:i32)->Option>{ASKED_ENCLAVE.with(|a|a.set(true));None} + fn sbio_expect_ok(&self,_:&())->Option<()>{None} + fn read_component_states(&self)->Option>{None} + fn save_catacomb(&self,_:sbio::CatacombUser,_:u8,_:&CStr)->bool{false} + @@RESTORE@@ + @@READ_STORED@@ + @@PROBE@@ +} +fn set(files:[i32;3],host:i32){FILE.with(|f|*f.borrow_mut()=files);HOST.with(|h|h.set(host));ASKED_ENCLAVE.with(|a|a.set(false));TRANSFERRED.with(|t|t.set(false));} +fn main(){ + let s=Sbio{dev:(),templates_restored:AtomicBool::new(false)}; + let user=PRIVATE_TYPE_CATACOMB_USER; + set([0,0,EIO],-1); + assert_eq!(s.restore_catacomb(Some(0),1,user,c"user"),RestoreOutcome::Failed,"unreadable catacomb restored as missing"); + assert!(!TRANSFERRED.with(|t|t.get()),"unreadable catacomb sent to the enclave"); + set([0,0,ENOENT],0); + assert_eq!(s.restore_catacomb(Some(0),1,user,c"user"),RestoreOutcome::Restored,"host-store copy not used"); + set([0,0,ENOENT],EIO); + assert_eq!(s.restore_catacomb(Some(0),1,user,c"user"),RestoreOutcome::Failed,"unreadable host store restored as missing"); + set([0,0,ENOENT],-1); + assert_eq!(s.restore_catacomb(Some(0),1,user,c"user"),RestoreOutcome::NoStoredFile,"genuine absence not reported"); + // Owner probe: positive control reaches the enclave; an unreadable owner file must not. + set([0,ENOENT,0],-1);s.probe_owner_export(); + assert!(ASKED_ENCLAVE.with(|a|a.get()),"owner probe never reached its enclave check"); + for e in [EIO,13]{set([0,e,0],-1);s.probe_owner_export();assert!(!ASKED_ENCLAVE.with(|a|a.get()),"unreadable owner catacomb treated as absent");} + set([EIO,ENOENT,0],-1);s.probe_owner_export(); + assert!(!ASKED_ENCLAVE.with(|a|a.get()),"unreadable master catacomb treated as saved"); + println!("caller cases passed"); +} +''' + + +@unittest.skipUnless(shutil.which('rustc'), 'rustc required') +class Preservation(unittest.TestCase): + def run_rust(self, text, should_pass=True): + with tempfile.TemporaryDirectory(prefix='sep-identity-readers-') as tmp: + p=Path(tmp);(p/'test.rs').write_text(COMMON+text) + subprocess.run(['rustc','--edition=2021',str(p/'test.rs'),'-o',str(p/'test')],check=True) + result=subprocess.run([str(p/'test')],capture_output=True,text=True) + if should_pass: + self.assertEqual(result.returncode,0,result.stderr) + else: + self.assertNotEqual(result.returncode,0) + self.assertIn('panicked',result.stderr) + + def test_refkey_requires_genuine_absence(self): + text=(ROOT/'refkey.rs').read_text() + method=body(text,' pub(crate) fn sks_machine_refkey(') + self.run_rust(REFKEY.replace('METHOD',method)) + old=re.sub(r' let existing = match.*? if let Some\(f\) = existing \{', + ' if let Ok(f) = shim::StoreFile::open_readonly(path) {',method,flags=re.S) + self.assertNotEqual(old,method) + self.run_rust(REFKEY.replace('METHOD',old),should_pass=False) + + def test_index_requires_complete_record(self): + method=body((ROOT/'bio.rs').read_text(),' pub(crate) fn load(store:') + self.run_rust(INDEX.replace('METHOD',method)) + old=method.replace('if raw.len() < 8 {\n return Err(EINVAL);','if raw.len() < 8 {\n return Ok(index);',1) + self.assertNotEqual(old,method) + self.run_rust(INDEX.replace('METHOD',old),should_pass=False) + + def test_refused_keybag_requires_canonical_writer_record(self): + text=(ROOT/'keybag.rs').read_text() + method=body(text,'pub(crate) fn read(slot:') + crc=body((ROOT/'store.rs').read_text(),'pub(crate) const fn crc16_ccitt_false(') + checksum=body(text,'fn checksum_input(') + make=lambda m:KEYBAG.replace('METHOD',m).replace('CHECKSUM',checksum).replace('@@CRC@@',crc) + self.run_rust(make(method)) + start=method.index(' if state == STATE_REFUSED {') + stop=start+len(body(method[start:],' if state == STATE_REFUSED {')) + old=method[:start]+' if state == STATE_REFUSED { return Ok(State::Absent(NoStoredKeyBag { slot })); }'+method[stop:] + self.run_rust(make(old),should_pass=False) + + def test_catacomb_read_error_is_not_absence(self): + text=(ROOT/'catacomb.rs').read_text() + method=body(text,'pub(crate) fn read(kind: u8)') + path=body(text,'fn path_for(kind: u8)').replace('crate::','') + crc=body((ROOT/'store.rs').read_text(),'pub(crate) const fn crc16_ccitt_false(') + make=lambda m:CATACOMB.replace('METHOD',m).replace('PATH',path).replace('@@CRC@@',crc) + self.run_rust(make(method)) + old=method.replace("Err(e) => return Err(e),","Err(e) => return Ok(None),",1) + self.assertNotEqual(old,method) + self.run_rust(make(old),should_pass=False) + + def test_callers_refuse_unreadable_catacombs(self): + cat=(ROOT/'catacomb.rs').read_text();sb=(ROOT/'sbio.rs').read_text() + parts={'IS_SAVED':body(cat,'pub(crate) fn is_saved('),'IS_ABSENT':body(cat,'pub(crate) fn is_absent('), + 'RESTORE':body(sb,' fn restore_catacomb('),'READ_STORED':body(sb,' fn read_stored('), + 'PROBE':body(sb,' fn probe_owner_export(')} + def make(**over): + text=CALLERS + for key,value in {**parts,**over}.items(): + text=text.replace('@@'+key+'@@',value.replace('crate::','')) + return text + self.run_rust(make()) + restore=parts['RESTORE'].replace('Err(_) => return RestoreOutcome::Failed,','Err(_) => return RestoreOutcome::NoStoredFile,',1) + absent=parts['IS_ABSENT'].replace('matches!(read(kind), Ok(None))','!matches!(read(kind), Ok(Some(_)))',1) + for name,old in (('RESTORE',restore),('IS_ABSENT',absent)): + self.assertNotEqual(old,parts[name]) + self.run_rust(make(**{name:old}),should_pass=False) + +if __name__=='__main__': + unittest.main() diff --git a/tools/aurora-sep/tests/test_store_open_errors.py b/tools/aurora-sep/tests/test_store_open_errors.py new file mode 100644 index 00000000000000..7625f1ddffea04 --- /dev/null +++ b/tools/aurora-sep/tests/test_store_open_errors.py @@ -0,0 +1,193 @@ +# SPDX-License-Identifier: MIT +"""The regular-file C/Rust boundary must not turn an I/O error into absence. + +Compile the actual C wrapper bodies with a fake open_as_kernel(). No kernel, +files, enclave, or stored identity is opened. When rustc is available, also +compile the unchanged Rust module against a tiny kernel API +facade and those C wrappers; this is a userspace boundary fixture, not a kernel +build or a real filesystem test. +""" +from pathlib import Path +import re +import shutil +import subprocess +import tempfile +import unittest + + +ROOT = Path(__file__).resolve().parents[3] +C_SOURCE = ROOT / 'drivers/soc/apple/store_shim.c' +RUST_SOURCE = ROOT / 'drivers/soc/apple/shim.rs' +OPENS = ('sep_store_open', 'sep_store_open_trunc', 'sep_store_open_ro') + + +def function(source, signature): + start = source.index(signature) + brace = source.index('{', start) + depth = 1 + end = brace + 1 + while depth: + depth += (source[end] == '{') - (source[end] == '}') + end += 1 + return source[start:end] + + +PREFIX = r''' +#include +#include +#include +#include +#ifndef O_LARGEFILE +#define O_LARGEFILE 0 +#endif +struct file { int value; }; +static struct file live_file; +static struct file *next_result; +static struct file *open_as_kernel(const char *path, int flags, unsigned mode) +{ + (void)path; (void)flags; (void)mode; + return next_result; +} +''' + +RUNNER = r''' +int main(void) +{ + void *(*opens[])(const char *) = { + sep_store_open, sep_store_open_trunc, sep_store_open_ro + }; + const int errors[] = { ENOENT, EACCES, EIO, ENOMEM, EROFS, ELOOP }; + for (unsigned i = 0; i < sizeof(opens) / sizeof(opens[0]); ++i) { + next_result = &live_file; + if (opens[i]("unused") != &live_file) return 2; + for (unsigned j = 0; j < sizeof(errors) / sizeof(errors[0]); ++j) { + next_result = (struct file *)(intptr_t)-errors[j]; + if (opens[i]("unused") != next_result) { + fprintf(stderr, "wrapper %u lost errno %d\n", i, errors[j]); + return 1; + } + } + } + puts("21 open results preserved"); + return 0; +} +''' + + +class StoreOpenErrors(unittest.TestCase): + def native(self, negative_control=False): + compiler = shutil.which('cc') + if compiler is None: + self.skipTest('C compiler unavailable') + source = C_SOURCE.read_text() + bodies = '\n'.join(function(source, 'void *' + name + '(') for name in OPENS) + prefix = PREFIX + if negative_control: + # Reintroduce the old error-pointer-to-NULL collapse. The same + # executable must reject it, rather than merely compile it. + prefix += r''' +static void *collapse(struct file *f) +{ + return (uintptr_t)f >= (uintptr_t)-4095 ? NULL : f; +} +''' + bodies, changed = re.subn(r'return (open_as_kernel\([^;]+\));', r'return collapse(\1);', bodies) + self.assertEqual(changed, 3) + with tempfile.TemporaryDirectory(prefix='sep-open-errors-') as tmp: + root = Path(tmp) + (root / 'test.c').write_text(prefix + bodies + RUNNER) + subprocess.run([compiler, '-std=c11', '-Wall', '-Wextra', '-Werror', + str(root / 'test.c'), '-o', str(root / 'test')], check=True) + return subprocess.run([str(root / 'test')], capture_output=True, text=True) + + def test_native_open_results(self): + result = self.native() + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn('21 open results preserved', result.stdout) + + def test_old_error_collapse_is_rejected(self): + result = self.native(negative_control=True) + self.assertEqual(result.returncode, 1) + self.assertIn('lost errno', result.stderr) + + def test_block_device_open_contract_unchanged(self): + body = function(C_SOURCE.read_text(), 'void *sep_store_open_block(') + self.assertRegex(body, r'if \(IS_ERR\(f\)\)\s+return NULL;') + + def test_native_rust_c_boundary(self): + compiler, rustc = shutil.which('cc'), shutil.which('rustc') + if compiler is None or rustc is None: + self.skipTest('C and Rust compilers required') + bodies = '\n'.join(function(C_SOURCE.read_text(), 'void *' + name + '(') for name in OPENS) + c = PREFIX + bodies + r''' +static unsigned closes; +void fixture_result(int error) +{ + next_result = error ? (struct file *)(intptr_t)-error : &live_file; +} +void sep_store_close(void *file) +{ + if (file == &live_file) ++closes; +} +unsigned fixture_closes(void) { return closes; } +''' + rust = r''' +#![allow(dead_code)] +extern crate self as kernel; +pub mod error { + #[derive(Debug, PartialEq)] + pub struct Error(pub i32); + impl Error { pub fn from_errno(n: i32) -> Self { Self(n) } } + pub type Result = core::result::Result; + pub const EIO: Error = Error(-5); + pub const ENODEV: Error = Error(-19); + pub fn to_result(n: i32) -> Result { if n < 0 { Err(Error(n)) } else { Ok(()) } } + pub unsafe fn from_err_ptr(p: *mut T) -> Result<*mut T> { + if p as usize >= usize::MAX - 4094 { Err(Error(p as isize as i32)) } else { Ok(p) } + } +} +pub mod prelude { + pub use core::ffi::{c_char,c_int,c_void,c_long,c_uint,c_ulong}; + pub use crate::error::{Error, Result, EIO, ENODEV}; + pub struct CStr(pub &'static [u8]); + impl CStr { pub fn as_char_ptr(&self) -> *const c_char { self.0.as_ptr().cast() } } +} +#[path="shim.rs"] mod shim; +extern "C" { fn fixture_result(error: i32); fn fixture_closes() -> u32; } +fn main() { + let path = prelude::CStr(b"unused\0"); + let opens: [fn(&prelude::CStr) -> error::Result; 3] = [ + shim::StoreFile::open, shim::StoreFile::open_trunc, shim::StoreFile::open_readonly + ]; + for open in opens { + unsafe { fixture_result(0); } + drop(open(&path).expect("successful handle")); + for errno in [2, 13, 5, 12, 30, 40] { + unsafe { fixture_result(errno); } + match open(&path) { + Err(e) if e == error::Error(-errno) => (), + _ => panic!("Rust lost an open error"), + } + } + } + if unsafe { fixture_closes() } != 3 { panic!("only successful handles may close"); } + println!("21 Rust/C results preserved; 3 successful handles closed"); +} +''' + with tempfile.TemporaryDirectory(prefix='sep-open-rust-') as tmp: + root = Path(tmp) + (root / 'test.c').write_text(c) + (root / 'test.rs').write_text(rust) + shutil.copyfile(RUST_SOURCE, root / 'shim.rs') + subprocess.run([compiler, '-std=c11', '-Wall', '-Wextra', '-Werror', '-c', + str(root / 'test.c'), '-o', str(root / 'test.o')], check=True) + subprocess.run([rustc, '--edition=2021', '-C', 'opt-level=1', + '-C', 'link-arg=' + str(root / 'test.o'), + str(root / 'test.rs'), '-o', str(root / 'test')], check=True) + result = subprocess.run([str(root / 'test')], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn('21 Rust/C results preserved', result.stdout) + + +if __name__ == '__main__': + unittest.main()