diff --git a/docs/IPODTOUCH4.md b/docs/IPODTOUCH4.md index 00c0bf1ed..5d3345775 100644 --- a/docs/IPODTOUCH4.md +++ b/docs/IPODTOUCH4.md @@ -35,7 +35,13 @@ iOS 6.1.6 is untethered). The completed bootstrap provides: - OpenSSH on device port 22; - a dedicated RSA client key and pinned device host key; - `PasswordAuthentication no` after public-key login succeeds; -- `ldid`, `uicache`, and `uiopen` for application deployment. +- `ldid`, `uicache`, and `uiopen` for application deployment; +- **AppSync Unified and its Cydia Substrate dependencies** for local self-signed + User applications. Install the `iphoneos-arm` package from the + [upstream release](https://github.com/akemin-dayo/AppSync/releases), then + reboot once to activate it. `doctor` checks the installed package. The + deployment command reports an installation failure if the signing support + is inactive; it does not fall back to a System application. The default local files are: @@ -72,6 +78,7 @@ bun ipodtouch4 deploy bun ipodtouch4 launch bun ipodtouch4 status [--require-action] bun ipodtouch4 capture +bun ipodtouch4 uninstall # removes the app and its data ``` `build` resolves `apps/clear/pocket.json` against the `ipodtouch4-dev` @@ -81,12 +88,36 @@ embedded as `__pocket_js` / `__pocket_pak` sections. The build id hashes the plan, the guest artifacts, every native object, the sysroot stubs, and the baked artwork. -`deploy` copies the bundle over the USB SSH tunnel, verifies every file's -SHA-256 on the device against the local receipt, and installs under a leased -transactional lock with rollback — the same protocol as the iPhone 4S, with -device paths under `pocketjs-ipodtouch4`. - -`status` reads `/private/var/tmp/pocketjs-ipodtouch4.status` twice and +**`build` also produces `dist/ipodtouch4/PocketJSiPodTouch4.ipa`.** `deploy` +transfers that IPA over the pinned USB SSH tunnel and calls iOS 6 +`MobileInstallationInstall` with `ApplicationType=User`. **iOS creates the +UUID container under `/var/mobile/Applications`, owns updates, and preserves +`Documents` and `Library` on update.** Every installed bundle file, including +the build receipt, must match its local SHA-256. A kernel file lock serializes +installation and CLI removal; process exit releases the lock. + +The first deployment migrates the former `/Applications/PocketJSiPodTouch4.app` +installation. It checks the bundle identifier, retains the old bundle in a +root-owned migration journal, and refreshes its System registration before +installing the User app. The migration restarts `installd` to reload its +in-memory System map; SpringBoard is not restarted. A failed installation restores the old bundle; the +next deployment reconciles an interrupted migration. The journal is removed +after User registration and installed byte verification pass. The app's old +bundle-specific preferences are copied into its new container when present; +other files in the shared mobile home are not treated as app-owned data. + +**Long-pressing the User app on SpringBoard exposes the native delete badge.** +Deleting there, or running `bun ipodtouch4 uninstall`, uses iOS's uninstall +service and removes the application container, including its data. A later +`deploy` installs a fresh container. The CLI verifies that the registration +and container are gone. The privileged installer bridge stays under +`/var/root/Library/PocketJS`; its install/uninstall entitlement is never added +to the application binary. + +`launch`, `status`, and `capture` look up the current container from the iOS +installation record. The runtime resolves `NSTemporaryDirectory()` and keeps +its receipts and captures inside that container. **`status` reads +`/tmp/pocketjs.status` twice** and requires the running build id, an advancing frame counter and heartbeat, and the GLES1 640×960 density-2 drawable. With `--require-action` it additionally requires at least one completed touch sequence and a reported `clear_gesture` @@ -94,3 +125,5 @@ action — a receipt that a gesture interaction completed on the hardware. `capture` asks the running app for a raw RGBA frame and converts it to `dist/ipodtouch4/device-frame.png`. + +User application icons use **opaque 57×57 and 114×114 artwork**. SpringBoard applies the rounded mask and shadow; `UIPrerenderedIcon` suppresses the stock gloss. The System application path uses a precomposed transparent mask instead. Baking that mask into a User icon adds an inset rim under the native mask. Icon filenames include the artwork revision so an update selects a fresh SpringBoard cache entry. diff --git a/hosts/ios-legacy/svcwire.c b/hosts/ios-legacy/svcwire.c index 33299ccfc..99deae1df 100644 --- a/hosts/ios-legacy/svcwire.c +++ b/hosts/ios-legacy/svcwire.c @@ -152,6 +152,11 @@ static int set_nonblocking(int fd) { return flags >= 0 && fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0; } +static int disable_sigpipe(int fd) { + int enabled = 1; + return setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &enabled, sizeof enabled) == 0; +} + static void enter_state(SvcState next) { state = next; state_since_ms = now_ms(); @@ -232,7 +237,7 @@ static int poll_listener(int adopt) { if (listen_fd < 0) return 0; fd = accept(listen_fd, (struct sockaddr *)&peer, &peer_length); if (fd < 0) return 0; - if (!adopt || !set_nonblocking(fd)) { + if (!adopt || !set_nonblocking(fd) || !disable_sigpipe(fd)) { close(fd); return 0; } @@ -336,7 +341,7 @@ static void start_connect(void) { enter_state(SVC_STATE_BACKOFF); return; } - if (!set_nonblocking(fd)) { + if (!set_nonblocking(fd) || !disable_sigpipe(fd)) { close(fd); enter_state(SVC_STATE_BACKOFF); return; @@ -466,7 +471,8 @@ static void handle_frame(uint8_t type, const uint8_t *payload, uint32_t length) } static void pump_rx(void) { - for (;;) { + /* A continuously readable peer must still yield to the next UI frame. */ + for (unsigned reads = 0; reads < 4; ++reads) { int progressed; /* An oversized (or unwanted-bulk) payload is drained without buffering. */ if (skip_remaining > 0) { diff --git a/hosts/iphone4s/Icon.svg b/hosts/iphone4s/Icon.svg index 87a4b8334..cf9115836 100644 --- a/hosts/iphone4s/Icon.svg +++ b/hosts/iphone4s/Icon.svg @@ -4,10 +4,16 @@ + + + + + + diff --git a/hosts/ipodtouch4/Info.plist b/hosts/ipodtouch4/Info.plist index d3741d0d7..a145ac46d 100644 --- a/hosts/ipodtouch4/Info.plist +++ b/hosts/ipodtouch4/Info.plist @@ -16,10 +16,10 @@ CFBundleName PocketJS iPod touch 4 CFBundleIconFile - PocketClassic-v6.png + PocketClassic-User-v7.png CFBundleIconFiles - PocketClassic-v6 + PocketClassic-User-v7 CFBundlePackageType APPL diff --git a/hosts/ipodtouch4/README.md b/hosts/ipodtouch4/README.md index 56aa39472..697497245 100644 --- a/hosts/ipodtouch4/README.md +++ b/hosts/ipodtouch4/README.md @@ -16,5 +16,10 @@ for the legacy Apple hosts. SpringBoard artwork reuses the classic PocketJS icon pipeline (`tools/iphone-classic-icon.ts`). -Use `bun ipodtouch4 doctor`, then the build, deploy, launch, status, and -capture commands documented in `docs/IPODTOUCH4.md`. +Deployment produces an IPA and uses iOS 6 MobileInstallation to register a +**sandboxed User application with native SpringBoard deletion**. AppSync +Unified is required for these local self-signed builds. Updates retain app +data; uninstall removes the container and its runtime receipt files. + +Use `bun ipodtouch4 doctor`, then the build, deploy, launch, status, capture, +and uninstall commands documented in `docs/IPODTOUCH4.md`. diff --git a/hosts/ipodtouch4/installer-entitlements.plist b/hosts/ipodtouch4/installer-entitlements.plist new file mode 100644 index 000000000..b06cb060a --- /dev/null +++ b/hosts/ipodtouch4/installer-entitlements.plist @@ -0,0 +1,12 @@ + + + + + com.apple.private.mobileinstall.allowedSPI + + Install + Uninstall + Lookup + + + diff --git a/hosts/ipodtouch4/installer.c b/hosts/ipodtouch4/installer.c new file mode 100644 index 000000000..abd54b579 --- /dev/null +++ b/hosts/ipodtouch4/installer.c @@ -0,0 +1,145 @@ +/* iOS 6 MobileInstallation bridge. Runs as root over the pinned USB SSH + * transport; application binaries remain ordinary, sandboxed User apps. + * Like the legacy host, this uses runtime messaging to avoid modern ObjC + * metadata in the historical ARMv7 linker. No installation cache is edited. */ +#include +#include +#include +#include +#include +#include +#include + +typedef void *id; +typedef void *SEL; +extern id objc_getClass(const char *name); +extern SEL sel_registerName(const char *name); +extern void *objc_msgSend(void); + +static id send0(id object, const char *selector) { + return ((id (*)(id, SEL))objc_msgSend)(object, sel_registerName(selector)); +} +static id send1(id object, const char *selector, id value) { + return ((id (*)(id, SEL, id))objc_msgSend)(object, sel_registerName(selector), value); +} +static id string(const char *value) { + return send1(objc_getClass("NSString"), "stringWithUTF8String:", (id)value); +} +static const char *utf8(id value) { + return value ? (const char *)send0(value, "UTF8String") : ""; +} +static id get(id object, const char *key) { + return send1(object, "objectForKey:", string(key)); +} +static int identifier_valid(const char *value) { + if (!value[0] || value[0] == '.' || strlen(value) > 200) return 0; + for (const char *c = value; *c; ++c) + if (!((*c >= 'a' && *c <= 'z') || (*c >= 'A' && *c <= 'Z') || + (*c >= '0' && *c <= '9') || *c == '.' || *c == '-')) return 0; + return strchr(value, '.') != NULL && strstr(value, "..") == NULL; +} +static id lookup(const char *identifier, const char *type) { + id cache = send1(objc_getClass("NSDictionary"), "dictionaryWithContentsOfFile:", + string("/var/mobile/Library/Caches/com.apple.mobile.installation.plist")); + return get(get(cache, type), identifier); +} +static id user_record(const char *identifier) { + id record = lookup(identifier, "User"); + const char *path = utf8(get(record, "Path")); + const char *container = utf8(get(record, "Container")); + const char *prefix = "/private/var/mobile/Applications/"; + if (strncmp(container, prefix, strlen(prefix)) != 0) prefix = "/var/mobile/Applications/"; + if (strcmp(utf8(get(record, "ApplicationType")), "User") || + strcmp(utf8(get(record, "CFBundleIdentifier")), identifier) || + strncmp(container, prefix, strlen(prefix)) || + strlen(container) != strlen(prefix) + 36 || strstr(path, "/../") || + strncmp(path, container, strlen(container)) || path[strlen(container)] != '/' || + !path[strlen(container) + 1] || strchr(path + strlen(container) + 1, '/')) return NULL; + const char *uuid = container + strlen(prefix); + for (unsigned i = 0; i < 36; ++i) { + if (i == 8 || i == 13 || i == 18 || i == 23) { if (uuid[i] != '-') return NULL; } + else if (!((uuid[i] >= '0' && uuid[i] <= '9') || (uuid[i] >= 'a' && uuid[i] <= 'f') || + (uuid[i] >= 'A' && uuid[i] <= 'F'))) return NULL; + } + return record; +} +static void progress(id info, void *context) { + (void)context; + if (get(info, "Error")) fprintf(stderr, "%s\n", utf8(send0(info, "description"))); +} +/* Migration refreshes the global System map, so all apps share one lock. */ +static int acquire_lock(void) { + int fd = open("/var/root/Library/PocketJS/deployment.lock", O_CREAT | O_RDWR | O_NOFOLLOW, 0600); + if (fd < 0 || flock(fd, LOCK_EX | LOCK_NB)) { + perror("deployment busy (or lock unavailable)"); return -1; + } + return fd; +} +int main(int argc, char **argv) { + if (argc < 3 || getuid() != 0) { + fprintf(stderr, "usage (root): installer lookup|user-path|install|uninstall|bundle-id ; lock