From 1bfb1dd385ab4af77efb1c9cc0a0517b560f05c7 Mon Sep 17 00:00:00 2001 From: Claudius Date: Sat, 14 Nov 2026 00:09:09 +0000 Subject: [PATCH 1/7] Galaxy S9 modem on iOS 27: kern.hakios.cp.info/.addr for user space, scion-celld (pdp_ip0 as a configd network service, rank Last), idle polling in the steady state, cp_rfs=1 file opens answered ENOENT; all off by default --- drivers/modem/hakios.shannon.c | 106 +++++++++++- profiles/ios-27.conf | 2 +- tools/ios27/modem/66-celld.sh | 82 +++++++++ tools/ios27/modem/build.sh | 18 ++ tools/ios27/modem/scion-celld.c | 296 ++++++++++++++++++++++++++++++++ tools/ios27/modem/scion-fetch.m | 67 ++++++++ 6 files changed, 566 insertions(+), 5 deletions(-) create mode 100644 tools/ios27/modem/66-celld.sh create mode 100755 tools/ios27/modem/build.sh create mode 100644 tools/ios27/modem/scion-celld.c create mode 100644 tools/ios27/modem/scion-fetch.m diff --git a/drivers/modem/hakios.shannon.c b/drivers/modem/hakios.shannon.c index 5b133e0d2..f5a03d228 100644 --- a/drivers/modem/hakios.shannon.c +++ b/drivers/modem/hakios.shannon.c @@ -131,6 +131,8 @@ * cp_if_kb=N with cp_if=1: at most N KB through pdp_ip0 in a boot, both ways (2048 by default; the owner's mobile plan), then it drops * cp_route=1 with cp_if=1: an IPv4 default route through pdp_ip0 (only when there is none yet: Wi-Fi's wins) * cp_nv=2 the phone's own NV data read at boot from the EFS partition (/nv_data.bin, read-only, never mounted, never written), kept in RAM only; the CP's file requests for it (RFS NV read/write) are answered from that copy and its writes kept there for the run, never written back + * cp_rfs=1 with cp_nv=2: the CP's file opens (RFS command 0x11, e.g. CP_OMC_AUDIO_SLSI.bin) answered "not there" (handle -1, + * ENOENT) from RAM; nothing is read or opened. Off: they stay unanswered * cp_vss=0 the image's VSS part (the voice processor's) is not copied * cp_rev=N the board's revision told to the modem (26, this phone's) * cp_ds=N the dual SIM field told to the modem (0) @@ -154,6 +156,7 @@ #include #include #include +#include #include #include #include @@ -354,6 +357,13 @@ static struct { uint32_t if_on, route_on; /* cp_if=1, cp_route=1 */ uint32_t if_kb; /* cp_if_kb=N */ uint32_t dropped[NRINGS]; /* such frames dropped */ + /* the modem's state for user space (kern.hakios.cp.info; kern.hakios.cp.addr for root): no identity in any of it */ + uint8_t dns4b[4]; /* the context's second DNS server (never logged) */ + int tech, bars, dbm; /* the registration's technology, the icon's bars, the strength in dBm; -1/0 none yet */ + char oper[8]; /* the serving network's operator code (country + network: the same for everyone on it) */ + uint32_t gen; /* bumped on every change of the above */ + uint32_t rfs_open, rfs_opens; /* cp_rfs=1: the CP's file opens answered "not there" (from RAM: nothing is read) */ + uint64_t busy_ms; /* the last time something came from the CP or waited to go to it */ } cp; static int cp_go; @@ -1243,6 +1253,10 @@ fmt_received(const uint8_t *m, uint32_t n) /* the address and the first DNS server, for stage 7: used only if both look like host addresses */ memcpy(cp.ip4, m + 10, 4); memcpy(cp.dns4, m + 14, 4); + if (n >= 7 + 3 + 12) { + memcpy(cp.dns4b, m + 18, 4); + } + cp.gen++; cp.have4 = cp.ip4[0] != 0 && cp.ip4[0] != 127 && cp.ip4[0] < 224 && cp.dns4[0] != 0 && cp.dns4[0] != 127 && cp.dns4[0] < 224; if (!cp.ping_given) { @@ -1289,6 +1303,8 @@ fmt_received(const uint8_t *m, uint32_t n) cp.reg_changes++; } cp.reg_state = m[9]; + cp.tech = m[7]; + cp.gen++; if ((m[9] == 2 || m[9] == 6) && cp.registered_ms == 0 && cp.normal_asked) { cp.registered_ms = now_ms(); say("cp: REGISTERED %llu ms after normal mode was asked for\n", cp.registered_ms - cp.normal_ms); @@ -1315,10 +1331,20 @@ fmt_received(const uint8_t *m, uint32_t n) } } code[k] = 0; + if (k != 0) { + strlcpy(cp.oper, code, sizeof(cp.oper)); + cp.gen++; + } say("cp: SERVING NETWORK: operator code %s (of %u bytes)\n", k != 0 ? code : "not found in it", n - 7); } else if (group == 0x07 && (index == 0x01 || index == 0x06) && n >= 7 + 4) { /* signal: the display group's icon (number 1) and strength (number 6) words: levels, no identity */ cp.signals++; + if (index == 1 && (m[7] & 1) != 0) { + cp.bars = m[8]; /* the icon's flags (bit 0: the signal's part is valid), then its bars */ + } else if (index == 6) { + cp.dbm = -(int)m[7]; /* the strength report: minus dBm */ + } + cp.gen++; if (cp.signals <= 6 || cp.signals % 10 == 0) { say("cp: SIGNAL (%s, the %uth): %02x %02x %02x %02x %02x %02x\n", index == 1 ? "icons" : "strength", cp.signals, m[7], m[8], m[9], m[10], n > 11 ? m[11] : 0, n > 12 ? m[12] : 0); @@ -1473,8 +1499,20 @@ rfs_received(const uint8_t *m, uint32_t n) cp.rfs_requests, cmd, id, len); return; } - say("cp: FILE REQUEST %u from the CP (not answered): command %#x, id %u, %u bytes in all\n", cp.rfs_requests, cmd, id, - len); + if (cmd == 0x11 && cp.rfs_open != 0 && cp.nv_ram != NULL) { + /* cp_rfs=1: an OPEN FILE answered from RAM: the file is not there. The confirmation is the RFS header and two + * 32-bit words, the handle (-1) and errno (2, ENOENT): 14 bytes, as Samsung's file server sends it. Nothing is + * read or opened anywhere */ + static const int32_t none[2] = { -1, 2 }; + + cp.rfs_opens++; + (void)rfs_answer(0x11, id, (const uint8_t *)none, sizeof(none)); + say("cp: FILE REQUEST %u from the CP: OPEN FILE (id %u, %u bytes): answered \"not there\" (ENOENT), the %uth\n", + cp.rfs_requests, id, len, cp.rfs_opens); + } else { + say("cp: FILE REQUEST %u from the CP (not answered): command %#x, id %u, %u bytes in all\n", cp.rfs_requests, cmd, + id, len); + } /* a path, if the request starts with one after a few words: printable text of 4 characters or more with a '/' or '.' */ for (uint32_t i = 6; i + 4 < n && i < 64; i++) { uint32_t k = i; @@ -1829,6 +1867,50 @@ pdp_attach(void) } } +/* kern.hakios.cp.info: the modem's state as one line for user space (scion-celld, the status bar's library): + * registration, technology, bars, dBm, operator code, SIM status, phone state, data (1: pdp_ip0 up with its address), + * bytes through pdp_ip0 this boot, the budget, and a counter that moves on every change. No identity in it. */ +static int +cp_info_sysctl SYSCTL_HANDLER_ARGS +{ +#pragma unused(oidp, arg1, arg2) + char line[200]; + + snprintf(line, sizeof(line), "stage=%u state=%u reg=%d tech=%d bars=%d dbm=%d oper=%s sim=%d phone=%d data=%d used=%llu " + "budget=%llu gen=%u", cp.stage, cp.state, cp.reg_state, cp.tech, cp.bars, cp.dbm, cp.oper[0] != 0 ? cp.oper : "-", + cp.sim_status, cp.phone_state, pdp.addressed && cp.data_step == 3 ? 1 : 0, pdp.in_bytes + pdp.out_bytes, + (uint64_t)cp.if_kb * 1024, cp.gen); + return SYSCTL_OUT(req, line, strlen(line) + 1); +} + +/* kern.hakios.cp.addr: "ADDRESS DNS1 DNS2" of the active context, root only (empty while there is none): what + * scion-celld gives configd. Never logged. */ +static int +cp_addr_sysctl SYSCTL_HANDLER_ARGS +{ +#pragma unused(oidp, arg1, arg2) + char line[64] = ""; + + if (req->newptr != USER_ADDR_NULL) { + return EPERM; + } + if (!kauth_cred_issuser(kauth_cred_get())) { + return EPERM; + } + if (pdp.addressed && cp.have4 && cp.data_step == 3) { + snprintf(line, sizeof(line), "%u.%u.%u.%u %u.%u.%u.%u %u.%u.%u.%u", cp.ip4[0], cp.ip4[1], cp.ip4[2], cp.ip4[3], + cp.dns4[0], cp.dns4[1], cp.dns4[2], cp.dns4[3], cp.dns4b[0], cp.dns4b[1], cp.dns4b[2], cp.dns4b[3]); + } + int e = SYSCTL_OUT(req, line, strlen(line) + 1); + bzero(line, sizeof(line)); + return e; +} + +SYSCTL_PROC(_kern_hakios_cp, OID_AUTO, info, CTLTYPE_STRING | CTLFLAG_RD | CTLFLAG_LOCKED, NULL, 0, cp_info_sysctl, "A", + "the modem's state for user space: registration, technology, bars, dBm, operator code, SIM, phone state, data"); +SYSCTL_PROC(_kern_hakios_cp, OID_AUTO, addr, CTLTYPE_STRING | CTLFLAG_RD | CTLFLAG_LOCKED, NULL, 0, cp_addr_sysctl, "A", + "the data context's address and DNS servers (root only; empty without one)"); + static void data_received(uint8_t ch, const uint8_t *p, uint32_t n) { @@ -2224,6 +2306,8 @@ poll_once(void) if (pending != 0) { uint16_t msg = (uint16_t)rd(cp.mbox, MBOX_ISSR(WORD_CP_MSG)); + cp.busy_ms = now_ms(); + wr(cp.mbox, MBOX_INTCR0, pending); if (pending & (1u << (16 + IRQ_CP_MSG))) { alive = handle_msg(msg); @@ -2662,7 +2746,19 @@ cp_thread(__unused void *arg, __unused wait_result_t result) if (now_ms() >= until) { break; } - IOSleep(1); + if (cp.if_on && cp.data_step == 3 && pdp.ifp != NULL && cp.normal_asked && !cp.back_asked) { + /* the everyday steady state (cp_if=1, the context up): every 1 ms while anything moves, every 10 ms after + * 0.3 s of quiet, every 50 ms after 5 s (the mailbox is polled: its interrupts stay masked) */ + uint64_t quiet; + + if (pdp.head != pdp.tail) { + cp.busy_ms = now_ms(); + } + quiet = now_ms() - cp.busy_ms; + IOSleep(quiet > 5000 ? 50 : quiet > 300 ? 10 : 1); + } else { + IOSleep(1); + } } if (cp.normal_asked) { say("cp: STAGE 5 SUMMARY: normal mode asked; registered: %s; last registration state %d; %u changes of it; %u " @@ -2698,7 +2794,7 @@ hakios_shannon_quiesce(void) { if (cp.started) { cp_go = 2; - for (uint32_t i = 0; i < 300 && cp.started; i++) { + for (uint32_t i = 0; i < 500 && cp.started; i++) { IOSleep(10); } } @@ -2719,6 +2815,8 @@ hakios_shannon_init(void) cp.wait_s = cp.stage >= 4 ? 25 : 20; cp.delay_s = 30; cp.phone_state = cp.sim_status = cp.reg_state = -1; + cp.tech = cp.bars = -1; + PE_parse_boot_argn("cp_rfs", &cp.rfs_open, sizeof(cp.rfs_open)); cp.vss_on = 1; PE_parse_boot_argn("cp_nv", &cp.nv_file, sizeof(cp.nv_file)); PE_parse_boot_argn("cp_if", &cp.if_on, sizeof(cp.if_on)); diff --git a/profiles/ios-27.conf b/profiles/ios-27.conf index 04a9378c8..08d247e05 100644 --- a/profiles/ios-27.conf +++ b/profiles/ios-27.conf @@ -14,7 +14,7 @@ profile ios-27 xnu_commit ac9718fb1af618d5ce8678d0dc6e8a58f252216f xnu_fork https://github.com/Scion-Project/xnu.git fork_branch scion/xnu-12377 - fork_commit 0c9b2cd0e93f9c6aace51f0a392cb5bfeb2618dd + fork_commit 618114acd839cf950f52b781a5959d8d9082894a fork_copy profiles/ios-27.copy fork_stage xnu/stage/00_setup_coreentitlements.sh drivers none diff --git a/tools/ios27/modem/66-celld.sh b/tools/ios27/modem/66-celld.sh new file mode 100644 index 000000000..6926ed4a9 --- /dev/null +++ b/tools/ios27/modem/66-celld.sh @@ -0,0 +1,82 @@ +#!/bin/bash +# 66-celld.sh ROOT BIN [undo|test|untest] : the modem's data context as an iOS network service. SPDX-License-Identifier: MIT-0 +# +# Installs scion-celld (BIN/scion-celld, from build.sh) as /usr/local/bin/scion-celld and its job com.scion.celld in iOS's +# launchd cache (/System/Library/xpc/launchd.plist): run at load as root; it exits 0 at once on a boot without the modem +# (no cp=) and launchd leaves it; it is started again only if it fails. With the modem's cp_if=1 it publishes pdp_ip0 to +# configd (see scion-celld.c), so names resolve and iOS routes over cellular when nothing better is up. +# test also scion-fetch and the TEST job com.scion.celltest (never on a release root): once a boot it waits up to 15 min +# for the context, then into /private/var/log/scion-celltest.log (and the console) configd's view, the routes, +# and one https GET by name (CELLTEST_URL in /private/var/scion-test/celltest-url, default https://example.com/) +# untest removes the test job and scion-fetch; undo removes everything this step added +set -euo pipefail +R=$1; B=${2:-}; how=${3:-add} +J=$R/System/Library/xpc/launchd.plist +[[ -f $J ]] || { echo "66-celld: $R has no launchd.plist" >&2; exit 1; } +T=$R/usr/local/libexec/scion-celltest.sh +case $how in +add|test) + install -d "$R/usr/local/bin" "$R/usr/local/libexec" + install -m 0755 "$B/scion-celld" "$R/usr/local/bin/scion-celld" + if [[ $how == test ]]; then + install -m 0755 "$B/scion-fetch" "$R/usr/local/bin/scion-fetch" + mkdir -p "$R/private/var/scion-test" + cat > "$T" <<'SH' +#!/var/jb/bin/bash +# scion-celltest (tools/ios27/modem/66-celld.sh test): a test root's cellular check, once a boot +export PATH=/var/jb/usr/bin:/var/jb/usr/sbin:/var/jb/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin +L=/private/var/log/scion-celltest.log +say() { echo "$*" >> $L; echo "scion-celltest: $*" > /dev/console 2>/dev/null || true; } +U=$(cat /private/var/scion-test/celltest-url 2>/dev/null || echo https://example.com/) +say "=== $(date -u +%FT%TZ) celltest, url $U" +for i in $(seq 1 180); do + info=$(sysctl -n kern.hakios.cp.info 2>/dev/null || true) + case "$info" in *" data=1 "*) break;; esac + case "$info" in *"stage=0 "*|"") say "no modem in this boot: $info"; exit 0;; esac + sleep 5 +done +say "after $((i * 5)) s: $info" +sleep 10 +scion-celld -d 2>&1 | grep -v '^kern.hakios.cp.addr' | while IFS= read -r l; do say "$l"; done +netstat -rn -f inet 2>&1 | head -12 | while IFS= read -r l; do say "$l"; done +say "default route: $(route -n get default 2>&1 | grep -E 'interface' | tr -s ' ')" +for k in 1 2; do + scion-fetch "$U" 2>&1 | while IFS= read -r l; do say "$l"; done + sleep 5 +done +say "after: $(sysctl -n kern.hakios.cp.info 2>/dev/null)" +say "=== $(date -u +%FT%TZ) celltest done" +SH + chmod 0755 "$T" + fi + [[ $EUID != 0 ]] || chown 0:0 "$R/usr/local/bin/scion-celld" "$R/usr/local/bin/scion-fetch" "$T" 2>/dev/null || true + ;; +untest) + rm -f "$T" "$R/usr/local/bin/scion-fetch";; +undo) + rm -f "$T" "$R/usr/local/bin/scion-fetch" "$R/usr/local/bin/scion-celld";; +*) echo "66-celld: add, test, untest or undo" >&2; exit 2;; +esac +/usr/bin/python3 - "$J" "$how" <<'PY' +import plistlib, sys +path, how = sys.argv[1:] +d = plistlib.load(open(path, 'rb')); jobs = d['LaunchDaemons'] +K = '/System/Library/LaunchDaemons/com.scion.celld.plist' +KT = '/System/Library/LaunchDaemons/com.scion.celltest.plist' +if how in ('add', 'test'): + jobs[K] = {'Label': 'com.scion.celld', 'ProgramArguments': ['/usr/local/bin/scion-celld'], 'RunAtLoad': True, + 'KeepAlive': {'SuccessfulExit': False}, 'ThrottleInterval': 30, 'UserName': 'root', + 'LimitLoadToSessionType': 'System', 'ProcessType': 'Adaptive'} +if how == 'test': + jobs[KT] = {'Label': 'com.scion.celltest', 'ProgramArguments': ['/var/jb/bin/bash', '/usr/local/libexec/scion-celltest.sh'], + 'RunAtLoad': True, 'WatchPaths': ['/private/var/jb/.prepared'], 'LaunchOnlyOnce': True, + 'LimitLoadToSessionType': 'System', 'ProcessType': 'Background'} +if how in ('untest', 'undo'): + jobs.pop(KT, None) +if how == 'undo': + jobs.pop(K, None) +data = plistlib.dumps(d, fmt=plistlib.FMT_BINARY); plistlib.loads(data) +with open(path, 'r+b') as f: + f.truncate(0); f.write(data) +print('66-celld: %s: celld %s, celltest %s' % (how, 'in' if K in jobs else 'out', 'in' if KT in jobs else 'out')) +PY diff --git a/tools/ios27/modem/build.sh b/tools/ios27/modem/build.sh new file mode 100755 index 000000000..5a9beffb1 --- /dev/null +++ b/tools/ios27/modem/build.sh @@ -0,0 +1,18 @@ +#!/bin/sh +# SPDX-License-Identifier: MIT-0 +# tools/ios27/modem/build.sh [OUT]: scion-celld (pdp_ip0 as a configd network service) and scion-fetch (a test GET through +# iOS's own stack) for the iOS 27 root -> OUT (default: ./out). arm64 only, signed ad hoc. Needs Xcode's iPhoneOS SDK. +set -eu +HERE=$(cd "$(dirname "$0")" && pwd) +OUT=${1:-$HERE/out} +mkdir -p "$OUT" +SDK=$(xcrun --sdk iphoneos --show-sdk-path) +xcrun --sdk iphoneos clang -arch arm64 -isysroot "$SDK" -miphoneos-version-min=15.0 -O2 -Wall -Wextra -Werror \ + -framework CoreFoundation -framework SystemConfiguration -o "$OUT/scion-celld" "$HERE/scion-celld.c" +xcrun --sdk iphoneos clang -arch arm64 -isysroot "$SDK" -miphoneos-version-min=15.0 -O2 -Wall -fobjc-arc \ + -framework Foundation -o "$OUT/scion-fetch" "$HERE/scion-fetch.m" +for b in scion-celld scion-fetch; do + codesign -f -s - "$OUT/$b" 2>/dev/null + [ "$(lipo -archs "$OUT/$b")" = arm64 ] || { echo "build.sh: $b is not plain arm64" >&2; exit 1; } +done +(cd "$OUT" && shasum -a 256 scion-celld scion-fetch) diff --git a/tools/ios27/modem/scion-celld.c b/tools/ios27/modem/scion-celld.c new file mode 100644 index 000000000..a9817a114 --- /dev/null +++ b/tools/ios27/modem/scion-celld.c @@ -0,0 +1,296 @@ +// SPDX-License-Identifier: MIT-0 +/* + * scion-celld: the Galaxy S9's cellular data context (scion-kernel drivers/modem/hakios.shannon.c, cp_if=1: pdp_ip0) + * made a network service iOS knows, so that names resolve and iOS routes over cellular when nothing better is up. + * + * It reads two of the kernel's sysctls every few seconds: + * kern.hakios.cp.info one line: stage, registration, technology, bars, dBm, operator code, SIM, data (1: pdp_ip0 up) + * kern.hakios.cp.addr root only: "ADDRESS DNS1 DNS2" of the active context, empty without one + * and, while the context is up, publishes into configd's dynamic store (what CommCenter does on an iPhone): + * State:/Network/Service/ PrimaryRank = Last (Wi-Fi, or anything else with a default route, wins) + * State:/Network/Service//IPv4 Addresses, InterfaceName pdp_ip0, Router = the address (point to point: every + * route goes straight out of the interface) + * State:/Network/Service//DNS ServerAddresses = the context's DNS servers + * IPMonitor then makes the default route, the resolver configuration and the network state from them as for any + * service. When the context goes away the keys are removed. Nothing else is touched; no Apple file is changed. + * + * Without the modem (cp= absent: stage 0) or when its run has ended (state 100 or more) it exits 0 and launchd leaves + * it. A line per change goes to /private/var/log/scion-celld.log (the addresses are never written there). + * + * scion-celld the daemon + * scion-celld -d one look: the kernel's line and configd's global IPv4 and DNS state, then exit + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* SystemConfiguration's dynamic store (public C API; declared here because the iPhoneOS SDK does not ship its header) */ +typedef const struct __SCDynamicStore *SCDynamicStoreRef; +extern SCDynamicStoreRef SCDynamicStoreCreate(CFAllocatorRef, CFStringRef, void *, void *); +extern Boolean SCDynamicStoreSetMultiple(SCDynamicStoreRef, CFDictionaryRef, CFArrayRef, CFArrayRef); +extern CFPropertyListRef SCDynamicStoreCopyValue(SCDynamicStoreRef, CFStringRef); +extern int SCError(void); +extern const char *SCErrorString(int); + +#define SERVICE_ID "5C10D000-CE11-4A7A-9000-5C1000000001" +#define LOG "/private/var/log/scion-celld.log" + +static void +say(const char *fmt, ...) +{ + char line[512]; + time_t t = time(NULL); + struct tm tm; + struct stat st; + va_list ap; + FILE *f; + + gmtime_r(&t, &tm); + va_start(ap, fmt); + vsnprintf(line, sizeof(line), fmt, ap); + va_end(ap); + if (stat(LOG, &st) == 0 && st.st_size > 256 * 1024) { + (void)truncate(LOG, 0); + } + if ((f = fopen(LOG, "a")) != NULL) { + fprintf(f, "%04d-%02d-%02dT%02d:%02d:%02dZ %s\n", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, + tm.tm_min, tm.tm_sec, line); + fclose(f); + } + fprintf(stderr, "scion-celld: %s\n", line); +} + +static bool +read_line(const char *name, char *out, size_t n) +{ + size_t len = n; + const char *fake = getenv("SCION_CELLD_FAKE"); + + out[0] = 0; + if (fake != NULL) { + /* a model run off the phone: the file's first line stands for kern.hakios.cp.info, its second for .addr */ + FILE *f = fopen(fake, "r"); + char l1[256] = "", l2[256] = ""; + + if (f == NULL) { + return false; + } + if (fgets(l1, sizeof(l1), f) != NULL) { + (void)fgets(l2, sizeof(l2), f); + } + fclose(f); + strlcpy(out, strstr(name, ".addr") != NULL ? l2 : l1, n); + out[strcspn(out, "\n")] = 0; + return true; + } + if (sysctlbyname(name, out, &len, NULL, 0) != 0) { + out[0] = 0; + return false; + } + out[n - 1] = 0; + return true; +} + +/* the value of KEY=... in the kernel's info line, as an integer (DEF if absent) */ +static long +field(const char *info, const char *key, long def) +{ + char pat[24]; + const char *p; + + snprintf(pat, sizeof(pat), "%s=", key); + for (p = info; (p = strstr(p, pat)) != NULL; p++) { + if (p == info || p[-1] == ' ') { + return strtol(p + strlen(pat), NULL, 10); + } + } + return def; +} + +static bool +is_addr(const char *s) +{ + unsigned a, b, c, d; + char x; + + return sscanf(s, "%u.%u.%u.%u%c", &a, &b, &c, &d, &x) == 4 && a > 0 && a < 224 && a != 127 && b < 256 && c < 256 && + d < 256; +} + +static CFStringRef +str(const char *s) +{ + return CFStringCreateWithCString(NULL, s, kCFStringEncodingUTF8); +} + +static CFStringRef +key(const char *entity) +{ + return entity == NULL ? str("State:/Network/Service/" SERVICE_ID) : + CFStringCreateWithFormat(NULL, NULL, CFSTR("State:/Network/Service/" SERVICE_ID "/%s"), entity); +} + +static bool +publish(SCDynamicStoreRef store, const char *ip, const char *dns1, const char *dns2) +{ + CFMutableDictionaryRef all = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); + CFMutableDictionaryRef svc = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); + CFMutableDictionaryRef v4 = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); + CFMutableDictionaryRef dns = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); + CFMutableArrayRef servers = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks); + CFStringRef a = str(ip), k1 = key(NULL), k2 = key("IPv4"), k3 = key("DNS"), t; + CFArrayRef addrs = CFArrayCreate(NULL, (const void **)&a, 1, &kCFTypeArrayCallBacks); + Boolean ok; + + CFDictionarySetValue(svc, CFSTR("PrimaryRank"), CFSTR("Last")); + CFDictionarySetValue(svc, CFSTR("UserDefinedName"), CFSTR("Cellular (scion-celld)")); + CFDictionarySetValue(v4, CFSTR("Addresses"), addrs); + CFDictionarySetValue(v4, CFSTR("DestAddresses"), addrs); + CFDictionarySetValue(v4, CFSTR("InterfaceName"), getenv("SCION_CELLD_IF") != NULL ? str(getenv("SCION_CELLD_IF")) : + CFSTR("pdp_ip0")); /* (SCION_CELLD_IF: the model run's stand-in interface; leaks one string, once) */ + CFDictionarySetValue(v4, CFSTR("Router"), a); + if (is_addr(dns1)) { + t = str(dns1); + CFArrayAppendValue(servers, t); + CFRelease(t); + } + if (is_addr(dns2) && strcmp(dns1, dns2) != 0) { + t = str(dns2); + CFArrayAppendValue(servers, t); + CFRelease(t); + } + CFDictionarySetValue(dns, CFSTR("ServerAddresses"), servers); + CFDictionarySetValue(all, k1, svc); + CFDictionarySetValue(all, k2, v4); + if (CFArrayGetCount(servers) != 0) { + CFDictionarySetValue(all, k3, dns); + } + ok = SCDynamicStoreSetMultiple(store, all, NULL, NULL); + if (!ok) { + say("configd refused the service: %s", SCErrorString(SCError())); + } + CFRelease(all); CFRelease(svc); CFRelease(v4); CFRelease(dns); CFRelease(servers); CFRelease(a); CFRelease(addrs); + CFRelease(k1); CFRelease(k2); CFRelease(k3); + return ok; +} + +static void +withdraw(SCDynamicStoreRef store) +{ + CFStringRef keys[3] = { key(NULL), key("IPv4"), key("DNS") }; + CFArrayRef gone = CFArrayCreate(NULL, (const void **)keys, 3, &kCFTypeArrayCallBacks); + + if (!SCDynamicStoreSetMultiple(store, NULL, gone, NULL)) { + say("configd refused the removal: %s", SCErrorString(SCError())); + } + CFRelease(gone); + for (int i = 0; i < 3; i++) { + CFRelease(keys[i]); + } +} + +static void +show(SCDynamicStoreRef store, const char *k) +{ + CFStringRef s = str(k); + CFPropertyListRef v = SCDynamicStoreCopyValue(store, s); + char buf[2048] = "(none)"; + + if (v != NULL) { + CFStringRef d = CFCopyDescription(v); + + CFStringGetCString(d, buf, sizeof(buf), kCFStringEncodingUTF8); + CFRelease(d); + CFRelease(v); + } + printf("%s = %s\n", k, buf); + CFRelease(s); +} + +int +main(int argc, char **argv) +{ + SCDynamicStoreRef store; + char info[256], addr[96], last[96] = ""; + bool up = false; + long gen = -1; + + store = SCDynamicStoreCreate(NULL, CFSTR("scion-celld"), NULL, NULL); + if (argc > 1 && strcmp(argv[1], "-d") == 0) { + read_line("kern.hakios.cp.info", info, sizeof(info)); + printf("kern.hakios.cp.info = %s\n", info[0] ? info : "(none)"); + read_line("kern.hakios.cp.addr", addr, sizeof(addr)); + printf("kern.hakios.cp.addr = %s\n", addr[0] ? "(set; not shown)" : "(empty)"); + if (store == NULL) { + printf("configd: no session\n"); + return 1; + } + show(store, "State:/Network/Global/IPv4"); + show(store, "State:/Network/Global/DNS"); + show(store, "State:/Network/Service/" SERVICE_ID "/IPv4"); + return 0; + } + if (!read_line("kern.hakios.cp.info", info, sizeof(info)) || field(info, "stage", 0) == 0) { + say("no modem in this boot (cp= absent): nothing to do"); + return 0; + } + if (store == NULL) { + say("no configd session: %s", SCErrorString(SCError())); + return 1; + } + say("started: %s", info); + withdraw(store); + for (;;) { + long state, data; + + if (!read_line("kern.hakios.cp.info", info, sizeof(info))) { + info[0] = 0; + } + state = field(info, "state", 0); + data = field(info, "data", 0); + if (field(info, "gen", -1) != gen) { + gen = field(info, "gen", -1); + if (gen % 20 == 0 || data != up) { + say("%s", info); + } + } + read_line("kern.hakios.cp.addr", addr, sizeof(addr)); + if (data == 1 && addr[0] != 0) { + if (!up || strcmp(addr, last) != 0) { + char ip[32] = "", d1[32] = "", d2[32] = ""; + + sscanf(addr, "%31s %31s %31s", ip, d1, d2); + if (is_addr(ip) && publish(store, ip, d1, d2)) { + up = true; + strlcpy(last, addr, sizeof(last)); + say("pdp_ip0 published to configd as service " SERVICE_ID " (rank Last, %s DNS)", + is_addr(d1) ? (is_addr(d2) ? "two" : "one") : "no"); + } + } + } else if (up) { + withdraw(store); + up = false; + last[0] = 0; + say("the data context is gone: service withdrawn (%s)", info); + } + if (state >= 100) { + say("the modem's run is over (%s): exiting", info); + if (up) { + withdraw(store); + } + return 0; + } + sleep(up ? 5 : 2); + } +} diff --git a/tools/ios27/modem/scion-fetch.m b/tools/ios27/modem/scion-fetch.m new file mode 100644 index 000000000..77c12033d --- /dev/null +++ b/tools/ios27/modem/scion-fetch.m @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT-0 +/* + * scion-fetch URL [IFNAME]: a test tool for an iOS 27 test root: one GET of URL through iOS's own stack (the name through + * the system resolver, TLS, the route IPMonitor chose), then one line: the resolved addresses, the HTTP status, the bytes, + * the time, and which interface the default route uses. With IFNAME the request is bound to that interface + * (NSURLSession has no such knob: the name is resolved and the route checked only). Exit 0 on a 2xx/3xx answer. + */ +#import +#include +#include +#include +#include + +int +main(int argc, char **argv) +{ + @autoreleasepool { + if (argc < 2) { + fprintf(stderr, "usage: scion-fetch URL\n"); + return 2; + } + NSURL *url = [NSURL URLWithString:[NSString stringWithUTF8String:argv[1]]]; + struct addrinfo hints = { .ai_family = AF_UNSPEC, .ai_socktype = SOCK_STREAM }, *res = NULL, *r; + NSDate *t0 = [NSDate date]; + int e = getaddrinfo(url.host.UTF8String, "443", &hints, &res); + NSMutableString *names = [NSMutableString string]; + + for (r = res; e == 0 && r != NULL; r = r->ai_next) { + char a[64]; + + if (getnameinfo(r->ai_addr, r->ai_addrlen, a, sizeof(a), NULL, 0, NI_NUMERICHOST) == 0) { + [names appendFormat:@"%s%s", names.length ? "," : "", a]; + } + } + if (res != NULL) { + freeaddrinfo(res); + } + printf("resolve %s: %s (%.0f ms)\n", url.host.UTF8String, e == 0 ? names.UTF8String : gai_strerror(e), + -[t0 timeIntervalSinceNow] * 1000); + + NSURLSessionConfiguration *c = [NSURLSessionConfiguration ephemeralSessionConfiguration]; + c.timeoutIntervalForRequest = 30; + c.allowsCellularAccess = YES; + NSURLSession *s = [NSURLSession sessionWithConfiguration:c]; + dispatch_semaphore_t done = dispatch_semaphore_create(0); + __block long status = -1; + __block NSUInteger bytes = 0; + __block NSString *err = nil; + t0 = [NSDate date]; + [[s dataTaskWithURL:url completionHandler:^(NSData *d, NSURLResponse *resp, NSError *error) { + if (error != nil) { + err = [NSString stringWithFormat:@"%@ %ld", error.domain, (long)error.code]; + } + if ([resp isKindOfClass:[NSHTTPURLResponse class]]) { + status = ((NSHTTPURLResponse *)resp).statusCode; + } + bytes = d.length; + dispatch_semaphore_signal(done); + }] resume]; + if (dispatch_semaphore_wait(done, dispatch_time(DISPATCH_TIME_NOW, 45 * NSEC_PER_SEC)) != 0) { + err = @"timed out (45 s)"; + } + printf("GET %s: status %ld, %lu bytes, %.0f ms%s%s\n", argv[1], status, (unsigned long)bytes, + -[t0 timeIntervalSinceNow] * 1000, err ? ", error " : "", err ? err.UTF8String : ""); + return status >= 200 && status < 400 ? 0 : 1; + } +} From 9777c90be776abd62e6dcc17138ed546a5d16569 Mon Sep 17 00:00:00 2001 From: Claudius Date: Sat, 14 Nov 2026 00:09:52 +0000 Subject: [PATCH 2/7] boot-args: regenerated (cp_rfs and the ones the branch had already added) --- configs/boot-args.lst | 1 + configs/symbols.tsv | 1 + docs/boot-args.md | 5 +++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/configs/boot-args.lst b/configs/boot-args.lst index 5140ce5e5..d2e8c5cae 100644 --- a/configs/boot-args.lst +++ b/configs/boot-args.lst @@ -192,6 +192,7 @@ cp_if_kb k cp_nv k cp_ping k cp_rev k +cp_rfs k cp_route k cp_sim k cp_vss k diff --git a/configs/symbols.tsv b/configs/symbols.tsv index f227ba4e6..306e1aec0 100644 --- a/configs/symbols.tsv +++ b/configs/symbols.tsv @@ -182,6 +182,7 @@ CP_IF_KB arg cp_if_kb User space k hakios.shannon.c with cp_if=1: at most N KB t CP_NV arg cp_nv User space k hakios.shannon.c the phone's own NV data, a file nv_data.bin the image's builder put beside the kernel, is given to the modem (none: zeros, and no stage 4) CP_PING arg cp_ping User space k hakios.shannon.c the address stage 7 sends its one echo request to (none: the context's first DNS server) CP_REV arg cp_rev User space k hakios.shannon.c the board's revision told to the modem (26, this phone's) +CP_RFS arg cp_rfs User space k hakios.shannon.c an OPEN FILE answered from RAM: the file is not there CP_ROUTE arg cp_route User space k hakios.shannon.c with cp_if=1: an IPv4 default route through pdp_ip0 (only when there is none yet: Wi-Fi's wins) CP_SIM arg cp_sim User space k hakios.shannon.c in low power mode, once the SIM is ready, one read of its administrative data file (EF_AD, 4 bytes: no identity) is asked, to see if the modem takes CP_VSS arg cp_vss User space k hakios.shannon.c the image's VSS part (the voice processor's) is not copied diff --git a/docs/boot-args.md b/docs/boot-args.md index 4e8133bcb..c28d2600d 100644 --- a/docs/boot-args.md +++ b/docs/boot-args.md @@ -2,7 +2,7 @@ Made by `tools/boot-args.py --markdown`; CI fails if it is out of date with the source (`tools/boot-args.py --check`). -Every argument that the kernel, the loader or user space of this port reads, and which file reads it. **688 arguments**: 597 read by the kernel, 23 by the loader, 85 by user space (one is read in more than one place). +Every argument that the kernel, the loader or user space of this port reads, and which file reads it. **689 arguments**: 598 read by the kernel, 23 by the loader, 85 by user space (one is read in more than one place). This is a catalogue of what exists and who reads it, not a manual. What an argument means is in the head of the file that reads it. The stable switches have a symbol in the configuration (`tools/hakios-config.py help SYMBOL`, `docs/configs.md`); the rest are the knobs of bring-up (a driver's steps, pauses, traces, test modes) and can change with the driver. Any of them can be given in a configuration with `CONFIG_BOOTARGS="name=value ..."`, which checks that each name is in this list. What the arguments of the two Metal paths do: [Metal on the Adreno](reference/metal.md#boot-arguments). @@ -593,7 +593,7 @@ Layers: **K** the kernel, **L** the loader, **U** user space (through `sysctl ke | `ufs_probe` | L | `loader.c` | `CONFIG_UFS_PROBE` | | | `ufs_write_guid` | L | `loader.c` | `CONFIG_UFS_WRITE_GUID` | | -## User space (183) +## User space (184) | Argument | Layer | Read by | Configuration | Set by | |---|---|---|---|---| @@ -668,6 +668,7 @@ Layers: **K** the kernel, **L** the loader, **U** user space (through `sysctl ke | `cp_nv` | K | `hakios.shannon.c` | `CONFIG_CP_NV` | | | `cp_ping` | K | `hakios.shannon.c` | `CONFIG_CP_PING` | | | `cp_rev` | K | `hakios.shannon.c` | `CONFIG_CP_REV` | | +| `cp_rfs` | K | `hakios.shannon.c` | `CONFIG_CP_RFS` | | | `cp_route` | K | `hakios.shannon.c` | `CONFIG_CP_ROUTE` | | | `cp_sim` | K | `hakios.shannon.c` | `CONFIG_CP_SIM` | | | `cp_vss` | K | `hakios.shannon.c` | `CONFIG_CP_VSS` | | From dd3a51edd635feb86047c22c61fd85f14ff57c2a Mon Sep 17 00:00:00 2001 From: Claudius Date: Sat, 14 Nov 2026 00:11:07 +0000 Subject: [PATCH 3/7] iOS 27 Wi-Fi: 63-wifi-netservice.sh, the Wi-Fi network service written before the first boot (first-boot DHCP waited ~4 min for iOS to create it) --- tools/ios27/wifi/63-wifi-netservice.sh | 55 ++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100755 tools/ios27/wifi/63-wifi-netservice.sh diff --git a/tools/ios27/wifi/63-wifi-netservice.sh b/tools/ios27/wifi/63-wifi-netservice.sh new file mode 100755 index 000000000..96bd6d7c2 --- /dev/null +++ b/tools/ios27/wifi/63-wifi-netservice.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# 63-wifi-netservice.sh ROOT [undo] : the Wi-Fi network service in configd's preferences before the first boot. SPDX-License-Identifier: MIT-0 +# +# Why: a root that has never had Wi-Fi has a preferences.plist with no network service at all. On its first Wi-Fi boot the +# network was joined at once but DHCP (configd's IPConfiguration) did not send a frame for about 4 minutes, until iOS had +# created the Wi-Fi service itself (seen on phone 1, 13 Nov 2026: join 18:02:48, DHCP 18:07:00; later boots: DHCP 2 s after +# the join). This step writes the same service iOS writes (en0, "Wi-Fi", AirPort, IPv4 DHCP, IPv6 automatic) and a set +# ordering it, only when preferences.plist has no service for en0. Nothing else is changed; no network name or key. +set -euo pipefail +R=$1 +P=$R/private/var/preferences/SystemConfiguration/preferences.plist +[[ -d $(dirname "$P") ]] || { echo "63-wifi-netservice: $R has no SystemConfiguration preferences directory" >&2; exit 1; } +/usr/bin/python3 - "$P" "${2:-add}" <<'PY' +import os, plistlib, sys +path, how = sys.argv[1:] +SVC = '5C10D000-0E00-4A7A-9000-5C1000000E00' +SET = '5C10D000-5E70-4A7A-9000-5C1000005E70' +d = plistlib.load(open(path, 'rb')) if os.path.exists(path) else {'__VERSION__': 20191120} +svcs = d.setdefault('NetworkServices', {}) +if how == 'undo': + svcs.pop(SVC, None) + if d.get('Sets', {}).pop(SET, None) is not None and d.get('CurrentSet') == '/Sets/' + SET: + d.pop('CurrentSet') + if not svcs: + d.pop('NetworkServices') + if not d.get('Sets', 1): + d.pop('Sets') + print('63-wifi-netservice: ours removed (if it was there)') +else: + have = [k for k, v in svcs.items() if v.get('Interface', {}).get('DeviceName') == 'en0'] + if have: + print('63-wifi-netservice: preferences.plist already has a service for en0 (%s): unchanged' % have[0]) + sys.exit(0) + svcs[SVC] = {'DNS': {}, 'IPv4': {'ConfigMethod': 'DHCP'}, 'IPv6': {'ConfigMethod': 'Automatic'}, + 'Interface': {'DeviceName': 'en0', 'Hardware': 'AirPort', 'Type': 'Ethernet', 'UserDefinedName': 'Wi-Fi'}, + 'Proxies': {'ExceptionsList': ['*.local', '169.254/16'], 'FTPPassive': 1}, 'UserDefinedName': 'Wi-Fi'} + sets = d.setdefault('Sets', {}) + cur = d.get('CurrentSet', '').replace('/Sets/', '') + if cur in sets: + net = sets[cur].setdefault('Network', {}) + net.setdefault('Service', {})[SVC] = {'__LINK__': '/NetworkServices/' + SVC} + order = net.setdefault('Global', {}).setdefault('IPv4', {}).setdefault('ServiceOrder', []) + order.insert(0, SVC) + net.setdefault('Interface', {}).setdefault('en0', {'AirPort': {'JoinMode': 'Automatic'}}) + else: + sets[SET] = {'Network': {'Global': {'IPv4': {'ServiceOrder': [SVC]}}, + 'Interface': {'en0': {'AirPort': {'JoinMode': 'Automatic'}}}, + 'Service': {SVC: {'__LINK__': '/NetworkServices/' + SVC}}}, + 'UserDefinedName': 'Automatic'} + d['CurrentSet'] = '/Sets/' + SET + d.setdefault('Model', 'N104AP') + print('63-wifi-netservice: the Wi-Fi service for en0 added') +data = plistlib.dumps(d, fmt=plistlib.FMT_XML); plistlib.loads(data) +open(path, 'wb').write(data) +PY From 2b2143e6334a703b81c92a3ef5ba1e9389878956 Mon Sep 17 00:00:00 2001 From: Claudius Date: Sat, 14 Nov 2026 00:14:13 +0000 Subject: [PATCH 4/7] iOS 27 modem: scion-bars (c.dylib in SpringBoard): the status bar's cellular entry from kern.hakios.cp.info, off unless /private/var/local/scion-bars; 66-celld installs it --- tools/ios27/modem/66-celld.sh | 11 +- tools/ios27/modem/build.sh | 6 + tools/ios27/modem/scion-bars.m | 314 +++++++++++++++++++++++++++++++++ 3 files changed, 330 insertions(+), 1 deletion(-) create mode 100644 tools/ios27/modem/scion-bars.m diff --git a/tools/ios27/modem/66-celld.sh b/tools/ios27/modem/66-celld.sh index 6926ed4a9..9fcb8b5cd 100644 --- a/tools/ios27/modem/66-celld.sh +++ b/tools/ios27/modem/66-celld.sh @@ -18,6 +18,14 @@ case $how in add|test) install -d "$R/usr/local/bin" "$R/usr/local/libexec" install -m 0755 "$B/scion-celld" "$R/usr/local/bin/scion-celld" + # the status bar's library (c.dylib, scion-bars.m): loaded only through the boot-arg sweetinject (/usr/lib/c.dylib,SpringBoard) + # and on only while /private/var/local/scion-bars exists + if [[ -f $B/c.dylib ]]; then + install -m 0755 "$B/c.dylib" "$R/usr/lib/c.dylib" + mkdir -p "$R/private/var/local" + [[ -f $R/private/var/local/scion-bars ]] || printf 'mode=hook\n' > "$R/private/var/local/scion-bars" + [[ $how != test ]] || grep -q '^discover=' "$R/private/var/local/scion-bars" || echo discover=1 >> "$R/private/var/local/scion-bars" + fi if [[ $how == test ]]; then install -m 0755 "$B/scion-fetch" "$R/usr/local/bin/scion-fetch" mkdir -p "$R/private/var/scion-test" @@ -54,7 +62,8 @@ SH untest) rm -f "$T" "$R/usr/local/bin/scion-fetch";; undo) - rm -f "$T" "$R/usr/local/bin/scion-fetch" "$R/usr/local/bin/scion-celld";; + rm -f "$T" "$R/usr/local/bin/scion-fetch" "$R/usr/local/bin/scion-celld" "$R/usr/lib/c.dylib" "$R/private/var/local/scion-bars" \ + "$R/private/var/local/scion-bars.boots";; *) echo "66-celld: add, test, untest or undo" >&2; exit 2;; esac /usr/bin/python3 - "$J" "$how" <<'PY' diff --git a/tools/ios27/modem/build.sh b/tools/ios27/modem/build.sh index 5a9beffb1..c55599d04 100755 --- a/tools/ios27/modem/build.sh +++ b/tools/ios27/modem/build.sh @@ -16,3 +16,9 @@ for b in scion-celld scion-fetch; do [ "$(lipo -archs "$OUT/$b")" = arm64 ] || { echo "build.sh: $b is not plain arm64" >&2; exit 1; } done (cd "$OUT" && shasum -a 256 scion-celld scion-fetch) +# scion-bars: c.dylib, the status bar's cellular entry from the modem (SpringBoard, through sweetinject) +xcrun --sdk iphoneos clang -arch arm64 -isysroot "$SDK" -miphoneos-version-min=15.0 -O2 -Wall -fobjc-arc -dynamiclib \ + -install_name /usr/lib/c.dylib -framework Foundation -o "$OUT/c.dylib" "$HERE/scion-bars.m" +codesign -f -s - "$OUT/c.dylib" 2>/dev/null +[ "$(lipo -archs "$OUT/c.dylib")" = arm64 ] || { echo "build.sh: c.dylib is not plain arm64" >&2; exit 1; } +(cd "$OUT" && shasum -a 256 c.dylib) diff --git a/tools/ios27/modem/scion-bars.m b/tools/ios27/modem/scion-bars.m new file mode 100644 index 000000000..1edad8b94 --- /dev/null +++ b/tools/ios27/modem/scion-bars.m @@ -0,0 +1,314 @@ +// SPDX-License-Identifier: MIT-0 +/* + * scion-bars (c.dylib, injected into SpringBoard through the boot-arg sweetinject): the status bar's cellular entry - signal + * bars and the network's name - from the S9's own modem (scion-kernel drivers/modem/hakios.shannon.c, kern.hakios.cp.info), + * because iOS's CommCenter does not know that modem and shows no service. Cosmetic only: Settings still says "No SIM", and + * nothing of CoreTelephony changes. Only our own code; no Apple file is changed. + * + * On only when /private/var/local/scion-bars exists. That file may hold lines KEY=VALUE: + * name=TEXT the network name shown (default: from the operator code, a small table; else the code itself) + * status=N the entry's status value for "registered" (default 3: active), type=N the technology (default: found by + * its description, "LTE"), mode=hook|publish|both (default hook: SpringBoard's own status bar data; + * publish: StatusKit's status bar overrides domain), discover=1 (the classes' methods into the log) + * Safety: a boot counter (/private/var/local/scion-bars.boots) turns it off after 3 SpringBoard starts in a row that did not + * live 90 s. Everything it does is logged to the console as "SCIONBAR[pid]: ...". + */ +#import +#include +#include +#include +#include +#include +#include +#include + +#define ON_FILE "/private/var/local/scion-bars" +#define BOOTS "/private/var/local/scion-bars.boots" + +static void +say(NSString *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + NSString *s = [[NSString alloc] initWithFormat:fmt arguments:ap]; + va_end(ap); + int fd = open("/dev/console", O_WRONLY | O_NOCTTY | O_NONBLOCK); + if (fd >= 0) { + dprintf(fd, "SCIONBAR[%d]: %s\n", getpid(), s.UTF8String); + close(fd); + } +} + +static NSDictionary *cfg; +static id current; /* our cellular entry, nil while the modem is not registered */ +static IMP orig_getter, orig_entry_for_key; +static long last_gen = -2; + +static long +field(const char *info, const char *key, long def) +{ + char pat[24]; + snprintf(pat, sizeof(pat), "%s=", key); + for (const char *p = info; (p = strstr(p, pat)) != NULL; p++) { + if (p == info || p[-1] == ' ') { + return strtol(p + strlen(pat), NULL, 10); + } + } + return def; +} + +static NSString * +oper_name(const char *info) +{ + static const struct { const char *code, *name; } t[] = { + { "27201", "Vodafone IE" }, { "27202", "3" }, { "27203", "eir" }, { "27205", "3" }, { "27211", "Tesco Mobile" }, + { "27213", "Lycamobile" }, { "23410", "O2 - UK" }, { "23415", "Vodafone UK" }, { "23420", "3" }, { "23430", "EE" }, + }; + const char *p = strstr(info, "oper="); + char code[8] = ""; + if (p != NULL) { + sscanf(p + 5, "%7[0-9]", code); + } + for (size_t i = 0; i < sizeof(t) / sizeof(t[0]); i++) { + if (strcmp(code, t[i].code) == 0) { + return @(t[i].name); + } + } + return code[0] ? @(code) : @""; +} + +static void +dump_class(const char *name) +{ + Class c = objc_getClass(name); + if (c == Nil) { + say(@"class %s: none", name); + return; + } + for (int meta = 0; meta < 2; meta++) { + unsigned n = 0; + Method *m = class_copyMethodList(meta ? object_getClass((id)c) : c, &n); + NSMutableString *s = [NSMutableString string]; + for (unsigned i = 0; i < n; i++) { + [s appendFormat:@" %s%s", meta ? "+" : "", sel_getName(method_getName(m[i]))]; + } + free(m); + say(@"class %s (super %s) %s methods %u:%@", name, class_getName(class_getSuperclass(c)), meta ? "class" : "instance", + n, s.length > 3000 ? [s substringToIndex:3000] : s); + } +} + +/* the technology's value: the one whose description says LTE (or 4G), unless type= is given */ +static long +lte_type(void) +{ + if (cfg[@"type"] != nil) { + return [cfg[@"type"] integerValue]; + } + NSString *(*desc)(long) = (NSString * (*)(long))dlsym(RTLD_DEFAULT, "STStatusBarDataCellularTypeDescription"); + if (desc == NULL) { + desc = (NSString * (*)(long))dlsym(RTLD_DEFAULT, "_STStatusBarDataCellularTypeDescription"); + } + long best = -1; + for (long v = 0; desc != NULL && v < 24; v++) { + NSString *d = desc(v); + if (d && best < 0 && ([d rangeOfString:@"LTE" options:NSCaseInsensitiveSearch].location != NSNotFound || + [d isEqualToString:@"4G"])) { + best = v; + } + } + return best >= 0 ? best : 5; +} + +/* a cellular entry: a copy of SpringBoard's own one (or a new one) with our values set by key-value coding */ +static id +make_entry(id base, NSString *name, long bars, long dbm, long type) +{ + Class c = objc_getClass("STStatusBarDataCellularEntry"); + id e = nil; + @try { + e = base != nil ? [base copy] : [[c alloc] init]; + long status = cfg[@"status"] ? [cfg[@"status"] integerValue] : 3; + NSDictionary *v = @{ @"stringValue": name, @"type": @(type), @"status": @(status), @"displayValue": @(bars), + @"rawValue": @(dbm), @"displayRawValue": @NO }; + for (NSString *k in v) { + @try { + [e setValue:v[k] forKey:k]; + } @catch (NSException *x) { + say(@"entry: %@ not set: %@", k, x.reason); + } + } + } @catch (NSException *x) { + say(@"entry: not made: %@", x.reason); + e = nil; + } + return e; +} + +static id +my_getter(id self, SEL _cmd) +{ + id e = current; + return e != nil ? e : ((id (*)(id, SEL))orig_getter)(self, _cmd); +} + +static id +my_entry_for_key(id self, SEL _cmd, id key) +{ + id e = current; + if (e != nil && [key isKindOfClass:[NSString class]] && [key isEqualToString:@"cellularEntry"]) { + return e; + } + return ((id (*)(id, SEL, id))orig_entry_for_key)(self, _cmd, key); +} + +static void +hook(void) +{ + Class c = objc_getClass("STStatusBarData"); + Method m = c ? class_getInstanceMethod(c, sel_registerName("cellularEntry")) : NULL; + if (m != NULL) { + orig_getter = method_setImplementation(m, (IMP)my_getter); + } + Method k = c ? class_getInstanceMethod(c, sel_registerName("entryForKey:")) : NULL; + if (k != NULL) { + orig_entry_for_key = method_setImplementation(k, (IMP)my_entry_for_key); + } + say(@"hook: STStatusBarData -cellularEntry %s, -entryForKey: %s", m ? "hooked" : "NOT FOUND", k ? "hooked" : "not found"); +} + +static void +publish(id entry) +{ + static id pub; + Class pc = objc_getClass("STStatusBarOverridesStatusDomainPublisher"); + if (pc == Nil) { + say(@"publish: no publisher class"); + return; + } + @try { + if (pub == nil) { + pub = [[pc alloc] init]; + } + void (^fill)(id) = ^(id data) { + id ov = [data valueForKey:@"customOverrides"]; + if (ov == nil) { + ov = [[objc_getClass("STMutableStatusBarData") alloc] init]; + [data setValue:ov forKey:@"customOverrides"]; + } + [ov setValue:entry forKey:@"cellularEntry"]; + }; + if ([pub respondsToSelector:sel_registerName("updateDataWithBlock:")]) { + ((void (*)(id, SEL, id))objc_msgSend)(pub, sel_registerName("updateDataWithBlock:"), fill); + say(@"publish: updateDataWithBlock: done"); + } else { + say(@"publish: the publisher has no updateDataWithBlock:"); + } + } @catch (NSException *x) { + say(@"publish: %@", x.reason); + } +} + +static void +tick(void) +{ + char info[256] = ""; + size_t len = sizeof(info); + if (sysctlbyname("kern.hakios.cp.info", info, &len, NULL, 0) != 0) { + info[0] = 0; + } + info[sizeof(info) - 1] = 0; + long gen = field(info, "gen", -1); + if (gen == last_gen) { + return; + } + last_gen = gen; + long reg = field(info, "reg", -1), bars = field(info, "bars", -1), dbm = field(info, "dbm", 0); + bool registered = reg == 2 || reg == 6; + id e = nil; + if (registered) { + NSString *name = cfg[@"name"] ?: oper_name(info); + if (bars < 0) { + bars = dbm == 0 ? 1 : dbm > -90 ? 4 : dbm > -100 ? 3 : dbm > -110 ? 2 : 1; + } + Class sb = objc_getClass("STStatusBarData"); + id base = nil; + if (orig_getter != NULL && sb != Nil) { + /* (a fresh data object's own entry, as a template; no current one is reachable from here) */ + id d = [[sb alloc] init]; + base = ((id (*)(id, SEL))orig_getter)(d, sel_registerName("cellularEntry")); + } + e = make_entry(base, name, bars > 4 ? 4 : bars, dbm, lte_type()); + } + current = e; + say(@"modem: %s -> %@", info, e != nil ? [e description] : @"(no entry: not registered)"); + NSString *mode = cfg[@"mode"] ?: @"hook"; + if (e != nil && ([mode isEqualToString:@"publish"] || [mode isEqualToString:@"both"])) { + publish(e); + } +} + +__attribute__((constructor)) static void +scion_bars_init(void) +{ + if (strcmp(getprogname(), "SpringBoard") != 0 || access(ON_FILE, F_OK) != 0) { + return; + } + int boots = 0; + FILE *f = fopen(BOOTS, "r"); + if (f) { + fscanf(f, "%d", &boots); + fclose(f); + } + if (boots >= 3) { + say(@"off: SpringBoard started %d times without living 90 s with it (remove %s to try again)", boots, BOOTS); + return; + } + if ((f = fopen(BOOTS, "w"))) { + fprintf(f, "%d\n", boots + 1); + fclose(f); + } + NSMutableDictionary *d = [NSMutableDictionary dictionary]; + for (NSString *l in [[NSString stringWithContentsOfFile:@ON_FILE encoding:NSUTF8StringEncoding error:nil] ?: @"" + componentsSeparatedByString:@"\n"]) { + NSRange r = [l rangeOfString:@"="]; + if (r.location != NSNotFound) { + d[[l substringToIndex:r.location]] = [l substringFromIndex:r.location + 1]; + } + } + cfg = d; + say(@"on (config %@)", cfg); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + if ([cfg[@"discover"] isEqualToString:@"1"]) { + const char *names[] = { "STStatusBarDataCellularEntry", "STStatusBarData", "STMutableStatusBarData", + "STStatusBarOverridesStatusDomainPublisher", "STStatusBarOverridesStatusDomainData", + "STMutableStatusBarOverridesStatusDomainData", "SBStatusBarStateAggregator" }; + for (size_t i = 0; i < sizeof(names) / sizeof(names[0]); i++) { + dump_class(names[i]); + } + NSString *(*desc)(long) = (NSString * (*)(long))dlsym(RTLD_DEFAULT, "STStatusBarDataCellularTypeDescription"); + for (long v = 0; desc != NULL && v < 24; v++) { + say(@"cellular type %ld = %@", v, desc(v)); + } + if (desc == NULL) { + say(@"no STStatusBarDataCellularTypeDescription"); + } + } + NSString *mode = cfg[@"mode"] ?: @"hook"; + if ([mode isEqualToString:@"hook"] || [mode isEqualToString:@"both"]) { + hook(); + } + dispatch_source_t t = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); + dispatch_source_set_timer(t, dispatch_time(DISPATCH_TIME_NOW, 0), 10 * NSEC_PER_SEC, NSEC_PER_SEC); + dispatch_source_set_event_handler(t, ^{ + tick(); + }); + dispatch_resume(t); + static dispatch_source_t keep; + keep = t; + }); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 90 * NSEC_PER_SEC), dispatch_get_global_queue(0, 0), ^{ + unlink(BOOTS); + say(@"alive 90 s: the boot counter is cleared"); + }); +} From f0d7490a46dd0bfbf30b63d0631d6b7101b77fb9 Mon Sep 17 00:00:00 2001 From: Claudius Date: Sat, 14 Nov 2026 00:14:49 +0000 Subject: [PATCH 5/7] Galaxy S9 modem: with cp_if=1 a lost registration is given 10 minutes to come back before the modem goes to low power --- drivers/modem/hakios.shannon.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/drivers/modem/hakios.shannon.c b/drivers/modem/hakios.shannon.c index f5a03d228..0bdaa1252 100644 --- a/drivers/modem/hakios.shannon.c +++ b/drivers/modem/hakios.shannon.c @@ -364,6 +364,7 @@ static struct { uint32_t gen; /* bumped on every change of the above */ uint32_t rfs_open, rfs_opens; /* cp_rfs=1: the CP's file opens answered "not there" (from RAM: nothing is read) */ uint64_t busy_ms; /* the last time something came from the CP or waited to go to it */ + uint64_t lost_ms; /* when a registration was lost (0: registered, or never was) */ } cp; static int cp_go; @@ -1302,6 +1303,9 @@ fmt_received(const uint8_t *m, uint32_t n) if (m[9] != (uint8_t)cp.reg_state) { cp.reg_changes++; } + if ((m[9] == 2 || m[9] == 6) != (cp.reg_state == 2 || cp.reg_state == 6)) { + cp.lost_ms = (m[9] == 2 || m[9] == 6) ? 0 : now_ms(); + } cp.reg_state = m[9]; cp.tech = m[7]; cp.gen++; @@ -2684,7 +2688,8 @@ cp_thread(__unused void *arg, __unused wait_result_t result) back = "NOT registered 90 s after normal mode was asked for"; why5 = "not registered in time"; } else if (cp.registered_ms != 0 && cp.reg_state != 2 && cp.reg_state != 6 && - now_ms() - cp.registered_ms > 5000) { + now_ms() - cp.registered_ms > 5000 && (!cp.if_on || now_ms() - cp.lost_ms > 600000)) { + /* (cp_if=1, the everyday mode: the CP looks for its network again by itself; given 10 minutes) */ back = "the registration was lost"; why5 = "registration lost"; } else if (cp.stop || go == 2) { From ac054b427217faa063492aa97b2585061d4a0ec9 Mon Sep 17 00:00:00 2001 From: Claudius Date: Sat, 14 Nov 2026 00:15:14 +0000 Subject: [PATCH 6/7] docs/ios27/MODEM-IOS.md: cp_rfs, the status sysctls, scion-celld, scion-bars, everyday-mode behaviour; the CommCenter shim plan (not started) --- docs/ios27/MODEM-IOS.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/docs/ios27/MODEM-IOS.md b/docs/ios27/MODEM-IOS.md index 5d506426a..e942342c8 100644 --- a/docs/ios27/MODEM-IOS.md +++ b/docs/ios27/MODEM-IOS.md @@ -14,6 +14,17 @@ binary, offset or table is in this file. | `cp_if=1` | The active data context becomes `pdp_ip0`: IPv4, point to point, with the context's address on it. The context stays up until `sysctl kern.hakios.cp.go=2` | | `cp_route=1` | An IPv4 default route through `pdp_ip0`, if there is none yet | | `cp_if_kb=N` | A per-boot byte budget on `pdp_ip0`, 2048 KB by default | +| `cp_rfs=1` | With `cp_nv=2`: the CP's file opens (RFS command 0x11, e.g. `CP_OMC_AUDIO_SLSI.bin`) are answered "not there" (handle -1, errno 2) from RAM. Nothing is read or opened | +| (always) | `kern.hakios.cp.info`: one line for user space (stage, registration, technology, bars, dBm, operator code, SIM, data, bytes used, budget, a change counter). `kern.hakios.cp.addr` (root only): the context's address and DNS servers | +| (with `cp_if=1`) | Idle polling in the steady state (1 ms while anything moves, 10 ms after 0.3 s quiet, 50 ms after 5 s); a lost registration gets 10 minutes to come back; the modem is put in low power and reset on an iOS shutdown or restart (xnu `PEHaltRestartInternal`) | + +User space (`tools/ios27/modem/`, built by `build.sh`, installed by `66-celld.sh`): + +| Piece | What it does | +|---|---| +| `scion-celld` | Root daemon (job `com.scion.celld`). While the context is up it publishes `State:/Network/Service/` (`PrimaryRank` Last), `.../IPv4` (address, `InterfaceName` pdp_ip0, `Router` = the address) and `.../DNS` (the context's servers) to configd; withdraws them when it goes. Exits at once on a boot without the modem | +| `scion-bars` (`c.dylib`) | Injected into SpringBoard (`sweetinject=...;/usr/lib/c.dylib,SpringBoard`), on only while `/private/var/local/scion-bars` exists: the status bar's cellular entry (bars, the network's name) from `kern.hakios.cp.info` | +| `scion-fetch`, job `com.scion.celltest` | Test roots only: one https GET by name through iOS's own stack, logged with configd's view and the routes | Seen on phone 1 (SM-G960F, GoMo SIM on eir, 272-03), 13 Nov 2026: - SIM ready, registered HOME. @@ -82,3 +93,29 @@ What stays out of every path: (step 1 above). 3. Then the status-bar library (step 2). 4. Then decide on step 3 with the owner. + +## The CommCenter shim (plan only; not started: needs the owner's go) + +What a real CoreTelephony baseband would take, written down so that the decision can be made on facts: + +1. **Find what CommCenter opens.** On the iPhone12,1 identity CommCenter expects an Intel baseband: IOKit services of + Apple's baseband drivers (the PCIe "ARI" transport and its user client), a boot/firmware path (BBUpdater), and the + ARI protocol once the modem is up. On this port none of these services exist, so CommCenter decides there is no + baseband. The first step is read-only: boot with CommCenter's own logging and list every IOKit service, XPC service + and file it looks for (no code). +2. **Two shapes, both large:** + - *An ARI baseband in the kernel:* a fake transport service (our own IOService with the matching names and a user + client) that answers ARI messages for the few groups CommCenter needs to say "SIM ready, registered, signal N, + data context up", translating to and from Samsung IPC (`hakios.shannon.c` already speaks the SIM status, + registration, signal and the data context). ARI's message set for this firmware generation is public only in + part (ARIstoteles, WiSec 2021); every message would have to be learned from CommCenter's behaviour, never from + Apple binaries copied into the tree. + - *A replacement for CommCenter's XPC services:* our own daemon answering `CTServerConnection` clients. The + interface is large and private; Settings, SpringBoard and many daemons use it. +3. **What it would give:** SIM in Settings, the operator from the carrier bundle, bars everywhere, data through iOS's own + path (CommCenter makes `pdp_ip` and the configd service itself, replacing `scion-celld`). +4. **Limits that stay:** no IMEI or NV change, no unlock, no calls or SMS without the owner's say-so, EFS read-only, + no Apple binary patched. +5. **Smallest useful first step if the owner says go:** step 1 (the list), then a kernel-only stub service that makes + CommCenter log "baseband found" and nothing more, behind a boot-arg. + From 5cae1b707c1467c6d9785188dc396fba76be837f Mon Sep 17 00:00:00 2001 From: Claudius Date: Sat, 14 Nov 2026 01:16:35 +0000 Subject: [PATCH 7/7] scion-bars: the aggregator's setter hooked too (what is encoded), poke=SELECTOR to rebuild SpringBoard's status bar data --- tools/ios27/modem/scion-bars.m | 46 +++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/tools/ios27/modem/scion-bars.m b/tools/ios27/modem/scion-bars.m index 1edad8b94..01035aa43 100644 --- a/tools/ios27/modem/scion-bars.m +++ b/tools/ios27/modem/scion-bars.m @@ -9,7 +9,8 @@ * name=TEXT the network name shown (default: from the operator code, a small table; else the code itself) * status=N the entry's status value for "registered" (default 3: active), type=N the technology (default: found by * its description, "LTE"), mode=hook|publish|both (default hook: SpringBoard's own status bar data; - * publish: StatusKit's status bar overrides domain), discover=1 (the classes' methods into the log) + * publish: StatusKit's status bar overrides domain), discover=1 (the classes' methods into the log), + * poke=SELECTOR (a method of SpringBoard's status bar aggregator called after a change, to rebuild its data) * Safety: a boot counter (/private/var/local/scion-bars.boots) turns it off after 3 SpringBoard starts in a row that did not * live 90 s. Everything it does is logged to the console as "SCIONBAR[pid]: ...". */ @@ -41,7 +42,7 @@ static NSDictionary *cfg; static id current; /* our cellular entry, nil while the modem is not registered */ -static IMP orig_getter, orig_entry_for_key; +static IMP orig_getter, orig_entry_for_key, orig_setter; static long last_gen = -2; static long @@ -162,6 +163,38 @@ return ((id (*)(id, SEL, id))orig_entry_for_key)(self, _cmd, key); } +static void +my_setter(id self, SEL _cmd, id entry) +{ + id e = current; + ((void (*)(id, SEL, id))orig_setter)(self, _cmd, e != nil ? e : entry); +} + +/* poke=SELECTOR: a no-argument method of SpringBoard's status bar aggregator called after each change, so that it builds + * its data again (found with discover=1) */ +static void +poke(void) +{ + NSString *sel = cfg[@"poke"]; + Class c = objc_getClass("SBStatusBarStateAggregator"); + if (sel.length == 0 || c == Nil) { + return; + } + @try { + id agg = [c respondsToSelector:sel_registerName("sharedInstance")] ? + ((id (*)(id, SEL))objc_msgSend)(c, sel_registerName("sharedInstance")) : nil; + SEL s = NSSelectorFromString(sel); + if (agg != nil && [agg respondsToSelector:s]) { + ((void (*)(id, SEL))objc_msgSend)(agg, s); + say(@"poke: -[SBStatusBarStateAggregator %@] called", sel); + } else { + say(@"poke: no aggregator (%@) or no %@", agg, sel); + } + } @catch (NSException *x) { + say(@"poke: %@", x.reason); + } +} + static void hook(void) { @@ -174,7 +207,13 @@ if (k != NULL) { orig_entry_for_key = method_setImplementation(k, (IMP)my_entry_for_key); } - say(@"hook: STStatusBarData -cellularEntry %s, -entryForKey: %s", m ? "hooked" : "NOT FOUND", k ? "hooked" : "not found"); + Class mc = objc_getClass("STMutableStatusBarData"); + Method st = mc ? class_getInstanceMethod(mc, sel_registerName("setCellularEntry:")) : NULL; + if (st != NULL) { + orig_setter = method_setImplementation(st, (IMP)my_setter); + } + say(@"hook: STStatusBarData -cellularEntry %s, -entryForKey: %s, STMutableStatusBarData -setCellularEntry: %s", + m ? "hooked" : "NOT FOUND", k ? "hooked" : "not found", st ? "hooked" : "not found"); } static void @@ -246,6 +285,7 @@ if (e != nil && ([mode isEqualToString:@"publish"] || [mode isEqualToString:@"both"])) { publish(e); } + poke(); } __attribute__((constructor)) static void