diff --git a/.github/workflows/sep-source.yml b/.github/workflows/sep-source.yml index 51ce8828d55ab..27cffa4ae1db5 100644 --- a/.github/workflows/sep-source.yml +++ b/.github/workflows/sep-source.yml @@ -35,6 +35,20 @@ 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 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 diff --git a/Documentation/ABI/testing/sysfs-driver-apple-sep b/Documentation/ABI/testing/sysfs-driver-apple-sep index 2bddd85d523d4..e81af09585c76 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 @@ -30,6 +32,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 @@ -360,6 +382,27 @@ Description: 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. + What: /sys/bus/platform/devices/.sep/diag/attach_ms What: /sys/bus/platform/devices/.sep/diag/ready_ms Date: November 2026 diff --git a/drivers/soc/apple/bio.rs b/drivers/soc/apple/bio.rs index 45831d5b3d930..3a5ba0217c9db 100644 --- a/drivers/soc/apple/bio.rs +++ b/drivers/soc/apple/bio.rs @@ -390,22 +390,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 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/diag.rs b/drivers/soc/apple/diag.rs index c1eae7458e64d..77a31f2e00ffb 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(); @@ -84,6 +93,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. /// @@ -94,6 +125,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. @@ -115,6 +147,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 fe405306d7969..38ffecc753d95 100644 --- a/drivers/soc/apple/diag_shim.c +++ b/drivers/soc/apple/diag_shim.c @@ -27,7 +27,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 @@ -71,6 +71,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; @@ -147,6 +154,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) { @@ -516,6 +571,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/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/profile.rs b/drivers/soc/apple/profile.rs index 5accaf70cb404..005a1265eb443 100644 --- a/drivers/soc/apple/profile.rs +++ b/drivers/soc/apple/profile.rs @@ -860,3 +860,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/refkey.rs b/drivers/soc/apple/refkey.rs index a8b01c4346e97..6f9178d802ed3 100644 --- a/drivers/soc/apple/refkey.rs +++ b/drivers/soc/apple/refkey.rs @@ -185,7 +185,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 32009e6d624bb..e161c1a334abd 100644 --- a/drivers/soc/apple/sbio.rs +++ b/drivers/soc/apple/sbio.rs @@ -1251,7 +1251,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; } @@ -1390,9 +1390,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; @@ -1528,8 +1528,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; @@ -1646,8 +1648,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; @@ -1673,17 +1677,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), } } @@ -1737,7 +1750,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; @@ -1751,7 +1764,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; } @@ -1790,7 +1803,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; } @@ -1846,7 +1859,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; } @@ -5551,7 +5564,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(); @@ -5614,13 +5627,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()) { @@ -5647,7 +5664,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); } @@ -6061,7 +6078,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" @@ -6069,7 +6086,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()); @@ -6081,7 +6098,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); @@ -6168,7 +6185,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/sep.rs b/drivers/soc/apple/sep.rs index a2bd2d5dfb4c7..386e32eb5f2a9 100644 --- a/drivers/soc/apple/sep.rs +++ b/drivers/soc/apple/sep.rs @@ -971,6 +971,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 { @@ -1011,6 +1023,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 } @@ -1018,6 +1034,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(), }; @@ -1694,16 +1711,19 @@ 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); diag::os_identity(diag::OsIdentity::Explicit); 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); diag::os_identity(diag::OsIdentity::Chosen); 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); diag::os_identity(diag::OsIdentity::Unavailable); dev_warn!(self.dev, "xART: /chosen/apfs-preboot-uuid is unavailable\n"); } @@ -3017,7 +3037,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); } @@ -3025,12 +3054,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()) { @@ -3097,6 +3130,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", diff --git a/drivers/soc/apple/shim.h b/drivers/soc/apple/shim.h index d58ae573fbbad..63713828e7f66 100644 --- a/drivers/soc/apple/shim.h +++ b/drivers/soc/apple/shim.h @@ -189,6 +189,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/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_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) 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') 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..f420ec74b167f --- /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<&[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 0000000000000..23d99e3e7b6d3 --- /dev/null +++ b/tools/aurora-sep/tests/test_store_open_errors.py @@ -0,0 +1,194 @@ +# 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 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> { + 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, EROFS, 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()