FBDeviceControl/Commands/FBDeviceDebugSymbolsCommands.m (332 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "FBDeviceDebugSymbolsCommands.h" #import <dlfcn.h> #import "FBDevice.h" #import "FBAMDServiceConnection.h" #import "FBDeviceControlError.h" // This signature for this function is shown in the OSS release of dyld (ex: https://opensource.apple.com/source/dyld/dyld-433.5/launch-cache/dsc_extractor.cpp.auto.html) typedef int (*SharedCacheExtractor)(const char *sharedCachePath, const char *extractionRootDirectory, void (^progressCallback)(int current, int total)); @interface FBDeviceDebugSymbolsCommands () @property (nonatomic, weak, readonly) FBDevice *device; @property (nonatomic, copy, nullable, readwrite) NSArray<NSString *> *cachedFileListing; @end @implementation FBDeviceDebugSymbolsCommands #pragma mark Initializers + (instancetype)commandsWithTarget:(FBDevice *)target { return [[self alloc] initWithDevice:target]; } - (instancetype)initWithDevice:(FBDevice *)device { self = [super init]; if (!self) { return nil; } _device = device; return self; } #pragma mark FBDeviceActivationCommands Implementation static const uint32_t ListFilesPlistCommand = 0x30303030; static const uint32_t ListFilesPlistAck = ListFilesPlistCommand; static const uint32_t GetFileCommand = 0x01000000; static const uint32_t GetFileAck = GetFileCommand; - (FBFuture<NSArray<NSString *> *> *)listSymbols { return [self fetchRemoteSymbolListing]; } - (FBFuture<NSString *> *)pullSymbolFile:(NSString *)fileName toDestinationPath:(NSString *)destinationPath { return [[self indexOfSymbolFile:fileName] onQueue:self.device.asyncQueue fmap:^(NSNumber *indexNumber) { uint32_t index = indexNumber.unsignedIntValue; return [self writeSymbolFileWithIndex:index toFileAtPath:destinationPath]; }]; } - (FBFuture<NSString *> *)pullAndExtractSymbolsToDestinationDirectory:(NSString *)destinationDirectory { NSError *error = nil; if (![NSFileManager.defaultManager createDirectoryAtPath:destinationDirectory withIntermediateDirectories:YES attributes:nil error:&error]) { return [[FBDeviceControlError describeFormat:@"Failed to create destination directory for symbol extraction: %@", error] failFuture]; } id<FBControlCoreLogger> logger = self.device.logger; return [[[self indicesAndRemotePathsOfSharedCache] onQueue:self.device.asyncQueue fmap:^(NSDictionary<NSNumber *, NSString *> *indicesToRemotePaths) { NSMutableDictionary<NSNumber *, NSString *> *indicesToLocalPaths = NSMutableDictionary.dictionary; for (NSNumber *fileIndex in indicesToRemotePaths.allKeys) { NSString *localFileName = indicesToRemotePaths[fileIndex].lastPathComponent; indicesToLocalPaths[fileIndex] = [destinationDirectory stringByAppendingPathComponent:localFileName]; } [logger logFormat:@"Extracting remote symbols %@", [FBCollectionInformation oneLineDescriptionFromArray:indicesToRemotePaths.allValues]]; return [self extractSymbolFilesWithIndicesMap:indicesToLocalPaths extractedPaths:@[]]; }] onQueue:self.device.asyncQueue fmap:^(NSArray<NSString *> *extractedSymbolFiles) { NSError *innerError = nil; NSString *sharedCachePath = [FBDeviceDebugSymbolsCommands extractSharedCachePathFromPaths:extractedSymbolFiles error:&innerError]; if (!sharedCachePath) { return [FBFuture futureWithError:innerError]; } if (![FBDeviceDebugSymbolsCommands extractSharedCacheFile:sharedCachePath toDestinationDirectory:destinationDirectory logger:self.device.logger error:&innerError]) { return [FBFuture futureWithError:innerError]; } for (NSString *extractedSymbolFile in extractedSymbolFiles) { [NSFileManager.defaultManager removeItemAtPath:extractedSymbolFile error:nil]; } return [FBFuture futureWithResult:destinationDirectory]; }]; } #pragma mark Private - (FBFuture<NSArray<NSString *> *> *)extractSymbolFilesWithIndicesMap:(NSDictionary<NSNumber *, NSString *> *)indicesToName extractedPaths:(NSArray<NSString *> *)extractedPaths { if (indicesToName.count == 0) { return [FBFuture futureWithResult:extractedPaths]; } NSNumber *nextIndexNumber = [indicesToName.allKeys firstObject]; NSString *nextPath = indicesToName[nextIndexNumber]; NSMutableDictionary<NSNumber *, NSString *> *nextIndicesToName = [indicesToName mutableCopy]; [nextIndicesToName removeObjectForKey:nextIndexNumber]; uint32_t nextIndex = nextIndexNumber.unsignedIntValue; return [[self writeSymbolFileWithIndex:nextIndex toFileAtPath:nextPath] onQueue:self.device.asyncQueue fmap:^(NSString *extractedPath) { NSMutableArray<NSString *> *nextExtractedPaths = [extractedPaths mutableCopy]; [nextExtractedPaths addObject:extractedPath]; return [self extractSymbolFilesWithIndicesMap:nextIndicesToName extractedPaths:nextExtractedPaths]; }]; } - (FBFuture<NSDictionary<NSNumber *, NSString *> *> *)indicesAndRemotePathsOfSharedCache { return [[self symbolServiceConnection] onQueue:self.device.asyncQueue pop:^(FBAMDServiceConnection *connection) { NSError *error = nil; NSArray<NSString *> *files = [FBDeviceDebugSymbolsCommands obtainFileListingFromService:connection error:&error]; if (!files) { return [FBFuture futureWithError:error]; } NSArray<NSString *> *matchingFiles = [FBDeviceDebugSymbolsCommands matchingPathsOfSharedCache:files]; NSDictionary<NSNumber *, NSString *> *indicesToFile = [FBDeviceDebugSymbolsCommands matchFiles:matchingFiles againstFileIndices:files error:&error]; if (!indicesToFile) { return [FBFuture futureWithError:error]; } return [FBFuture futureWithResult:indicesToFile]; }]; } - (FBFuture<NSNumber *> *)indexOfSymbolFile:(NSString *)fileName { return [[self symbolServiceConnection] onQueue:self.device.asyncQueue pop:^(FBAMDServiceConnection *connection) { NSError *error = nil; NSArray<NSString *> *files = [FBDeviceDebugSymbolsCommands obtainFileListingFromService:connection error:&error]; if (!files) { return [FBFuture futureWithError:error]; } NSUInteger index = [files indexOfObject:fileName]; if (index == NSNotFound) { return [[FBDeviceControlError describeFormat:@"Could not find %@ within %@", fileName, [FBCollectionInformation oneLineDescriptionFromArray:files]] failFuture]; } return [FBFuture futureWithResult:@(index)]; }]; } - (FBFuture<NSString *> *)writeSymbolFileWithIndex:(uint32_t)index toFileAtPath:(NSString *)destinationPath { return [[self symbolServiceConnection] onQueue:self.device.asyncQueue pop:^(FBAMDServiceConnection *connection) { NSError *error = nil; if(![FBDeviceDebugSymbolsCommands getFileWithIndex:index toDestinationPath:destinationPath onConnection:connection error:&error]) { return [FBFuture futureWithError:error]; } return [FBFuture futureWithResult:destinationPath]; }]; } - (FBFutureContext<FBAMDServiceConnection *> *)symbolServiceConnection { return [[self.device ensureDeveloperDiskImageIsMounted] onQueue:self.device.workQueue pushTeardown:^(FBDeveloperDiskImage *image) { return [self.device startService:@"com.apple.dt.fetchsymbols"]; }]; } - (FBFuture<NSArray<NSString *> *> *)fetchRemoteSymbolListing { return [[self symbolServiceConnection] onQueue:self.device.asyncQueue pop:^(FBAMDServiceConnection *connection) { NSError *error = nil; NSArray<NSString *> *files = [FBDeviceDebugSymbolsCommands obtainFileListingFromService:connection error:&error]; if (!files) { return [FBFuture futureWithError:error]; } return [FBFuture futureWithResult:files]; }]; } + (NSArray<NSString *> *)obtainFileListingFromService:(FBAMDServiceConnection *)connection error:(NSError **)error { if (![FBDeviceDebugSymbolsCommands sendCommand:ListFilesPlistCommand withAck:ListFilesPlistAck commandName:@"ListFilesPlist" onConnection:connection error:error]) { return nil; } NSError *innerError = nil; NSDictionary<NSString *, id> *message = [connection receiveMessageWithError:&innerError]; if (!message) { return [[FBDeviceControlError describeFormat:@"Failed to recieve ListFiles plist message %@", innerError] fail:error]; } NSArray<NSString *> *files = message[@"files"]; if (![FBCollectionInformation isArrayHeterogeneous:files withClass:NSString.class]) { return [[FBDeviceControlError describeFormat:@"ListFilesPlist expected Array<String> for 'files' but got %@", [FBCollectionInformation oneLineDescriptionFromArray:files]] fail:error]; } return files; } + (BOOL)sendCommand:(uint32_t)command withAck:(uint32_t)ack commandName:(NSString *)commandName onConnection:(FBAMDServiceConnection *)connection error:(NSError **)error { NSError *innerError = nil; BOOL success = [connection sendUnsignedInt32:command error:&innerError]; if (!success) { return [[FBDeviceControlError describeFormat:@"Failed to send '%@' command to symbol service %@", commandName, innerError] failBool:error]; } uint32_t response = 0; success = [connection receiveUnsignedInt32:&response error:&innerError]; if (!success) { return [[FBDeviceControlError describeFormat:@"Failed to recieve '%@' response from %@", commandName, innerError] failBool:error]; } if (response != ack) { return [[FBDeviceControlError describeFormat:@"Incorrect '%@' ack from symbol service; got %u expected %u", commandName, response, ack] failBool:error]; } return YES; } + (BOOL)getFileWithIndex:(uint32_t)index toDestinationPath:(NSString *)destinationPath onConnection:(FBAMDServiceConnection *)connection error:(NSError **)error { // Send the command that we want to get a file if (![FBDeviceDebugSymbolsCommands sendCommand:GetFileCommand withAck:GetFileAck commandName:@"GetFiles" onConnection:connection error:error]) { return NO; } // Send the index of the file to pull back NSError *innerError = nil; uint32_t indexWire = OSSwapHostToBigInt32(index); if (![connection sendUnsignedInt32:indexWire error:&innerError]) { return [[FBDeviceControlError describeFormat:@"Failed to send GetFile file index %u packet %@", index, innerError] failBool:error]; } uint64_t recieveLengthWire = 0; if (![connection receiveUnsignedInt64:&recieveLengthWire error:&innerError]) { return [[FBDeviceControlError describeFormat:@"Failed to recieve GetFile file length %@", innerError] failBool:error]; } if (recieveLengthWire == 0) { return [[FBDeviceControlError describe:@"Failed to get file length, recieveLength not returned or is zero."] failBool:error]; } uint64_t recieveLength = OSSwapBigToHostInt64(recieveLengthWire); if (![NSFileManager.defaultManager createFileAtPath:destinationPath contents:nil attributes:nil]) { return [[FBDeviceControlError describeFormat:@"Failed to create destination file at path %@", destinationPath] failBool:error]; } NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:destinationPath]; if (!fileHandle) { return [[FBDeviceControlError describeFormat:@"Failed to open file for writing at %@", destinationPath] failBool:error]; } if (![connection receive:recieveLength toFile:fileHandle error:error]) { return NO; } return YES; } + (BOOL)extractSharedCacheFile:(NSString *)sharedCacheFile toDestinationDirectory:(NSString *)destinationDirectory logger:(id<FBControlCoreLogger>)logger error:(NSError **)error { SharedCacheExtractor extractor = [self getSharedCacheExtractorWithError:error]; if (!extractor) { return NO; } [logger logFormat:@"Extracting shared cache at %@ to directory at %@", sharedCacheFile, destinationDirectory]; int status = extractor(sharedCacheFile.UTF8String, destinationDirectory.UTF8String, ^(int completed, int total){ [logger logFormat:@"Completed %d Total %d", completed, total]; }); if (status != 0) { return [[FBDeviceControlError describeFormat:@"Failed to get extract shared cache directory %@ to %@ with status %d", sharedCacheFile, destinationDirectory, status] failBool:error]; } [logger logFormat:@"Shared cache extracted to %@", destinationDirectory]; return YES; } + (SharedCacheExtractor)getSharedCacheExtractorWithError:(NSError **)error { NSString *path = [self pathForSharedCacheExtractor:error]; if (!path) { return NULL; } void *handle = dlopen(path.UTF8String, RTLD_LAZY); if (!handle) { return [[FBControlCoreError describeFormat:@"Failed to dlopen() %@", path] failPointer:error]; } return FBGetSymbolFromHandle(handle, "dyld_shared_cache_extract_dylibs_progress"); } + (NSString *)pathForSharedCacheExtractor:(NSError **)error { NSString *path = [FBXcodeConfiguration.developerDirectory stringByAppendingPathComponent:@"Platforms/iPhoneOS.platform/usr/lib/dsc_extractor.bundle"]; if (![NSFileManager.defaultManager fileExistsAtPath:path]) { return [[FBDeviceControlError describeFormat:@"Expected dyld_shared_cache extractor library was not found at path %@", path] fail:error]; } return path; } + (NSArray<NSString *> *)matchingPathsOfSharedCache:(NSArray<NSString *> *)files { NSMutableArray<NSString *> *matchingFiles = NSMutableArray.array; for (NSString *file in files) { if (![file hasPrefix:@"/System/Library"]) { continue; } if (![file containsString:@"shared_cache"]) { continue; } [matchingFiles addObject:file]; } return matchingFiles; } + (NSDictionary<NSNumber *, NSString *> *)matchFiles:(NSArray<NSString *> *)files againstFileIndices:(NSArray<NSString *> *)fileIndices error:(NSError **)error { NSMutableDictionary<NSNumber *, NSString *> *indexToFileName = NSMutableDictionary.dictionary; for (NSString *file in files) { NSUInteger index = [fileIndices indexOfObject:file]; if (index == NSNotFound) { return [[FBDeviceControlError describeFormat:@"Could not find %@ within %@", file, [FBCollectionInformation oneLineDescriptionFromArray:fileIndices]] fail:error]; } indexToFileName[@(index)] = file; } return indexToFileName; } + (NSString *)extractSharedCachePathFromPaths:(NSArray<NSString *> *)paths error:(NSError **)error { for (NSString *path in paths) { if ([path.pathExtension isEqualToString:@""]) { return path; } } return [[FBDeviceControlError describeFormat:@"Could not find the shared cache file within %@", [FBCollectionInformation oneLineDescriptionFromArray:paths]] fail:error]; } @end