fix(xnu): let Safari start its WebKit extensions from the OS cryptex

Show the OS cryptex where iPadOS looks for it, under /private/preboot,
and let a board option skip the launch constraints that refused App
cryptex daemons and owner-trusted binaries. The App cryptex trust module
joins the owner module of the trust bundle, and the lookup trace also
records lsd.
This commit is contained in:
bluseliu50 committed 2026-10-25 22:53:57 +08:00
1 parent 688fa88c65
commit f6e4012f71
6 files changed
+249 -22

No files matched your search

+24 -18
View File
@@ -1,46 +1,52 @@
diff --git a/bsd/vfs/vfs_lookup.c b/bsd/vfs/vfs_lookup.c
index 3db098f..16d6bbe 100644
--- a/bsd/vfs/vfs_lookup.c
+++ b/bsd/vfs/vfs_lookup.c
@@ -100,6 +100,33 @@
@@ -100,6 +100,47 @@
#if NAMEDRSRCFORK
#include <sys/xattr.h>
#endif
+/*
+ * Board bring-up diagnostic: the distinct lookups that failed for MobileSafari (path and errno), read by the boot log.
+ * Board bring-up diagnostic: the distinct lookups that failed for MobileSafari (S) and lsd (L), errno and path (its last 96 characters
+ * when longer), read by the boot log. Localization folders, Trial and logging preferences are left out: they fail by design.
+ */
+char native_lookup_log[96][112];
+char native_lookup_log[160][112];
+volatile unsigned native_lookup_count;
+static bool
+native_lookup_has(const char *text, const char *part)
+{
+ const size_t length = strlen(part);
+ for (; *text; ++text) {
+ if (strncmp(text, part, length) == 0) {
+ return true;
+ }
+ }
+ return false;
+}
+static void
+native_lookup_note(const char *path, int error)
+{
+ char name[MAXCOMLEN + 1];
+ proc_selfname(name, sizeof(name));
+ if (strcmp(name, "MobileSafari") != 0) {
+ const char tag = !strcmp(name, "MobileSafari") ? 'S' : !strcmp(name, "lsd") ? 'L' : 0;
+ if (!tag || native_lookup_has(path, ".lproj") || native_lookup_has(path, "/Trial/") ||
+ native_lookup_has(path, "/Preferences/Logging/")) {
+ return;
+ }
+ const size_t length = strlen(path);
+ char entry[112];
+ snprintf(entry, sizeof(entry), "e%d %s", error, path);
+ unsigned seen = native_lookup_count < 96 ? native_lookup_count : 96;
+ snprintf(entry, sizeof(entry), "%c e%d %s%s", tag, error, length > 96 ? "..." : "", length > 96 ? path + length - 96 : path);
+ unsigned seen = native_lookup_count < 160 ? native_lookup_count : 160;
+ for (unsigned i = 0; i < seen; ++i) {
+ if (strncmp(native_lookup_log[i], entry, sizeof(entry)) == 0) {
+ return;
+ }
+ }
+ unsigned slot = os_atomic_inc_orig(&native_lookup_count, relaxed);
+ if (slot < 96) {
+ if (slot < 160) {
+ strlcpy(native_lookup_log[slot], entry, sizeof(native_lookup_log[slot]));
+ }
+}
+
/*
* The minimum volfs-style pathname is 9.
* Example: "/.vol/1/2"
@@ -526,6 +553,9 @@
vnode_put(ndp->ni_vp);
}
error_out:
+ if (error && error != ERECYCLE && cnp->cn_pnbuf) {
+ native_lookup_note(cnp->cn_pnbuf, error);
+ }
if (startdir_with_usecount) {
vnode_rele(startdir_with_usecount);
startdir_with_usecount = NULLVP;
@@ -0,0 +1,176 @@
diff --git a/bsd/kern/kern_exec.c b/bsd/kern/kern_exec.c
--- a/bsd/kern/kern_exec.c
+++ b/bsd/kern/kern_exec.c
@@ -7483,7 +7483,162 @@
native_gpu_addon_written++;
}
at += (packed + 7) & ~7ULL;
+ }
+}
+/* The OS cryptex where iPadOS looks for it. The root volume's entries for cryptex content are links into /System/Cryptexes/OS, which
+ * leads to /private/preboot/Cryptexes/OS; WebKit's processes are ExtensionKit extensions reached that way
+ * (/System/Library/ExtensionKit/Extensions/WebContentExtension.appex and its Networking and GPU siblings), so with the path missing
+ * Safari could not start them and its pages never loaded. The SystemOS volume is already the upper layer of the root union: its
+ * /System and /usr are bound read-only over /private/preboot/Cryptexes/OS/{System,usr}, not as unions, so a lookup there stays on the
+ * SystemOS volume and never meets the root's links again. The two mount points are owner Data directories under
+ * /private/var/ngpu/preboot, shown over the root's empty /private/preboot as a read-only union. Never fatal; stage and status go to the
+ * boot log: 1 mount points made, 2 preboot shown, 3 System bound, 4 usr bound, 5 an extension of WebKit visible through the path. */
+volatile int native_cryptex_os_stage = 0;
+volatile int native_cryptex_os_status = -1;
+static int
+native_cryptex_os_bind(proc_t p, user_addr_t scratch, const char *source, const char *point, int flags)
+{
+ struct mount_args request = {.type = scratch + 0xa00, .path = scratch + 0xa20, .flags = MNT_RDONLY | flags,
+ .data = scratch + 0xb00};
+ int32_t result = 0;
+ int error;
+ if ((error = copyout("bindfs", request.type, 7)) || (error = copyout(point, request.path, strlen(point) + 1)) ||
+ (error = copyout(source, request.data, strlen(source) + 1))) {
+ return error;
+ }
+ native_bindfs_boot_mount = 1;
+ error = mount(p, &request, &result);
+ native_bindfs_boot_mount = 0;
+ return error;
+}
+static void
+native_cryptex_os_view(proc_t p, user_addr_t scratch)
+{
+ static const char *const points[] = {"/private/var/ngpu/preboot/Cryptexes/OS/System/", "/private/var/ngpu/preboot/Cryptexes/OS/usr/"};
+ vnode_t probe = NULLVP;
+ int error = 0;
+ for (size_t i = 0; i < sizeof(points) / sizeof(points[0]) && !error; ++i) {
+ error = native_gpu_addon_parents(p, scratch, points[i]);
+ }
+ if (error) {
+ native_cryptex_os_status = error;
+ return;
+ }
+ native_cryptex_os_stage = 1;
+ if ((error = native_cryptex_os_bind(p, scratch, "/private/var/ngpu/preboot", "/private/preboot", MNT_UNION))) {
+ native_cryptex_os_status = error;
+ return;
+ }
+ native_cryptex_os_stage = 2;
+ if ((error = native_cryptex_os_bind(p, scratch, "/System", "/private/preboot/Cryptexes/OS/System", 0))) {
+ native_cryptex_os_status = error;
+ return;
+ }
+ native_cryptex_os_stage = 3;
+ if ((error = native_cryptex_os_bind(p, scratch, "/usr", "/private/preboot/Cryptexes/OS/usr", 0))) {
+ native_cryptex_os_status = error;
+ return;
+ }
+ native_cryptex_os_stage = 4;
+ error = vnode_lookup("/System/Cryptexes/OS/System/Library/ExtensionKit/Extensions/WebContentExtension.appex/Info.plist", 0, &probe,
+ vfs_context_current());
+ if (!error) {
+ vnode_put(probe);
+ native_cryptex_os_stage = 5;
+ }
+ native_cryptex_os_status = error;
+}
+/* The device answers MobileGestalt cached while the display gamut was P3: removed so they are worked out again from the device tree
+ * (now sRGB). 0 removed, ENOENT none there (already regenerated or never made). */
+volatile int native_gestalt_cache_status = -1;
+static void
+native_gestalt_cache_drop(proc_t p, user_addr_t scratch)
+{
+ static const char cache[] = "/private/var/containers/Shared/SystemGroup/systemgroup.com.apple.mobilegestaltcache/Library/Caches/"
+ "com.apple.MobileGestalt.plist";
+ struct unlink_args request = {.path = scratch + 0xc00};
+ int32_t result = 0;
+ int error = copyout(cache, request.path, sizeof(cache));
+ if (!error) {
+ error = unlink(p, &request, &result);
+ }
+ native_gestalt_cache_status = error;
+}
+/* LaunchServices' database of this Data store was built while /System/Cryptexes/OS led nowhere, so it never registered what lies behind
+ * it (WebKit's extensions among them) and lsd does not register system content again on a later boot of the same OS build. Its files
+ * are removed once, so lsd builds the database again with the path in place; the marker file keeps later boots from repeating it.
+ * Status for the boot log: files removed, or -2 done before, or -errno. */
+#include <sys/dirent.h>
+volatile int native_ls_store_status = -1;
+static void
+native_ls_store_drop(proc_t p, user_addr_t scratch)
+{
+ static const char marker[] = "/private/var/ngpu/ls-store-rebuilt-1";
+ static const char caches[] = "/private/var/mobile/Containers/Data/InternalDaemon/5C583048-0C53-456F-B110-BB12068B787F/Library/Caches";
+ static const char prefix[] = "com.apple.LaunchServices-";
+ vfs_context_t ctx = vfs_context_current();
+ vnode_t vp = NULLVP;
+ if (!vnode_lookup(marker, 0, &vp, ctx)) {
+ vnode_put(vp);
+ native_ls_store_status = -2;
+ return;
+ }
+ int error = vnode_lookup(caches, 0, &vp, ctx);
+ if (error) {
+ native_ls_store_status = -error;
+ return;
+ }
+ char names[8][64];
+ unsigned found = 0;
+ char *buffer = kalloc_data(4096, Z_WAITOK);
+ uio_t uio = buffer ? uio_create(1, 0, UIO_SYSSPACE, UIO_READ) : NULL;
+ int eof = 0;
+ while (uio && !eof && found < 8) {
+ uio_reset(uio, uio_offset(uio), UIO_SYSSPACE, UIO_READ);
+ uio_addiov(uio, CAST_USER_ADDR_T(buffer), 4096);
+ int entries = 0;
+ if ((error = VNOP_READDIR(vp, uio, 0, &eof, &entries, ctx))) {
+ break;
+ }
+ const size_t used = 4096 - (size_t)uio_resid(uio);
+ if (!used) {
+ break;
+ }
+ for (size_t at = 0; at + sizeof(struct dirent) - sizeof(((struct dirent *)0)->d_name) <= used && found < 8;) {
+ const struct dirent *entry = (const struct dirent *)(buffer + at);
+ if (!entry->d_reclen) {
+ break;
+ }
+ if (entry->d_namlen < sizeof(names[0]) && !strncmp(entry->d_name, prefix, sizeof(prefix) - 1)) {
+ memcpy(names[found], entry->d_name, entry->d_namlen);
+ names[found++][entry->d_namlen] = 0;
+ }
+ at += entry->d_reclen;
+ }
+ }
+ if (uio) {
+ uio_free(uio);
+ }
+ if (buffer) {
+ kfree_data(buffer, 4096);
+ }
+ vnode_put(vp);
+ if (error) {
+ native_ls_store_status = -error;
+ return;
+ }
+ int removed = 0;
+ char path[192];
+ for (unsigned i = 0; i < found; ++i) {
+ snprintf(path, sizeof(path), "%s/%s", caches, names[i]);
+ struct unlink_args request = {.path = scratch + 0xc00};
+ int32_t result = 0;
+ if (!copyout(path, request.path, strlen(path) + 1) && !unlink(p, &request, &result)) {
+ removed++;
+ }
}
+ (void)native_gpu_addon_write(marker, (const uint8_t *)"1\n", 2, 0644, ctx);
+ native_ls_store_status = removed;
}
static void
native_gpu_addon_prepare(proc_t p, user_addr_t scratch)
@@ -7512,6 +7667,9 @@
"7570.iPadOS_17_Flat_Background-1024w-1366h@2x~ipad.ca/assets/J120.HEIC",
"/Library/Wallpaper/Collections/iPadOS_17~ipad.wallpaperCollection/Wallpapers/7570.iPadOS_17_Flat-1024w-1366h@2x~ipad.wallpaper/"
"7570.iPadOS_17_Flat_Background-1024w-1366h@2x~ipad.ca/J120.png", &native_gpu_addon3_stage, &native_gpu_addon3_status);
+ native_cryptex_os_view(p, scratch);
+ native_gestalt_cache_drop(p, scratch);
+ native_ls_store_drop(p, scratch);
}
/* The deletable system apps (Weather, Notes, Calendar, ...) ship in the OS volume's /private/var/staged_system_apps. A restore copies
@@ -0,0 +1,31 @@
diff --git a/security/mac_process.c b/security/mac_process.c
index b64da95..c548aa1 100644
--- a/security/mac_process.c
+++ b/security/mac_process.c
@@ -77,6 +77,7 @@
#include <mach/mach_types.h>
#include <kern/task.h>
#include <kern/zalloc.h>
+#include <pexpert/pexpert.h>
#include <os/hash.h>
@@ -892,9 +893,18 @@ mac_proc_check_settid(proc_t curp, uid_t uid, gid_t gid)
return error;
}
+// Board bring-up: a system service the original AMFI gates on a platform binary is refused with "Launch Constraint
+// Violation" when the binary is an App cryptex daemon or an owner-trusted one, although its signature and trust-cache
+// entry are valid: the constraint category of an owner or cryptex binary is not the platform one. The original release
+// AMFI ignores its own amfi_* boot arguments, so the check is cleared here when the board asks for it. An option of the
+// instrumented board only: the boot argument, not the default, decides.
int
mac_proc_check_launch_constraints(proc_t curp, struct image_params *imgp, os_reason_t *reasonp)
{
+ uint32_t skip = 0;
+ if (PE_parse_boot_argn("sheng-no-lc", &skip, sizeof(skip)) && skip != 0) {
+ return 0;
+ }
char *fatal_failure_desc = NULL;
size_t fatal_failure_desc_len = 0;
+2
View File
@@ -51,3 +51,5 @@
../../../../../patches/xnu/zzx-pmap-sync-64bit-pa.patch
../../../../../patches/xnu/zzy-native-gpu-addon-mount.patch
../../../../../patches/xnu/zzy-staged-system-apps.patch
../../../../../patches/xnu/zzyb-native-cryptex-os-view.patch
../../../../../patches/xnu/zzyc-owner-skip-launch-constraints.patch
+3 -2
View File
@@ -37,8 +37,9 @@ if [ -z "${UNIXNU_PATCH_LIST:-}" ]; then
# Remove them first so earlier independent patches retain their idempotent checks.
# The same holds for the boot-time GPU addon mount and the staged system apps copy (zzy), which edit the owner Data mount function
# inside the systemos patch. Reverse newest first; the loop below re-applies them in name order.
for LATE_PATCH in "$ROOT/patches/xnu/zzz-boot-progress.patch" "$ROOT/patches/xnu/zzy-staged-system-apps.patch" \
"$ROOT/patches/xnu/zzy-native-gpu-addon-mount.patch"; do
for LATE_PATCH in "$ROOT/patches/xnu/zzz-boot-progress.patch" "$ROOT/patches/xnu/zzyb-native-cryptex-os-view.patch" \
"$ROOT/patches/xnu/zzy-staged-system-apps.patch" "$ROOT/patches/xnu/zzy-native-gpu-addon-mount.patch" \
"$ROOT/patches/xnu/zzyc-owner-skip-launch-constraints.patch"; do
if git -C .cache/xnu-work apply --reverse --check "$LATE_PATCH" >/dev/null 2>&1; then
git -C .cache/xnu-work apply --reverse "$LATE_PATCH"
fi
+13 -2
View File
@@ -17,9 +17,20 @@ assert count == 2
offsets = struct.unpack_from('<2I', original, 4)
originals = [original[offsets[0]:offsets[1]], original[offsets[1]:]]
trust = json.loads((ROOT / 'build/adreno-metal-plugin/trust.json').read_text())
# The App cryptex (Cryptex1,AppOS) runs from owner Data, so its own trust module's hashes must be trusted too: the bundle
# carries exactly one owner module followed by both original Apple modules, and the cryptex entries join the owner module.
cryptex_trust = ROOT / 'build/app-cryptex/app-trustcache.bin'
cryptex_hashes = []
if cryptex_trust.is_file():
module = cryptex_trust.read_bytes()
entries = struct.unpack_from('<I', module, 20)[0]
if struct.unpack_from('<I', module)[0] != 2 or len(module) != 24 + entries * 24:
raise SystemExit('The App cryptex trust module is not a complete version-2 module')
cryptex_hashes = [module[24 + 24 * entry:24 + 24 * entry + 20] for entry in range(entries)]
# DarwinBoot's ValidTrustCaches (and the kernel's binary search) need the hashes of a module in strictly ascending order.
hashes = sorted([bytes.fromhex(trust['cdhash']), bytes.fromhex(trust['daemon_cdhash']), bytes.fromhex(trust['turnip_cdhash'])])
if len(set(hashes)) != len(hashes):
hashes = sorted({bytes.fromhex(trust['cdhash']), bytes.fromhex(trust['daemon_cdhash']),
bytes.fromhex(trust['turnip_cdhash']), *cryptex_hashes})
if len(hashes) != 3 + len(set(cryptex_hashes)):
raise SystemExit('Duplicate CDHash')
cdhash = hashes[0]
identifier = uuid.uuid5(uuid.NAMESPACE_URL, 'org.blu-sharky.adreno-metal:' + ''.join(h.hex() for h in hashes))