Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 112 additions & 13 deletions Service/Sources/EDOClientService.m
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,52 @@ + (id)unwrappedObjectFromObject:(id)object {
return resolvedInstance;
}
}
return object;
}

if ([objClass isSubclassOfClass:[NSArray class]]) {
BOOL modified = NO;
NSMutableArray<id> *newArray =
[NSMutableArray arrayWithCapacity:[((NSArray<id> *)object) count]];
for (id item in ((NSArray<id> *)object)) {
id unwrapped = [self unwrappedObjectFromObject:item];
if (unwrapped != item) {
modified = YES;
}
[newArray addObject:unwrapped ?: [NSNull null]];
}
return modified ? [newArray copy] : object;
}
if ([objClass isSubclassOfClass:[NSSet class]]) {
BOOL modified = NO;
NSMutableSet<id> *newSet = [NSMutableSet setWithCapacity:[((NSSet<id> *)object) count]];
for (id item in ((NSSet<id> *)object)) {
id unwrapped = [self unwrappedObjectFromObject:item];
if (unwrapped != item) {
modified = YES;
}
if (unwrapped) {
[newSet addObject:unwrapped];
}
}
return modified ? [newSet copy] : object;
}
if ([objClass isSubclassOfClass:[NSDictionary class]]) {
BOOL modified = NO;
NSMutableDictionary<id, id> *newDict =
[NSMutableDictionary dictionaryWithCapacity:[((NSDictionary<id, id> *)object) count]];
for (id key in ((NSDictionary<id, id> *)object)) {
id value = ((NSDictionary<id, id> *)object)[key];
id unwrappedKey = [self unwrappedObjectFromObject:key];
id unwrappedValue = [self unwrappedObjectFromObject:value];
if (unwrappedKey != key || unwrappedValue != value) {
modified = YES;
}
if (unwrappedKey && unwrappedValue) {
newDict[unwrappedKey] = unwrappedValue;
}
}
return modified ? [newDict copy] : object;
}

