r/computerforensics • u/BraveSuccotash8180 • 17h ago
I reverse-engineered iMazing and built my own iPhone migration tool — backup works perfectly, but every restore ends up with zero data. Where am I going wrong?
TL;DR — I reverse-engineered iMazing to build my own iPhone migration tool: I mapped how it classifies a backup into data sets (domain / category model), how it draws the line around photos, and then implemented the whole thing myself on top of what iOS does natively — the mobilebackup2 protocol, via pymobiledevice3. The goal is simple: move everything except photos/videos from an old iPhone to a new one, without paying for or depending on iMazing.
Backup works perfectly. Restore does not work at all. The device accepts the restore, writes everything into its own staging area (/private/var/mobile/.backup.i), logs BackupAgent2 =restorable= Restoring attributes …, finishes with ErrorCode 0, reboots — and the data is simply not there. Free space goes back up, i.e. the staging area was discarded.
I've made 9 attempts and eliminated 6 hypotheses by experiment. I'm posting the evidence so someone can tell me where my mistake is, instead of me guessing again.
Environment
- iPhone 15 Pro, iOS 26.6.2, macOS 26.6.2, USB
pymobiledevice311.43.1 (Python 3.11)- Backup taken from the same device (I know this is unusual — it's a test device I erased)
- Backup: 55.2 GB / 1,202,310 entries, unencrypted (
IsEncrypted: false,Version: 10.0,SystemDomainsVersion: 24.0,WasPasscodeSet: true), 130 apps inInfo.plist, photos pruned out viapatch_manifest - Restore call in use:
python
await Mobilebackup2Service(provider).restore(
backup_directory=work_parent, # contains <source_udid>/{Manifest.db,Manifest.plist,Info.plist,Status.plist,...}
source=source_udid,
reboot=True, copy=True, settings=True, remove=False,
skip_apps=False, system=False,
)
What works (so you can trust the rest of the setup)
- Backup: 55.2 GB / 1,202,310 entries land on disk; classification of 28 categories validated against a real 133 GB iMazing backup (unmapped domains 1.00% → 0.0004%).
- Export: contacts DB exports with
ABPerson373 rows andABMultiValue2027;sms.dbwith 8054 messages; 143 voice memos are real.m4a. So reading side is solid.
The failure, three variants
(A) copy=False (RestoreDontCopyBackup=True): the full 55 GB streams to the device, progress hits 100%, the client gets a success response. After reboot: 0 user apps installed, free space unchanged (481.8 / 486.2 GB). Info.plist name is restored, so something was applied, but no data.
(B) copy=True (RestoreDontCopyBackup=False): the device starts copying into its own storage — BackupAgent2 logs Creating safe harbor for container … / Writing …, free space drops 499.0 → 495.55 GB in the first ~50 s — but the client's progress callback stays at 0.0% the entire time. I wrongly killed the process at that point in two earlier attempts. When I finally let a small pruned backup run to completion, the command returned 0 after 5.1 minutes and the device logged:
BackupAgent2: =restorable= Restoring attributes u/gid 501/501, b/m/c/atime …, permissions 0755 to <private>
(lots of these — it was restoring attributes for ~136k directories). Then it rebooted, and afterwards: data gone, free space back to the pre-restore value (481.83 → 481.56 during restore, then back to 481.80).
(C) A pruned backup without directory rows produced a very informative error, which I fixed:
ErrorCode 2: '_restoreRegularFiles:size: rename error: No such file or directory (2)
at path "/private/var/mobile/.backup.i/var/mobile/Library/Preferences/com.apple.mobilephone.plist"'
Cause: my "fast prune" kept only regular files (flags=1) and deleted directory rows (flags=2), so the device had no parent directory to rename into. Fixing that (keep all flags != 1) removed this error — and let the restore reach case (B).
Gates I've already found and satisfied
These all produce their own errors, and all of them are now cleared before the attempt:
| Gate | Error if not satisfied | How I handle it |
|---|---|---|
| Device screen locked (backup) | 208 MBErrorDomain/208 Device locked |
keep unlocked, screen on |
Stale sync lock /com.apple.itunes.lock_sync |
host hangs, zero bytes, no error | post com.apple.itunes-mobdev.syncDidFinish, wait, retry (posting syncCancelRequest instead kills the connection) |
| Device wants the user's passcode | Please enter the device passcode to continue the backup |
user confirms on device |
| Find My iPhone enabled | 211 MBErrorDomain/211 Find My iPhone must be disabled |
user signs out of Apple ID entirely |
| Device not activated | The device does not offer the requested service (mobilebackup2 absent) |
complete Setup Assistant (without Apple ID) |
| Host not paired | lockdownd returns GetProhibitedError for every GetValue |
re-pair (lockdown pair) |
So the restore attempt in (B) starts from: activated, paired, no Apple ID, no Find My, unlocked, preflight all green.
What I've ruled out (by experiment, not by reading)
- Incomplete backup / pruned photos — case (A) used the unpruned 55 GB backup. Still discarded.
- Client disconnecting too early — the 5.1-minute run finished on its own with
ErrorCode 0; I didn't touch it. Still discarded. - Missing directory entries — fixed (case C → B). Doesn't explain the discard.
- Wrong
Status.plist— mine isBackupState: new,SnapshotState: finished(libimobiledevice refuses to restore unlessSnapshotState == finished, so this is required and satisfied). - Encryption — backup is unencrypted,
WillEncrypt == false, no password involved. - Target device state (setup assistant vs. desktop) — tried both: from Setup Assistant the service isn't exposed yet (
does not offer the requested service); from a completed setup it accepts the restore.
What I suspect, ranked — please correct me
- The last hop needs Apple's own host. In Finder/iTunes the device reboots mid-restore and then reconnects to the Mac to finish applying data (
MobileDevice.frameworkdriving the second phase). With a singlemobilebackup2session that connection is gone, so the device has nobody to serve the remaining data and drops the staged copy. Is that actually how it works? Doesmobilebackup2'sRestoreever work without the host being re-attached after reboot? - **
RestorePreserveSettingsis inverted in my call.** pymobiledevice3'srestore(settings=True)writesRestorePreserveSettings=True, whose meaning (per libimobiledevice source:RestorePreserveSettings = ((flags & CMD_FLAG_RESTORE_SETTINGS) == 0)) is "preserve the target's settings, do not restore the backup's" — the opposite of a Finder restore. For a full restore, should this befalse? Couldtruesend the device down a "merge" path that ends up discarding staging? - Restoring onto the same UDID as the backup's source takes a special code path. I can't test a cross-device restore (only one test device available, and I must not touch the other phone). Does anyone know if
source == targetis handled differently? - iOS 26 changed the restore flow and the open-source implementations haven't caught up.
My questions
- Has anyone successfully done a backup restore (not IPSW) on iOS 18/19/26 with libimobiledevice's
idevicebackup2 restoreor pymobiledevice3? If yes, with which exact option combination, and does it survive the reboot? - Is there a documented/observed second handshake after the reboot in Apple's restore flow, and what service does it use? If so, is it reachable outside
MobileDevice.framework? - What is the correct value of
RestorePreserveSettings/RemoveItemsNotRestored/RestoreSystemFilesfor a full "make this phone identical to that backup" restore? - Any way to see why the device discarded staging — a log level, a preference, or a file under
/private/var/mobile/.backup.iorBackupAgent2's own logs (readable from the host)?syslog liveonly shows live lines; the decisive message is probably printed during the reboot, which I can't capture. - Am I fighting something fundamentally not reimplementable, i.e. should I give up on this layer and drive
MobileDevice.frameworkinstead? (The alternative is to drop the backup into~/Library/Application Support/MobileSync/Backup/and let Finder do the restore — which definitely works, but it's not "my" code doing it.)
Extra detail that may be relevant
- Trimmed backups for a single category: 13 manifest rows + all 136,357 directory rows + 191 special rows.
Manifest.dbrebuilt andVACUUMed (49 KB). Files hardlinked, not copied. This is what case (B) used. - Device-side logs I can see with
pymobiledevice3 syslog live:Creating safe harbor for container …,Writing …,=restorable= Restoring attributes …, and during backup=scanning= Not backed up (attribute)Added regular file. - I never see anything like "restore complete/failed" after the reboot.
- The device ends up activated and empty after the attempt (free space as before, 0 user apps), i.e. it erased itself and then didn't apply the backup.
Happy to share the exact commands, the manifest layout, or run any experiment that would be informative — the device is a dedicated test unit and I can wipe it freely. What would you test next?
Appendix A — raw device responses (personal data removed)
These are the unmodified payloads the device returned, in the order I hit them.
(1) Backup, screen locked
{'ErrorCode': 208, 'ErrorDescription': 'Device locked (MBErrorDomain/208)', 'MessageName': 'Response'}
(2) Restore, Find My iPhone still enabled
{'ErrorCode': 211, 'ErrorDescription': 'Find My iPhone must be disabled before restoring (MBErrorDomain/211)', 'MessageName': 'Response'}
(3) Restore on a non-activated device
pymobiledevice3.exceptions.InvalidServiceError: The device does not offer the requested service.
(4) Restore, after my bad prune (this one was my bug and is now fixed)
{'ErrorCode': 2, 'ErrorDescription': '_restoreRegularFiles:size: rename error: No such file or directory (2) at path
"/private/var/mobile/.backup.i/var/mobile/Library/Preferences/com.apple.mobilephone.plist" (MBErrorDomain/2).
Underlying error: The operation couldn't be completed. No such file or directory (NSPOSIXErrorDomain/2).',
'MessageName': 'Response'}
(5) The "successful" restore that still lost everything — device-side log during the run
BackupAgent2[648] <INFO>: Creating safe harbor for container <private> with type <private> at <private>
BackupAgent2[648] <DEBUG>: Creating directory <private>
BackupAgent2[648] <DEBUG>: Writing <private>
BackupAgent2[522] <INFO>: =restorable= Restoring attributes u/gid 501/501, b/m/c/atime
1792477268/1792477268/1792477268/1793029156, permissions 0700 to <private>
... (thousands of these, for ~136k directories)
Then the host side printed 恢复流程已结束 / restore returned 0, and after the reboot:
before restore : free 481.83 GB
during restore : free 481.56 GB (i.e. ~0.27 GB written to staging)
after reboot : free 481.80 GB (staging gone, nothing applied)
user apps after: 0
Appendix B — the exact options I send
RestoreShouldReboot = True # reboot when done
RestoreDontCopyBackup = False # => copy the backup into device storage first
RestorePreserveSettings = True # pymobiledevice3's default. In libimobiledevice,
# this is set TRUE when -s ("restore settings") is NOT given,
# so it appears to mean "keep the target's settings"
RestoreSystemFiles = False
RemoveItemsNotRestored = False
For a "make this phone identical to that backup" restore, should RestorePreserveSettings be false? And does RestoreDontCopyBackup = false (copy first) conflict with that?
Appendix C — what I can share on request
No personal data in any of it:
- the full
Manifest.dbschema plus sample rows for one category, - the complete host-side log of all restore attempts,
- the sanitized restore code path,
- a trimmed one-category backup (a few MB) that reproduces the discard, so someone can try it on their own device.