Inside the iOS bug that made deleted photos reappear

Written by Quentin Salingue - 23/05/2024 - in Reverse-engineering - Download

Last week, Apple released iOS 17.5. Since then multiple people reported seeing photos on their phone they had previously deleted. The bug was fixed in 17.5.1. In this blogpost we will dive into how the bug appeared and how it was fixed by Apple.

Getting the updates

For this analysis, we will use the iPhone 13 as an example to compare the changes made between iOS 17.5 and iOS 17.5.1.

All iOS updates are available on Apple servers. There are multiple websites listing those updates such as appledb.dev or ipsw.me. You can also download them using the awesome ipsw tool. This tool also contains a lot of very useful features that will help us for our analysis. Refer to the documentation in the repository for the installation or just use the docker image.

Updates are distributed in the IPSW format which is just a ZIP file under the hood. This ZIP file contains:

  • disk images containing the filesystem
  • firmware files for many coprocessors
  • BuildManifest.plist: a XML file describing the content of the update

On iOS, most of the userland code is located in the DYLD shared cache. It is a huge bundle of almost all the libraries on the filesystem which can be found in the DMG named Cryptex inside the build manifest. You can also look at the size of the files: the largest DMG is the filesystem and the second one is the shared cache:

$ ls -lt 17.5/*.dmg
-rw-r--r-- 1 1000 1000  201326619 Jan  9  2007 17.5/090-24459-112.dmg
-rw-r--r-- 1 1000 1000  203423771 Jan  9  2007 17.5/090-24511-112.dmg
-rw-r--r-- 1 1000 1000   10485760 Jan  9  2007 17.5/090-24896-112.dmg
-rw-r--r-- 1 1000 1000 6024037986 Jan  9  2007 17.5/090-36922-095.dmg
-rw-r--r-- 1 1000 1000 3558866944 Jan  9  2007 17.5/090-37181-098.dmg

Here 090-36922-095.dmg is the filesystem DMG and 090-37181-098.dmg is the DYLD DMG. Both files are APFS (APple FileSystem) disk images, they can be mounted natively on macOS or using apfs-fuse on Linux:

$ apfs-fuse 17.5/090-37181-098.dmg /mnt/

apfs-fuse will create a fake 'root' directory containing the filesystem. The shared cache is located at /System/Library/Caches/com.apple.dyld/ and is made of many files:

$ ls /mnt/root/System/Library/Caches/com.apple.dyld/
dyld_shared_cache_arm64e     dyld_shared_cache_arm64e.10  dyld_shared_cache_arm64e.20  dyld_shared_cache_arm64e.30               dyld_shared_cache_arm64e.40  dyld_shared_cache_arm64e.50
dyld_shared_cache_arm64e.01  dyld_shared_cache_arm64e.11  dyld_shared_cache_arm64e.21  dyld_shared_cache_arm64e.31               dyld_shared_cache_arm64e.41  dyld_shared_cache_arm64e.51
dyld_shared_cache_arm64e.02  dyld_shared_cache_arm64e.12  dyld_shared_cache_arm64e.22  dyld_shared_cache_arm64e.32               dyld_shared_cache_arm64e.42  dyld_shared_cache_arm64e.52
dyld_shared_cache_arm64e.03  dyld_shared_cache_arm64e.13  dyld_shared_cache_arm64e.23  dyld_shared_cache_arm64e.33.dylddata      dyld_shared_cache_arm64e.43  dyld_shared_cache_arm64e.53
dyld_shared_cache_arm64e.04  dyld_shared_cache_arm64e.14  dyld_shared_cache_arm64e.24  dyld_shared_cache_arm64e.34.dyldlinkedit  dyld_shared_cache_arm64e.44  dyld_shared_cache_arm64e.54
dyld_shared_cache_arm64e.05  dyld_shared_cache_arm64e.15  dyld_shared_cache_arm64e.25  dyld_shared_cache_arm64e.35               dyld_shared_cache_arm64e.45  dyld_shared_cache_arm64e.55.dylddata
dyld_shared_cache_arm64e.06  dyld_shared_cache_arm64e.16  dyld_shared_cache_arm64e.26  dyld_shared_cache_arm64e.36               dyld_shared_cache_arm64e.46  dyld_shared_cache_arm64e.56.dyldlinkedit
dyld_shared_cache_arm64e.07  dyld_shared_cache_arm64e.17  dyld_shared_cache_arm64e.27  dyld_shared_cache_arm64e.37               dyld_shared_cache_arm64e.47  dyld_shared_cache_arm64e.symbols
dyld_shared_cache_arm64e.08  dyld_shared_cache_arm64e.18  dyld_shared_cache_arm64e.28  dyld_shared_cache_arm64e.38               dyld_shared_cache_arm64e.48
dyld_shared_cache_arm64e.09  dyld_shared_cache_arm64e.19  dyld_shared_cache_arm64e.29  dyld_shared_cache_arm64e.39               dyld_shared_cache_arm64e.49

Hopefully, the tools we will use handle this format pretty well.

Now that we have downloaded the updates and identified the components, we can start the diffing process.

Comparing the updates

Finding where the code changed

Remember ipsw? One of the many features it has is comparing DYLD shared caches at a high level. This is done with the command ipsw dyld info using the arguments --delta and --dylibs:

$ ipsw dyld info --delta 17.5/com.apple.dyld/dyld_shared_cache_arm64e 17.5.1/com.apple.dyld/dyld_shared_cache_arm64e --dylibs
### 🆕 dylibs


### ❌ removed dylibs


### ⬆️ (delta) updated dylibs

- (0.1.0.0.0) `/System/Library/PrivateFrameworks/PencilPairingUI.framework/PencilPairingUI`  

---

- (0.0.10.0.0) `/System/Library/Accounts/Notification/PhotosAccountNotificationPlugin.bundle/PhotosAccountNotificationPlugin`  
- (0.0.10.0.0) `/System/Library/Frameworks/AssetsLibrary.framework/AssetsLibrary`  
- (0.0.10.0.0) `/System/Library/Frameworks/Photos.framework/Photos`  
- (0.0.10.0.0) `/System/Library/Frameworks/PhotosUI.framework/PhotosUI`  
- (0.0.10.0.0) `/System/Library/Frameworks/_PhotosUI_SwiftUI.framework/_PhotosUI_SwiftUI`  
- (0.0.10.0.0) `/System/Library/PrivateFrameworks/AssetExplorer.framework/AssetExplorer`  
- (0.0.10.0.0) `/System/Library/PrivateFrameworks/CPAnalytics.framework/CPAnalytics`  
- (0.0.10.0.0) `/System/Library/PrivateFrameworks/CameraKit.framework/CameraKit`  
- (0.0.10.0.0) `/System/Library/PrivateFrameworks/CloudPhotoLibrary.framework/CloudPhotoLibrary`  
- (0.0.10.0.0) `/System/Library/PrivateFrameworks/CloudPhotoServices.framework/CloudPhotoServices`  
- (0.0.10.0.0) `/System/Library/PrivateFrameworks/CoreMediaStream.framework/CoreMediaStream`  
- (0.0.10.0.0) `/System/Library/PrivateFrameworks/KnowledgeGraphKit.framework/KnowledgeGraphKit`  
- (0.0.10.0.0) `/System/Library/PrivateFrameworks/MediaConversionService.framework/MediaConversionService`  
- (0.0.10.0.0) `/System/Library/PrivateFrameworks/MediaMiningKit.framework/MediaMiningKit`  
- (0.0.10.0.0) `/System/Library/PrivateFrameworks/MediaStream.framework/MediaStream`  
- (0.0.10.0.0) `/System/Library/PrivateFrameworks/NeutrinoCore.framework/NeutrinoCore`  
- (0.0.10.0.0) `/System/Library/PrivateFrameworks/NeutrinoKit.framework/NeutrinoKit`  
- (0.0.10.0.0) `/System/Library/PrivateFrameworks/PhotoAnalysis.framework/PhotoAnalysis`  
- (0.0.10.0.0) `/System/Library/PrivateFrameworks/PhotoFoundation.framework/PhotoFoundation`  
- (0.0.10.0.0) `/System/Library/PrivateFrameworks/PhotoImaging.framework/PhotoImaging`  
- (0.0.10.0.0) `/System/Library/PrivateFrameworks/PhotoLibrary.framework/PhotoLibrary`  
- (0.0.10.0.0) `/System/Library/PrivateFrameworks/PhotoLibraryServices.framework/PhotoLibraryServices`  
- (0.0.10.0.0) `/System/Library/PrivateFrameworks/PhotoLibraryServicesCore.framework/PhotoLibraryServicesCore`  
- (0.0.10.0.0) `/System/Library/PrivateFrameworks/PhotosFormats.framework/PhotosFormats`  
- (0.0.10.0.0) `/System/Library/PrivateFrameworks/PhotosGraph.framework/PhotosGraph`  
- (0.0.10.0.0) `/System/Library/PrivateFrameworks/PhotosImagingFoundation.framework/PhotosImagingFoundation`  
- (0.0.10.0.0) `/System/Library/PrivateFrameworks/PhotosIntelligence.framework/PhotosIntelligence`  
- (0.0.10.0.0) `/System/Library/PrivateFrameworks/PhotosKnowledgeGraph.framework/PhotosKnowledgeGraph`  
- (0.0.10.0.0) `/System/Library/PrivateFrameworks/PhotosPlayer.framework/PhotosPlayer`  
- (0.0.10.0.0) `/System/Library/PrivateFrameworks/PhotosUICore.framework/PhotosUICore`  
- (0.0.10.0.0) `/System/Library/PrivateFrameworks/PhotosUIEdit.framework/PhotosUIEdit`  
- (0.0.10.0.0) `/System/Library/PrivateFrameworks/PhotosUIPrivate.framework/PhotosUIPrivate`  
- (0.0.10.0.0) `/System/Library/PrivateFrameworks/PlacesKit.framework/PlacesKit`  
- (0.0.10.0.0) `/usr/lib/swift/libswiftAssetsLibrary.dylib`  
- (0.0.10.0.0) `/usr/lib/swift/libswiftPhotos.dylib`  
- (0.0.10.0.0) `/usr/lib/swift/libswiftPhotosUI.dylib`  

---

- (0.0.4.0.0) `/System/Library/PrivateFrameworks/ATFoundation.framework/ATFoundation`  
- (0.0.4.0.0) `/System/Library/PrivateFrameworks/AirTraffic.framework/AirTraffic`  
- (0.0.4.0.0) `/System/Library/PrivateFrameworks/AirTrafficDevice.framework/AirTrafficDevice`  

---

- (0.0.1.0.0) `/System/Library/Frameworks/UIKit.framework/UIKit`  
- (0.0.1.0.0) `/System/Library/PrivateFrameworks/CollectionViewCore.framework/CollectionViewCore`  
- (0.0.1.0.0) `/System/Library/PrivateFrameworks/KeyboardArbiter.framework/KeyboardArbiter`  
- (0.0.1.0.0) `/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore`  
- (0.0.1.0.0) `/System/Library/PrivateFrameworks/UIKitServices.framework/UIKitServices`  
- (0.0.1.0.0) `/usr/lib/swift/libswiftUIKit.dylib`  

---

- (0.0.0.2.1) `/System/Library/Accounts/Notification/IDSAccountNotificationPlugin.bundle/IDSAccountNotificationPlugin`  
- (0.0.0.2.1) `/System/Library/DataClassMigrators/FaceTimeMigrator.migrator/FaceTimeMigrator`  
- (0.0.0.2.1) `/System/Library/PrivateFrameworks/BagKit.framework/BagKit`  
- (0.0.0.2.1) `/System/Library/PrivateFrameworks/FTClientServices.framework/FTClientServices`  
- (0.0.0.2.1) `/System/Library/PrivateFrameworks/FTServices.framework/FTServices`  
- (0.0.0.2.1) `/System/Library/PrivateFrameworks/IDS.framework/IDS`  
- (0.0.0.2.1) `/System/Library/PrivateFrameworks/IDSFoundation.framework/IDSFoundation`  
- (0.0.0.2.1) `/System/Library/PrivateFrameworks/IDSHashPersistence.framework/IDSHashPersistence`  
- (0.0.0.2.1) `/System/Library/PrivateFrameworks/IDSKVStore.framework/IDSKVStore`  
- (0.0.0.2.1) `/System/Library/PrivateFrameworks/PDS.framework/PDS`  
- (0.0.0.2.1) `/System/Library/PrivateFrameworks/PDSAgent.framework/PDSAgent`  

---

- (0.0.0.1.0) `/System/Library/Frameworks/PencilKit.framework/PencilKit` 

A lot of photo-related libraries were updated. We extract those libraries with ipsw dyld extract:

$ for dylib in $(cat ../dylibs); do ipsw dyld extract com.apple.dyld/dyld_shared_cache_arm64e $dylib; done
   • Created com.apple.dyld/IDSAccountNotificationPlugin
   • Created com.apple.dyld/PhotosAccountNotificationPlugin
   • Created com.apple.dyld/FaceTimeMigrator
   • Created com.apple.dyld/AssetsLibrary
   • Created com.apple.dyld/Photos
   • Created com.apple.dyld/PhotosUI
   • Created com.apple.dyld/_PhotosUI_SwiftUI
   • Created com.apple.dyld/UIKit
   • Created com.apple.dyld/AirTrafficDevice
   • Created com.apple.dyld/AirTraffic
   • Created com.apple.dyld/AssetExplorer
   • Created com.apple.dyld/ATFoundation
   • Created com.apple.dyld/BagKit
   • Created com.apple.dyld/CameraKit
   • Created com.apple.dyld/CloudPhotoLibrary
   • Created com.apple.dyld/CloudPhotoServices
   • Created com.apple.dyld/CollectionViewCore
   • Created com.apple.dyld/CoreMediaStream
   • Created com.apple.dyld/CPAnalytics
   • Created com.apple.dyld/FTClientServices
   • Created com.apple.dyld/FTServices
   • Created com.apple.dyld/IDSFoundation
   • Created com.apple.dyld/IDS
   • Created com.apple.dyld/IDSHashPersistence
   • Created com.apple.dyld/IDSKVStore
   • Created com.apple.dyld/KeyboardArbiter
   • Created com.apple.dyld/KnowledgeGraphKit
   • Created com.apple.dyld/MediaConversionService
   • Created com.apple.dyld/MediaMiningKit
   • Created com.apple.dyld/MediaStream
   • Created com.apple.dyld/NeutrinoCore
   • Created com.apple.dyld/NeutrinoKit
   • Created com.apple.dyld/PDSAgent
   • Created com.apple.dyld/PDS
   • Created com.apple.dyld/PencilPairingUI
   • Created com.apple.dyld/PhotoAnalysis
   • Created com.apple.dyld/PhotoFoundation
   • Created com.apple.dyld/PhotoImaging
   • Created com.apple.dyld/PhotoLibrary
   • Created com.apple.dyld/PhotoLibraryServicesCore
   • Created com.apple.dyld/PhotoLibraryServices
   • Created com.apple.dyld/PhotosFormats
   • Created com.apple.dyld/PhotosGraph
   • Created com.apple.dyld/PhotosImagingFoundation
   • Created com.apple.dyld/PhotosIntelligence
   • Created com.apple.dyld/PhotosKnowledgeGraph
   • Created com.apple.dyld/PhotosPlayer
   • Created com.apple.dyld/PhotosUICore
   • Created com.apple.dyld/PhotosUIEdit
   • Created com.apple.dyld/PhotosUIPrivate
   • Created com.apple.dyld/PlacesKit
   • Created com.apple.dyld/UIKitCore
   • Created com.apple.dyld/UIKitServices
   • Created com.apple.dyld/libswiftAssetsLibrary.dylib
   • Created com.apple.dyld/libswiftPhotos.dylib
   • Created com.apple.dyld/libswiftPhotosUI.dylib
   • Created com.apple.dyld/libswiftUIKit.dylib

Now we can use the cmp utility and some bash scripting to see how much of those files were modified relative to their size:

#!/bin/sh

dylib=$1
f=$(basename $dylib);

diff_count=$(cmp -l 17.5/com.apple.dyld/$f 17.5.1/com.apple.dyld/$f | wc -l)
size=$(du -b 17.5/com.apple.dyld/$f | cut -f 1)

python -c "print('$dylib: %.2f'%(100.*$diff_count/$size))"
$ for dylib in $(cat dylibs); do ./cmp_dylibs.sh $dylib ; done
/System/Library/Accounts/Notification/IDSAccountNotificationPlugin.bundle/IDSAccountNotificationPlugin: 0.54
/System/Library/Accounts/Notification/PhotosAccountNotificationPlugin.bundle/PhotosAccountNotificationPlugin: 0.52
/System/Library/DataClassMigrators/FaceTimeMigrator.migrator/FaceTimeMigrator: 0.20
/System/Library/Frameworks/AssetsLibrary.framework/AssetsLibrary: 0.27
/System/Library/Frameworks/Photos.framework/Photos: 0.19
/System/Library/Frameworks/PhotosUI.framework/PhotosUI: 0.18
/System/Library/Frameworks/_PhotosUI_SwiftUI.framework/_PhotosUI_SwiftUI: 0.06
/System/Library/Frameworks/UIKit.framework/UIKit: 0.05
/System/Library/PrivateFrameworks/AirTrafficDevice.framework/AirTrafficDevice: 7.25
/System/Library/PrivateFrameworks/AirTraffic.framework/AirTraffic: 0.33
/System/Library/PrivateFrameworks/AssetExplorer.framework/AssetExplorer: 0.29
/System/Library/PrivateFrameworks/ATFoundation.framework/ATFoundation: 0.42
/System/Library/PrivateFrameworks/BagKit.framework/BagKit: 0.23
/System/Library/PrivateFrameworks/CameraKit.framework/CameraKit: 0.23
/System/Library/PrivateFrameworks/CloudPhotoLibrary.framework/CloudPhotoLibrary: 0.42
/System/Library/PrivateFrameworks/CloudPhotoServices.framework/CloudPhotoServices: 0.39
/System/Library/PrivateFrameworks/CollectionViewCore.framework/CollectionViewCore: 0.55
/System/Library/PrivateFrameworks/CoreMediaStream.framework/CoreMediaStream: 0.43
/System/Library/PrivateFrameworks/CPAnalytics.framework/CPAnalytics: 0.35
/System/Library/PrivateFrameworks/FTClientServices.framework/FTClientServices: 0.27
/System/Library/PrivateFrameworks/FTServices.framework/FTServices: 0.54
/System/Library/PrivateFrameworks/IDSFoundation.framework/IDSFoundation: 12.34
/System/Library/PrivateFrameworks/IDS.framework/IDS: 0.22
/System/Library/PrivateFrameworks/IDSHashPersistence.framework/IDSHashPersistence: 0.13
/System/Library/PrivateFrameworks/IDSKVStore.framework/IDSKVStore: 0.20
/System/Library/PrivateFrameworks/KeyboardArbiter.framework/KeyboardArbiter: 0.29
/System/Library/PrivateFrameworks/KnowledgeGraphKit.framework/KnowledgeGraphKit: 0.10
/System/Library/PrivateFrameworks/MediaConversionService.framework/MediaConversionService: 0.46
/System/Library/PrivateFrameworks/MediaMiningKit.framework/MediaMiningKit: 0.19
/System/Library/PrivateFrameworks/MediaStream.framework/MediaStream: 0.59
/System/Library/PrivateFrameworks/NeutrinoCore.framework/NeutrinoCore: 0.40
/System/Library/PrivateFrameworks/NeutrinoKit.framework/NeutrinoKit: 0.33
/System/Library/PrivateFrameworks/PDSAgent.framework/PDSAgent: 0.31
/System/Library/PrivateFrameworks/PDS.framework/PDS: 0.28
/System/Library/PrivateFrameworks/PencilPairingUI.framework/PencilPairingUI: 19.24
/System/Library/PrivateFrameworks/PhotoAnalysis.framework/PhotoAnalysis: 0.11
/System/Library/PrivateFrameworks/PhotoFoundation.framework/PhotoFoundation: 0.47
/System/Library/PrivateFrameworks/PhotoImaging.framework/PhotoImaging: 0.46
/System/Library/PrivateFrameworks/PhotoLibrary.framework/PhotoLibrary: 0.51
/System/Library/PrivateFrameworks/PhotoLibraryServicesCore.framework/PhotoLibraryServicesCore: 0.39
/System/Library/PrivateFrameworks/PhotoLibraryServices.framework/PhotoLibraryServices: 4.75
/System/Library/PrivateFrameworks/PhotosFormats.framework/PhotosFormats: 0.44
/System/Library/PrivateFrameworks/PhotosGraph.framework/PhotosGraph: 0.10
/System/Library/PrivateFrameworks/PhotosImagingFoundation.framework/PhotosImagingFoundation: 0.39
/System/Library/PrivateFrameworks/PhotosIntelligence.framework/PhotosIntelligence: 0.13
/System/Library/PrivateFrameworks/PhotosKnowledgeGraph.framework/PhotosKnowledgeGraph: 0.09
/System/Library/PrivateFrameworks/PhotosPlayer.framework/PhotosPlayer: 0.50
/System/Library/PrivateFrameworks/PhotosUICore.framework/PhotosUICore: 0.22
/System/Library/PrivateFrameworks/PhotosUIEdit.framework/PhotosUIEdit: 0.25
/System/Library/PrivateFrameworks/PhotosUIPrivate.framework/PhotosUIPrivate: 0.23
/System/Library/PrivateFrameworks/PlacesKit.framework/PlacesKit: 0.97
/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore: 0.27
/System/Library/PrivateFrameworks/UIKitServices.framework/UIKitServices: 0.30
/usr/lib/swift/libswiftAssetsLibrary.dylib: 0.06
/usr/lib/swift/libswiftPhotos.dylib: 0.05
/usr/lib/swift/libswiftPhotosUI.dylib: 0.05
/usr/lib/swift/libswiftUIKit.dylib: 0.05

Most of the files have less than 1% of bytes differing. This is due to the format of the shared cache, all libraries are glued together and contains offsets to each other. If a dependency is modified, you have to tweak the offsets related to this dependency, hence all the small modifications.

We only have 4 dylibs that had substantial changes:

/System/Library/PrivateFrameworks/AirTrafficDevice.framework/AirTrafficDevice: 7.25
/System/Library/PrivateFrameworks/IDSFoundation.framework/IDSFoundation: 12.34
/System/Library/PrivateFrameworks/PencilPairingUI.framework/PencilPairingUI: 19.24
/System/Library/PrivateFrameworks/PhotoLibraryServices.framework/PhotoLibraryServices: 4.75

Given that the bug is related to photos, it makes more sense to look at PhotoLibraryServices. Using yet another command from ipsw, ipsw macho info we see that code was likely removed from the 17.5.1 version:

$ ipsw macho info 17.5/com.apple.dyld/PhotoLibraryServices | grep text
    sz=0x005b6410 off=0x00003838-0x005b9c48 addr=0x19fe8f838-0x1a0445c48               __TEXT.__text            PureInstructions|SomeInstructions
$ ipsw macho info 17.5.1/com.apple.dyld/PhotoLibraryServices | grep text
    sz=0x005b63d4 off=0x00003874-0x005b9c48 addr=0x19fe8f874-0x1a0445c48               __TEXT.__text            PureInstructions|SomeInstructions

Old size was 0x005b6410, new size size is 0x005b63d4, a change of 60 bytes.

Diffing with IDA

Now is the time to fire up IDA and BinDiff to find the fix. IDA has a good support of the DYLD shared cache, we load the library and let the auto-analysis do all the hard work for us. Once it is done for both 17.5 and 17.5.1 we run BinDiff. The shared cache contains a lot of Objective-C related symbols, which helps the binary diffing process. All symbols are matched using their name which speeds up the process. The results are shown below:

Results of BinDiff, showing changes in function _PLModelMigrationActionRegistration_17000

As expected not much changed between both versions. Out of the ~65000 functions, BinDiff detected that only 19 functions were different. Upon closer inspection, only _PLModelMigrationActionRegistration_17000 had any meaningful changes. Not shown in the above screen, but BinDiff tells us that the old function had 224 instructions whereas the new one only has 209. The code is AARCH64 assembly which has 4-byte instructions. Guess what, (224-209)*4 = 60, exactly the amount of bytes missing in the new version.

Below is the pseudo-code diff of this function:

diff --git a/tmp/old b/tmp/new
index b154afe..75960e1 100644
--- a/tmp/old
+++ b/tmp/new
@@ -14,6 +14,7 @@ void __fastcall PLModelMigrationActionRegistration_17000(void *a1)
     "registerActionClass:onCondition:",
     objc_opt_class_0(&OBJC_CLASS___PLModelMigrationAction_ResetRevGeoAndShiftedLocation),
     v2 >> 2 == 4250);
+  v5 = v2 - 17000;
   objc_msgSend(
     v9,
     "registerActionClass:onCondition:",
@@ -59,18 +60,14 @@ void __fastcall PLModelMigrationActionRegistration_17000(void *a1)
     "registerActionClass:onCondition:",
     objc_opt_class_0(&OBJC_CLASS___PLModelMigrationAction_DeletePetPersonsAndDetectedFaces),
     v2 < 17051);
-  v5 = v4 != (void *)3;
   objc_msgSend(
     v9,
     "registerActionClass:onCondition:",
     objc_opt_class_0(&OBJC_CLASS___PLModelMigrationAction_ResetExternalAssets),
     (v2 - 17000 < 58) & (unsigned __int8)(v4 != (void *)3));
-  LOBYTE(v4) = v4 == (void *)1;
-  objc_msgSend(
-    v9,
-    "registerActionClass:onCondition:",
-    objc_opt_class_0(&OBJC_CLASS___PLModelMigrationAction_PushAssetsWithPetSyncableFaces),
-    (v2 - 17000 < 59) & (unsigned __int8)v4);
+  v6 = objc_opt_class_0(&OBJC_CLASS___PLModelMigrationAction_PushAssetsWithPetSyncableFaces);
+  v8 = v2 - 17000 < 59 && v4 == (void *)1;
+  objc_msgSend(v9, "registerActionClass:onCondition:", v6, v8);
   objc_msgSend(
     v9,
     "registerActionClass:onCondition:",
@@ -85,7 +82,7 @@ void __fastcall PLModelMigrationActionRegistration_17000(void *a1)
     v9,
     "registerActionClass:onCondition:",
     objc_opt_class_0(&OBJC_CLASS___PLModelMigrationAction_FixupDefaultStickerConfidenceScoreValues),
-    v2 - 17000 < 48);
+    v5 < 48);
   objc_msgSend(
     v9,
     "registerActionClass:onCondition:",
@@ -101,16 +98,11 @@ void __fastcall PLModelMigrationActionRegistration_17000(void *a1)
     "registerActionClass:onCondition:",
     objc_opt_class_0(&OBJC_CLASS___PLModelMigrationAction_ReevaluateAllowedForAnalysisForMontageAssets),
     v2 < 17401);
-  objc_msgSend(
-    v9,
-    "registerActionClass:onCondition:",
-    objc_opt_class_0(&OBJC_CLASS___PLModelMigrationAction_ResetFilesystemImportToken),
-    (v2 - 17400 < 100) & (unsigned __int8)v4);
   objc_msgSend(
     v9,
     "resetBackgroundActionClass:onCondition:",
     objc_opt_class_0(&OBJC_CLASS___PLModelMigrationAction_RebuildHighlights),
-    (v2 - 17000 < 102) & (unsigned __int8)v5);
+    (v5 < 102) & (unsigned __int8)(v4 != (void *)3));
   objc_msgSend(
     v9,
     "resetBackgroundActionClass:onCondition:",

PLModelMigrationActionRegistration_17000 is the function in charge of registering data migration handlers for iOS 17. These handlers are called to convert data from an older format to the latest version. This is done by checking the previousStoreVersion (which is the version we are upgrading from) against a set of versions. If our previous version matches the condition, the method performActionWithManagedObjectContext:error: of the handler will be called.

In the above pseudocode, slight changes occured around the PLModelMigrationAction_PushAssetsWithPetSyncableFaces registration but the behavior is unchanged. Just below, the registration of PLModelMigrationAction_ResetFilesystemImportToken was removed. The condition associated with this handler was v2 - 17400 < 100 meaning iOS < 17.5.

Inside the function -[PLModelMigrationAction_ResetFilesystemImportToken performActionWithManagedObjectContext:error:], the boolean value didImportFileSystemAssets is set to 0. This value is checked in multiple places of the code, one of them being -[PLModelMigrator _loadFileSystemDataIntoDatabaseIfNeededWithReason:progress:] which will then call -[PLModelMigrator _importAllDCIMAssetsInLibrary:progress:progressFraction:rebuildComplete:]. As the name implies, it scans the filesystem looking for photos to add to the photo library.

The exact list of paths that will be queried is hard to determine only with static analysis, as you have to traverse many layers of abstractions (as well as many Objective-C selectors). The interested reader can start looking into these methods:

  • -[PLModelMigrator _orderedAssetsToImportInLibrary:]
  • -[PLModelMigrator _orderedAssetsToImportInLibrary:cameraRollOnly:]

Based on this code, we can say that the photos that reappeared were still lying around on the filesystem and that they were just found by the migration routine added in iOS 17.5. The reason why those files were there in the first place is unknown.

Looking around for another fix

The fix detailed in the previous section only prevents photos present on disk to reappear in the library, but they are still there nonetheless. We quickly looked at the other dylibs but as expected, changes did not seem related to the issue.

So where can we look now? Two places might be interesting:

  • code added to the ramdisk that performs the update
  • file changed on the main filesytem

The update ramdisk is one of the two smaller DMG (~200M), it is in the Update subsection of the BuildManifest.plist. For 17.5.1 it is 090-24896-117.dmg. The ramdisks are in the IM4P file format, and the underlying APFS disk can be extracted with img4lib. Mounting both the old and new ramdisks using apfs-fuse, we can diff them recursively:

$ diff -r /mnt/apple/rd_{old,new}

diff: /mnt/apple/rd_old/root/sbin/fsck_hfs: No such file or directory
diff: /mnt/apple/rd_new/root/sbin/fsck_hfs: No such file or directory
diff: /mnt/apple/rd_old/root/sbin/mount_hfs: No such file or directory
diff: /mnt/apple/rd_new/root/sbin/mount_hfs: No such file or directory
diff: /mnt/apple/rd_old/root/sbin/newfs_hfs: No such file or directory
diff: /mnt/apple/rd_new/root/sbin/newfs_hfs: No such file or directory
diff -r /mnt/apple/rd_old/root/System/Library/CoreServices/SystemVersion.plist /mnt/apple/rd_new/root/System/Library/CoreServices/SystemVersion.plist
6c6
<     <string>210B8A2C-09C3-11EF-9DB8-273A64AEFA1C</string>
---
>     <string>400002B2-1393-11EF-B638-7DF77EE7F452</string>
8c8
<     <string>21F79</string>
---
>     <string>21F90</string>
14c14
<     <string>17.5</string>
---
>     <string>17.5.1</string>
Binary files /mnt/apple/rd_old/root/System/Library/PrivateFrameworks/IOMobileFramebuffer.framework/IOMobileFramebuffer and /mnt/apple/rd_new/root/System/Library/PrivateFrameworks/IOMobileFramebuffer.framework/IOMobileFramebuffer differ

Nothing very interesting here, just a bump of a version file and modifications to an unrelated library (missing files are due to broken symlinks, but irrelevant).

We repeat the same process with the filesystem where we have more hits:

$ diff -r /mnt/apple/{old,new} > fs_diff

$ wc -l fs_diff
1297 fs_diff

We wrote a script that parses the output of the diff to only compare MachO executables:

#!/usr/bin/env python

import re
from lief import MachO
from sys import argv

MACHO_MAGIC = b"\xcf\xfa\xed\xfe"

path = argv[1]

for line in open(path):
    if "Binary files" not in line:
        continue

    m = re.match("Binary files (.*) and (.*) differ", line)
    old_path, new_path = m.groups()

    if open(old_path, "rb").read(4) != MACHO_MAGIC:
        continue

    old = MachO.parse(old_path).at(0)
    new = MachO.parse(new_path).at(0)

    for old_sec in old.sections:
        new_sec = new.get_section(old_sec.segment.name, old_sec.name)

        if old_sec.size != new_sec.size:
            print(f"size of section {old_sec.segment.name}:{old_sec.name} in {old_path} changed")
            continue

        if old_sec.offset != new_sec.offset:
            print(f"offset of {old_sec.segment.name}:{old_sec.name} in {old_path} changed")
            continue

Running this script only shows differences on identityservicesd which seems to contain obfuscated code, and is most probably irrelevant to the bug we are examining.

Conclusion

The 17.5.1 update removed the scanning of the filesystem that was added in 17.5 to prevent deleted photos stored outside of the photo library to re-appear. According to our analysis, no code was added to purge the imported photos from the library as well as the "deleted" pictures lying on the filesystem.

Based only on this analysis, it is not possible to conclude how the photos remained on the filesystem in the first place, but this comment on Reddit has some plausible explanations.

Shout out to Saagar Jha who tweeted before us!