FBDeviceControl/Management/FBInstrumentsClient.m (449 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 "FBInstrumentsClient.h" #import "FBAMDServiceConnection.h" #pragma mark DTX Internals /** Understanding of the DTXMessage protocol is informed by the [ios_instruments_client project](https://github.com/troybowman/ios_instruments_client) */ typedef struct { uint32 magic; uint32 cb; uint16 fragmentId; uint16 fragmentCount; uint32 length; uint32 identifier; uint32 conversationIndex; uint32 channelCode; uint32 expectsReply; } DTXMessageHeader; typedef struct { uint32 flags; uint32 auxiliaryLength; uint64 totalLength; } DTXMessagePayloadHeader; #pragma mark Object Internals typedef struct { BOOL success; uint32 messageIdentifier; uint32 channelCode; id returnValue; NSArray<id> *auxillaryValues; } ResponsePayload; typedef struct { NSString *selector; NSArray<NSData *> *argumentsData; uint32 messageIdentifier; uint32 channelCode; BOOL expectsReply; } RequestPayload; static const ResponsePayload InvalidResponsePayload = { .success = NO, .messageIdentifier = 0, .channelCode = 0, .returnValue = nil, .auxillaryValues = nil, }; @interface FBInstrumentsClient () @property (nonatomic, assign, readwrite) uint32 lastMessageIdentifier; @property (nonatomic, assign, readwrite) int32_t lastChannelIdentifier; @property (nonatomic, copy, readonly) NSDictionary<NSString *, id> *channels; @property (nonatomic, strong, readonly) FBAMDServiceConnection *connection; @property (nonatomic, strong, readonly) dispatch_queue_t queue; @property (nonatomic, strong, readonly) id<FBControlCoreLogger> logger; @end @implementation FBInstrumentsClient #pragma mark Initializers + (FBFuture<FBInstrumentsClient *> *)instrumentsClientWithServiceConnection:(FBAMDServiceConnection *)connection logger:(id<FBControlCoreLogger>)logger { dispatch_queue_t queue = dispatch_queue_create("com.facebook.fbdevicecontrol.fbinstrumentsclient", DISPATCH_QUEUE_SERIAL); return [FBFuture onQueue:queue resolveValue:^ FBInstrumentsClient * (NSError **error) { uint32 responseMessageIdentifier = 0; NSDictionary<NSString *, id> *channels = [FBInstrumentsClient getAvailableChannels:connection responseMessageIdentifierOut:&responseMessageIdentifier error:error]; if (!channels) { return nil; } return [[self alloc] initWithConnection:connection channels:channels lastMessageIdentifier:responseMessageIdentifier queue:queue logger:logger]; }]; } - (instancetype)initWithConnection:(FBAMDServiceConnection *)connection channels:(NSDictionary<NSString *, id> *)channels lastMessageIdentifier:(uint32)lastMessageIdentifier queue:(dispatch_queue_t)queue logger:(id<FBControlCoreLogger>)logger { self = [super init]; if (!self) { return nil; } _connection = connection; _channels = channels; _queue = queue; _logger = logger; _lastMessageIdentifier = lastMessageIdentifier; _lastChannelIdentifier = 0; return self; } #pragma mark Public Methods static NSString *const ProcessControlChannel = @"com.apple.instruments.server.services.processcontrol"; - (FBFuture<NSNumber *> *)launchApplication:(FBApplicationLaunchConfiguration *)configuration { return [FBFuture onQueue:self.queue resolveValue:^ NSNumber * (NSError **error) { NSDictionary<NSString *, NSNumber *> *options = @{ @"StartSuspendedKey": @(configuration.waitForDebugger), @"KillExisting": @(configuration.launchMode != FBApplicationLaunchModeFailIfRunning), }; ResponsePayload response = [self onChannelIdentifier:ProcessControlChannel performSelector:@"launchSuspendedProcessWithDevicePath:bundleIdentifier:environment:arguments:options:" argumentsData:@[ [FBInstrumentsClient argumentDataForArgument:@""], // devicePath: [FBInstrumentsClient argumentDataForArgument:configuration.bundleID], // bundleIdentifier: [FBInstrumentsClient argumentDataForArgument:configuration.environment], // environment: [FBInstrumentsClient argumentDataForArgument:configuration.arguments], // arguments: [FBInstrumentsClient argumentDataForArgument:options], // options: ] error:error]; if (response.success == NO) { return nil; } return response.returnValue; }]; } - (FBFuture<NSNull *> *)killProcess:(pid_t)processIdentifier { return [FBFuture onQueue:self.queue resolveValue:^ NSNull * (NSError **error) { ResponsePayload response = [self onChannelIdentifier:ProcessControlChannel performSelector:@"killPid:" argumentsData:@[ [FBInstrumentsClient argumentDataForArgument:@(processIdentifier)], // pid: ] error:error]; if (response.success == NO) { return nil; } return NSNull.null; }]; } #pragma mark Private Class Methods + (NSData *)capabilitiesArgumentData { static dispatch_once_t onceToken; static NSData *data; dispatch_once(&onceToken, ^{ data = [self argumentDataForArgument:@{@"com.apple.private.DTXBlockCompression": @2, @"com.apple.private.DTXConnection": @1}]; }); return data; } + (NSSet<Class> *)supportedReturnSerializerValues { static dispatch_once_t onceToken; static NSSet<Class> *classes; dispatch_once(&onceToken, ^{ classes = [NSSet setWithArray:@[NSString.class, NSNumber.class, NSDate.class, NSError.class, NSData.class, NSDictionary.class, NSArray.class]]; }); return classes; } + (NSDictionary<NSString *, id> *)getAvailableChannels:(FBAMDServiceConnection *)connection responseMessageIdentifierOut:(uint32 *)responseMessageIdentifierOut error:(NSError **)error { RequestPayload request = { .selector = @"_notifyOfPublishedCapabilities:", .argumentsData = @[self.capabilitiesArgumentData], .messageIdentifier = 1, .channelCode = 0, .expectsReply = NO, }; const ResponsePayload response = [self onConnection:connection requestSendAndReceive:request error:error]; if (response.success == NO) { return nil; } NSDictionary<NSString *, NSNumber *> *channels = response.auxillaryValues.firstObject; if (![channels isKindOfClass:NSDictionary.class]) { return [[FBControlCoreError describeFormat:@"%@ is not a dictionary", channels] fail:error]; } return channels; } + (ResponsePayload)onConnection:(FBAMDServiceConnection *)connection requestSendAndReceive:(RequestPayload)request error:(NSError **)error { NSData *requestData = [self requestDataFromRequest:request]; if (![connection send:requestData error:error]) { return InvalidResponsePayload; } return [self receiveMessage:connection request:request error:error]; } static const uint32 DTXMessageHeaderMagic = 0x1F3D5B79; + (NSData *)requestDataFromRequest:(RequestPayload)request { // Arguments are serialized into the auxillary data. NSData *auxillaryData = [self auxillaryDataFromArgumentsData:request.argumentsData]; // The selector is the "return value" of a request. In a response this will be the return value of the remote method. NSError *error = nil; NSData *selectorData = [NSKeyedArchiver archivedDataWithRootObject:request.selector requiringSecureCoding:NO error:&error]; NSAssert(selectorData, @"%@", error); // Message header is derivable from payload sizing. DTXMessagePayloadHeader payloadHeader; payloadHeader.flags = 0x2 | (request.expectsReply ? 0x1000 : 0); payloadHeader.auxiliaryLength = (uint32) auxillaryData.length; payloadHeader.totalLength = auxillaryData.length + selectorData.length; // All messages have a magic number. DTXMessageHeader messageHeader; messageHeader.magic = DTXMessageHeaderMagic; messageHeader.cb = sizeof(DTXMessageHeader); // We're sending data in a single fragment. messageHeader.fragmentId = 0; messageHeader.fragmentCount = 1; messageHeader.length = (uint32) sizeof(payloadHeader) + (uint32) payloadHeader.totalLength; messageHeader.identifier = request.messageIdentifier; messageHeader.conversationIndex = 0; messageHeader.channelCode = request.channelCode; messageHeader.expectsReply = (request.expectsReply ? 1 : 0); // Construct the payload from the slices of data. // This is not a multi-part message so is: // 1) The message header, containing the total length of the entire payload. // 2) The payload header, containing sizing for the aux and selector/return payloads. // 3) The aux data (arguments to the remote call). // 4) The selector/return payload (the selector to perform on the remote object). NSMutableData *data = NSMutableData.data; [data appendBytes:&messageHeader length:sizeof(messageHeader)]; [data appendBytes:&payloadHeader length:sizeof(payloadHeader)]; [data appendData:auxillaryData]; [data appendData:selectorData]; return data; } static const uint64 ArgumentMagic = 0x1F0; static const uint32 EmptyDictionaryKey = 10; static const uint32 ObjectArgumentType = 2; static const uint32 Int32ArgumentType = 3; + (NSData *)auxillaryDataFromArgumentsData:(nullable NSArray<NSData *> *)arguments { if (arguments == nil) { return NSData.data; } NSMutableData *argumentsData = NSMutableData.data; for (NSData *argument in arguments) { [argumentsData appendData:argument]; } uint64 payloadLength = argumentsData.length; NSMutableData *data = NSMutableData.data; [data appendBytes:&ArgumentMagic length:sizeof(ArgumentMagic)]; [data appendBytes:&payloadLength length:sizeof(payloadLength)]; [data appendData:argumentsData]; return data; } + (NSData *)argumentDataForArgument:(id)argument { NSError *error = nil; NSData *argumentData = [NSKeyedArchiver archivedDataWithRootObject:argument requiringSecureCoding:NO error:&error]; NSAssert(argumentData, @"%@", error); uint32 argumentSize = (uint32) argumentData.length; NSMutableData *data = NSMutableData.data; [data appendBytes:&EmptyDictionaryKey length:sizeof(EmptyDictionaryKey)]; [data appendBytes:&ObjectArgumentType length:sizeof(ObjectArgumentType)]; [data appendBytes:&argumentSize length:sizeof(argumentSize)]; [data appendData:argumentData]; return data; } + (NSData *)argumentDataForInt32:(int32_t)value { NSMutableData *data = NSMutableData.data; [data appendBytes:&EmptyDictionaryKey length:sizeof(EmptyDictionaryKey)]; [data appendBytes:&Int32ArgumentType length:sizeof(ObjectArgumentType)]; [data appendBytes:&value length:sizeof(value)]; return data; } + (NSArray<id> *)objectArgumentsFromAuxillaryData:(NSData *)data error:(NSError **)error { if (data.length < 16) { return [[FBControlCoreError describeFormat:@"Data is of insufficient length %@", data] fail:error]; } uint64 magic = 0; data = [self advanceData:data buffer:&magic length:sizeof(magic)]; uint64 payloadLength = 0; data = [self advanceData:data buffer:&payloadLength length:sizeof(payloadLength)]; // We need at least the length of the length of the argument data within the buffer. NSMutableArray<id> *arguments = NSMutableArray.array; while (data.length > (sizeof(uint32) * 3)) { uint32 dictionaryKey = 0; uint32 argumentType = 0; uint32 argumentLength = 0; data = [self advanceData:data buffer:&dictionaryKey length:sizeof(dictionaryKey)]; data = [self advanceData:data buffer:&argumentType length:sizeof(argumentType)]; if (argumentType != 2) { return [[FBControlCoreError describeFormat:@"Canot decode argument of type %d", argumentType] fail:error]; } data = [self advanceData:data buffer:&argumentLength length:sizeof(argumentLength)]; NSData *argumentData = nil; data = [self advanceData:data dataOut:&argumentData length:argumentLength]; id argument = [NSKeyedUnarchiver unarchivedObjectOfClasses:self.supportedReturnSerializerValues fromData:argumentData error:error]; if (!argument) { return [[FBControlCoreError describeFormat:@"Failed to decode argument %@", data] fail:error]; } [arguments addObject:argument]; } return arguments; } + (NSData *)advanceData:(NSData *)data buffer:(void *)buffer length:(size_t)length { [data getBytes:buffer length:length]; return [data subdataWithRange:NSMakeRange(length, data.length - length)]; } + (NSData *)advanceData:(NSData *)data dataOut:(NSData **)dataOut length:(size_t)length { if (dataOut) { *dataOut = [data subdataWithRange:NSMakeRange(0, length)]; } return [data subdataWithRange:NSMakeRange(length, data.length - length)]; } + (ResponsePayload)receiveMessage:(FBAMDServiceConnection *)connection request:(RequestPayload)request error:(NSError **)error { // This header will start the first iteration of the loop, then is overwritten on each iteration. DTXMessageHeader messageHeader = { .magic = 0, .cb = 0, .fragmentId = 0, .fragmentCount = UINT16_MAX, .length = 0, .identifier = 0, .conversationIndex = 0, .channelCode = 0, .expectsReply = 0, }; NSMutableData *payloadData = NSMutableData.data; // Will execute at least once, exiting when there are no more fragments. while (messageHeader.fragmentId < messageHeader.fragmentCount - 1) { // Obtain the header payload in this iteration. if (![connection receive:&messageHeader ofSize:sizeof(messageHeader) error:error]) { return InvalidResponsePayload; } // The data is corrupted in some way if the magic number from the header is missing. if (messageHeader.magic != DTXMessageHeaderMagic) { return InvalidResponsePayload; } // We should always expect that identifiers are increasing. if (messageHeader.conversationIndex == 0 && messageHeader.identifier < request.messageIdentifier) { [[FBControlCoreError describeFormat:@"Response identifier %d with lower identifier than that requested (%d)", messageHeader.identifier, request.messageIdentifier] fail:error]; return InvalidResponsePayload; } if (messageHeader.conversationIndex == 1 && messageHeader.identifier != request.messageIdentifier) { [[FBControlCoreError describeFormat:@"Response identifier %d is not the same as requested identifier (%d)", messageHeader.identifier, request.messageIdentifier] fail:error]; return InvalidResponsePayload; } // First message in a multi-part fragment has no payload, move onto the next fragment which does. if (messageHeader.fragmentCount > 1 && messageHeader.fragmentId == 0) { continue; } // Consume all data from this fragment and accumilate it. NSData *fragmentData = [connection receive:messageHeader.length error:error]; if (!fragmentData) { return InvalidResponsePayload; } [payloadData appendData:fragmentData]; } return [self consumePayloadData:payloadData messageHeader:messageHeader error:error]; } + (ResponsePayload)consumePayloadData:(NSData *)payloadData messageHeader:(DTXMessageHeader)messageHeader error:(NSError **)error { // There is a single payload header at the start of the payload, even if it is a multi-part message. DTXMessagePayloadHeader payloadHeader; payloadData = [self advanceData:payloadData buffer:&payloadHeader length:sizeof(payloadHeader)]; uint8 compression = (payloadHeader.flags & 0xFF000) >> 12; if (compression != 0) { return InvalidResponsePayload; } // First comes the auxillary data. size_t auxillaryDataLength = payloadHeader.auxiliaryLength; NSData *auxillaryData = nil; if (auxillaryDataLength) { payloadData = [self advanceData:payloadData dataOut:&auxillaryData length:auxillaryDataLength]; } // Then comes the return value size_t returnValueDataLength = payloadHeader.totalLength - payloadHeader.auxiliaryLength; NSData *returnValueData = nil; if (returnValueDataLength) { [self advanceData:payloadData dataOut:&returnValueData length:returnValueDataLength]; } // Then parse the payload items. return [self parseReturnValueData:returnValueData auxillaryData:auxillaryData messageHeader:messageHeader error:error]; } + (ResponsePayload)parseReturnValueData:(NSData *)returnValueData auxillaryData:(NSData *)auxillaryData messageHeader:(DTXMessageHeader)messageHeader error:(NSError **)error { // Auxillary data comes first. This is typically only used in the handshake id auxillaryValues = nil; if (auxillaryData && auxillaryData.length > 0) { auxillaryValues = [self objectArgumentsFromAuxillaryData:auxillaryData error:error]; if (!auxillaryValues) { return InvalidResponsePayload; } } // Then the return value of the RPC call. For some calls this will be the selector name. id returnValue = nil; if (returnValueData && returnValueData.length > 0) { returnValue = [NSKeyedUnarchiver unarchivedObjectOfClasses:self.supportedReturnSerializerValues fromData:returnValueData error:error]; if (!returnValue) { return InvalidResponsePayload; } if ([returnValue isKindOfClass:NSError.class]) { if (error) { *error = returnValue; } return InvalidResponsePayload; } } return (ResponsePayload) { .success = YES, .messageIdentifier = messageHeader.identifier, .channelCode = messageHeader.channelCode, .returnValue = returnValue, .auxillaryValues = auxillaryValues, }; } #pragma mark Private Instance Methods - (ResponsePayload)onChannelIdentifier:(NSString *)channelIdentifier performSelector:(NSString *)selector argumentsData:(nullable NSArray<NSData *> *)argumentsData error:(NSError **)error { NSNumber *channelCode = [self makeChannelWithIdentifier:channelIdentifier error:error]; if (!channelCode) { return InvalidResponsePayload; } return [self onChannelCode:channelCode.unsignedIntValue performSelector:selector argumentsData:argumentsData error:error]; } - (ResponsePayload)onChannelCode:(uint32)channelCode performSelector:(NSString *)selector argumentsData:(nullable NSArray<NSData *> *)argumentsData error:(NSError **)error { RequestPayload request = { .selector = selector, .argumentsData = argumentsData, .messageIdentifier = [self nextMessageIdentifier], .channelCode = channelCode, .expectsReply = YES, }; return [self requestSendAndReceive:request error:error]; } - (NSNumber *)makeChannelWithIdentifier:(NSString *)identifier error:(NSError **)error { if (self.channels[identifier] == nil) { return [[FBControlCoreError describeFormat:@"Could not make a channel %@ as it is not one of %@", identifier, self.channels.allKeys] fail:error]; } int32_t channelIdentifier = [self nextChannelIdentifier]; RequestPayload request = { .selector = @"_requestChannelWithCode:identifier:", .argumentsData = @[ [FBInstrumentsClient argumentDataForInt32:channelIdentifier], [FBInstrumentsClient argumentDataForArgument:identifier], ], .messageIdentifier = [self nextMessageIdentifier], .channelCode = 0, .expectsReply = YES, }; ResponsePayload response = [self requestSendAndReceive:request error:error]; if (response.success == NO) { return nil; } return @(channelIdentifier); } - (ResponsePayload)requestSendAndReceive:(RequestPayload)request error:(NSError **)error { ResponsePayload response = [FBInstrumentsClient onConnection:self.connection requestSendAndReceive:request error:error]; if (response.success == NO) { return InvalidResponsePayload; } self.lastMessageIdentifier = response.messageIdentifier; return response; } - (uint32)nextMessageIdentifier { uint32 identifier = self.lastMessageIdentifier + 1; self.lastMessageIdentifier = identifier; return identifier; } - (int32_t)nextChannelIdentifier { int32_t identifier = self.lastChannelIdentifier + 1; self.lastChannelIdentifier = identifier; return identifier; } @end