return object;
Expand Down Expand Up @@ -218,22 +264,75 @@ + (id)cachedEDOFromObjectUpdateIfNeeded:(id)object {
[EDOBlockObject isBlock:object] ? [EDOBlockObject EDOBlockObjectFromBlock:object] : object;
Class objClass = object_getClass(edoObject);
if (objClass == [EDOObject class] || objClass == [EDOBlockObject class]) {
id localObject = [self distantObjectReferenceForRemoteAddress:edoObject.remoteAddress];
EDOObject *localEDO = localObject;
if ([EDOBlockObject isBlock:localObject]) {
localEDO = [EDOBlockObject EDOBlockObjectFromBlock:localEDO];
NSNumber *edoKey = [NSNumber numberWithLongLong:edoObject.remoteAddress];
__block id result = object;
__block id objectToRelease = nil;
dispatch_sync(self.edoSyncQueue, ^{
id localObject = [self.localDistantObjects objectForKey:edoKey];
EDOObject *localEDO = localObject;
if ([EDOBlockObject isBlock:localObject]) {
localEDO = [EDOBlockObject EDOBlockObjectFromBlock:localObject];
}
// Verify the service in case the old address is overwritten by a new service.
if (localObject && [edoObject.servicePort match:localEDO.servicePort]) {
result = localObject;
} else {
if (localObject) {
objectToRelease = localObject;
}
// Track the new remote object.
[self.localDistantObjects setObject:object forKey:edoKey];
}
});
objectToRelease = nil;
return result;
}

if ([objClass isSubclassOfClass:[NSArray class]]) {
BOOL modified = NO;
NSMutableArray<id> *newArray =
[NSMutableArray arrayWithCapacity:[((NSArray<id> *)object) count]];
for (id item in ((NSArray<id> *)object)) {
id cached = [self cachedEDOFromObjectUpdateIfNeeded:item];
if (cached != item) {
modified = YES;
}
[newArray addObject:cached ?: [NSNull null]];
}
// Verify the service in case the old address is overwritten by a new service.
if ([edoObject.servicePort match:localEDO.servicePort]) {
// Since we already have the EDOObject in the cache, the new decoded EDOObject is
// taken as a temporary local object, which does not send release message.
edoObject.local = YES;
return localObject;
} else {
// Track the new remote object.
[self addDistantObjectReference:object];
return modified ? [newArray copy] : object;
}
if ([objClass isSubclassOfClass:[NSSet class]]) {
BOOL modified = NO;
NSMutableSet<id> *newSet = [NSMutableSet setWithCapacity:[((NSSet<id> *)object) count]];
for (id item in ((NSSet<id> *)object)) {
id cached = [self cachedEDOFromObjectUpdateIfNeeded:item];
if (cached != item) {
modified = YES;
}
if (cached) {
[newSet addObject:cached];
}
}
return modified ? [newSet copy] : object;
}
if ([objClass isSubclassOfClass:[NSDictionary class]]) {
BOOL modified = NO;
NSMutableDictionary<id, id> *newDict =
[NSMutableDictionary dictionaryWithCapacity:[((NSDictionary<id, id> *)object) count]];
for (id key in ((NSDictionary<id, id> *)object)) {
id value = ((NSDictionary<id, id> *)object)[key];
id cachedKey = [self cachedEDOFromObjectUpdateIfNeeded:key];
id cachedValue = [self cachedEDOFromObjectUpdateIfNeeded:value];
if (cachedKey != key || cachedValue != value) {
modified = YES;
}
if (cachedKey && cachedValue) {
newDict[cachedKey] = cachedValue;
}
}
return modified ? [newDict copy] : object;
}

return object;
}

Expand Down
16 changes: 8 additions & 8 deletions Service/Sources/EDODeallocationTracker.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,26 @@

NS_ASSUME_NONNULL_BEGIN

@class EDOHostPort;
@class EDOServicePort;
@class EDOWeakObject;

/**
* The EDODeallocationTracker is a tracker that manages local object's deallocation.
* The deallocation tracker to track the remote object lifecycle.
*
* The tracker is associated with the local object's life cycle. When the local object is wrapped,
* the tracker is attached to the local object. When the local object is no longer in use and
* deallocates, the EDODeallocationTracker will be deallocated as well. An EDOObjectReleaseRequest
* is then sent to remove the remote weak reference from the weak object dictionary.
* It is associated with the underlying object of a weak object. When the underlying object is
* released, the tracker is deallocated and it will send a release message to remove the weak object
* entry from the dictionary in the host service.
*/
@interface EDODeallocationTracker : NSObject

/**
* Creates an instance of the tracker that is associated with the underlying object.
*
* @param trackedObject The remote object that is stored in the weak object dictionary.
* @param hostPort The host port where weak object dictionary holds the remote object.
* @param servicePort The service port where weak object dictionary holds the remote object.
*/
+ (void)enableTrackingForObject:(EDOWeakObject *)trackedObject hostPort:(EDOHostPort *)hostPort;
+ (void)enableTrackingForObject:(EDOWeakObject *)trackedObject
servicePort:(EDOServicePort *)servicePort;

- (instancetype)init NS_UNAVAILABLE;

Expand Down
23 changes: 13 additions & 10 deletions Service/Sources/EDODeallocationTracker.m
Original file line number Diff line number Diff line change
Expand Up @@ -23,51 +23,54 @@
#import "Service/Sources/EDOClientService.h"
#import "Service/Sources/EDOObject+Private.h"
#import "Service/Sources/EDOObjectReleaseMessage.h"
#import "Service/Sources/EDOServicePort.h"
#import "Service/Sources/EDOWeakObject.h"

@interface EDODeallocationTracker ()

/** The tracked object address (EDOWeakObject) that is stored in the weak object dictionary. */
@property(readonly, nonatomic) EDOPointerType remoteObjectAddress;
/** The host port where weak object dictionary holds the remote object. */
@property(readonly, nonatomic) EDOHostPort *hostPort;
@property(readonly, nonatomic) EDOServicePort *servicePort;
@end

@implementation EDODeallocationTracker

+ (void)enableTrackingForObject:(EDOWeakObject *)trackedObject hostPort:(EDOHostPort *)hostPort {
+ (void)enableTrackingForObject:(EDOWeakObject *)trackedObject
servicePort:(EDOServicePort *)servicePort {
// This does not support multiple weak objects (e.g. from different services) that point to the
// same underlying object, as only one single deallocation tracker is associated with the
// underlying object. The host port is merely used to decide where the release request should be
// sent and only the first port associated with the underlying object is used.
// routed to.
EDODeallocationTracker *tracker = objc_getAssociatedObject(trackedObject.weakObject, &_cmd);
if (!tracker) {
tracker = [[self alloc] initWithTrackedObject:trackedObject hostPort:hostPort];
tracker = [[self alloc] initWithTrackedObject:trackedObject servicePort:servicePort];
objc_setAssociatedObject(trackedObject.weakObject, &_cmd, tracker, OBJC_ASSOCIATION_RETAIN);
} else {
NSAssert(
[tracker.hostPort isEqual:hostPort],
[tracker.servicePort match:servicePort],
@"Deallocation tracker does not support tracking the same object from multiple host ports."
@"Existing port: %@\nNew port: %@",
tracker.hostPort, hostPort);
tracker.servicePort, servicePort);
}
}

- (instancetype)initWithTrackedObject:(EDOWeakObject *)trackedObject
hostPort:(EDOHostPort *)hostPort {
servicePort:(EDOServicePort *)servicePort {
self = [super init];
if (self) {
_remoteObjectAddress = (EDOPointerType)trackedObject;
_hostPort = hostPort;
_servicePort = servicePort;
}
return self;
}

- (void)dealloc {
@try {
EDOObjectReleaseRequest *request =
[EDOObjectReleaseRequest requestWithWeakRemoteAddress:self.remoteObjectAddress];
[EDOClientService sendSynchronousRequest:request onPort:self.hostPort];
[EDOObjectReleaseRequest requestWithWeakRemoteAddress:self.remoteObjectAddress
servicePort:self.servicePort];
[EDOClientService sendSynchronousRequest:request onPort:self.servicePort.hostPort];
} @catch (NSException *e) {
// Safely ignore the exception because we don't care about the errors when we send the release
// message. The service could be terminated, or the message can't be processed, but either way,
Expand Down
12 changes: 12 additions & 0 deletions Service/Sources/EDOHostService+Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,18 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (BOOL)isObjectAliveWithPort:(EDOServicePort *)port remoteAddress:(EDOPointerType)remoteAddress;

/**
* Resolves the local object that this service has previously vended for the given address.
*
* The address is treated purely as an opaque lookup key into the service's tracked-object table; it
* is never dereferenced. This is the safe replacement for casting a wire-supplied @c EDOPointerType
* back to @c id.
*
* @param remoteAddress The address that was previously returned to the client in an @c EDOObject.
* @return The tracked local object, or @c nil if @c remoteAddress is not known to this service.
*/
- (nullable id)localObjectForAddress:(EDOPointerType)remoteAddress;

/**
* Removes an EDOObject with the specified address in the host cache.
*
Expand Down
37 changes: 36 additions & 1 deletion Service/Sources/EDOHostService.m
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ @interface EDOHostService ()
* the object.
*/
@property(nonatomic, readonly) NSMutableDictionary<NSNumber *, id> *localObjects;
/**
* The reference counts for the tracked objects in the service. The key is the address of a tracked
* object and the value is the reference count.
*/
@property(nonatomic, readonly) NSMutableDictionary<NSNumber *, NSNumber *> *localObjectsCounts;
/** The queue to update local objects atomically. */
@property(nonatomic, readonly) dispatch_queue_t localObjectsSyncQueue;
/**
Expand Down Expand Up @@ -219,6 +224,7 @@ - (instancetype)initWithPort:(UInt16)port
if (self) {
_registeredToDevice = NO;
_localObjects = [[NSMutableDictionary alloc] init];
_localObjectsCounts = [[NSMutableDictionary alloc] init];
_localObjectsSyncQueue =
dispatch_queue_create("com.google.edo.service.localObjects", DISPATCH_QUEUE_SERIAL);

Expand Down Expand Up @@ -374,6 +380,10 @@ - (EDOObject *)distantObjectForLocalObject:(id)object hostPort:(EDOHostPort *)ho
NSNumber *objectKey = [NSNumber numberWithLongLong:(EDOPointerType)object];
if (object != self.rootLocalObject) {
dispatch_sync(_localObjectsSyncQueue, ^{
NSNumber *countObj = [self.localObjectsCounts objectForKey:objectKey];
long count = countObj ? [countObj longValue] : 0;
[self.localObjectsCounts setObject:[NSNumber numberWithLong:count + 1] forKey:objectKey];

if (![self.localObjects objectForKey:objectKey]) {
[self.localObjects setObject:object forKey:objectKey];
}
Expand All @@ -389,6 +399,19 @@ - (EDOObject *)distantObjectForLocalObject:(id)object hostPort:(EDOHostPort *)ho
return [EDOObject edo_remoteProxyFromUnderlyingObject:object withPort:port];
}
}
- (id)localObjectForAddress:(EDOPointerType)remoteAddress {
// ivar is used directly here to avoid the service lazily creating the listen port.
if (_rootLocalObject && (EDOPointerType)_rootLocalObject == remoteAddress) {
return _rootLocalObject;
}
NSNumber *edoKey = [NSNumber numberWithLongLong:remoteAddress];
__block id object;
dispatch_sync(_localObjectsSyncQueue, ^{
object = self.localObjects[edoKey];
});

return object;
}

- (BOOL)isObjectAliveWithPort:(EDOServicePort *)port remoteAddress:(EDOPointerType)remoteAddress {
if (![_port match:port]) {
Expand All @@ -408,7 +431,19 @@ - (BOOL)removeObjectWithAddress:(EDOPointerType)remoteAddress {
NSNumber *edoKey = [NSNumber numberWithLongLong:remoteAddress];

dispatch_sync(_localObjectsSyncQueue, ^{
[self.localObjects removeObjectForKey:edoKey];
NSNumber *countObj = [self.localObjectsCounts objectForKey:edoKey];
if (countObj) {
long count = [countObj longValue];
if (count > 1) {
[self.localObjectsCounts setObject:[NSNumber numberWithLong:count - 1] forKey:edoKey];
} else {
[self.localObjectsCounts removeObjectForKey:edoKey];
[self.localObjects removeObjectForKey:edoKey];
}
} else {
// Fallback for safety, though it should ideally be tracked.
[self.localObjects removeObjectForKey:edoKey];
}
});
return YES;
}
Expand Down
32 changes: 29 additions & 3 deletions Service/Sources/EDOInvocationMessage.m
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,15 @@ static EDOMethodFamily MethodTypeOfRetainsReturn(const char *methodName, Class t
}
NSArray<NSString *> *exceptionStackTrace = [localException callStackSymbols];
NSArray<NSString *> *currentStackTrace = [NSThread callStackSymbols];
NSArray<NSString *> *majorStackTrace = [exceptionStackTrace
subarrayWithRange:NSMakeRange(0, exceptionStackTrace.count - currentStackTrace.count + 1)];
NSArray<NSString *> *majorStackTrace = exceptionStackTrace;

if (exceptionStackTrace.count >= currentStackTrace.count) {
NSUInteger length = exceptionStackTrace.count - currentStackTrace.count + 1;
if (length <= exceptionStackTrace.count) {
majorStackTrace = [exceptionStackTrace subarrayWithRange:NSMakeRange(0, length)];
}
}

return [[EDORemoteException alloc] initWithName:[localException name]
reason:[localException reason]
callStackSymbols:majorStackTrace];
Expand Down Expand Up @@ -341,7 +348,26 @@ + (EDORequestHandler)requestHandler {
NSAssert([request isKindOfClass:[EDOInvocationRequest class]],
@"EDOInvocationRequest is expected.");
EDOHostPort *hostPort = request.hostPort;
id target = (__bridge id)(void *)request.target;
// The target address arrives off the wire as a raw 64-bit integer. It must NOT be cast to id
// until the service has confirmed it is an object it previously vended; otherwise an attacker
// can supply an arbitrary pointer and obtain a wild dereference / objc_msgSend on a fake isa.
id target = [service localObjectForAddress:request.target];
if (!target) {
NSString *reason = [NSString
stringWithFormat:@"The target address (%llx) is not tracked by this service (selector: "
@"%@, servicePort: %u).",
request.target, request.selectorName, service.port.hostPort.port];
EDORemoteException *remoteException =
[[EDORemoteException alloc] initWithName:EDOServiceGenericException
reason:reason
callStackSymbols:[NSThread callStackSymbols]];

return [EDOInvocationResponse responseWithReturnValue:nil
exception:remoteException
outValues:nil
forRequest:request
targetClass:Nil];
}
SEL sel = NSSelectorFromString(request.selectorName);

EDOBoxedValueType *returnValue;
Expand Down
Loading