From 77d34e19e580bd5ef39586f9d2705d413db3b3c1 Mon Sep 17 00:00:00 2001 From: Satya Benson Date: Sun, 25 Oct 2026 21:29:40 -0400 Subject: [PATCH 01/10] soc: apple: sep: preserve backing-store open errors Signed-off-by: Satya Benson --- drivers/soc/apple/shim.rs | 25 +-- drivers/soc/apple/store_shim.c | 16 +- .../tests/test_store_open_errors.py | 203 ++++++++++++++++++ 3 files changed, 218 insertions(+), 26 deletions(-) create mode 100644 tools/aurora-sep/tests/test_store_open_errors.py diff --git a/drivers/soc/apple/shim.rs b/drivers/soc/apple/shim.rs index 6d546a5603f37..608a3cba4929d 100644 --- a/drivers/soc/apple/shim.rs +++ b/drivers/soc/apple/shim.rs @@ -51,11 +51,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 }) } @@ -73,11 +71,10 @@ 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 }) } @@ -91,11 +88,9 @@ impl StoreFile { } 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 77319a4702164..c1773fa2b6412 100644 --- a/drivers/soc/apple/store_shim.c +++ b/drivers/soc/apple/store_shim.c @@ -50,9 +50,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); } /* @@ -78,9 +76,7 @@ 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); } /* Admission check only: never create or truncate an existing record. */ @@ -92,14 +88,12 @@ void *sep_store_open_writable_existing(const char *path) } /* - * 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_store_open_errors.py b/tools/aurora-sep/tests/test_store_open_errors.py new file mode 100644 index 0000000000000..b55626f14f536 --- /dev/null +++ b/tools/aurora-sep/tests/test_store_open_errors.py @@ -0,0 +1,203 @@ +# 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. Rust assertions cover the source +wiring to the same from_err_ptr API already used by open_host(). 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_rust_propagates_each_regular_open_error(self): + source = RUST_SOURCE.read_text() + for method, call in zip(('open', 'open_trunc', 'open_readonly'), OPENS): + with self.subTest(method=method): + body = function(source, 'pub(crate) fn ' + method + '(') + self.assertIn('kernel::error::from_err_ptr(' + call + '(path.as_char_ptr()))?', body) + self.assertNotIn('is_null()', body) + self.assertNotIn('Err(ENOENT)', body) + + 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() From 91c26b2035c7f869c0c306c0c273fe144df77f30 Mon Sep 17 00:00:00 2001 From: Satya Benson Date: Sun, 25 Oct 2026 21:46:02 -0400 Subject: [PATCH 02/10] soc: apple: sep: refuse unreadable and partial identity records Signed-off-by: Satya Benson --- drivers/soc/apple/bio.rs | 15 +- drivers/soc/apple/keybag.rs | 9 + drivers/soc/apple/refkey.rs | 14 +- .../tests/test_identity_preservation.py | 219 ++++++++++++++++++ 4 files changed, 247 insertions(+), 10 deletions(-) create mode 100644 tools/aurora-sep/tests/test_identity_preservation.py diff --git a/drivers/soc/apple/bio.rs b/drivers/soc/apple/bio.rs index 573830b7973d0..1c01e3ca4a19e 100644 --- a/drivers/soc/apple/bio.rs +++ b/drivers/soc/apple/bio.rs @@ -308,22 +308,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/keybag.rs b/drivers/soc/apple/keybag.rs index 919035b1a09de..22dc6d27929eb 100644 --- a/drivers/soc/apple/keybag.rs +++ b/drivers/soc/apple/keybag.rs @@ -181,6 +181,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 e505fb8d03bea..ebb7df218ea66 100644 --- a/drivers/soc/apple/refkey.rs +++ b/drivers/soc/apple/refkey.rs @@ -179,7 +179,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/tools/aurora-sep/tests/test_identity_preservation.py b/tools/aurora-sep/tests/test_identity_preservation.py new file mode 100644 index 0000000000000..6308c59d40dd4 --- /dev/null +++ b/tools/aurora-sep/tests/test_identity_preservation.py @@ -0,0 +1,219 @@ +# 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"); +} +''' + + +@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) + + +if __name__=='__main__': + unittest.main() From ef4c4274c4232dc69f173edcdb3fdead676284f1 Mon Sep 17 00:00:00 2001 From: Satya Benson Date: Sun, 25 Oct 2026 22:22:26 -0400 Subject: [PATCH 03/10] soc: apple: sep: distinguish unreadable catacombs from missing ones catacomb::read() mapped every failure to None, which the enrollment, restore, deletion and owner-probe paths read as "no saved catacomb". Return Result> so that only a missing file reads as absent, add is_saved()/is_absent() for the callers, and make read_stored() propagate read errors so restore reports Failed rather than NoStoredFile. Test both callers that run on these paths: restore_catacomb() must fail without contacting the enclave on an unreadable catacomb or host store, and the owner probe must stop before its enclave check when the owner or master file cannot be read. Drop a source-text assertion made redundant by the native boundary test. Signed-off-by: Satya Benson --- drivers/soc/apple/catacomb.rs | 43 ++++-- drivers/soc/apple/sbio.rs | 71 +++++---- .../tests/test_identity_preservation.py | 145 +++++++++++++++++- .../tests/test_store_open_errors.py | 14 +- 4 files changed, 220 insertions(+), 53 deletions(-) diff --git a/drivers/soc/apple/catacomb.rs b/drivers/soc/apple/catacomb.rs index d71b415fcb321..bae0396a5b9c5 100644 --- a/drivers/soc/apple/catacomb.rs +++ b/drivers/soc/apple/catacomb.rs @@ -64,28 +64,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/sbio.rs b/drivers/soc/apple/sbio.rs index 98a048443da18..91158725d5084 100644 --- a/drivers/soc/apple/sbio.rs +++ b/drivers/soc/apple/sbio.rs @@ -552,7 +552,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; } @@ -638,9 +638,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; @@ -776,8 +776,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; @@ -894,8 +896,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; @@ -915,17 +919,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), } } @@ -979,7 +992,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; @@ -993,7 +1006,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; } @@ -1032,7 +1045,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; } @@ -1088,7 +1101,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; } @@ -3571,7 +3584,7 @@ impl SepData { let kinds = [PRIVATE_TYPE_CATACOMB_OWNER, PRIVATE_TYPE_CATACOMB_USER, PRIVATE_TYPE_CATACOMB_MASTER]; let readable = kinds.map(|kind| !catacomb_needed_for_enrol(kind, live_count) - || crate::catacomb::read(kind).is_some()); + || crate::catacomb::is_saved(kind)); let writable = kinds.map(|kind| !catacomb_needed_for_enrol(kind, live_count) || crate::catacomb::writable(kind)); let index = self.bio_index.lock(); @@ -3634,13 +3647,17 @@ impl SepData { if !ids.is_empty() { // An existing identity must already have a durable owner file; // selecting SYSTEM here could disturb its live context. - return if crate::catacomb::read(PRIVATE_TYPE_CATACOMB_OWNER).is_some() { + return if crate::catacomb::is_saved(PRIVATE_TYPE_CATACOMB_OWNER) { bio::ok() } else { Err(ENODATA) }; } - if crate::catacomb::read(PRIVATE_TYPE_CATACOMB_OWNER).is_some() { + if crate::catacomb::is_saved(PRIVATE_TYPE_CATACOMB_OWNER) { return bio::ok(); } + // An unreadable owner record is not permission to select a new context. + if !crate::catacomb::is_absent(PRIVATE_TYPE_CATACOMB_OWNER) { + return Err(EIO); + } if !self.keybag_designated.load(Relaxed) || !self.enrol_material.lock().as_ref() .is_some_and(|material| material.snapshot_identity.is_some()) { @@ -3667,7 +3684,7 @@ impl SepData { if !prepared { return Err(EIO); } break; } - if crate::catacomb::read(PRIVATE_TYPE_CATACOMB_OWNER).is_none() + if !crate::catacomb::is_saved(PRIVATE_TYPE_CATACOMB_OWNER) || !crate::catacomb::writable(PRIVATE_TYPE_CATACOMB_OWNER) { return Err(EROFS); } @@ -4069,7 +4086,7 @@ impl SepData { // owner Catacomb provides the recovery path even when the bag snapshot // has no binding; export that owner first if this install lacks a file. for kind in [PRIVATE_TYPE_CATACOMB_USER, PRIVATE_TYPE_CATACOMB_MASTER] { - if crate::catacomb::read(kind).is_none() || !crate::catacomb::writable(kind) { + if !crate::catacomb::is_saved(kind) || !crate::catacomb::writable(kind) { dev_err!( self.dev, "bio: not deleting: user/master Catacomb is unavailable or unwritable\n" @@ -4077,7 +4094,7 @@ impl SepData { return Err(EIO); } } - if orphan && crate::catacomb::read(PRIVATE_TYPE_CATACOMB_OWNER).is_none() { + if orphan && crate::catacomb::is_absent(PRIVATE_TYPE_CATACOMB_OWNER) { let Some(reply) = self.read_component_states() else { return Err(EIO); }; let state = crate::sbio::ComponentStates::new(&reply) .state_for(crate::sbio::CatacombUser::OWNER.value()); @@ -4089,7 +4106,7 @@ impl SepData { return Err(EIO); } } - if crate::catacomb::read(PRIVATE_TYPE_CATACOMB_OWNER).is_none() + if !crate::catacomb::is_saved(PRIVATE_TYPE_CATACOMB_OWNER) || !crate::catacomb::writable(PRIVATE_TYPE_CATACOMB_OWNER) { return Err(EIO); @@ -4176,7 +4193,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/tools/aurora-sep/tests/test_identity_preservation.py b/tools/aurora-sep/tests/test_identity_preservation.py index 6308c59d40dd4..3f15f10c7d191 100644 --- a/tools/aurora-sep/tests/test_identity_preservation.py +++ b/tools/aurora-sep/tests/test_identity_preservation.py @@ -51,7 +51,7 @@ def body(source, signature): 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}} + impl Param {pub fn value(&self)->u8{0}} } mod shim { use super::*; @@ -172,6 +172,122 @@ def body(source, signature): } ''' +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): @@ -214,6 +330,33 @@ def test_refused_keybag_requires_canonical_writer_record(self): 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 index b55626f14f536..7625f1ddffea0 100644 --- a/tools/aurora-sep/tests/test_store_open_errors.py +++ b/tools/aurora-sep/tests/test_store_open_errors.py @@ -2,9 +2,8 @@ """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. Rust assertions cover the source -wiring to the same from_err_ptr API already used by open_host(). When rustc is -available, also compile the unchanged Rust module against a tiny kernel API +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. """ @@ -111,15 +110,6 @@ def test_old_error_collapse_is_rejected(self): self.assertEqual(result.returncode, 1) self.assertIn('lost errno', result.stderr) - def test_rust_propagates_each_regular_open_error(self): - source = RUST_SOURCE.read_text() - for method, call in zip(('open', 'open_trunc', 'open_readonly'), OPENS): - with self.subTest(method=method): - body = function(source, 'pub(crate) fn ' + method + '(') - self.assertIn('kernel::error::from_err_ptr(' + call + '(path.as_char_ptr()))?', body) - self.assertNotIn('is_null()', body) - self.assertNotIn('Err(ENOENT)', body) - 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;') From 6b46c9f34436cf2bb1bcdc085402ec54353c80ed Mon Sep 17 00:00:00 2001 From: Satya Benson Date: Tue, 3 Nov 2026 10:59:42 -0500 Subject: [PATCH 04/10] soc: apple: sep: preserve the J613 26.6.2 existing-bag policy Keep the existing T8122 transport and sensor profile. On the owner-qualified OS-firmware lane, refuse writable attachment without the boot-forwarded identity and guard, and fail closed on host-store/index read errors. Add opt-outs for trusted-key and FileVault API registration with existing defaults preserved. No new provisioning or firmware protocol is introduced. Signed-off-by: Satya Benson --- .../ABI/testing/sysfs-driver-apple-sep | 20 ++++++++++ drivers/soc/apple/profile.rs | 27 ++++++++++++++ drivers/soc/apple/sep.rs | 37 +++++++++++++++++-- 3 files changed, 80 insertions(+), 4 deletions(-) diff --git a/Documentation/ABI/testing/sysfs-driver-apple-sep b/Documentation/ABI/testing/sysfs-driver-apple-sep index b48d26187f67d..5ee6cd0df614d 100644 --- a/Documentation/ABI/testing/sysfs-driver-apple-sep +++ b/Documentation/ABI/testing/sysfs-driver-apple-sep @@ -30,6 +30,26 @@ Description: by the boot-time bring-up, so the sensor and touchid attributes only move on once something has opened it. + This deployment uses the existing T8122 profile and v4 + /dev/sep-bio interface. It does not add a new sensor protocol. + OS firmware and system firmware are separate: qualify both. + + For an existing bag, select xart_writes=1, provision_keybag=0, + xart_guard=1 and xarm_trace=1. trusted_keys=0 and + filevault_api=0 omit the optional kernel APIs; both default + to 1 to preserve existing deployments. There is no boot_uuid + parameter: the OS identity comes from /chosen/apfs-preboot-uuid. + Writable J613 26.6.2 attachment rejects a missing DT identity, + explicit UUID override or disabled guard before opening xART. + Host-store or identity-index read failure refuses attachment. + + Preserve the current keybag, host store, reference key, three + Catacombs, ownership journal and fprint records. Different + Linux roots must share the same current files globally, before + module load; the kernel opens them from init_task's root. + Never replace them with a snapshot or enrol again to migrate. + Readiness diagnostics do not prove a successful finger match. + What: /sys/bus/platform/devices/.sep/diag/profile Date: October 2026 KernelVersion: 7.1 diff --git a/drivers/soc/apple/profile.rs b/drivers/soc/apple/profile.rs index aaefc62b9528b..2b9a1c9105953 100644 --- a/drivers/soc/apple/profile.rs +++ b/drivers/soc/apple/profile.rs @@ -710,3 +710,30 @@ mod dart_window_tests { } } } + +/// The owner-qualified J613 OS-firmware lane; system firmware may differ. +pub(crate) fn j613_26_6_2() -> bool { + machine_has(b"apple,j613") + && chosen_string(c"asahi,os-fw-version") + .as_deref() + .is_some_and(os_firmware_26_6_2) +} + +fn os_firmware_26_6_2(raw: &[u8]) -> bool { + raw == b"26.6.2" || raw == b"26.6.2\0" +} + +#[cfg(CONFIG_APPLE_SEP_KUNIT_TEST)] +#[kernel::macros::kunit_tests(apple_sep_j613_firmware)] +mod j613_firmware_tests { + use super::*; + + #[test] + fn exact_os_firmware_only() { + assert!(os_firmware_26_6_2(b"26.6.2")); + assert!(os_firmware_26_6_2(b"26.6.2\0")); + for raw in [&b"14.8.3"[..], &b"26.6.20"[..], &b"27.0"[..], &b""[..]] { + assert!(!os_firmware_26_6_2(raw)); + } + } +} diff --git a/drivers/soc/apple/sep.rs b/drivers/soc/apple/sep.rs index 5fd41c5bebf60..10fe16a9b79d4 100644 --- a/drivers/soc/apple/sep.rs +++ b/drivers/soc/apple/sep.rs @@ -857,6 +857,18 @@ impl SepData { let xart_start = module_parameters::xart_start_sector.value(); let xart_guard = xart_guard::level(module_parameters::xart_guard.value(), profile.xart_guard); + // The qualified 26.6.2 lane uses only the boot-forwarded identity. + // Check before opening the shared xART store writable. + if profile::j613_26_6_2() && xart_writes { + if dt::preboot_uuid().is_none() + || module_parameters::os_uuid_hi.value() != 0 + || module_parameters::os_uuid_lo.value() != 0 + || xart_guard.is_none() + { + dev_err!(dev, "J613 26.6.2 requires a DT identity and xART guard\n"); + return Err(EINVAL); + } + } let store = match xart_store::Store::open_owner(xart_writes, xart_start) { Ok(mut store) => { match xart_guard { @@ -897,6 +909,10 @@ impl SepData { let mut host_store = match store::Store::open() { Ok(store) => Some(store), Err(e) => { + if profile::j613_26_6_2() { + dev_err!(dev, "J613 26.6.2 host-state store unavailable: {:?}\n", e); + return Err(e); + } dev_warn!(dev, "Linux host-state store unavailable: {:?}\n", e); None } @@ -904,6 +920,7 @@ impl SepData { let bio_index = match host_store.as_mut().map(bio::IdentityIndex::load) { Some(Ok(index)) => index, + Some(Err(e)) if profile::j613_26_6_2() => return Err(e), Some(Err(_)) => bio::IdentityIndex::new(), None => bio::IdentityIndex::new(), }; @@ -2668,12 +2685,16 @@ impl platform::Driver for SepDriver { data.attach(&sep_node)?; - if let Err(e) = data.register_fv_kernel() { - dev_err!(dev, "could not register the FileVault kernel API: {:?}\n", e); + if module_parameters::filevault_api.value() != 0 { + if let Err(e) = data.register_fv_kernel() { + dev_err!(dev, "could not register the FileVault kernel API: {:?}\n", e); + } } - if let Err(e) = trusted::register(data.clone()) { - dev_warn!(data.dev, "trusted-keys: registration failed ({:?})\n", e); + if module_parameters::trusted_keys.value() != 0 { + if let Err(e) = trusted::register(data.clone()) { + dev_warn!(data.dev, "trusted-keys: registration failed ({:?})\n", e); + } } if let Err(e) = pm::register(data.clone()) { @@ -2740,6 +2761,14 @@ module! { description: "Apple SEP coprocessor: warm attach, endpoint discovery and tag-correlated control endpoint", license: "Dual MIT/GPL", params: { + trusted_keys: u8 { + default: 1, + description: "Register the SEP trusted-key source; 0 leaves it disabled", + }, + filevault_api: u8 { + default: 1, + description: "Register the SEP FileVault kernel API; 0 leaves it disabled", + }, xart_writes: u8 { default: 1, description: "Allow shared xART writes only if APFS proves a single unsnapshotted .gl extent; set to 0 for read-only operation", From b40f6bb15e73589d7b68deddcdb671638a2b224d Mon Sep 17 00:00:00 2001 From: Satya Benson Date: Tue, 3 Nov 2026 13:23:57 -0500 Subject: [PATCH 05/10] soc: apple: sep: expose passive selected-policy diagnostics Signed-off-by: Satya Benson --- .../ABI/testing/sysfs-driver-apple-sep | 29 ++++++- drivers/soc/apple/diag.rs | 44 ++++++++++ drivers/soc/apple/diag_shim.c | 58 ++++++++++++- drivers/soc/apple/sep.rs | 14 +++- drivers/soc/apple/shim.h | 4 + tools/aurora-sep/tests/test_diag_policy.py | 82 +++++++++++++++++++ 6 files changed, 226 insertions(+), 5 deletions(-) create mode 100644 tools/aurora-sep/tests/test_diag_policy.py diff --git a/Documentation/ABI/testing/sysfs-driver-apple-sep b/Documentation/ABI/testing/sysfs-driver-apple-sep index 5ee6cd0df614d..634f67162bc5c 100644 --- a/Documentation/ABI/testing/sysfs-driver-apple-sep +++ b/Documentation/ABI/testing/sysfs-driver-apple-sep @@ -3,9 +3,10 @@ Date: October 2026 KernelVersion: 7.1 Contact: Chris Kearney Description: - (RO) Version of the diag interface: "2" for the attributes + (RO) Version of the diag interface: "3" for the attributes and values described in this file. Version 2 added - sensor_attach and restore; version 1 had neither. + sensor_attach and restore; version 1 had neither. Version 3 + adds the policy snapshot. Within a version, attributes are only added, and an existing attribute keeps its values and their meaning. A reader tests @@ -19,7 +20,8 @@ Description: bring-up got without reading the kernel log. It exists while the driver is bound to the SEP: it is created at probe and removed at unbind. Every attribute in it is read-only and - reads as one ASCII token followed by a newline. + reads as ASCII followed by a newline. The policy snapshot + contains space-separated key=value tokens. Each value is the state the driver recorded when it made the corresponding decision. Reading an attribute sends nothing @@ -379,3 +381,24 @@ Description: tools/aurora-sep/XART-SAFETY.md. During a boot the value can only move from "unverified" to "blocked", once the guard refuses a write. + +What: /sys/bus/platform/devices/.sep/diag/policy +Date: November 2026 +KernelVersion: 7.2 +Contact: Satya Benson +Description: + (RO, ABI 3) A space-separated key=value snapshot of the + driver policy selected at probe: xart_writes (effective store + writability), provision_keybag, xart_guard (effective level), + trusted_keys, filevault_api, xarm_trace, refkey_v2 and + identity_override (0 or 1). API flags describe the selected + policy, not successful registration if that API later fails. + + identity_source is pending before the driver selects it, dt + for /chosen/apfs-preboot-uuid, override for explicit parameters, + or missing. It is updated at the actual identity decision. + No UUID value, state, identity or key bytes are exposed. + Reads only use cached facts and never contact the enclave. + Module-parameter sysfs files need not exist; their absence + is not evidence of a mismatched policy. Older diagnostic + ABIs retain xart/xart_guard and kernel decision messages. diff --git a/drivers/soc/apple/diag.rs b/drivers/soc/apple/diag.rs index 1b6fdd2288c52..b05b7b90c6e25 100644 --- a/drivers/soc/apple/diag.rs +++ b/drivers/soc/apple/diag.rs @@ -30,6 +30,15 @@ extern "C" { sepos13: bool, xart: bool, ) -> c_int; + fn sep_diag_set_policy( + provision_keybag: bool, + trusted_keys: bool, + filevault_api: bool, + xarm_trace: bool, + refkey_v2: bool, + identity_override: bool, + ); + fn sep_diag_set_identity_source(source: c_uint); fn sep_diag_set_attach(attached: bool); fn sep_diag_set_endpoints(count: c_uint); fn sep_diag_set_keystore_open(); @@ -66,6 +75,28 @@ pub(crate) enum Restore { Unproven = 3, } +/// Boot-time policy, without identity values or key material. +pub(crate) struct Policy { + pub provision_keybag: bool, + pub trusted_keys: bool, + pub filevault_api: bool, + pub xarm_trace: bool, + pub refkey_v2: bool, + pub identity_override: bool, +} + +/// The source selected by prepare_os_uuid, not the UUID itself. +pub(crate) enum IdentitySource { + DeviceTree = 1, + Override = 2, + Missing = 3, +} + +pub(crate) fn identity_source(source: IdentitySource) { + // SAFETY: this only stores the source enum in the passive cache. + unsafe { sep_diag_set_identity_source(source as c_uint) }; +} + /// Adds the `diag` group to the SEP device, with the facts fixed at probe. The /// group goes away when the driver unbinds. /// @@ -76,6 +107,7 @@ pub(crate) fn register( profile: &profile::PlatformProfile, xart_writable: bool, guard: Option<(u8, bool)>, + policy: Policy, ) -> Result { // Exhaustive, so a new bootstrap or key store fails to build here until // the ABI has a value for it. @@ -97,6 +129,18 @@ pub(crate) fn register( // SAFETY: no preconditions; stores three words. unsafe { sep_diag_set_xart_guard(c_uint::from(level), list_loaded, matching() as c_int) }; + // SAFETY: the facts are fixed before the attribute group is published. + unsafe { + sep_diag_set_policy( + policy.provision_keybag, + policy.trusted_keys, + policy.filevault_api, + policy.xarm_trace, + policy.refkey_v2, + policy.identity_override, + ) + }; + // SAFETY: `dev` is bound for the duration of the call, which is what devres // needs. `profile.name` is a `str`, valid for reads of `profile.name.len()` // bytes; the shim copies at most that many, needs no NUL terminator, and diff --git a/drivers/soc/apple/diag_shim.c b/drivers/soc/apple/diag_shim.c index 5c54d5d8410b3..9b8773b011260 100644 --- a/drivers/soc/apple/diag_shim.c +++ b/drivers/soc/apple/diag_shim.c @@ -23,7 +23,7 @@ #include "shim.h" /* The contract version; see Documentation/ABI/testing/sysfs-driver-apple-sep. */ -#define SEP_DIAG_ABI 2 +#define SEP_DIAG_ABI 3 #define SEP_DIAG_ATTACH_PENDING 0 #define SEP_DIAG_ATTACH_ATTACHED 1 @@ -64,6 +64,13 @@ static struct { bool cold; bool sepos13; bool xart; + bool provision_keybag; + bool trusted_keys; + bool filevault_api; + bool xarm_trace; + bool refkey_v2; + bool identity_override; + atomic_t identity_source; /* The xART guard's effective level (0: none runs), and its list. */ unsigned int xart_guard; bool xart_guard_list; @@ -120,6 +127,54 @@ static ssize_t xart_show(struct device *dev, struct device_attribute *attr, } static DEVICE_ATTR_RO(xart); +/* Parameter policy is captured at probe; the identity choice is cached later. */ +static ssize_t policy_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + const char *identity; + + switch (atomic_read(&sep_diag.identity_source)) { + case 1: + identity = "dt"; + break; + case 2: + identity = "override"; + break; + case 3: + identity = "missing"; + break; + default: + identity = "pending"; + break; + } + return sysfs_emit(buf, + "xart_writes=%u provision_keybag=%u xart_guard=%u " + "trusted_keys=%u filevault_api=%u xarm_trace=%u " + "refkey_v2=%u identity_override=%u identity_source=%s\n", + sep_diag.xart, sep_diag.provision_keybag, sep_diag.xart_guard, + sep_diag.trusted_keys, sep_diag.filevault_api, sep_diag.xarm_trace, + sep_diag.refkey_v2, sep_diag.identity_override, identity); +} +static DEVICE_ATTR_RO(policy); + +void sep_diag_set_policy(bool provision_keybag, bool trusted_keys, + bool filevault_api, bool xarm_trace, bool refkey_v2, + bool identity_override) +{ + sep_diag.provision_keybag = provision_keybag; + sep_diag.trusted_keys = trusted_keys; + sep_diag.filevault_api = filevault_api; + sep_diag.xarm_trace = xarm_trace; + sep_diag.refkey_v2 = refkey_v2; + sep_diag.identity_override = identity_override; + atomic_set(&sep_diag.identity_source, 0); +} + +void sep_diag_set_identity_source(unsigned int source) +{ + atomic_set(&sep_diag.identity_source, source); +} + static ssize_t attach_show(struct device *dev, struct device_attribute *attr, char *buf) { @@ -346,6 +401,7 @@ static struct attribute *sep_diag_attrs[] = { &dev_attr_boot.attr, &dev_attr_protocol.attr, &dev_attr_xart.attr, + &dev_attr_policy.attr, &dev_attr_attach.attr, &dev_attr_endpoints.attr, &dev_attr_keystore.attr, diff --git a/drivers/soc/apple/sep.rs b/drivers/soc/apple/sep.rs index 10fe16a9b79d4..e0f9f9a766e2f 100644 --- a/drivers/soc/apple/sep.rs +++ b/drivers/soc/apple/sep.rs @@ -1568,14 +1568,17 @@ impl SepData { match os_identity::select(hi, lo, dt::preboot_uuid) { os_identity::Selection::Explicit(uuid) => { self.xarm.lock().os_uuid = Some(uuid); + diag::identity_source(diag::IdentitySource::Override); dev_info!(self.dev, "xART: using explicit OS identity\n"); } os_identity::Selection::Chosen(uuid) => { self.xarm.lock().os_uuid = Some(uuid); + diag::identity_source(diag::IdentitySource::DeviceTree); dev_info!(self.dev, "xART: using /chosen/apfs-preboot-uuid\n"); } os_identity::Selection::Unavailable => { self.xarm.lock().os_uuid = None; + diag::identity_source(diag::IdentitySource::Missing); dev_warn!(self.dev, "xART: /chosen/apfs-preboot-uuid is unavailable\n"); } } @@ -2677,7 +2680,16 @@ impl platform::Driver for SepDriver { // As soon as the fixed facts are known, so a tool can watch the attach // from "pending" on. Diagnostics are not worth failing the probe for. let guard = data.with_store(|store| store.guard_state()).flatten(); - if let Err(e) = diag::register(dev, data.profile, data.xart_writable(), guard) { + let policy = diag::Policy { + provision_keybag: module_parameters::provision_keybag.value() != 0, + trusted_keys: module_parameters::trusted_keys.value() != 0, + filevault_api: module_parameters::filevault_api.value() != 0, + xarm_trace: module_parameters::xarm_trace.value() != 0, + refkey_v2: module_parameters::refkey_v2.value() != 0, + identity_override: module_parameters::os_uuid_hi.value() != 0 + || module_parameters::os_uuid_lo.value() != 0, + }; + if let Err(e) = diag::register(dev, data.profile, data.xart_writable(), guard, policy) { dev_warn!(dev, "diag: could not add the sysfs attributes ({:?})\n", e); } diff --git a/drivers/soc/apple/shim.h b/drivers/soc/apple/shim.h index ca0d3d8a797ee..e478deaddd0e7 100644 --- a/drivers/soc/apple/shim.h +++ b/drivers/soc/apple/shim.h @@ -179,6 +179,10 @@ struct device; int sep_diag_register(struct device *dev, const char *profile, size_t profile_len, bool cold, bool sepos13, bool xart); +void sep_diag_set_policy(bool provision_keybag, bool trusted_keys, + bool filevault_api, bool xarm_trace, bool refkey_v2, + bool identity_override); +void sep_diag_set_identity_source(unsigned int source); void sep_diag_set_attach(bool attached); void sep_diag_set_endpoints(unsigned int count); void sep_diag_set_keystore_open(void); diff --git a/tools/aurora-sep/tests/test_diag_policy.py b/tools/aurora-sep/tests/test_diag_policy.py new file mode 100644 index 0000000000000..be4b93f55c2bf --- /dev/null +++ b/tools/aurora-sep/tests/test_diag_policy.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Compile the real passive policy cache/show bodies against synthetic flags.""" +from pathlib import Path +import re +import shutil +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[3] +SOURCE = ROOT / 'drivers/soc/apple/diag_shim.c' + + +def function(text, name): + match = re.search(r'^(?:static ssize_t|void) ' + name + r'\(', text, re.M) + if not match: + raise AssertionError(name) + start = text.index('{', match.start()) + depth = 1 + at = start + 1 + while depth: + depth += (text[at] == '{') - (text[at] == '}') + at += 1 + return text[match.start():at] + + +class Tests(unittest.TestCase): + def test_selected_policy_and_identity_source_only(self): + compiler = shutil.which('cc') + self.assertIsNotNone(compiler, 'C compiler required; do not count a skip as a pass') + text = SOURCE.read_text() + state = re.search(r'static struct \{.*?\} sep_diag;', text, re.S).group(0) + body = '\n'.join(function(text, name) for name in + ('policy_show', 'sep_diag_set_policy', 'sep_diag_set_identity_source')) + prefix = '''#include +#include +#include +#include +typedef int atomic_t; +struct device { int unused; }; +struct device_attribute { int unused; }; +#define atomic_read(p) (*(p)) +#define atomic_set(p, v) (*(p) = (v)) +static int sysfs_emit(char *out, const char *fmt, ...) __attribute__((format(printf, 2, 3))); +static int sysfs_emit(char *out, const char *fmt, ...) { + va_list args; va_start(args, fmt); int n = vsnprintf(out, 4096, fmt, args); va_end(args); return n; +} +''' + main = '''int main(void) { + char out[4096]; sep_diag.xart = true; sep_diag.xart_guard = 1; + sep_diag_set_policy(false, false, false, true, false, false); + for (unsigned int i = 0; i < 4; i++) { + sep_diag_set_identity_source(i); policy_show(NULL, NULL, out); fputs(out, stdout); + } + sep_diag_set_policy(true, true, true, false, true, true); + sep_diag.xart = false; sep_diag.xart_guard = 0; + policy_show(NULL, NULL, out); fputs(out, stdout); return 0; +} +''' + with tempfile.TemporaryDirectory() as tmp: + source, program = Path(tmp) / 'policy.c', Path(tmp) / 'policy' + source.write_text(prefix + state + body + main) + subprocess.run([compiler, '-std=gnu11', '-Wall', '-Wextra', '-Werror', + '-Wno-unused-parameter', str(source), '-o', str(program)], check=True) + lines = subprocess.check_output([str(program)], text=True).splitlines() + expected = dict(xart_writes='1', provision_keybag='0', xart_guard='1', + trusted_keys='0', filevault_api='0', xarm_trace='1', + refkey_v2='0', identity_override='0') + for line, identity in zip(lines[:4], ('pending', 'dt', 'override', 'missing')): + self.assertEqual(dict(word.split('=') for word in line.split()), + {**expected, 'identity_source': identity}) + self.assertEqual(dict(word.split('=') for word in lines[4].split()), + dict(xart_writes='0', provision_keybag='1', xart_guard='0', + trusted_keys='1', filevault_api='1', xarm_trace='0', + refkey_v2='1', identity_override='1', identity_source='pending')) + self.assertNotIn('os_uuid_hi=', '\n'.join(lines)) + self.assertNotIn('os_uuid_lo=', '\n'.join(lines)) + + +if __name__ == '__main__': + unittest.main(verbosity=2) From 4a3e51627f70feaed5ad32c05b6ced7b91463572 Mon Sep 17 00:00:00 2001 From: Satya Benson Date: Tue, 3 Nov 2026 13:54:50 -0500 Subject: [PATCH 06/10] soc: apple: sep: update sensor host tests for diagnostic ABI 3 Signed-off-by: Satya Benson --- tools/aurora-sep/tests/test_diag_sensor.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tools/aurora-sep/tests/test_diag_sensor.py b/tools/aurora-sep/tests/test_diag_sensor.py index 01c7a8c1d97ba..16623f1f3e358 100644 --- a/tools/aurora-sep/tests/test_diag_sensor.py +++ b/tools/aurora-sep/tests/test_diag_sensor.py @@ -160,7 +160,7 @@ sep_diag_set_xart_guard(0, false, SEP_DIAG_MATCHING_USABLE); sep_diag_register(&dev, "T8140/J700", 10, false, false, true); /* The guard's attributes were added within version 2. */ - expect("probe", "abi", "2"); + expect("probe", "abi", "3"); expect("probe", "sensor", "unbound"); expect("probe", "touchid", "unknown"); expect("probe", "xart_guard", "0"); @@ -295,14 +295,14 @@ def test_matching_follows_the_guard(self): usable, unverified, blocked = 1, 2, 3 # SEP_DIAG_MATCHING_* in shim.h cases = [ # level, list, matching at probe, refusals, matching after them - ((0, 0, usable, 0, 0), '2 0 none 0 usable'), # Neo, M1 Pro (pinned 0) - ((1, 1, unverified, 0, 0), '2 1 loaded 0 unverified'), # M2 under the guard - ((1, 1, unverified, 2, blocked), '2 1 loaded 2 blocked'), # ... after refusals - ((2, 1, unverified, 0, 0), '2 2 loaded 0 unverified'), - ((1, 0, blocked, 0, 0), '2 1 unavailable 0 blocked'), # no list - ((1, 1, blocked, 0, 0), '2 1 loaded 0 blocked'), # blocked at probe - ((0, 0, 0, 0, 0), '2 0 none 0 unknown'), # never set - ((0, 0, 7, 0, 0), '2 0 none 0 unknown'), # out of range + ((0, 0, usable, 0, 0), '3 0 none 0 usable'), # Neo, M1 Pro (pinned 0) + ((1, 1, unverified, 0, 0), '3 1 loaded 0 unverified'), # M2 under the guard + ((1, 1, unverified, 2, blocked), '3 1 loaded 2 blocked'), # ... after refusals + ((2, 1, unverified, 0, 0), '3 2 loaded 0 unverified'), + ((1, 0, blocked, 0, 0), '3 1 unavailable 0 blocked'), # no list + ((1, 1, blocked, 0, 0), '3 1 loaded 0 blocked'), # cold boot, level 1 + ((0, 0, 0, 0, 0), '3 0 none 0 unknown'), # never set + ((0, 0, 7, 0, 0), '3 0 none 0 unknown'), # out of range ] with tempfile.TemporaryDirectory() as directory: harness = self.build(Path(directory), GUARD_HARNESS, 'guard') From 02eeffd807a1f5787b4bb034fe94bfa9f718c40f Mon Sep 17 00:00:00 2001 From: Satya Benson Date: Wed, 4 Nov 2026 21:17:33 -0500 Subject: [PATCH 07/10] selftests: sep: include the read-only store error in the shim fixture Signed-off-by: Satya Benson --- tools/aurora-sep/tests/test_store_open_errors.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/aurora-sep/tests/test_store_open_errors.py b/tools/aurora-sep/tests/test_store_open_errors.py index 7625f1ddffea0..23d99e3e7b6d3 100644 --- a/tools/aurora-sep/tests/test_store_open_errors.py +++ b/tools/aurora-sep/tests/test_store_open_errors.py @@ -140,6 +140,7 @@ def test_native_rust_c_boundary(self): 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 EROFS: Error = Error(-30); 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> { @@ -148,7 +149,7 @@ def test_native_rust_c_boundary(self): } 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 use crate::error::{Error, Result, EIO, EROFS, ENODEV}; pub struct CStr(pub &'static [u8]); impl CStr { pub fn as_char_ptr(&self) -> *const c_char { self.0.as_ptr().cast() } } } From 3965338688b11aa16ae04bdf5e5b4ae11357103d Mon Sep 17 00:00:00 2001 From: Satya Benson Date: Wed, 4 Nov 2026 21:49:04 -0500 Subject: [PATCH 08/10] ci: run the carried SEP identity-preservation regressions Signed-off-by: Satya Benson --- .github/workflows/sep-source.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/sep-source.yml b/.github/workflows/sep-source.yml index e77ffd8ad7340..114f5c240434e 100644 --- a/.github/workflows/sep-source.yml +++ b/.github/workflows/sep-source.yml @@ -35,6 +35,16 @@ jobs: EOF cc -Wall -Werror -Iinclude/uapi /tmp/sep-abi.c -o /tmp/sep-abi /tmp/sep-abi + - name: Select the identity-test Rust toolchain + run: | + rustup toolchain install 1.85.0 --profile minimal --no-self-update + echo "RUSTUP_TOOLCHAIN=1.85.0" >> "$GITHUB_ENV" + - name: Test saved identity readers + run: | + python3 -m unittest tools/aurora-sep/tests/test_store_open_errors.py -v + python3 -O -m unittest tools/aurora-sep/tests/test_store_open_errors.py -v + python3 -m unittest tools/aurora-sep/tests/test_identity_preservation.py -v + python3 -O -m unittest tools/aurora-sep/tests/test_identity_preservation.py -v - name: Test recovery utility and package sources run: | python3 -m unittest tools/aurora-sep/tests/test_sep_bio_identities.py -v From a2dbf7ecde3015d447341b1761d78a8bb6ab44f5 Mon Sep 17 00:00:00 2001 From: Satya Benson Date: Wed, 4 Nov 2026 21:49:04 -0500 Subject: [PATCH 09/10] ci: run the J613 selected-policy diagnostic regressions Signed-off-by: Satya Benson --- .github/workflows/sep-source.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/sep-source.yml b/.github/workflows/sep-source.yml index 114f5c240434e..fe24b285eb6b5 100644 --- a/.github/workflows/sep-source.yml +++ b/.github/workflows/sep-source.yml @@ -45,6 +45,10 @@ jobs: python3 -O -m unittest tools/aurora-sep/tests/test_store_open_errors.py -v python3 -m unittest tools/aurora-sep/tests/test_identity_preservation.py -v python3 -O -m unittest tools/aurora-sep/tests/test_identity_preservation.py -v + - name: Test selected-policy diagnostics + run: | + python3 -m unittest tools/aurora-sep/tests/test_diag_policy.py tools/aurora-sep/tests/test_diag_sensor.py -v + python3 -O -m unittest tools/aurora-sep/tests/test_diag_policy.py tools/aurora-sep/tests/test_diag_sensor.py -v - name: Test recovery utility and package sources run: | python3 -m unittest tools/aurora-sep/tests/test_sep_bio_identities.py -v From 4f5ecf698dcb9c36da8a9050e8955c119040dd35 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 10 Nov 2026 15:08:46 +1100 Subject: [PATCH 10/10] tools/aurora-sep: tests: take the refkey ACL argument in the identity fixture refkey.rs's sks_create_refkey() now takes an optional ACL (ed3d64765952, "sep-key: Touch ID keys the enclave guards"). The fixture in test_identity_preservation.py still declared the two-argument form, so rustc rejected the machine-refkey case. Declare the third argument. Assisted-by: LLM Signed-off-by: Ryan Murray --- tools/aurora-sep/tests/test_identity_preservation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/aurora-sep/tests/test_identity_preservation.py b/tools/aurora-sep/tests/test_identity_preservation.py index 3f15f10c7d191..f420ec74b167f 100644 --- a/tools/aurora-sep/tests/test_identity_preservation.py +++ b/tools/aurora-sep/tests/test_identity_preservation.py @@ -69,7 +69,7 @@ def body(source, signature): 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 sks_create_refkey(&self,_:u32,_:&[u8],_:Option<&[u8]>)->Option>{self.creates.set(self.creates.get()+1);None} fn cache_machine_refkey(&self,_:KVec)->bool{true} METHOD }