XPC, Swift, ObjC, and arrays

I create a protocol that had, among other things:

@objc func setList(_: [MyType], withReply: @escaping (Error?) -> Void)

The daemon part is in Swift, while the calling part is in Objective-C. Because why not? (Actually, because the calling part has to deal with C++ code, so that's ObjC++; however, I wanted the stronger typing and runtime checking for the daemon part, so I wrote it in Swift.) The ObjC part uses NSArray<MyType*>.

I set up an NSXPCConnection link, and create a (synchronous) proxy with the right protocol name. But when I try to do the XPC setList call, I get an error. I assume that's because it doesn't like the signature. (Surely this is logged somewhere? I couldn't find it, if so. 😩) But... if I have a signature of @objc func addItem(_: MyType, withReply: @escaping (Error?) -> Void), then it works. So I assume it's the array. (Oh, I've also tried it without the @objc; the protocol itself is defined as @objc.)

I've tried changing to protocol signature to using NSArray, but same thing.

Answered by DTS Engineer in 697020022

Objective-C array elements can be of any object type, and indeed different types of objects, which can come as a shock in the XPC world where the client can send you an array whose elements are of an unexpected type. To avoid any resulting security vulnerabilities you have to tell NSXPCConnection the type of elements to expect. Do this using the setClasses(_:for:argumentIndex:ofReply:) method on NSXPCInterface.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Accepted Answer

Objective-C array elements can be of any object type, and indeed different types of objects, which can come as a shock in the XPC world where the client can send you an array whose elements are of an unexpected type. To avoid any resulting security vulnerabilities you have to tell NSXPCConnection the type of elements to expect. Do this using the setClasses(_:for:argumentIndex:ofReply:) method on NSXPCInterface.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

That gets done in the part that will receive the data? And if I call that, I have to also specify String/NSString, Int/NSInt, etc., in addition to my custom classes?

I will be googling for examples later, since I have just woken up, but thanks as usual. 😄

I am in fact failing to find examples of setClasses(_:for:argumentIndex:ofReply:) for Swift. The big thing I'm running into is that the .class member of a class is not hashable. Specifically: note: only concrete types such as structs, enums and classes can conform to protocols

And, ok, I got that solved.

        let exportedInterface = NSXPCInterface(with: MyProtocol.self)
        let allowedClasses = exportedInterface.classes(for: #selector(setList(_:withReply:)), argumentIndex:0, ofReply:false)
        let newSet = allowedClasses.union(NSSet(object: MyClass.self) as! Set<AnyHashable>)
        exportedInterface.setClasses(newSet, for:#selector(setList(_:withReply:)), argumentIndex:0, ofReply:false)
        newConnection.exportedInterface = exportedInterface

(I have to do the same in ObjC for the "user" side, because it can get a list of MyClass in a reply, but ObjC is a lot easier, and better documented as well, for this.)

Thanks 😄

Thanks for the info so far -- it is really valuable. However - few questions remained un-answered for me (in similar but not identical scenario)

  1. Should the set of allowed classes be applied to the "exported interface" on both Client and Server side of the XPC connection? only Server side? only Client side?

  2. Your question involves your own custom class. But what Cocoa classes are acceptable by default (without calling the "exportedInterface.setClasses", and what should be manually added ? Where is the list documented?

B.T.W an ObjC version of the same snippet would be nice.

3, In my experience both server and client are Obj-C and I don't have any custom classes - only Cocoa classes), the behavior of XPC is very unclear. I experienced exceptions that claim exactly that - "unsupported classes" but only sometimes it fails, and sometimes it passes OK, and I can't understand when and what and why.

For example, I removed NSError NSAttributedString classes from my XPC protocol, and that removed the intermittent exceptions. Why?

  1. Last, you asked about logs have you found relevant log messages related to this? Where to look for them? In my case, the only "logs" were .ips crash-logs...

As the documentation says, property list types are all allowed by default. You do need to set it for both sides of the interface (I just found this out with FileHandle). The ObjC side of the code is

NSXPCInterface *interface = [NSXPCInterface interfaceWithProtocol:@protocol(RedirectorControlProtocol)]; 
NSSet<Class> *baseClasses;
baseClasses = [interface classesForSelector:@selector(getApplicationBypass:) argumentIndex:0 ofReply:YES]; 
NSSet<Class> *newClasses = [baseClasses setByAddingObject:[MyClass class]];  
[interface setClasses:newClasses forSelector:@selector(getApplicationBypass:) argumentIndex:0 ofReply:YES]; 
[interface setClasses:newClasses forSelector:@selector(addAppBypass:) argumentIndex:0 ofReply:NO]; 
_connection.remoteObjectInterface = interface;
For example, I removed NSError NSAttributedString classes from my XPC protocol, and that removed the intermittent exceptions. Why?

Both NSError and NSAttributedString allow for arbitrary ‘attachments’, and that can cause problems with secure coding. So, while they claim to support secure coding, they can’t always.

It’s easy to see this in action with NSError. At the end of this post you’ll find code for Waffle and SecureWaffle, where the latter supports secure coding. Let’s play around with that.

First, let’s encode an error with no user info:

NSError * e1 = [NSError errorWithDomain:@"WaffleDomain" code:42 userInfo:nil];
NSData * d1 = [NSKeyedArchiver archivedDataWithRootObject:e1 requiringSecureCoding:YES error:NULL];

This works, as you’d expect.

Now let’s try it with a secure waffle:

SecureWaffle * w2 = [[SecureWaffle alloc] init];
NSError * e2 = [NSError errorWithDomain:@"WaffleDomain" code:42 userInfo:@{ @"waffle": w2 }];
NSData * d2 = [NSKeyedArchiver archivedDataWithRootObject:e2 requiringSecureCoding:YES error:NULL];

This also works, because SecureWaffle support secure coding.

Finally, let’s try it with an insecure waffle:

NSError * error = nil;
Waffle * w3 = [[Waffle alloc] init];
NSError * e3 = [NSError errorWithDomain:@"WaffleDomain" code:43 userInfo:@{ @"waffle": w3 }];
NSData * d3 = [NSKeyedArchiver archivedDataWithRootObject:e3 requiringSecureCoding:YES error:&error];

This fails with an error because NSError can’t encode its user info. In the error you’ll see the text This decoder will only decode classes that adopt NSSecureCoding. Class 'Waffle' does not adopt it.

In XPC, everything has to support secure coding, so a test like this is a good place to start if you run into coding issues.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"


@interface Waffle : NSObject <NSCoding>
@property (nonatomic, copy, readwrite) NSString * varnishName;
@end

@implementation Waffle

- (instancetype)init {
    self = [super init];
    if (self != nil) {
        self->_varnishName = [@"Gloss" copy];
    }
    return self;
}

- (nullable instancetype)initWithCoder:(nonnull NSCoder *)coder {
    self = [super init];
    if (self != nil) {
        self->_varnishName = [coder decodeObjectOfClass:[NSString class] forKey:@"varnishName"];
    }
    return self;
}

- (void)encodeWithCoder:(nonnull NSCoder *)coder {
    [coder encodeObject:self->_varnishName forKey:@"varnishName"];
}

@end

@interface SecureWaffle : NSObject <NSSecureCoding>
@property (nonatomic, copy, readwrite) NSString * varnishName;
@end

@implementation SecureWaffle

+ (BOOL)supportsSecureCoding {
    return YES;
}

- (instancetype)init {
    self = [super init];
    if (self != nil) {
        self->_varnishName = [@"Gloss" copy];
    }
    return self;
}

- (nullable instancetype)initWithCoder:(nonnull NSCoder *)coder {
    self = [super init];
    if (self != nil) {
        self->_varnishName = [coder decodeObjectOfClass:[NSString class] forKey:@"varnishName"];
    }
    return self;
}

- (void)encodeWithCoder:(nonnull NSCoder *)coder {
    [coder encodeObject:self->_varnishName forKey:@"varnishName"];
}
@end
XPC, Swift, ObjC, and arrays
 
 
Q