From e6934f48180ad8bcd203a0652582faa2c6ce9837 Mon Sep 17 00:00:00 2001 From: Claudius Date: Mon, 9 Nov 2026 21:22:41 +0000 Subject: [PATCH 1/4] ios-27: the key store and data protection (docs/ios27/KEYSTORE.md), the on-phone test dptest, fork 5eac319 The fork's wip/ios27-keystore (on the built-in boot arguments): protection classes on the built-in HFS as labels (sweetspeed&0x100000, &0x200000 for MNT_CPROTECT) and the key store's answers for a device with no passcode (sweetspeed&0x400000). Built, not booted yet. --- docs/ios27-starlte.md | 1 + docs/ios27/KEYSTORE.md | 163 +++++++ tools/ios27/keystore/build.sh | 11 + .../keystore/com.scion.dptest.mobile.plist | 34 ++ tools/ios27/keystore/com.scion.dptest.plist | 31 ++ tools/ios27/keystore/dptest.entitlements | 12 + tools/ios27/keystore/dptest.m | 410 ++++++++++++++++++ tools/ios27/keystore/install-dptest.sh | 34 ++ tools/ios27/keystore/uninstall-dptest.sh | 15 + 9 files changed, 711 insertions(+) create mode 100644 docs/ios27/KEYSTORE.md create mode 100755 tools/ios27/keystore/build.sh create mode 100644 tools/ios27/keystore/com.scion.dptest.mobile.plist create mode 100644 tools/ios27/keystore/com.scion.dptest.plist create mode 100644 tools/ios27/keystore/dptest.entitlements create mode 100644 tools/ios27/keystore/dptest.m create mode 100755 tools/ios27/keystore/install-dptest.sh create mode 100755 tools/ios27/keystore/uninstall-dptest.sh diff --git a/docs/ios27-starlte.md b/docs/ios27-starlte.md index 6951db883..e1c748fca 100644 --- a/docs/ios27-starlte.md +++ b/docs/ios27-starlte.md @@ -31,6 +31,7 @@ builder's own, from their own IPSW; nothing of Apple's is in this repository. | `tools/ios27/` | build, packing and test tools (`build-xnu-ios.sh`, which `OS_PROFILE=ios-27 make kernel` runs, with `dxb-common.sh`, `dxb.pin`, `dxb-ios.patch` and `firehose-ios.py`; `build-xnu-12377.sh` and `-ios.sh`, `mkpayload-ios26.sh`, `make-xnu-boot.sh`, the ADT makers, `a64e/`, `gpu/`, `mesa-ios/`, `ca-gpu/`) and wardevsa's `herolte/` packing tools | | `tools/ios27/metal/` | Starbite's Metal stack (Metal 2 frontend, metal2vulkan, PanVK) rebuilt for iOS 27: build scripts, the root installer, `mtl27test`, `sbmtlc`; `docs/ios27/METAL.md` | | `tools/ios27/speed/` | the boot-time and trap-cost work's root tools (the cache's PACGA and LDRA rewrite, the daemon list, the Settings patch, the kbd and QuartzCore patches of `docs/ios27/NIGHT.md`, `bench.py`) and `tools/ios27/cmdline/starlte-fast.txt`, its command line; `docs/ios27/SPEED.md` | +| `tools/ios27/keystore/` | `dptest`, the on-phone test of the key store's answers, protection classes and the keychain, with its launchd jobs and installer; `docs/ios27/KEYSTORE.md` | | `tools/ios27/lse/` | the shared cache's LSE atomics rewritten into LL/SC thunks for the M3s (`lse-rewrite.py`, the root scripts, the differential test); `docs/ios27/EIGHT-CORES.md` | | `userland/src/sbca.c` | CoreAnimation's renderer through OpenGL ES on the Mali-G72 (Mesa Panfrost behind HakiOS's EAGL bridge), with iOS 27.0.1's addresses; the bridge's iOS 27 table is `tools/ios27/ca-gpu/hakios/` | | `docs/ios27/` | `IOS27-TARGET.md` (the iPhoneOS-target build and what it changes), `PARITY.md` (against Scion's 16.5 kernel), `EIGHT-CORES.md` (the eight cores: the LSE rewrite, the soft pins), `SPEED.md` (boot time and trap cost: what was measured, `sweetspeed`, the root edits), `OPTIMISE.md` (the sampling profiler's cost table and the changes made against it: FP16 forms, recommended cores, Panfrost's batch renaming, the DRM stand-in's answers, the surface hash; `tools/ios27/perf/`), `NIGHT.md` (what killed the daemons at boot, the 10 Hz, async GPU submits, the panel's brightness, Metal in CoreAnimation tried), `GPU-PATH.md` (what the GPU track found), `METAL.md` (Metal on the Mali), and the author's pages on the loader's USB console and fastboot, the microSD root and the recovery menu | diff --git a/docs/ios27/KEYSTORE.md b/docs/ios27/KEYSTORE.md new file mode 100644 index 000000000..859687f5e --- /dev/null +++ b/docs/ios27/KEYSTORE.md @@ -0,0 +1,163 @@ +# iOS 27 on the Galaxy S9: the key store and data protection (9 November 2026) + +What iOS 27.0.1 (24A446) asks of AppleKeyStore and of the data volume's protection classes, what this kernel answers, and what it does +not do. The kernel side is two files of the fork's ios-27 layer: `iokit/Kernel/arm/SweetKeyStore.cpp` (the stand-in for Apple's +AppleKeyStore) and `bsd/sweet_hfs/sweet_hfs_pclass.c` (protection classes on the built-in HFS Plus). The changes of this page are behind bits +of the boot-argument `sweetspeed=` and are **off by default**: + +| Bit | Change | Where | +|---|---|---| +| `0x100000` (1048576) | the built-in HFS Plus keeps, reports and inherits data-protection classes, as labels; what has no class reads back as D | `bsd/sweet_hfs/sweet_hfs_pclass.c`, hooks in `core/hfs_vnops.c`, `hfs_attrlist.c`, `hfs_readwrite.c` | +| `0x200000` (2097152) | with the bit above: the mount says `MNT_CPROTECT` (what `MKBDeviceFormattedForContentProtection()` and SQLite read), and an update mount keeps it | `core/hfs_vfsutils.c`, `bsd/vfs/vfs_syscalls.c` | +| `0x400000` (4194304) | the key store answers as a device with **no passcode** does, in full (below) | `iokit/Kernel/arm/SweetKeyStore.cpp` | + +**In short: nothing here encrypts anything.** There is no Secure Enclave, no passcode, no key that lives outside the kernel. A protection +class is a label that programs can set and read back; a file marked A is as readable from the flash, and while the screen is locked, as one +marked D. The keychain's "wrapped" keys are the keys behind an 8-byte tag (see the head of `SweetKeyStore.cpp`). Scion's iOS 16.5 kernel has a +real key bag for the keychain (`docs/keystore.md`, `docs/passcode.md`); it is not on this tree yet, and the last section says what bringing it +over takes. + +## What was wrong + +Found on a Galaxy S9 running 24A446 (the kernel's own log of key store calls, and the programs' logs), then read from the binaries: + +- **The lock state said "a passcode is set, and the device is unlocked".** MobileKeyBag's `MKBGetDeviceLockState` returns the `sls` value of + the device state (selector 17 or 35); ManagedConfiguration's "is a passcode set" is `MKBGetDeviceLockState() != 3`. The stand-in answered + `sls` 0 and state flags 4 (unlocked since boot). A device with no passcode answers `sls` 3 ("disabled") and flags 6 (no passcode, unlocked + since boot). With 0, SpringBoard asked the key store to lock at every screen-off (selectors 8 and 32, which failed, 199 and 38 times in one + day's log), and Settings and `profiled` took the passcode as set. +- **The configuration was unanswered.** `profiled` asked for it (selector 31, `MKBGetDeviceConfigurations`) 267 times in the same log, the + passcode settings 16 times, each answered "unsupported". +- **Files had no protection class.** `fcntl(F_GETPROTECTIONCLASS)` on the HFS Plus root answered `ENOTSUP`, and setting a class was accepted + and forgotten. InstallCoordination's `-[IXGlobalConfiguration createDirectories]` makes its data directory with class D + (`-[IXFileManager createDirectoryAtURL:withIntermediateDirectories:mode:class:error:]`: `open`, `fcntl(F_GETPROTECTIONCLASS)`, + `fcntl(F_SETPROTECTIONCLASS)`), and on any error logs "Failed to create data directory" and ends the process with `os_crash`: so + `installcoordinationd` could not start. PosterBoard calls its data store unusable when a directory it has just marked + `NSFileProtectionNone` does not read back as None (`NSURLFileProtectionKey`, through `getattrlist(ATTR_CMN_DATA_PROTECT_FLAGS)`). + +The selector numbers were not the problem: every `aks_*` function of 24A446 still calls the number it called on iOS 16 (table below); iOS 27 +only adds to the list. + +**What is verified, and how.** The kernel with these changes was built (the iOS-target STARLTE kernel) and **has not been booted yet**; the test below has not run. "What was wrong" is from a phone's logs and from the binaries; what the bits change is, until a phone run, what the code says. + +## What the key store answers + +`AppleKeyStore` is published under `IOResources` with the user client `AppleKeyStoreUserClient`, type 0; libaks opens it and calls selector 0 +once. Scalars in and out are 64 bit; passcodes and their lengths travel as scalars (an address in the caller and a length), larger answers in the +output structure. The device state and the configuration are DER: a SET of SEQUENCE { UTF8String key, INTEGER value }. + +| Selector | With `sweetspeed&0x400000` | Without it (as before) | +|---|---|---| +| 0 open, 4 unload bag, 5 set system, 9 unlock device, 12 unlock bag, 13 lock bag, 26/27 assertion hold/drop, 55 unload session bags, 101 assertion consume | success, nothing to do | the same | +| 7 lock state | flags 6: no passcode, unlocked since boot | flags 4: unlocked since boot | +| 8, 32 lock device | success, nothing to lock | unsupported | +| 10, 11 wrap, unwrap key | the key behind an 8-byte tag: **not protected at rest** | the same | +| 14 system bag | handle 0 (keybagd then loads no bag file) | the same | +| 17, 35 device state | `ss` 6, `sls` 3 (disabled), `sb` `sfa` `sgs` `sas` `sgpe` `srcd` 0 | `ss` 4, `sls` 0 | +| 23 bag UUID | 16 fixed bytes | unsupported | +| 30 set configuration | the grace period `gp` is kept for this boot (it applies to nothing) | unsupported | +| 31 configuration | `bd` 0, `gp`, `mua` 10 | unsupported | +| 40 generation | 0 | unsupported | +| 42 verify password | success: there is nothing to compare with | unsupported | +| 100 assertion promote, 115 prewarm, 160 enable cache flow | success | unsupported | +| 102, 103 "the first-unlock notification was posted" | remembered | accepted, and "not posted" for ever | +| 107 "SE support in restore mode is set" | 0 | unsupported | +| `IOConnectSetNotificationPort` (`aks_register_for_notifications`) | success; nothing is ever sent, the state cannot change | unsupported | +| everything else (57 remote peers, 135 beacon keys, 43 `aks_ref_key_*`, escrow and backup bags, the SE and memento calls, 15 change secret) | `kIOReturnUnsupported` | the same | + +So with the bit: SpringBoard and ManagedConfiguration see a device with no passcode; keychain items work as before and are not protected; +access-controlled keychain items (`SecAccessControl`, selector 43) cannot be made; **a passcode cannot be set** (selector 15 answers +unsupported, so keybagd's change fails and the state stays "no passcode"). + +## Protection classes on the data volume + +The root of an S9 is one HFS Plus volume on USERDATA, `/private/var` included. Apple's HFS keeps a file's class in its content-protection +attribute next to the file's wrapped key, on a volume formatted for it, and the storage driver encrypts with that key; this port builds HFS +without that (`CONFIG_PROTECT` off in `bsd/sweet_hfs/sweet_hfs_compat.h`): no key hierarchy for files, no inline encryption. With +`sweetspeed&0x100000` the bookkeeping half exists: + +- **Set:** `fcntl(F_SETPROTECTIONCLASS)`, `setattrlist(ATTR_CMN_DATA_PROTECT_FLAGS)`, `open_dprotected_np`, a create that names a class. + Files take A, B, C, D or CX; directories "none", A, B, C or D; F only at creation (as in Apple's HFS). Anything else is `EINVAL`; a vnode + that is neither a file nor a directory `EBADF`. +- **Kept:** in the extended attribute `com.apple.system.scion.pclass` (4 bytes: version 1, the class), a "system" attribute that the xattr + calls of user space neither list nor reach. A cnode remembers it once read. +- **Inherited:** a new file or directory takes the class its directory was given, as on iOS; one extra attribute write per create in such a + directory. +- **Read:** `fcntl(F_GETPROTECTIONCLASS)`, `getattrlist`, `getattrlistbulk` (for items in core), `NSURLFileProtectionKey`. What was never + given a class reads back as **D** (`NSFileProtectionNone`), which is the truth here. `F_GETPROTECTIONLEVEL` answers 5, + `F_GETDEFAULTPROTLEVEL` D. +- `sweetspeed&0x200000` adds `MNT_CPROTECT` to the mount's flags. That is a separate bit because it changes what user space does, not what + the kernel answers: `MKBDeviceFormattedForContentProtection()` is that flag on `/var`, and SQLite strips its file-protection open flags + without it ("volume does not support data protection"). + +An image of the volume attached on a Mac shows nothing of this (the attribute is hidden there too); a kernel without the bit ignores the +attributes and answers as before. + +## The user client as 24A446 calls it + +Read from `AppleKeyStore.framework` (libaks is a dylib of the shared cache on iOS 27), MobileKeyBag and keybagd: each `aks_*` function ends in +one `IOConnectCallMethod` with a constant selector. The numbers below are the ones this page needs; they are the numbers of iOS 16 (the +16.5 map is in `docs/keystore.md` and `docs/passcode.md`). + +| Selector | libaks function | Shape | +|---|---|---| +| 0 | (open) | none | +| 2 | `aks_create_bag` | 7 scalars in | +| 3 | `aks_save_bag` | handle in; the bag out, as a structure | +| 4 | `aks_unload_bag` | handle | +| 5 | `aks_set_system` (`_with_passcode`, `_with_opts`) | 5 scalars in | +| 6 | `aks_load_bag` | the bag in, as a structure; handle out | +| 7 | `aks_get_lock_state` | handle in; flags out | +| 8, 32 | `aks_lock_device` (32: now) | handle | +| 9 | `aks_unlock_device` | 4 scalars in: handle, the passcode's address, its length, an ACM handle | +| 10, 11 | `aks_wrap_key`, `aks_unwrap_key` | handle, class; key in; key out (10 also returns the class) | +| 12, 13 | `aks_unlock_bag`, `aks_lock_bag` | 5 scalars; handle | +| 14 | `aks_get_system` | handle out | +| 15 | `aks_change_secret` (`_opts`, `_with_kek`) | 12 scalars in: handle, two options, the old passcode's address and length, the new one's, an ACM blob's address and length, a parameter, the generation option, flags; 1 scalar out: the handle of the bag that now holds the new secret | +| 17, 35 | `aks_get_device_state`, `aks_get_extended_device_state` | handle in; DER out | +| 23 | `aks_get_bag_uuid` | 16 bytes out | +| 26, 27, 100, 101 | `aks_assert_hold`, `_drop`, `_promote`, `_consume` | 3, 2, 2, 1 scalars | +| 30, 31 | `aks_set_configuration`, `aks_get_configuration` | DER in; DER out (a 32 KB buffer) | +| 40 | `aks_generation` | 2 scalars in, 1 out | +| 42 | `aks_verify_password` | a packed list in, the passcode first | +| 43 | `aks_ref_key_*` (`_aks_operation`) | DER both ways | +| 55 | `aks_unload_session_bags` | 1 scalar | +| 57 | `aks_remote_peer_get_state` | | +| 102, 103 | (MobileKeyBag: the first-unlock notification was posted; was it) | 1 scalar out for 103 | +| 105 | `aks_change_secret_epilogue` | 4 scalars | +| 107 | `aks_se_support_in_rm_is_set` | 1 scalar out | +| 115 | `aks_prewarm_sps` | none | +| 135 | `aks_bak_get_beacon`, `aks_bak_get_swizzler` | 2 scalars; 16 KB out | +| 139 | `aks_memento_get_state` | 4 KB out | +| 160 | `aks_enable_cache_flow` | 1 scalar | + +Who was seen calling what on the phone (the kernel logs the first call of each selector with its caller): keybagd 0, 7, 14, 107; securityd +10, 11; SpringBoard 8, 26, 27, 32, 135; `profiled` and the passcode settings 31; containermanagerd, backboardd 17; powerd, sleepd, +cameracaptured, backboardd 35; sharingd 57. + +## What is not there, and what a passcode would take + +- **A passcode.** Settings changes it through `profiled` and keybagd's XPC service. keybagd's path for a device without a Secure Enclave + ("KBChangeSystemNonSeSecret" in its log) is: `aks_change_secret_opts` (15), which answers the handle of the bag that holds the new secret; + "KBSaveSystemBagHandle": `aks_save_bag` (3) on that handle and the answer written, with the opaque data that says what kind of passcode it + is, to `/private/var/keybags/systembag.kb`; `aks_set_system_with_opts` (5) when the handle is a new one; `aks_change_secret_epilogue` (105). + Before it, `aks_get_lock_state`, `aks_memento_get_state` (139), the configuration's user and group UUIDs, and MobileGestalt answers decide + between this path and the Secure Enclave ones. The lock screen then uses 9 (unlock), 8 and 32 (lock), 42 (verify), the state (7, 17, 35) + and IOKit's general-interest messages on the service. None of this is implemented on this tree: selector 15 answers "unsupported", so a + change fails and the device stays without a passcode. Scion's iOS 16.5 kernel has the state machine, the verifier, the delays after wrong + attempts and a real key bag for the keychain (`MiniAppleKeyStore.cpp`, `docs/passcode.md`, `docs/keystore.md`), behind its own daemon in + place of keybagd's service; the work here is to bring that core to this tree and to answer Apple's keybagd as above. It needs keybagd's own + log from a phone at each step. +- **Encryption of files.** Stage 4 of `docs/keystore.md` applies unchanged: a volume formatted for content protection, keys per file, and a + storage driver that encrypts. +- **Access-controlled keychain items and LocalAuthentication** (`aks_ref_key_*`, AppleCredentialManager): no stand-in. +- **Effaceable storage, escrow and backup bags, anything of the Secure Enclave**: no, and for the last no plan. + +## Testing + +`tools/ios27/keystore/` has a test that runs on the phone: `dptest`, a launchd job (as root and as `mobile`) that waits a minute after boot and +writes `/private/var/local/oslog/dptest.log` and `dptest-mobile.log`: the mount's flags; classes on new files and directories by every way of +setting and reading them, and after reopening; the key bag state through MobileKeyBag; the read-only selectors of the user client; a keychain +item added, read back and deleted for three accessibility classes; MobileGestalt's passcode answers; a system container path. `build.sh` +builds it with your Xcode; `install-dptest.sh MOUNTPOINT` puts it and its two jobs in an attached root image (and in the root's launchd job +cache), `uninstall-dptest.sh` takes them out. It calls no selector that changes the key store's state. diff --git a/tools/ios27/keystore/build.sh b/tools/ios27/keystore/build.sh new file mode 100755 index 000000000..797355a5e --- /dev/null +++ b/tools/ios27/keystore/build.sh @@ -0,0 +1,11 @@ +#!/bin/sh +# Build dptest (docs/ios27/KEYSTORE.md) for arm64e and arm64, ad hoc signed with its entitlements. Needs Xcode: DEVELOPER_DIR. +set -e +cd "$(dirname "$0")" +: "${DEVELOPER_DIR:?set DEVELOPER_DIR to your Xcode (its iPhoneOS SDK is used)}"; export DEVELOPER_DIR +for A in arm64e arm64; do + nice -n 10 xcrun --sdk iphoneos clang -arch $A -mios-version-min=16.0 -fobjc-arc -O1 -g -Wall -Wno-deprecated-declarations \ + -o dptest.$A dptest.m -framework Foundation -framework Security -framework IOKit -framework CoreFoundation + codesign -f -s - --entitlements dptest.entitlements dptest.$A +done +cp dptest.arm64 dptest # default installed binary = plain arm64 (what the other jobs here run) diff --git a/tools/ios27/keystore/com.scion.dptest.mobile.plist b/tools/ios27/keystore/com.scion.dptest.mobile.plist new file mode 100644 index 000000000..d1eab4bdc --- /dev/null +++ b/tools/ios27/keystore/com.scion.dptest.mobile.plist @@ -0,0 +1,34 @@ + + + + + GroupName + mobile + JetsamProperties + + JetsamMemoryLimit + 512 + + Label + com.scion.dptest.mobile + LaunchOnlyOnce + + POSIXSpawnType + Interactive + Program + /usr/local/bin/dptest + ProgramArguments + + /usr/local/bin/dptest + mobile + + RunAtLoad + + StandardErrorPath + /private/var/tmp/dptest-mobile.out + StandardOutPath + /private/var/tmp/dptest-mobile.out + UserName + mobile + + diff --git a/tools/ios27/keystore/com.scion.dptest.plist b/tools/ios27/keystore/com.scion.dptest.plist new file mode 100644 index 000000000..ccc6a244d --- /dev/null +++ b/tools/ios27/keystore/com.scion.dptest.plist @@ -0,0 +1,31 @@ + + + + + JetsamProperties + + JetsamMemoryLimit + 512 + + Label + com.scion.dptest + LaunchOnlyOnce + + POSIXSpawnType + Interactive + Program + /usr/local/bin/dptest + ProgramArguments + + /usr/local/bin/dptest + + RunAtLoad + + StandardErrorPath + /private/var/tmp/dptest.out + StandardOutPath + /private/var/tmp/dptest.out + UserName + root + + diff --git a/tools/ios27/keystore/dptest.entitlements b/tools/ios27/keystore/dptest.entitlements new file mode 100644 index 000000000..f3a36adda --- /dev/null +++ b/tools/ios27/keystore/dptest.entitlements @@ -0,0 +1,12 @@ + + + + + application-identifierdptest + keychain-access-groupsdptest + com.apple.keystore.access-keychain-keys + com.apple.keystore.device + com.apple.private.security.no-sandbox + platform-application + + diff --git a/tools/ios27/keystore/dptest.m b/tools/ios27/keystore/dptest.m new file mode 100644 index 000000000..bc556f75f --- /dev/null +++ b/tools/ios27/keystore/dptest.m @@ -0,0 +1,410 @@ +// dptest.m - private on-device data-protection / keystore probe (ios27-fable-keystore). Read-mostly. +// Parent re-execs itself once per section (fork-safe for XPC/ObjC): dptest [mobile] [nosleep] +// child: dptest --sec N [IDX] LOGPATH SCRATCH +#import +#import +#import +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern char **environ; +#define F_GETPROTECTIONCLASS_ 63 +#define F_SETPROTECTIONCLASS_ 64 +#define DPATTR 0x40000000u + +static int LF = -1; +static void L(const char *fmt, ...) { + char b[4096]; va_list ap; va_start(ap, fmt); + int n = vsnprintf(b, sizeof b - 2, fmt, ap); va_end(ap); + if (n < 0) return; if (n > (int)sizeof b - 2) n = sizeof b - 2; + b[n++] = '\n'; + if (LF >= 0) { write(LF, b, n); fsync(LF); } + write(2, b, n); +} +static void LS(const char *label, NSString *s) { L("%s: %s", label, s ? [s UTF8String] : "(nil)"); } +static void LO(const char *label, id o) { LS(label, o ? [NSString stringWithFormat:@"%@", o] : nil); } +static void LC(const char *label, CFTypeRef o) { + if (!o) { L("%s: (null)", label); return; } + CFStringRef d = CFCopyDescription(o); + char *buf = malloc(65536); buf[0] = 0; + if (d) { CFStringGetCString(d, buf, 65536, kCFStringEncodingUTF8); CFRelease(d); } + L("%s: %s", label, buf); free(buf); +} +static void hexdump(const unsigned char *p, size_t n) { + if (n > 256) n = 256; + for (size_t i = 0; i < n; i += 16) { + char line[128]; int o = snprintf(line, sizeof line, " %04zx:", i); + for (size_t j = i; j < i + 16 && j < n; j++) o += snprintf(line + o, sizeof line - o, " %02x", p[j]); + L("%s", line); + } +} +#define ERR(e) (e), strerror(e) + +static const char *LOGP, *SCR; + +// ---------- header ---------- +static void header(int mobile) { + struct timeval tv; gettimeofday(&tv, 0); char tb[64]; struct tm tm; time_t t = tv.tv_sec; gmtime_r(&t, &tm); + strftime(tb, sizeof tb, "%Y-%m-%dT%H:%M:%SZ", &tm); + struct utsname u; uname(&u); + char kv[512] = "?", ba[1024] = "?", bt[64] = "?"; size_t l = sizeof kv; sysctlbyname("kern.version", kv, &l, 0, 0); + l = sizeof ba; if (sysctlbyname("kern.bootargs", ba, &l, 0, 0)) snprintf(ba, sizeof ba, "(err %d %s)", ERR(errno)); + struct timeval bts; l = sizeof bts; if (!sysctlbyname("kern.boottime", &bts, &l, 0, 0)) snprintf(bt, sizeof bt, "%ld", (long)bts.tv_sec); + L(""); + L("==================== dptest start %s (unix %ld) mode=%s uid=%d euid=%d pid=%d ====================", tb, (long)tv.tv_sec, mobile ? "mobile" : "root", getuid(), geteuid(), getpid()); + L("uname: %s %s %s %s", u.sysname, u.release, u.version, u.machine); + L("kern.version: %s", kv); + L("kern.bootargs: %s", ba); + L("kern.boottime: %s", bt); +} + +// ---------- section 1 ---------- +static void sec1(void) { + const char *ps[] = {"/private/var", "/"}; + for (int i = 0; i < 2; i++) { + struct statfs s; memset(&s, 0, sizeof s); + if (statfs(ps[i], &s)) { L("statfs(%s) FAILED errno=%d %s", ps[i], ERR(errno)); continue; } + L("statfs(%s): fstypename=%s mntonname=%s mntfromname=%s f_flags=0x%x MNT_CPROTECT(0x80)=%s bsize=%u blocks=%llu bfree=%llu", + ps[i], s.f_fstypename, s.f_mntonname, s.f_mntfromname, s.f_flags, (s.f_flags & 0x80) ? "SET" : "clear", s.f_bsize, + (unsigned long long)s.f_blocks, (unsigned long long)s.f_bfree); + } +} + +// ---------- section 2 helpers ---------- +typedef struct { u_int32_t len; u_int32_t val; } __attribute__((packed)) dpbuf_t; +static int attr_get(int fd, const char *path, long long *out) { + struct attrlist al; memset(&al, 0, sizeof al); al.bitmapcount = ATTR_BIT_MAP_COUNT; al.commonattr = DPATTR; + dpbuf_t b; memset(&b, 0, sizeof b); + int r = fd >= 0 ? fgetattrlist(fd, &al, &b, sizeof b, 0) : getattrlist(path, &al, &b, sizeof b, 0); + if (r) return errno ? errno : -1; + *out = b.val; return 0; +} +static const char *dstr(long long v) { + static const char *n[] = {"?0(NONE/none-flag)", "A(Complete)", "B(CompleteUnlessOpen)", "C(AfterFirstUnlock)", "D(None)", "E(CompleteUnlessOpen-alt)"}; + return (v >= 0 && v <= 5) ? n[v] : "?"; +} +static void rep(const char *tag, int fd, const char *path) { + int c = fcntl(fd, F_GETPROTECTIONCLASS_); int e1 = errno; + long long a = -1; int ae = attr_get(fd, NULL, &a); + char s1[96], s2[96]; + if (c < 0) snprintf(s1, sizeof s1, "FAIL errno=%d %s", ERR(e1)); else snprintf(s1, sizeof s1, "%d %s", c, dstr(c)); + if (ae) snprintf(s2, sizeof s2, "FAIL errno=%d %s", ERR(ae)); else snprintf(s2, sizeof s2, "%lld (0x%llx)", a, a); + L(" [%s] %s: fcntl(GETPROTECTIONCLASS)=%s ; fgetattrlist(DATA_PROTECT_FLAGS)=%s", tag, path, s1, s2); +} +static void rep_path(const char *tag, const char *path) { + long long a = -1; int ae = attr_get(-1, path, &a); + int fd = open(path, O_RDONLY | O_NONBLOCK); int oe = errno; + char s1[96] = "", s2[96]; + if (fd < 0) snprintf(s1, sizeof s1, "open FAIL errno=%d %s", ERR(oe)); + else { int c = fcntl(fd, F_GETPROTECTIONCLASS_); int e1 = errno; if (c < 0) snprintf(s1, sizeof s1, "FAIL errno=%d %s", ERR(e1)); else snprintf(s1, sizeof s1, "%d %s", c, dstr(c)); close(fd); } + if (ae) snprintf(s2, sizeof s2, "FAIL errno=%d %s", ERR(ae)); else snprintf(s2, sizeof s2, "%lld (0x%llx)", a, a); + struct stat st; char sts[96] = "stat FAIL"; if (!stat(path, &st)) snprintf(sts, sizeof sts, "mode=%o uid=%d gid=%d%s", st.st_mode & 07777, st.st_uid, st.st_gid, S_ISDIR(st.st_mode) ? " DIR" : ""); + L(" [%s] %s: fcntl=%s ; getattrlist=%s ; %s", tag, path, s1, s2, sts); +} +static void listdir_cls(const char *dir, int cls) { + DIR *d = opendir(dir); if (!d) { L(" opendir(%s) FAIL errno=%d %s", dir, ERR(errno)); return; } + struct dirent *e; int n = 0; + while ((e = readdir(d))) { + if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; + if (++n > 200) { L(" ... (truncated at 200)"); break; } + char p[1024]; snprintf(p, sizeof p, "%s/%s", dir, e->d_name); + if (cls) rep_path("ls", p); else L(" entry: %s (d_type=%d)", p, e->d_type); + } + closedir(d); L(" (%d entries in %s)", n, dir); +} +static void setcls(int fd, int cls, const char *what) { + int r = fcntl(fd, F_SETPROTECTIONCLASS_, cls); int e = errno; + if (r) L(" SET class %d on %s: FAIL r=%d errno=%d %s", cls, what, r, ERR(e)); else L(" SET class %d on %s: ok r=%d", cls, what, r); +} + +static void sec2(void) { + char dir[512]; snprintf(dir, sizeof dir, "%s", SCR); + char cmd[600]; snprintf(cmd, sizeof cmd, "%s", dir); + // mkdir -p + for (char *p = cmd + 1; *p; p++) if (*p == '/') { *p = 0; mkdir(cmd, 0755); *p = '/'; } + if (mkdir(dir, 0755) && errno != EEXIST) L("mkdir(%s) FAIL errno=%d %s", dir, ERR(errno)); + L("scratch dir: %s", dir); + rep_path("scratchdir", dir); + char f1[600]; snprintf(f1, sizeof f1, "%s/plain.%d", dir, getpid()); unlink(f1); + int fd = open(f1, O_CREAT | O_RDWR | O_TRUNC, 0644); + if (fd < 0) { L("open(%s, O_CREAT) FAIL errno=%d %s", f1, ERR(errno)); } + else { + write(fd, "dptest\n", 7); fsync(fd); + L("-- 2a plain open() file %s", f1); + rep("fresh", fd, f1); + for (int c = 1; c <= 4; c++) { + char w[32]; snprintf(w, sizeof w, "class%d", c); + setcls(fd, c, f1); rep(w, fd, f1); + } + // set class 3 then persistence test per class: close/reopen after setting each + for (int c = 1; c <= 4; c++) { + setcls(fd, c, f1); close(fd); + fd = open(f1, O_RDWR); if (fd < 0) { L(" reopen FAIL errno=%d %s", ERR(errno)); break; } + char w[32]; snprintf(w, sizeof w, "reopen-after-%d", c); rep(w, fd, f1); + } + if (fd >= 0) close(fd); + // by-path getattrlist + rep_path("bypath", f1); + // setattrlist by path + { + struct attrlist al; memset(&al, 0, sizeof al); al.bitmapcount = ATTR_BIT_MAP_COUNT; al.commonattr = DPATTR; + for (u_int32_t v = 1; v <= 4; v++) { + u_int32_t val = v; + int r = setattrlist(f1, &al, &val, sizeof val, 0); + L(" setattrlist(DATA_PROTECT_FLAGS=%u) by path: r=%d errno=%d %s", v, r, r ? errno : 0, r ? strerror(errno) : "ok"); + rep_path("after-setattrlist", f1); + } + } + } + // open_dprotected_np + L("-- 2b open_dprotected_np(path, O_CREAT|O_RDWR, 3, 0, 0644)"); + typedef int (*odp_t)(const char *, int, int, int, ...); + odp_t odp = (odp_t)dlsym(RTLD_DEFAULT, "open_dprotected_np"); + char f2[600]; snprintf(f2, sizeof f2, "%s/dp3.%d", dir, getpid()); unlink(f2); + if (!odp) L(" open_dprotected_np: dlsym NULL"); + else { + int g = odp(f2, O_CREAT | O_RDWR, 3, 0, 0644); int e = errno; + if (g < 0) L(" open_dprotected_np FAIL errno=%d %s", ERR(e)); + else { L(" open_dprotected_np ok fd=%d", g); rep("dp3", g, f2); close(g); rep_path("dp3-bypath", f2); } + } + // directory + L("-- 2c directory class + inheritance"); + char d2[600]; snprintf(d2, sizeof d2, "%s/sub.%d", dir, getpid()); mkdir(d2, 0755); + int dfd = open(d2, O_RDONLY); + if (dfd < 0) L(" open(dir) FAIL errno=%d %s", ERR(errno)); + else { + rep("dir-fresh", dfd, d2); setcls(dfd, 3, d2); rep("dir-after3", dfd, d2); close(dfd); + char f3[700]; snprintf(f3, sizeof f3, "%s/child", d2); + int h = open(f3, O_CREAT | O_RDWR, 0644); + if (h < 0) L(" create in dir FAIL errno=%d %s", ERR(errno)); else { rep("inherit-child", h, f3); close(h); } + rep_path("inherit-child-bypath", f3); + } + // NSFileManager / NSURL + L("-- 2d NSFileManager / NSURL"); + @autoreleasepool { + NSFileManager *fm = [NSFileManager defaultManager]; + NSString *p = [NSString stringWithFormat:@"%s/ns.%d", dir, getpid()]; + [fm removeItemAtPath:p error:nil]; + BOOL ok = [fm createFileAtPath:p contents:[@"x" dataUsingEncoding:NSUTF8StringEncoding] attributes:nil]; + L(" createFileAtPath ok=%d", ok); + NSError *er = nil; + NSDictionary *at = [fm attributesOfItemAtPath:p error:&er]; + LO(" attributesOfItemAtPath NSFileProtectionKey", at[NSFileProtectionKey]); LO(" err", er); + NSURL *u = [NSURL fileURLWithPath:p]; id v = nil; er = nil; + [u getResourceValue:&v forKey:NSURLFileProtectionKey error:&er]; + LO(" NSURLFileProtectionKey", v); LO(" err", er); + NSArray *prots = @[NSFileProtectionNone, NSFileProtectionComplete]; + NSArray *pn = @[@"None", @"Complete"]; + for (int i = 0; i < 2; i++) { + er = nil; BOOL r = [fm setAttributes:@{NSFileProtectionKey : prots[i]} ofItemAtPath:p error:&er]; + L(" setAttributes NSFileProtection%s: ret=%d", [pn[i] UTF8String], r); LO(" err", er); + er = nil; at = [fm attributesOfItemAtPath:p error:&er]; + LO(" readback NSFileProtectionKey", at[NSFileProtectionKey]); + v = nil; [[NSURL fileURLWithPath:p] getResourceValue:&v forKey:NSURLFileProtectionKey error:&er]; LO(" readback NSURLFileProtectionKey", v); + rep_path("ns-after", [p UTF8String]); + } + } + // existing paths + L("-- 2e existing paths (read only)"); + const char *ex[] = {"/private/var/mobile/Library/Preferences", "/private/var/mobile/Library/Keychains", "/private/var/keybags", + "/private/var/mobile/Containers", "/private/var/containers", "/private/var/db/lsd", "/private/var/mobile/Library/Caches", "/usr/libexec/keybagd"}; + for (unsigned i = 0; i < sizeof ex / sizeof *ex; i++) rep_path("exist", ex[i]); + L("-- Keychains dir files:"); listdir_cls("/private/var/mobile/Library/Keychains", 1); + L("-- /private/var/keybags files:"); listdir_cls("/private/var/keybags", 1); + L("-- /private/var/db/lsd listing:"); listdir_cls("/private/var/db/lsd", 0); +} + +// ---------- section 3 ---------- +static void sec3(void) { + void *h = dlopen("/System/Library/PrivateFrameworks/MobileKeyBag.framework/MobileKeyBag", RTLD_NOW); + if (!h) { L("dlopen MobileKeyBag FAIL: %s", dlerror()); return; } + L("dlopen MobileKeyBag ok"); + typedef int (*i_d)(CFDictionaryRef); typedef int (*i_v)(void); typedef CFDictionaryRef (*d_d)(CFDictionaryRef); + i_d gls = (i_d)dlsym(h, "MKBGetDeviceLockState"); i_v uns = (i_v)dlsym(h, "MKBDeviceUnlockedSinceBoot"); + d_d gli = (d_d)dlsym(h, "MKBGetDeviceLockStateInfo"); i_v fmt = (i_v)dlsym(h, "MKBDeviceFormattedForContentProtection"); + d_d gcf = (d_d)dlsym(h, "MKBGetDeviceConfigurations"); + L("symbols: GetDeviceLockState=%p UnlockedSinceBoot=%p LockStateInfo=%p Formatted=%p GetDeviceConfigurations=%p", gls, uns, gli, fmt, gcf); + if (gls) { errno = 0; int r = gls(NULL); L("MKBGetDeviceLockState(NULL) = %d (0x%x)", r, r); } + if (uns) { int r = uns(); L("MKBDeviceUnlockedSinceBoot() = %d (0x%x)", r, r); } + if (gli) { CFDictionaryRef d = gli(NULL); LC("MKBGetDeviceLockStateInfo(NULL)", d); } + if (fmt) { int r = fmt(); L("MKBDeviceFormattedForContentProtection() = %d (0x%x)", r, r); } + if (gcf) { CFDictionaryRef d = gcf(NULL); LC("MKBGetDeviceConfigurations(NULL)", d); } else L("MKBGetDeviceConfigurations: symbol missing, skipped"); +} + +// ---------- section 4 ---------- +typedef struct { int sel; int nin; uint64_t in[2]; int nout; size_t sout; const char *desc; } call_t; +static const call_t CALLS[] = { + {7, 1, {0, 0}, 1, 0, "sel7 in=0"}, + {7, 1, {(uint64_t)-3, 0}, 1, 0, "sel7 in=-3"}, + {14, 0, {0, 0}, 1, 0, "sel14"}, + {17, 1, {0, 0}, 0, 4096, "sel17 in=0 sout=4096"}, + {35, 1, {0, 0}, 0, 4096, "sel35 in=0 sout=4096"}, + {31, 1, {0, 0}, 0, 32768, "sel31 in=0 sout=32768"}, + {23, 1, {0, 0}, 0, 16, "sel23 in=0 sout=16"}, + {40, 2, {0, 0}, 1, 0, "sel40 in=0,0"}, + {107, 0, {0, 0}, 1, 0, "sel107"}, + {103, 0, {0, 0}, 1, 0, "sel103"}, +}; +#define NCALLS ((int)(sizeof CALLS / sizeof *CALLS)) +static void sec4(int idx) { + io_service_t svc = IOServiceGetMatchingService(0, IOServiceMatching("AppleKeyStore")); + if (idx == 0) L("IOServiceGetMatchingService(AppleKeyStore) = 0x%x", svc); + if (!svc) { L("[%d] no AppleKeyStore service", idx); return; } + io_connect_t c = 0; kern_return_t kr = IOServiceOpen(svc, mach_task_self(), 0, &c); + L("[%d] IOServiceOpen(type 0) kr=0x%x (%s) conn=0x%x", idx, kr, mach_error_string(kr), c); + if (kr) return; + const call_t *k = &CALLS[idx]; + uint64_t out[4] = {0}; uint32_t nout = k->nout; + void *so = k->sout ? calloc(1, k->sout) : NULL; size_t sol = k->sout; + kr = IOConnectCallMethod(c, k->sel, k->nin ? k->in : NULL, k->nin, NULL, 0, k->nout ? out : NULL, k->nout ? &nout : NULL, so, so ? &sol : NULL); + L("[%d] %s: kr=0x%x (%s) nScalarOut=%u scalar0=0x%llx (%lld) structOutSize=%zu", idx, k->desc, kr, mach_error_string(kr), k->nout ? nout : 0, + (unsigned long long)out[0], (long long)out[0], so ? sol : 0); + if (so && sol) hexdump(so, sol > k->sout ? k->sout : sol); + IOServiceClose(c); IOObjectRelease(svc); +} + +// ---------- section 5 ---------- +static void kc_one(const char *acct, CFTypeRef acc, int idx) { + CFStringRef a = CFStringCreateWithCString(0, acct, kCFStringEncodingUTF8); + unsigned char rnd[16]; FILE *f = fopen("/dev/urandom", "r"); if (f) { fread(rnd, 1, 16, f); fclose(f); } else for (int i = 0; i < 16; i++) rnd[i] = rand(); + CFDataRef data = CFDataCreate(0, rnd, 16); + CFMutableDictionaryRef q = CFDictionaryCreateMutable(0, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + CFDictionarySetValue(q, kSecClass, kSecClassGenericPassword); CFDictionarySetValue(q, kSecAttrService, CFSTR("dptest")); CFDictionarySetValue(q, kSecAttrAccount, a); + SecItemDelete(q); + CFMutableDictionaryRef add = CFDictionaryCreateMutableCopy(0, 0, q); + CFDictionarySetValue(add, kSecValueData, data); CFDictionarySetValue(add, kSecAttrAccessible, acc); + OSStatus s = SecItemAdd(add, NULL); + CFStringRef m = SecCopyErrorMessageString(s, NULL); char mb[256] = ""; if (m) { CFStringGetCString(m, mb, sizeof mb, kCFStringEncodingUTF8); CFRelease(m); } + LC(" accessible-constant", acc); + L(" [%s] SecItemAdd -> %d (%s)", acct, (int)s, mb); + CFMutableDictionaryRef g = CFDictionaryCreateMutableCopy(0, 0, q); + CFDictionarySetValue(g, kSecReturnData, kCFBooleanTrue); CFDictionarySetValue(g, kSecMatchLimit, kSecMatchLimitOne); + CFTypeRef out = NULL; s = SecItemCopyMatching(g, &out); + m = SecCopyErrorMessageString(s, NULL); mb[0] = 0; if (m) { CFStringGetCString(m, mb, sizeof mb, kCFStringEncodingUTF8); CFRelease(m); } + L(" [%s] SecItemCopyMatching -> %d (%s) dataMatches=%s", acct, (int)s, mb, (out && CFGetTypeID(out) == CFDataGetTypeID() && CFEqual(out, data)) ? "YES" : "no"); + if (out) CFRelease(out); + CFMutableDictionaryRef g2 = CFDictionaryCreateMutableCopy(0, 0, q); + CFDictionarySetValue(g2, kSecReturnAttributes, kCFBooleanTrue); CFTypeRef o2 = NULL; s = SecItemCopyMatching(g2, &o2); + if (!s) LC(" attrs", o2); if (o2) CFRelease(o2); + (void)idx; +} +static void sec5(void) { + L("kSecAttrAccessibleAfterFirstUnlock"); kc_one("a1", kSecAttrAccessibleAfterFirstUnlock, 0); + L("kSecAttrAccessibleWhenUnlocked"); kc_one("a2", kSecAttrAccessibleWhenUnlocked, 1); + L("kSecAttrAccessibleAlwaysThisDeviceOnly"); kc_one("a3", kSecAttrAccessibleAlwaysThisDeviceOnly, 2); + const char *ac[] = {"a1", "a2", "a3"}; + for (int i = 0; i < 3; i++) { + CFMutableDictionaryRef q = CFDictionaryCreateMutable(0, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + CFDictionarySetValue(q, kSecClass, kSecClassGenericPassword); CFDictionarySetValue(q, kSecAttrService, CFSTR("dptest")); + CFStringRef a = CFStringCreateWithCString(0, ac[i], kCFStringEncodingUTF8); CFDictionarySetValue(q, kSecAttrAccount, a); + OSStatus s = SecItemDelete(q); L(" [%s] SecItemDelete -> %d", ac[i], (int)s); + } +} + +// ---------- section 6 ---------- +static void sec6(void) { + void *h = dlopen("/usr/lib/libMobileGestalt.dylib", RTLD_NOW); + if (!h) { L("dlopen libMobileGestalt FAIL: %s", dlerror()); return; } + typedef CFTypeRef (*mg_t)(CFStringRef, CFDictionaryRef); + mg_t mg = (mg_t)dlsym(h, "MGCopyAnswer"); if (!mg) { L("MGCopyAnswer: dlsym NULL"); return; } + const char *ks[] = {"PasswordConfigured", "PasswordProtected", "HasSEP", "ProductType", "HWModelStr", "BuildVersion", "UniqueDeviceID", "SerialNumber", "DeviceClass", "ActivationState"}; + for (unsigned i = 0; i < sizeof ks / sizeof *ks; i++) { + CFStringRef k = CFStringCreateWithCString(0, ks[i], kCFStringEncodingUTF8); + CFTypeRef r = mg(k, NULL); char lab[128]; snprintf(lab, sizeof lab, "MGCopyAnswer(%s)", ks[i]); LC(lab, r); + } +} + +// ---------- section 7 ---------- +static void count_dir(const char *p) { + struct stat st; if (stat(p, &st)) { L(" stat(%s) FAIL errno=%d %s", p, ERR(errno)); return; } + L(" stat(%s): mode=%o uid=%d gid=%d", p, st.st_mode & 07777, st.st_uid, st.st_gid); + DIR *d = opendir(p); if (!d) { L(" opendir FAIL errno=%d %s", ERR(errno)); return; } + int n = 0; struct dirent *e; while ((e = readdir(d))) { if (strcmp(e->d_name, ".") && strcmp(e->d_name, "..")) n++; } closedir(d); + L(" entries in %s: %d", p, n); +} +static void sec7(void) { + void *h = dlopen("/usr/lib/system/libsystem_containermanager.dylib", RTLD_NOW); + L("dlopen libsystem_containermanager: %s", h ? "ok" : dlerror()); + typedef char *(*cf_t)(void *, const char *, void *); + cf_t f = (cf_t)dlsym(RTLD_DEFAULT, "container_system_group_path_for_identifier"); + if (!f) L("container_system_group_path_for_identifier: dlsym NULL"); + else { + unsigned char err[256]; memset(err, 0, sizeof err); // real 3rd arg is a container_error_t*, oversized buffer on purpose (guess) + char *p = f(NULL, "systemgroup.com.apple.configurationprofiles", err); + L("container_system_group_path_for_identifier(configurationprofiles) = %s ; errbuf first8=%02x%02x%02x%02x%02x%02x%02x%02x", p ? p : "NULL", + err[0], err[1], err[2], err[3], err[4], err[5], err[6], err[7]); + } + count_dir("/private/var/containers/Shared/SystemGroup"); + count_dir("/private/var/mobile/Containers/Data/Application"); +} + +// ---------- driver ---------- +static void child(int argc, char **argv) { + int sec = atoi(argv[2]); int ai = 3; int idx = 0; + if (sec == 4) idx = atoi(argv[ai++]); + LOGP = argv[ai++]; SCR = argv[ai++]; + LF = open(LOGP, O_WRONLY | O_APPEND | O_CREAT, 0666); + alarm(20); + @autoreleasepool { + switch (sec) { + case 1: sec1(); break; case 2: sec2(); break; case 3: sec3(); break; case 4: sec4(idx); break; + case 5: sec5(); break; case 6: sec6(); break; case 7: sec7(); break; + } + } + _exit(0); +} +static int run_child(const char *self, int sec, int idx, int mobile) { + char ss[8], is[8]; snprintf(ss, sizeof ss, "%d", sec); snprintf(is, sizeof is, "%d", idx); + char *av[10]; int n = 0; av[n++] = (char *)self; av[n++] = "--sec"; av[n++] = ss; if (sec == 4) av[n++] = is; + av[n++] = (char *)LOGP; av[n++] = (char *)SCR; av[n] = NULL; + pid_t pid; int r = posix_spawn(&pid, self, NULL, NULL, av, environ); + if (r) { L(" posix_spawn FAIL %d %s", ERR(r)); return -1; } + int st = 0; for (int i = 0; i < 300; i++) { pid_t w = waitpid(pid, &st, WNOHANG); if (w == pid) goto done; usleep(100000); } + kill(pid, SIGKILL); waitpid(pid, &st, 0); L(" !! section %d killed by parent after 30s timeout", sec); return -2; +done: + if (WIFSIGNALED(st)) L(" !! section %d (idx %d) died with signal %d (%s)", sec, idx, WTERMSIG(st), strsignal(WTERMSIG(st))); + else L(" (section %d idx %d exit status %d)", sec, idx, WEXITSTATUS(st)); + (void)mobile; return 0; +} +int main(int argc, char **argv) { + setvbuf(stderr, NULL, _IONBF, 0); + if (argc > 2 && !strcmp(argv[1], "--sec")) child(argc, argv); + int mobile = 0, nosleep = 0; + for (int i = 1; i < argc; i++) { if (!strcmp(argv[i], "mobile")) mobile = 1; if (!strcmp(argv[i], "nosleep")) nosleep = 1; } + LOGP = mobile ? "/private/var/local/oslog/dptest-mobile.log" : "/private/var/local/oslog/dptest.log"; + SCR = mobile ? "/private/var/mobile/Library/dptest" : "/private/var/mobile/Library/dptest-root"; + LF = open(LOGP, O_WRONLY | O_APPEND | O_CREAT, 0666); + if (LF < 0) { fprintf(stderr, "cannot open log %s: %s\n", LOGP, strerror(errno)); return 1; } + fchmod(LF, 0666); + header(mobile); + char self[1024]; uint32_t sl = sizeof self; + if (_NSGetExecutablePath(self, &sl)) strcpy(self, "/usr/local/bin/dptest"); + if (!nosleep) { L("sleeping 60 s (SpringBoard warm-up)"); sleep(60); L("sleep done"); } + const char *names[] = {"", "1 FS statfs", "2 data-protection classes", "3 MobileKeyBag", "4 AppleKeyStore user client", "5 Keychain", "6 MobileGestalt", "7 Containers"}; + for (int s = 1; s <= 7; s++) { + L(""); + L("######## SECTION %s ########", names[s]); + if (s == 4) for (int i = 0; i < NCALLS; i++) run_child(self, 4, i, mobile); + else run_child(self, s, 0, mobile); + } + L("==================== dptest done ===================="); + return 0; +} diff --git a/tools/ios27/keystore/install-dptest.sh b/tools/ios27/keystore/install-dptest.sh new file mode 100755 index 000000000..e7481c4c1 --- /dev/null +++ b/tools/ios27/keystore/install-dptest.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# install-dptest.sh MOUNTPOINT : idempotent. Run with sudo on a root image attached with `hdiutil attach -owners on`. +set -euo pipefail +M=${1:?usage: install-dptest.sh MOUNTPOINT}; H=$(cd "$(dirname "$0")" && pwd) +[ -d "$M/System/Library/LaunchDaemons" ] || { echo "not an iOS root: $M"; exit 1; } +BIN=${DPTEST_BIN:-$H/dptest.arm64} # DPTEST_BIN=$H/dptest.arm64e to try the PAC build +install -d -m 0755 "$M/usr/local/bin" +cp "$BIN" "$M/usr/local/bin/dptest"; chmod 0755 "$M/usr/local/bin/dptest" +for p in com.scion.dptest.plist com.scion.dptest.mobile.plist; do + cp "$H/$p" "$M/System/Library/LaunchDaemons/$p"; chmod 0644 "$M/System/Library/LaunchDaemons/$p" +done +mkdir -p "$M/private/var/local/oslog"; chmod 0777 "$M/private/var/local/oslog" +if [ "$(id -u)" = 0 ]; then + chown 0:0 "$M/usr/local/bin/dptest" "$M/System/Library/LaunchDaemons/com.scion.dptest.plist" "$M/System/Library/LaunchDaemons/com.scion.dptest.mobile.plist" + chown 0:0 "$M/usr/local/bin" 2>/dev/null || true +else echo "WARNING: not root, ownership not set"; fi +# this root loads jobs from the combined cache System/Library/xpc/launchd.plist (key = plist path), like install-metal27.py +/usr/bin/python3 - "$M" "$H" <<'PY' +import plistlib, sys, os, shutil +M, H = sys.argv[1:3] +lp = M + '/System/Library/xpc/launchd.plist' +if os.path.exists(lp): + if not os.path.exists(lp + '.before-dptest'): shutil.copy2(lp, lp + '.before-dptest') + d = plistlib.load(open(lp, 'rb')) + for n in ('com.scion.dptest', 'com.scion.dptest.mobile'): + k = '/System/Library/LaunchDaemons/%s.plist' % n + d['LaunchDaemons'][k] = plistlib.load(open('%s/%s.plist' % (H, n), 'rb')) + with open(lp, 'r+b') as f: + f.seek(0); f.write(plistlib.dumps(d, fmt=plistlib.FMT_BINARY)); f.truncate() + print('launchd.plist: jobs added/updated (%d jobs total)' % len(d['LaunchDaemons'])) +else: + print('no xpc/launchd.plist cache; relying on LaunchDaemons dir only') +PY +echo "installed: $M/usr/local/bin/dptest ($(basename "$BIN")), 2 plists, oslog dir. Results: /private/var/local/oslog/dptest.log, dptest-mobile.log" diff --git a/tools/ios27/keystore/uninstall-dptest.sh b/tools/ios27/keystore/uninstall-dptest.sh new file mode 100755 index 000000000..81631c5db --- /dev/null +++ b/tools/ios27/keystore/uninstall-dptest.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# uninstall-dptest.sh MOUNTPOINT : removes binary, plists, launchd cache entries (logs left in place) +set -euo pipefail +M=${1:?usage: uninstall-dptest.sh MOUNTPOINT} +rm -f "$M/usr/local/bin/dptest" "$M/System/Library/LaunchDaemons/com.scion.dptest.plist" "$M/System/Library/LaunchDaemons/com.scion.dptest.mobile.plist" +/usr/bin/python3 - "$M" <<'PY' +import plistlib, sys, os +lp = sys.argv[1] + '/System/Library/xpc/launchd.plist' +if os.path.exists(lp): + d = plistlib.load(open(lp, 'rb')) + for n in ('com.scion.dptest', 'com.scion.dptest.mobile'): d['LaunchDaemons'].pop('/System/Library/LaunchDaemons/%s.plist' % n, None) + with open(lp, 'r+b') as f: + f.seek(0); f.write(plistlib.dumps(d, fmt=plistlib.FMT_BINARY)); f.truncate() +PY +echo "uninstalled from $M" From e1106b15ba0fcc78c76ecca5d23489a0c4d1a6d1 Mon Sep 17 00:00:00 2001 From: Claudius Date: Mon, 9 Nov 2026 22:03:38 +0000 Subject: [PATCH 2/4] ios-27 key store: what a phone run proved, the purgeable fsctl()s, fork f8ad9e5 --- docs/ios27/KEYSTORE.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/ios27/KEYSTORE.md b/docs/ios27/KEYSTORE.md index 859687f5e..1596e10fa 100644 --- a/docs/ios27/KEYSTORE.md +++ b/docs/ios27/KEYSTORE.md @@ -10,6 +10,7 @@ of the boot-argument `sweetspeed=` and are **off by default**: | `0x100000` (1048576) | the built-in HFS Plus keeps, reports and inherits data-protection classes, as labels; what has no class reads back as D | `bsd/sweet_hfs/sweet_hfs_pclass.c`, hooks in `core/hfs_vnops.c`, `hfs_attrlist.c`, `hfs_readwrite.c` | | `0x200000` (2097152) | with the bit above: the mount says `MNT_CPROTECT` (what `MKBDeviceFormattedForContentProtection()` and SQLite read), and an update mount keeps it | `core/hfs_vfsutils.c`, `bsd/vfs/vfs_syscalls.c` | | `0x400000` (4194304) | the key store answers as a device with **no passcode** does, in full (below) | `iokit/Kernel/arm/SweetKeyStore.cpp` | +| `0x1000000` (16777216) | APFS's purgeable-file and directory-statistics `fsctl()`s are accepted (and forgotten) by the built-in HFS Plus | `core/hfs_readwrite.c` | **In short: nothing here encrypts anything.** There is no Secure Enclave, no passcode, no key that lives outside the kernel. A protection class is a label that programs can set and read back; a file marked A is as readable from the flash, and while the screen is locked, as one @@ -38,7 +39,7 @@ Found on a Galaxy S9 running 24A446 (the kernel's own log of key store calls, an The selector numbers were not the problem: every `aks_*` function of 24A446 still calls the number it called on iOS 16 (table below); iOS 27 only adds to the list. -**What is verified, and how.** The kernel with these changes was built (the iOS-target STARLTE kernel) and **has not been booted yet**; the test below has not run. "What was wrong" is from a phone's logs and from the binaries; what the bits change is, until a phone run, what the code says. +**What is verified, and how.** One Galaxy S9, the fork's commit 2adf535 (iOS-target STARLTE kernel), `sweetspeed` bits `0x100000` and `0x400000`, and the test of the last section (`dptest`, as root and as `mobile`), one boot: classes set by `fcntl`, `setattrlist`, `open_dprotected_np` and `NSFileManager` read back by `fcntl`, `getattrlist` and `NSURL`, also after reopening; a file made in a directory of class C came out C; what had no class read D. `MKBGetDeviceLockState` 3, `MKBDeviceUnlockedSinceBoot` 1, `MKBGetDeviceConfigurations` a dictionary (grace period 0, 10 attempts); selectors 7, 14, 17, 23, 31, 35, 40, 103, 107 as the table below says. A keychain item added, read back and deleted for each of "after first unlock", "when unlocked" and "always, this device only". `installcoordinationd` logged "Created .../InstallCoordination" and ".../Removability" and stayed up (it ended in `os_crash` before). The boot before that one found a panic: the configuration is asked for with a 32 KB buffer, which arrives as a memory descriptor, and the stand-in wrote to it unprepared ("IOGMD: not wired"); fixed in 2adf535. **Not run on a phone yet:** `0x200000` (`MNT_CPROTECT`) and `0x1000000` (the purgeable calls). A day of use with the bits on has not happened either. ## What the key store answers @@ -90,6 +91,12 @@ without that (`CONFIG_PROTECT` off in `bsd/sweet_hfs/sweet_hfs_compat.h`): no ke the kernel answers: `MKBDeviceFormattedForContentProtection()` is that flag on `/var`, and SQLite strips its file-protection open flags without it ("volume does not support data protection"). +**Purgeable files (`sweetspeed&0x1000000`).** PosterFoundation marks every directory of the wallpaper store purgeable +(`-[NSURL pf_setPurgable:afterDate:error:]`) with APFS's `fsctl()`s, whatever the volume is: group `'J'`, 3 (is this directory counted, 24 +bytes out), 113 (count it, 272 bytes), 111 (mark purgeable, 48 bytes), and 109 to ask (48 bytes out). On HFS they were `ENOTTY`: "Error +marking ... as purgable" 115 times a boot, and no poster store. This volume keeps no such accounting and nothing here purges files, so with +the bit the calls succeed and are forgotten; the questions answer zeroes ("not counted", "not purgeable"). + An image of the volume attached on a Mac shows nothing of this (the attribute is hidden there too); a kernel without the bit ignores the attributes and answers as before. @@ -109,11 +116,11 @@ one `IOConnectCallMethod` with a constant selector. The numbers below are the on | 6 | `aks_load_bag` | the bag in, as a structure; handle out | | 7 | `aks_get_lock_state` | handle in; flags out | | 8, 32 | `aks_lock_device` (32: now) | handle | -| 9 | `aks_unlock_device` | 4 scalars in: handle, the passcode's address, its length, an ACM handle | +| 9 | `aks_unlock_device` | 4 scalars in: handle, the passcode's address, its length, 0 | | 10, 11 | `aks_wrap_key`, `aks_unwrap_key` | handle, class; key in; key out (10 also returns the class) | | 12, 13 | `aks_unlock_bag`, `aks_lock_bag` | 5 scalars; handle | | 14 | `aks_get_system` | handle out | -| 15 | `aks_change_secret` (`_opts`, `_with_kek`) | 12 scalars in: handle, two options, the old passcode's address and length, the new one's, an ACM blob's address and length, a parameter, the generation option, flags; 1 scalar out: the handle of the bag that now holds the new secret | +| 15 | `aks_change_secret` (`_opts`, `_with_kek`) | 12 scalars in: [0] handle, [1] the generation option (keybagd: 4 "make a new bag", 0 for a bag kept in xART), [3] and [4] the old passcode's address and length, [5] and [6] the new one's, [7] to [9] an ACM blob and a parameter, [11] flags; 1 scalar out: the handle of the bag that holds the new secret | | 17, 35 | `aks_get_device_state`, `aks_get_extended_device_state` | handle in; DER out | | 23 | `aks_get_bag_uuid` | 16 bytes out | | 26, 27, 100, 101 | `aks_assert_hold`, `_drop`, `_promote`, `_consume` | 3, 2, 2, 1 scalars | From 82ac79893984d0ef606f15a0488ba4c49871a612 Mon Sep 17 00:00:00 2001 From: Claudius Date: Mon, 9 Nov 2026 22:37:21 +0000 Subject: [PATCH 3/4] ios-27 key store notes: the purgeable run, sandbox extensions, fork b328695 --- docs/ios27/KEYSTORE.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/ios27/KEYSTORE.md b/docs/ios27/KEYSTORE.md index 1596e10fa..191eb2819 100644 --- a/docs/ios27/KEYSTORE.md +++ b/docs/ios27/KEYSTORE.md @@ -11,6 +11,7 @@ of the boot-argument `sweetspeed=` and are **off by default**: | `0x200000` (2097152) | with the bit above: the mount says `MNT_CPROTECT` (what `MKBDeviceFormattedForContentProtection()` and SQLite read), and an update mount keeps it | `core/hfs_vfsutils.c`, `bsd/vfs/vfs_syscalls.c` | | `0x400000` (4194304) | the key store answers as a device with **no passcode** does, in full (below) | `iokit/Kernel/arm/SweetKeyStore.cpp` | | `0x1000000` (16777216) | APFS's purgeable-file and directory-statistics `fsctl()`s are accepted (and forgotten) by the built-in HFS Plus | `core/hfs_readwrite.c` | +| `0x2000000` (33554432) | sandbox extensions are issued and consumed by the sandbox stand-in: tokens that grant nothing, there being no sandbox | `bsd/kern/sweet_amfi.c` | **In short: nothing here encrypts anything.** There is no Secure Enclave, no passcode, no key that lives outside the kernel. A protection class is a label that programs can set and read back; a file marked A is as readable from the flash, and while the screen is locked, as one @@ -39,7 +40,7 @@ Found on a Galaxy S9 running 24A446 (the kernel's own log of key store calls, an The selector numbers were not the problem: every `aks_*` function of 24A446 still calls the number it called on iOS 16 (table below); iOS 27 only adds to the list. -**What is verified, and how.** One Galaxy S9, the fork's commit 2adf535 (iOS-target STARLTE kernel), `sweetspeed` bits `0x100000` and `0x400000`, and the test of the last section (`dptest`, as root and as `mobile`), one boot: classes set by `fcntl`, `setattrlist`, `open_dprotected_np` and `NSFileManager` read back by `fcntl`, `getattrlist` and `NSURL`, also after reopening; a file made in a directory of class C came out C; what had no class read D. `MKBGetDeviceLockState` 3, `MKBDeviceUnlockedSinceBoot` 1, `MKBGetDeviceConfigurations` a dictionary (grace period 0, 10 attempts); selectors 7, 14, 17, 23, 31, 35, 40, 103, 107 as the table below says. A keychain item added, read back and deleted for each of "after first unlock", "when unlocked" and "always, this device only". `installcoordinationd` logged "Created .../InstallCoordination" and ".../Removability" and stayed up (it ended in `os_crash` before). The boot before that one found a panic: the configuration is asked for with a 32 KB buffer, which arrives as a memory descriptor, and the stand-in wrote to it unprepared ("IOGMD: not wired"); fixed in 2adf535. **Not run on a phone yet:** `0x200000` (`MNT_CPROTECT`) and `0x1000000` (the purgeable calls). A day of use with the bits on has not happened either. +**What is verified, and how.** One Galaxy S9, the fork's commit 2adf535 (iOS-target STARLTE kernel), `sweetspeed` bits `0x100000` and `0x400000`, and the test of the last section (`dptest`, as root and as `mobile`), one boot: classes set by `fcntl`, `setattrlist`, `open_dprotected_np` and `NSFileManager` read back by `fcntl`, `getattrlist` and `NSURL`, also after reopening; a file made in a directory of class C came out C; what had no class read D. `MKBGetDeviceLockState` 3, `MKBDeviceUnlockedSinceBoot` 1, `MKBGetDeviceConfigurations` a dictionary (grace period 0, 10 attempts); selectors 7, 14, 17, 23, 31, 35, 40, 103, 107 as the table below says. A keychain item added, read back and deleted for each of "after first unlock", "when unlocked" and "always, this device only". `installcoordinationd` logged "Created .../InstallCoordination" and ".../Removability" and stayed up (it ended in `os_crash` before). The boot before that one found a panic: the configuration is asked for with a 32 KB buffer, which arrives as a memory descriptor, and the stand-in wrote to it unprepared ("IOGMD: not wired"); fixed in 2adf535. **`0x1000000` (the purgeable calls):** a second S9 ran 207 s with the newer forms accepted (six poster extensions started; PosterBoard still logged the error, it uses the one-word form); with that form accepted too (8c3e2c6) the phone left the USB bus 66 s into the boot, with nothing on the console. Not understood yet (the store coming up starts the poster renderers; the GPU side's wired memory is the suspect), so the bit is not cleared. **Not run on a phone yet:** `0x200000` (`MNT_CPROTECT`), `0x2000000` (sandbox extensions). A day of use with the bits on has not happened either. ## What the key store answers @@ -100,6 +101,17 @@ the bit the calls succeed and are forgotten; the questions answer zeroes ("not c An image of the volume attached on a Mac shows nothing of this (the attribute is hidden there too); a kernel without the bit ignores the attributes and answers as before. +## Sandbox extensions (`sweetspeed&0x2000000`) + +This kernel runs without a sandbox: the stand-in policy named "Sandbox" (`bsd/kern/sweet_amfi.c`) allows every check. A sandbox extension is +how one process hands another the right to a file: `sandbox_extension_issue_*` makes a token (`__mac_syscall("Sandbox", 5, ...)`), the other +side's `sandbox_extension_consume` (6) turns it into a handle, `release` (7) gives it back. The stand-in failed 5 and 6 with `ENOENT`, and +the programs check: PosterKit logs "failed to issue sandbox extension to PID ..." for every poster path it sends to a poster extension. With +the bit, issue answers a token (a fixed prefix, then the class and the path, readable) and consume answers a handle (a counter); nothing is +recorded and nothing is checked, because there is nothing to grant. The argument layouts are libsystem_sandbox's of iOS 27.0.1: issue takes +seven 8-byte fields (class, type, data, flags, the token buffer of 0x3ff0 bytes, pid, more flags), consume three (token, its length, where +the handle goes). **Built, not run on a phone yet.** + ## The user client as 24A446 calls it Read from `AppleKeyStore.framework` (libaks is a dylib of the shared cache on iOS 27), MobileKeyBag and keybagd: each `aks_*` function ends in From ab2073361e32587ce18eafa74b0957eedf02f185 Mon Sep 17 00:00:00 2001 From: Claudius Date: Tue, 10 Nov 2026 00:25:43 +0000 Subject: [PATCH 4/4] ios-27 key store notes: the passcode run, the wallpaper picker's state; dptest's passcode mode --- docs/ios27/KEYSTORE.md | 9 +- .../keystore/com.scion.dptest.passcode.plist | 32 +++ tools/ios27/keystore/dptest.entitlements | 6 + tools/ios27/keystore/dptest.m | 192 +++++++++++++++++- tools/ios27/keystore/install-dptest.sh | 15 +- tools/ios27/keystore/uninstall-dptest.sh | 4 +- 6 files changed, 243 insertions(+), 15 deletions(-) create mode 100644 tools/ios27/keystore/com.scion.dptest.passcode.plist diff --git a/docs/ios27/KEYSTORE.md b/docs/ios27/KEYSTORE.md index 191eb2819..5239ea6f5 100644 --- a/docs/ios27/KEYSTORE.md +++ b/docs/ios27/KEYSTORE.md @@ -12,6 +12,7 @@ of the boot-argument `sweetspeed=` and are **off by default**: | `0x400000` (4194304) | the key store answers as a device with **no passcode** does, in full (below) | `iokit/Kernel/arm/SweetKeyStore.cpp` | | `0x1000000` (16777216) | APFS's purgeable-file and directory-statistics `fsctl()`s are accepted (and forgotten) by the built-in HFS Plus | `core/hfs_readwrite.c` | | `0x2000000` (33554432) | sandbox extensions are issued and consumed by the sandbox stand-in: tokens that grant nothing, there being no sandbox | `bsd/kern/sweet_amfi.c` | +| `0x4000000` (67108864) | a passcode that gates the screen, answered to Apple's keybagd (the fork's `wip/ios27-keystore-passcode`, not in `wip/ios27-keystore`) | `iokit/Kernel/arm/SweetKeyStorePasscode.cpp` | **In short: nothing here encrypts anything.** There is no Secure Enclave, no passcode, no key that lives outside the kernel. A protection class is a label that programs can set and read back; a file marked A is as readable from the flash, and while the screen is locked, as one @@ -40,7 +41,7 @@ Found on a Galaxy S9 running 24A446 (the kernel's own log of key store calls, an The selector numbers were not the problem: every `aks_*` function of 24A446 still calls the number it called on iOS 16 (table below); iOS 27 only adds to the list. -**What is verified, and how.** One Galaxy S9, the fork's commit 2adf535 (iOS-target STARLTE kernel), `sweetspeed` bits `0x100000` and `0x400000`, and the test of the last section (`dptest`, as root and as `mobile`), one boot: classes set by `fcntl`, `setattrlist`, `open_dprotected_np` and `NSFileManager` read back by `fcntl`, `getattrlist` and `NSURL`, also after reopening; a file made in a directory of class C came out C; what had no class read D. `MKBGetDeviceLockState` 3, `MKBDeviceUnlockedSinceBoot` 1, `MKBGetDeviceConfigurations` a dictionary (grace period 0, 10 attempts); selectors 7, 14, 17, 23, 31, 35, 40, 103, 107 as the table below says. A keychain item added, read back and deleted for each of "after first unlock", "when unlocked" and "always, this device only". `installcoordinationd` logged "Created .../InstallCoordination" and ".../Removability" and stayed up (it ended in `os_crash` before). The boot before that one found a panic: the configuration is asked for with a 32 KB buffer, which arrives as a memory descriptor, and the stand-in wrote to it unprepared ("IOGMD: not wired"); fixed in 2adf535. **`0x1000000` (the purgeable calls):** a second S9 ran 207 s with the newer forms accepted (six poster extensions started; PosterBoard still logged the error, it uses the one-word form); with that form accepted too (8c3e2c6) the phone left the USB bus 66 s into the boot, with nothing on the console. Not understood yet (the store coming up starts the poster renderers; the GPU side's wired memory is the suspect), so the bit is not cleared. **Not run on a phone yet:** `0x200000` (`MNT_CPROTECT`), `0x2000000` (sandbox extensions). A day of use with the bits on has not happened either. +**What is verified, and how.** One Galaxy S9, the fork's commit 2adf535 (iOS-target STARLTE kernel), `sweetspeed` bits `0x100000` and `0x400000`, and the test of the last section (`dptest`, as root and as `mobile`), one boot: classes set by `fcntl`, `setattrlist`, `open_dprotected_np` and `NSFileManager` read back by `fcntl`, `getattrlist` and `NSURL`, also after reopening; a file made in a directory of class C came out C; what had no class read D. `MKBGetDeviceLockState` 3, `MKBDeviceUnlockedSinceBoot` 1, `MKBGetDeviceConfigurations` a dictionary (grace period 0, 10 attempts); selectors 7, 14, 17, 23, 31, 35, 40, 103, 107 as the table below says. A keychain item added, read back and deleted for each of "after first unlock", "when unlocked" and "always, this device only". `installcoordinationd` logged "Created .../InstallCoordination" and ".../Removability" and stayed up (it ended in `os_crash` before). The boot before that one found a panic: the configuration is asked for with a 32 KB buffer, which arrives as a memory descriptor, and the stand-in wrote to it unprepared ("IOGMD: not wired"); fixed in 2adf535. **`0x1000000` (the purgeable calls) and `0x2000000` (sandbox extensions):** on the second S9, one boot of ten minutes on the full root with all four bits (and the GPU side's memory cap): PosterBoard's log had none of the 111 purgeable errors and none of the 16 sandbox-extension errors of the boots before. Before that, a boot with the purgeable calls on an older GPU line left the USB bus at 66 s with nothing on the console (no panic log); it did not come back with the newer kernel, and what it was is not known. **The wallpaper picker is not there yet:** Settings' Wallpaper pane draws and "Add New Wallpaper" opens its sheet, which stays on a spinner; entering the pane, PosterBoard aborted twice right after `-[PRSServer fetchPosterSnapshotsWithRequest:]`, "CoreVideo: Failed to read HardwarePlatform, MG query returned NULL" and CMPhoto starting: the poster snapshot path, which is beyond this page. Settings' Passcode pane shows the state of a device without one ("Turn Passcode On"). **Not run on a phone yet:** `0x200000` (`MNT_CPROTECT`). ## What the key store answers @@ -156,13 +157,13 @@ cameracaptured, backboardd 35; sharingd 57. ## What is not there, and what a passcode would take -- **A passcode.** Settings changes it through `profiled` and keybagd's XPC service. keybagd's path for a device without a Secure Enclave +- **A passcode** is not in `wip/ios27-keystore`; it is the branch `wip/ios27-keystore-passcode` on top of it (`sweetspeed&0x4000000`), which ran once on a phone: set, lock, a wrong and the right passcode, and removal, by raw calls and through `MKBKeyBagChangeSystemSecret`, `MKBLockDevice` and `MKBUnlockDevice`, that is through Apple's keybagd, each with the lock state it should give (`dptest passcode`). It is a screen lock: a PBKDF2 verifier and the wrong-attempt delays of Scion's 16.5 key store in `/private/var/keybags/scion-passcode`, no encryption. Not seen yet: the lock screen's keypad by hand, a restart with a passcode set, Settings' own panes. How it meets keybagd: Settings changes the passcode through `profiled` and keybagd's XPC service. keybagd's path for a device without a Secure Enclave ("KBChangeSystemNonSeSecret" in its log) is: `aks_change_secret_opts` (15), which answers the handle of the bag that holds the new secret; "KBSaveSystemBagHandle": `aks_save_bag` (3) on that handle and the answer written, with the opaque data that says what kind of passcode it is, to `/private/var/keybags/systembag.kb`; `aks_set_system_with_opts` (5) when the handle is a new one; `aks_change_secret_epilogue` (105). Before it, `aks_get_lock_state`, `aks_memento_get_state` (139), the configuration's user and group UUIDs, and MobileGestalt answers decide between this path and the Secure Enclave ones. The lock screen then uses 9 (unlock), 8 and 32 (lock), 42 (verify), the state (7, 17, 35) - and IOKit's general-interest messages on the service. None of this is implemented on this tree: selector 15 answers "unsupported", so a + and IOKit's general-interest messages on the service. Without the passcode branch selector 15 answers "unsupported", so a change fails and the device stays without a passcode. Scion's iOS 16.5 kernel has the state machine, the verifier, the delays after wrong attempts and a real key bag for the keychain (`MiniAppleKeyStore.cpp`, `docs/passcode.md`, `docs/keystore.md`), behind its own daemon in place of keybagd's service; the work here is to bring that core to this tree and to answer Apple's keybagd as above. It needs keybagd's own @@ -179,4 +180,4 @@ writes `/private/var/local/oslog/dptest.log` and `dptest-mobile.log`: the mount' setting and reading them, and after reopening; the key bag state through MobileKeyBag; the read-only selectors of the user client; a keychain item added, read back and deleted for three accessibility classes; MobileGestalt's passcode answers; a system container path. `build.sh` builds it with your Xcode; `install-dptest.sh MOUNTPOINT` puts it and its two jobs in an attached root image (and in the root's launchd job -cache), `uninstall-dptest.sh` takes them out. It calls no selector that changes the key store's state. +cache), `uninstall-dptest.sh` takes them out. It calls no selector that changes the key store's state. A third job, installed only with `DPTEST_PASSCODE=1 install-dptest.sh`, is the passcode test (`dptest passcode`, report `dptest-passcode.log`): it sets a passcode, locks, tries a wrong and the right passcode, and removes it again, first by raw calls and then through MobileKeyBag; a last step in a separate process removes the passcode whatever happened before. It needs the passcode branch's kernel; on any other it changes nothing. diff --git a/tools/ios27/keystore/com.scion.dptest.passcode.plist b/tools/ios27/keystore/com.scion.dptest.passcode.plist new file mode 100644 index 000000000..17565c481 --- /dev/null +++ b/tools/ios27/keystore/com.scion.dptest.passcode.plist @@ -0,0 +1,32 @@ + + + + + JetsamProperties + + JetsamMemoryLimit + 512 + + Label + com.scion.dptest.passcode + LaunchOnlyOnce + + POSIXSpawnType + Interactive + Program + /usr/local/bin/dptest + ProgramArguments + + /usr/local/bin/dptest + passcode + + RunAtLoad + + StandardErrorPath + /private/var/tmp/dptest-passcode.out + StandardOutPath + /private/var/tmp/dptest-passcode.out + UserName + root + + diff --git a/tools/ios27/keystore/dptest.entitlements b/tools/ios27/keystore/dptest.entitlements index f3a36adda..0204afdda 100644 --- a/tools/ios27/keystore/dptest.entitlements +++ b/tools/ios27/keystore/dptest.entitlements @@ -8,5 +8,11 @@ com.apple.keystore.device com.apple.private.security.no-sandbox platform-application + com.apple.keystore.device.verify + com.apple.keystore.config.set + com.apple.mobile.keybagd.UserManager.sync + com.apple.mobile.keybagd.xpc + com.apple.keystore.stash.access + com.apple.private.keychain.keybag diff --git a/tools/ios27/keystore/dptest.m b/tools/ios27/keystore/dptest.m index bc556f75f..05b887a76 100644 --- a/tools/ios27/keystore/dptest.m +++ b/tools/ios27/keystore/dptest.m @@ -1,5 +1,5 @@ // dptest.m - private on-device data-protection / keystore probe (ios27-fable-keystore). Read-mostly. -// Parent re-execs itself once per section (fork-safe for XPC/ObjC): dptest [mobile] [nosleep] +// Parent re-execs itself once per section (fork-safe for XPC/ObjC): dptest [mobile|passcode] [nosleep] // child: dptest --sec N [IDX] LOGPATH SCRATCH #import #import @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -61,7 +62,7 @@ static void hexdump(const unsigned char *p, size_t n) { static const char *LOGP, *SCR; // ---------- header ---------- -static void header(int mobile) { +static void header(const char *mode) { struct timeval tv; gettimeofday(&tv, 0); char tb[64]; struct tm tm; time_t t = tv.tv_sec; gmtime_r(&t, &tm); strftime(tb, sizeof tb, "%Y-%m-%dT%H:%M:%SZ", &tm); struct utsname u; uname(&u); @@ -69,7 +70,7 @@ static void header(int mobile) { l = sizeof ba; if (sysctlbyname("kern.bootargs", ba, &l, 0, 0)) snprintf(ba, sizeof ba, "(err %d %s)", ERR(errno)); struct timeval bts; l = sizeof bts; if (!sysctlbyname("kern.boottime", &bts, &l, 0, 0)) snprintf(bt, sizeof bt, "%ld", (long)bts.tv_sec); L(""); - L("==================== dptest start %s (unix %ld) mode=%s uid=%d euid=%d pid=%d ====================", tb, (long)tv.tv_sec, mobile ? "mobile" : "root", getuid(), geteuid(), getpid()); + L("==================== dptest start %s (unix %ld) mode=%s uid=%d euid=%d pid=%d ====================", tb, (long)tv.tv_sec, mode, getuid(), geteuid(), getpid()); L("uname: %s %s %s %s", u.sysname, u.release, u.version, u.machine); L("kern.version: %s", kv); L("kern.bootargs: %s", ba); @@ -356,6 +357,176 @@ static void sec7(void) { count_dir("/private/var/mobile/Containers/Data/Application"); } +// ---------- passcode mode (root): dptest passcode [nosleep] ---------- +// Parent: list /private/var/keybags, re-exec self as "--pc LOG" (parts A+B), 150 s kill timer, then FINAL SAFETY NET in the parent. +static sigjmp_buf PCJB; +static void pc_alrm(int s) { (void)s; siglongjmp(PCJB, 1); } +#define GUARD(label, ...) do { if (sigsetjmp(PCJB, 1) == 0) { alarm(25); __VA_ARGS__; alarm(0); } else { alarm(0); L("%s: TIMEOUT (25s)", label); } } while (0) + +typedef int (*mkb_chg_t)(CFDataRef, CFDataRef, CFDataRef, CFDictionaryRef, void *, void *); +typedef int (*mkb_unl_t)(CFDataRef, CFDictionaryRef, void *, void *); +typedef int (*mkb_lock_t)(CFDictionaryRef, void *, void *); +typedef int (*mkb_gls_t)(CFDictionaryRef); +typedef int (*mkb_uns_t)(void); +typedef CFDictionaryRef (*mkb_gli_t)(CFDictionaryRef); +static mkb_chg_t m_chg; static mkb_unl_t m_unl; static mkb_lock_t m_lock; static mkb_gls_t m_gls; static mkb_uns_t m_uns; static mkb_gli_t m_gli; + +static void mkb_load(void) { + void *h = dlopen("/System/Library/PrivateFrameworks/MobileKeyBag.framework/MobileKeyBag", RTLD_NOW); + if (!h) { L("dlopen MobileKeyBag FAIL: %s", dlerror()); return; } + m_chg = (mkb_chg_t)dlsym(h, "MKBKeyBagChangeSystemSecret"); m_unl = (mkb_unl_t)dlsym(h, "MKBUnlockDevice"); + m_lock = (mkb_lock_t)dlsym(h, "MKBLockDevice"); m_gls = (mkb_gls_t)dlsym(h, "MKBGetDeviceLockState"); + m_uns = (mkb_uns_t)dlsym(h, "MKBDeviceUnlockedSinceBoot"); m_gli = (mkb_gli_t)dlsym(h, "MKBGetDeviceLockStateInfo"); + L("MKB symbols: ChangeSystemSecret=%p UnlockDevice=%p LockDevice=%p GetDeviceLockState=%p UnlockedSinceBoot=%p LockStateInfo=%p", m_chg, m_unl, m_lock, m_gls, m_uns, m_gli); +} + +static void pc_keybags(const char *when) { + L("-- /private/var/keybags listing (%s):", when); + DIR *d = opendir("/private/var/keybags"); if (!d) { L(" opendir FAIL errno=%d %s", ERR(errno)); return; } + struct dirent *e; int n = 0; + while ((e = readdir(d))) { + if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; + char p[1024]; snprintf(p, sizeof p, "/private/var/keybags/%s", e->d_name); struct stat st; + if (stat(p, &st)) { L(" %s: stat FAIL errno=%d %s", e->d_name, ERR(errno)); continue; } + L(" %s: size=%lld mtime=%ld mode=%o", e->d_name, (long long)st.st_size, (long)st.st_mtime, st.st_mode & 07777); n++; + } + closedir(d); L(" (%d entries)", n); +} + +static io_connect_t ks_open(void) { + io_service_t svc = IOServiceGetMatchingService(0, IOServiceMatching("AppleKeyStore")); + if (!svc) { L("no AppleKeyStore service"); return 0; } + io_connect_t c = 0; kern_return_t kr = IOServiceOpen(svc, mach_task_self(), 0, &c); + L("IOServiceOpen(AppleKeyStore, type 0) kr=0x%x (%s) conn=0x%x", kr, mach_error_string(kr), c); + IOObjectRelease(svc); return kr ? 0 : c; +} +static kern_return_t ks_call(io_connect_t c, uint32_t sel, const uint64_t *in, uint32_t nin, uint32_t nout, const void *sin, size_t sinl, const char *desc) { + uint64_t out[4] = {0}; uint32_t no = nout; + kern_return_t kr = IOConnectCallMethod(c, sel, in, nin, sin, sinl, nout ? out : NULL, nout ? &no : NULL, NULL, NULL); + if (nout) L(" %s (sel %u): kr=0x%x (%s) nScalarOut=%u out0=%llu (0x%llx)", desc, sel, kr, mach_error_string(kr), no, (unsigned long long)out[0], (unsigned long long)out[0]); + else L(" %s (sel %u): kr=0x%x (%s)", desc, sel, kr, mach_error_string(kr)); + return kr; +} +static char EMPTYBUF[16]; +static kern_return_t ks_setpass(io_connect_t c, const char *oldp, const char *newp, const char *desc) { + uint64_t in[12] = {0}; + in[3] = (uint64_t)(uintptr_t)(oldp ? oldp : EMPTYBUF); in[4] = oldp ? strlen(oldp) : 0; + in[5] = (uint64_t)(uintptr_t)(newp ? newp : EMPTYBUF); in[6] = newp ? strlen(newp) : 0; + return ks_call(c, 15, in, 12, 1, NULL, 0, desc); +} +static kern_return_t ks_unlock(io_connect_t c, const char *p, const char *desc) { + uint64_t in[4] = {0, (uint64_t)(uintptr_t)p, strlen(p), 0}; + return ks_call(c, 9, in, 4, 0, NULL, 0, desc); +} +static void cfdesc(char *buf, size_t n, CFTypeRef o) { + buf[0] = 0; if (!o) { snprintf(buf, n, "(none)"); return; } + CFStringRef d = CFCopyDescription(o); if (d) { CFStringGetCString(d, buf, n, kCFStringEncodingUTF8); CFRelease(d); } + for (char *p = buf; *p; p++) if (*p == '\n' || *p == '\t') *p = ' '; +} +// STATE line; returns MKBGetDeviceLockState (or -1 if unavailable/timeout) +static int pc_state(io_connect_t c) { + volatile int ls = -1, us = -1; char fa[256] = "n/a", boff[256] = "n/a", lsv[256] = "n/a", infoerr[64] = ""; + uint64_t in0[1] = {0}, out[2] = {0}; uint32_t no = 1; kern_return_t kr = 0xdead; + if (c) { kr = IOConnectCallMethod(c, 7, in0, 1, NULL, 0, out, &no, NULL, NULL); } + if (m_gls) GUARD("STATE MKBGetDeviceLockState", ls = m_gls(NULL)); else L("STATE: MKBGetDeviceLockState symbol NULL, skipped"); + if (m_uns) GUARD("STATE MKBDeviceUnlockedSinceBoot", us = m_uns()); else L("STATE: MKBDeviceUnlockedSinceBoot symbol NULL, skipped"); + if (m_gli) { + volatile CFDictionaryRef d = NULL; GUARD("STATE MKBGetDeviceLockStateInfo", d = m_gli(NULL)); + if (d && CFGetTypeID(d) == CFDictionaryGetTypeID()) { + cfdesc(fa, sizeof fa, CFDictionaryGetValue(d, CFSTR("fa"))); cfdesc(boff, sizeof boff, CFDictionaryGetValue(d, CFSTR("boff"))); cfdesc(lsv, sizeof lsv, CFDictionaryGetValue(d, CFSTR("ls"))); + } else snprintf(infoerr, sizeof infoerr, " (info not a dictionary)"); + } + L("STATE: MKBGetDeviceLockState=%d (0x%x) MKBDeviceUnlockedSinceBoot=%d (0x%x) raw sel7 kr=0x%x flags=0x%llx ; info fa=%s boff=%s ls=%s%s", + ls, ls, us, us, kr, (unsigned long long)out[0], fa, boff, lsv, infoerr); + return ls; +} +static CFDataRef cfd(const char *s) { return CFDataCreate(0, (const UInt8 *)s, s ? (CFIndex)strlen(s) : 0); } + +static int pc_chg(const char *label, CFDataRef o, CFDataRef n) { + volatile int r = -99999; + if (!m_chg) { L("%s: symbol NULL, skipped", label); return -99999; } + GUARD(label, r = m_chg(o, n, NULL, NULL, NULL, NULL)); + L("%s = %d (0x%x)", label, r, r); return r; +} +static void pc_child(const char *logp) { + LF = open(logp, O_WRONLY | O_APPEND | O_CREAT, 0666); + signal(SIGALRM, pc_alrm); + L(""); L("######## PASSCODE PART A/B (pid %d) ########", getpid()); + @autoreleasepool { + mkb_load(); + io_connect_t c = ks_open(); + if (!c) L("A: no raw connection, part A skipped"); + else { + static const char P[] = "2468", W[] = "0000"; + L("-- A0"); pc_state(c); + L("-- A1 set passcode 2468 (sel 15)"); ks_setpass(c, NULL, P, "A1 set"); pc_state(c); + L("-- A2 lock (sel 8)"); { uint64_t in[1] = {0}; ks_call(c, 8, in, 1, 0, NULL, 0, "A2 lock"); } pc_state(c); + L("-- A3 unlock WRONG 0000 (sel 9)"); ks_unlock(c, W, "A3 unlock wrong"); pc_state(c); + L("-- A4 unlock 2468 (sel 9)"); ks_unlock(c, P, "A4 unlock"); pc_state(c); + L("-- A5 verify password (sel 42)"); + { uint8_t st[12]; uint32_t cnt = 1, len = 4; memcpy(st, &cnt, 4); memcpy(st + 4, &len, 4); memcpy(st + 8, P, 4); uint64_t in[1] = {0}; + uint64_t out[2] = {0}; uint32_t no = 0; kern_return_t kr = IOConnectCallMethod(c, 42, in, 1, st, sizeof st, out, &no, NULL, NULL); + L(" A5 verify (sel 42): kr=0x%x (%s) nScalarOut=%u out0=%llu", kr, mach_error_string(kr), no, (unsigned long long)out[0]); } + L("-- A6 remove passcode (sel 15)"); ks_setpass(c, P, NULL, "A6 remove"); + int ls = pc_state(c); + if (ls != 3) L("A: PASSCODE STILL SET !!!!!!!! (lock state %d after A6)", ls); + } + L("-- PART B"); + int ls = pc_state(c); + if (ls != 3) L("B skipped: lock state is %d, not 3 (no passcode) at start of B", ls); + else { + int r1 = pc_chg("B1 MKBKeyBagChangeSystemSecret(NULL, 135790, NULL, NULL)", NULL, cfd("135790")); pc_state(c); + if (r1 != 0) L("B2 skipped: B1 returned %d", r1); + else { + volatile int r = 0; + if (m_lock) { GUARD("B2 MKBLockDevice(NULL)", r = m_lock(NULL, NULL, NULL)); L("B2 MKBLockDevice(NULL) = %d (0x%x)", r, r); } else L("B2 MKBLockDevice: symbol NULL, skipped"); + pc_state(c); + if (m_unl) { + r = -99999; GUARD("B2 MKBUnlockDevice(000000)", r = m_unl(cfd("000000"), NULL, NULL, NULL)); L("B2 MKBUnlockDevice(000000) = %d (0x%x)", r, r); pc_state(c); + r = -99999; GUARD("B2 MKBUnlockDevice(135790)", r = m_unl(cfd("135790"), NULL, NULL, NULL)); L("B2 MKBUnlockDevice(135790) = %d (0x%x)", r, r); pc_state(c); + } else L("B2 MKBUnlockDevice: symbol NULL, skipped"); + int rr = pc_chg("B2 MKBKeyBagChangeSystemSecret(135790, NULL, NULL, NULL)", cfd("135790"), NULL); + if (rr != 0) rr = pc_chg("B2 retry MKBKeyBagChangeSystemSecret(135790, empty CFData, NULL, NULL)", cfd("135790"), cfd("")); + pc_state(c); + } + } + } + L("-- PART A/B child done"); + _exit(0); +} +static void pc_safety_net(void) { + L(""); L("######## FINAL SAFETY NET ########"); + signal(SIGALRM, pc_alrm); + mkb_load(); + io_connect_t c = ks_open(); + int ls = pc_state(c); + if (ls != 3) { + if (!c) L("safety net: no raw connection, cannot remove passcode"); + else { + const char *ps[] = {"2468", "135790"}; + for (int i = 0; i < 2; i++) { char d[64]; snprintf(d, sizeof d, "net unlock %s", ps[i]); ks_unlock(c, ps[i], d); } + for (int i = 0; i < 2; i++) { char d[64]; snprintf(d, sizeof d, "net remove old=%s", ps[i]); ks_setpass(c, ps[i], NULL, d); } + } + ls = pc_state(c); + } + L("FINAL: lock state %d (3 = no passcode)", ls); + if (ls != 3) L("FINAL: PASSCODE MAY STILL BE SET (lock state != 3)"); + pc_keybags("after final step"); +} +static void pc_parent(const char *self, const char *logp) { + pc_keybags("before A0"); + char *av[4] = {(char *)self, "--pc", (char *)logp, NULL}; pid_t pid; int st = 0; + int r = posix_spawn(&pid, self, NULL, NULL, av, environ); + if (r) L("posix_spawn FAIL %d %s", ERR(r)); + else { + int done = 0; for (int i = 0; i < 1500; i++) { if (waitpid(pid, &st, WNOHANG) == pid) { done = 1; break; } usleep(100000); } + if (!done) { kill(pid, SIGKILL); waitpid(pid, &st, 0); L(" !! passcode child killed by parent after 150 s"); } + else if (WIFSIGNALED(st)) L(" !! passcode child died with signal %d (%s)", WTERMSIG(st), strsignal(WTERMSIG(st))); + else L(" (passcode child exit status %d)", WEXITSTATUS(st)); + } + pc_safety_net(); +} + // ---------- driver ---------- static void child(int argc, char **argv) { int sec = atoi(argv[2]); int ai = 3; int idx = 0; @@ -387,16 +558,23 @@ static int run_child(const char *self, int sec, int idx, int mobile) { int main(int argc, char **argv) { setvbuf(stderr, NULL, _IONBF, 0); if (argc > 2 && !strcmp(argv[1], "--sec")) child(argc, argv); - int mobile = 0, nosleep = 0; - for (int i = 1; i < argc; i++) { if (!strcmp(argv[i], "mobile")) mobile = 1; if (!strcmp(argv[i], "nosleep")) nosleep = 1; } - LOGP = mobile ? "/private/var/local/oslog/dptest-mobile.log" : "/private/var/local/oslog/dptest.log"; + if (argc > 2 && !strcmp(argv[1], "--pc")) pc_child(argv[2]); + int mobile = 0, nosleep = 0, passcode = 0; + for (int i = 1; i < argc; i++) { if (!strcmp(argv[i], "mobile")) mobile = 1; if (!strcmp(argv[i], "nosleep")) nosleep = 1; if (!strcmp(argv[i], "passcode")) passcode = 1; } + LOGP = passcode ? "/private/var/local/oslog/dptest-passcode.log" : mobile ? "/private/var/local/oslog/dptest-mobile.log" : "/private/var/local/oslog/dptest.log"; SCR = mobile ? "/private/var/mobile/Library/dptest" : "/private/var/mobile/Library/dptest-root"; LF = open(LOGP, O_WRONLY | O_APPEND | O_CREAT, 0666); if (LF < 0) { fprintf(stderr, "cannot open log %s: %s\n", LOGP, strerror(errno)); return 1; } fchmod(LF, 0666); - header(mobile); + header(passcode ? "passcode" : mobile ? "mobile" : "root"); char self[1024]; uint32_t sl = sizeof self; if (_NSGetExecutablePath(self, &sl)) strcpy(self, "/usr/local/bin/dptest"); + if (passcode) { + if (!nosleep) { L("sleeping 75 s (SpringBoard warm-up)"); sleep(75); L("sleep done"); } + pc_parent(self, LOGP); + L("==================== dptest passcode done ===================="); + return 0; + } if (!nosleep) { L("sleeping 60 s (SpringBoard warm-up)"); sleep(60); L("sleep done"); } const char *names[] = {"", "1 FS statfs", "2 data-protection classes", "3 MobileKeyBag", "4 AppleKeyStore user client", "5 Keychain", "6 MobileGestalt", "7 Containers"}; for (int s = 1; s <= 7; s++) { diff --git a/tools/ios27/keystore/install-dptest.sh b/tools/ios27/keystore/install-dptest.sh index e7481c4c1..0e2c85edb 100755 --- a/tools/ios27/keystore/install-dptest.sh +++ b/tools/ios27/keystore/install-dptest.sh @@ -9,13 +9,19 @@ cp "$BIN" "$M/usr/local/bin/dptest"; chmod 0755 "$M/usr/local/bin/dptest" for p in com.scion.dptest.plist com.scion.dptest.mobile.plist; do cp "$H/$p" "$M/System/Library/LaunchDaemons/$p"; chmod 0644 "$M/System/Library/LaunchDaemons/$p" done +# passcode job: only with DPTEST_PASSCODE=1, otherwise removed (file here, cache entry in the python block) +PCP="$M/System/Library/LaunchDaemons/com.scion.dptest.passcode.plist" +if [ "${DPTEST_PASSCODE:-0}" = 1 ]; then + cp "$H/com.scion.dptest.passcode.plist" "$PCP"; chmod 0644 "$PCP" + [ "$(id -u)" = 0 ] && chown 0:0 "$PCP" +else rm -f "$PCP"; fi mkdir -p "$M/private/var/local/oslog"; chmod 0777 "$M/private/var/local/oslog" if [ "$(id -u)" = 0 ]; then chown 0:0 "$M/usr/local/bin/dptest" "$M/System/Library/LaunchDaemons/com.scion.dptest.plist" "$M/System/Library/LaunchDaemons/com.scion.dptest.mobile.plist" chown 0:0 "$M/usr/local/bin" 2>/dev/null || true else echo "WARNING: not root, ownership not set"; fi # this root loads jobs from the combined cache System/Library/xpc/launchd.plist (key = plist path), like install-metal27.py -/usr/bin/python3 - "$M" "$H" <<'PY' +DPTEST_PASSCODE=${DPTEST_PASSCODE:-0} /usr/bin/python3 - "$M" "$H" <<'PY' import plistlib, sys, os, shutil M, H = sys.argv[1:3] lp = M + '/System/Library/xpc/launchd.plist' @@ -25,10 +31,15 @@ if os.path.exists(lp): for n in ('com.scion.dptest', 'com.scion.dptest.mobile'): k = '/System/Library/LaunchDaemons/%s.plist' % n d['LaunchDaemons'][k] = plistlib.load(open('%s/%s.plist' % (H, n), 'rb')) + pk = '/System/Library/LaunchDaemons/com.scion.dptest.passcode.plist' + if os.environ.get('DPTEST_PASSCODE') == '1': + d['LaunchDaemons'][pk] = plistlib.load(open(H + '/com.scion.dptest.passcode.plist', 'rb')) + else: + d['LaunchDaemons'].pop(pk, None) with open(lp, 'r+b') as f: f.seek(0); f.write(plistlib.dumps(d, fmt=plistlib.FMT_BINARY)); f.truncate() print('launchd.plist: jobs added/updated (%d jobs total)' % len(d['LaunchDaemons'])) else: print('no xpc/launchd.plist cache; relying on LaunchDaemons dir only') PY -echo "installed: $M/usr/local/bin/dptest ($(basename "$BIN")), 2 plists, oslog dir. Results: /private/var/local/oslog/dptest.log, dptest-mobile.log" +echo "installed: $M/usr/local/bin/dptest ($(basename "$BIN")), 2 plists (+passcode job: ${DPTEST_PASSCODE:-0}), oslog dir. Results: /private/var/local/oslog/dptest.log, dptest-mobile.log" diff --git a/tools/ios27/keystore/uninstall-dptest.sh b/tools/ios27/keystore/uninstall-dptest.sh index 81631c5db..cdd41d6af 100755 --- a/tools/ios27/keystore/uninstall-dptest.sh +++ b/tools/ios27/keystore/uninstall-dptest.sh @@ -2,13 +2,13 @@ # uninstall-dptest.sh MOUNTPOINT : removes binary, plists, launchd cache entries (logs left in place) set -euo pipefail M=${1:?usage: uninstall-dptest.sh MOUNTPOINT} -rm -f "$M/usr/local/bin/dptest" "$M/System/Library/LaunchDaemons/com.scion.dptest.plist" "$M/System/Library/LaunchDaemons/com.scion.dptest.mobile.plist" +rm -f "$M/usr/local/bin/dptest" "$M/System/Library/LaunchDaemons/com.scion.dptest.plist" "$M/System/Library/LaunchDaemons/com.scion.dptest.mobile.plist" "$M/System/Library/LaunchDaemons/com.scion.dptest.passcode.plist" /usr/bin/python3 - "$M" <<'PY' import plistlib, sys, os lp = sys.argv[1] + '/System/Library/xpc/launchd.plist' if os.path.exists(lp): d = plistlib.load(open(lp, 'rb')) - for n in ('com.scion.dptest', 'com.scion.dptest.mobile'): d['LaunchDaemons'].pop('/System/Library/LaunchDaemons/%s.plist' % n, None) + for n in ('com.scion.dptest', 'com.scion.dptest.mobile', 'com.scion.dptest.passcode'): d['LaunchDaemons'].pop('/System/Library/LaunchDaemons/%s.plist' % n, None) with open(lp, 'r+b') as f: f.seek(0); f.write(plistlib.dumps(d, fmt=plistlib.FMT_BINARY)); f.truncate() PY