Skip to content

Fix App Decrypt on large apps: OOM, Mach port leak, and live progress - #328

Open
ShintoKosei wants to merge 1 commit into
rooootdev:mainfrom
ShintoKosei:main
Open

ShintoKosei wants to merge 1 commit into
rooootdev:mainfrom
ShintoKosei:main

Conversation

@ShintoKosei

@ShintoKosei ShintoKosei commented Nov 7, 2026 •

Copy link
Copy Markdown

Summary

App Decrypt works for small apps but hangs and then gets killed on large ones. This PR makes it work on multi-GB apps and gives it a live progress bar.

Verified on an iPad: decrypting a ~2.45 GB app now finishes in roughly ten seconds and produces an IPA, and the progress bar updates in real time.

Root causes

  1. OOM in the ZIP writer (the crash). createZipArchive() in lara/classes/zipmgr.swift calls Data(contentsOf:) on every file of the bundle and keeps all of them alive in fileEntries until the archive is fully written. A 2.45 GB app therefore needs >2.45 GB of resident memory inside lara and is killed by jetsam. It also uses a pure-Swift byte-by-byte CRC loop and only ZIP32 fields (UInt32 sizes/offsets, UInt16 entry count).

  2. Mach port leak per page. decrypt_to_output() calls vmmapremotepage() once per page. vmcreateshmemwithobj() creates a memory-entry Mach port with mach_make_memory_entry_64() and bumps the remote vm_object refcount via a kernel write, but callers only mach_vm_deallocate() the mapping - shmem.port was never released, and two early-return paths leaked it as well. Leak and kernel work grow linearly with app size.

  3. No live progress. set_progress_callback() exists in C but was never registered from Swift. Once wired up, notifications were posted from the background decrypt queue; NotificationCenter.Publisher delivers synchronously on the posting thread, so .onReceive mutated @State off-main and SwiftUI only repainted at the very end - the bar appeared to jump straight to 100%. The size enumeration also ran on the main thread, and the only background task assertion ended ~3 s after launch.

  4. Smaller issues: a second full-size copy of the Mach-O was allocated; single-shot read()/write() did not handle short I/O; pg_virt & ~(page_size - 1) used a uint32_t mask and wiped the upper half of >4 GB addresses; copy failures were swallowed by try?.

Changes

  • zipmgr.swift - add createZipArchiveStreaming(): one file at a time, constant memory, zlib crc32, proper ZIP64 (LFH/CD extra fields, EOCD64 + locator), and a progress: closure. The original writer is left untouched for other callers.
  • TaskRop/vm.m / vm.h - add vmreleaseshmem() to release both the mapping and the memory-entry port; fix the two early-return paths that leaked the port.
  • decrypt.m - use it at all six call sites; patch the input buffer in place rather than allocating a second full Mach-O; loop read/write for short I/O; fix the 64-bit page mask; throttle progupdate() to ~1% boundaries (max ~101 callbacks per binary); log page progress.
  • DecryptView.swift - use the streaming writer; post progress notifications on the main queue with a lock-protected value; split the bar into copy (0-35%), page decrypt (35-85%) and pack (85-100%) phases with stage labels; keep it monotonic so framework binaries restarting at 0% cannot move it backwards; move size enumeration off the main thread; hold one background task assertion for the whole job; log free space and copy failures; preserve Payload/ if packing fails so the decrypted binary is recoverable.

The kernel exploit itself is not touched.

Testing

  • On-device, iPad: a ~2.45 GB app decrypts in ~10 s and saves an IPA (previously hung and crashed). Small apps still work.
  • On-device, iPad: progress bar updates live through all phases.
  • Built twice with the repository's own .github/workflows/build.yml (macos-26, Xcode, ldid) - both build and release jobs succeeded.

Related

@ShintoKosei

ShintoKosei commented Nov 7, 2026 •

Copy link
Copy Markdown
Author

CI note: the pull_request check here reports action_required with no jobs started - GitHub is waiting for a maintainer to approve workflows for a fork contribution, so this is not a build failure.

For reference, the exact same workflow was run against this PR's head commit (afa467c) on my fork and passed both jobs:
https://github.com/ShintoKosei/lara/actions/runs/42353871508

Two earlier builds of the same source also passed:

Maintainer edits are enabled on this PR; happy to adjust anything.

App Decrypt works for small apps but hangs and gets killed on large ones
(e.g. a ~2.45 GB app). Root causes:

* createZipArchive() loaded every file of the bundle into RAM via
  Data(contentsOf:) and retained them all until the archive was written,
  so a multi-GB app was an immediate jetsam kill. It also used ZIP32-only
  fields and a pure-Swift byte-by-byte CRC loop.
* decrypt_to_output() called vmmapremotepage() once per page. Each call
  creates a Mach memory-entry port and bumps the remote vm_object refcount
  via a kernel write, but only the mapping was deallocated - the port was
  never released. Cost/leak grows linearly with app size.
* The progress callback was wired in C but never registered from Swift,
  and the size enumeration ran on the main thread while the only
  background task assertion ended ~3 s after launch.

Changes:

* zipmgr.swift: add createZipArchiveStreaming() - one file at a time,
  constant memory, zlib crc32, proper ZIP64 (extra fields, EOCD64 and
  locator), and a progress closure. The original writer is left intact.
* vm.m / vm.h: add vmreleaseshmem() to release both the mapping and the
  memory-entry port; also fix two early-return paths that leaked the port.
* decrypt.m: use it at every call site; patch the input buffer in place
  instead of allocating a second full Mach-O copy; loop read/write for
  short I/O; fix a 32-bit page mask that wiped the upper half of >4 GB
  addresses; throttle progupdate() to ~1% boundaries; log page progress.
* DecryptView.swift: use the streaming writer, post progress notifications
  on the main queue (NotificationCenter.Publisher delivers on the posting
  thread, so background posts mutated @State off-main and the bar only
  jumped to 100% at the end), split the bar into copy/decrypt/pack phases
  with stage labels, move size enumeration off the main thread, keep one
  background task assertion for the whole job, log disk/copy failures and
  preserve Payload/ if packing fails.

Verified on device: decrypting a ~2.45 GB app now finishes in ~10 s
and produces an IPA; the progress bar updates live. Both the original fix
and the progress follow-up were built successfully with the repo's own
GitHub Actions workflow (macos-26, Xcode, ldid).

Relates to rooootdev#298, rooootdev#317.
@ShintoKosei

Copy link
Copy Markdown
Author

Hi @rooootdev and @neonmodder123 - when you have a moment, I'd appreciate a look at this.

One thing worth flagging: the pull_request workflow is sitting at action_required with no jobs started, since workflows from a fork need a maintainer to hit Approve and run before they can go green. The exact same workflow already passed both jobs on my fork against this head commit: https://github.com/ShintoKosei/lara/actions/runs/42353871508

This addresses the App Decrypt hang/crash on large apps reported in #298 and #317, and adds a live progress bar. It's been verified on-device with a ~2.45 GB app (about ten seconds end to end).

No rush at all - happy to split the change, rebase, or adjust anything to better fit the codebase. Thanks for maintaining lara!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant