XPC between Container App and System Extension

I'm trying to create an XPC service to communicate between my Endpoint Security Extension and its Container App.

I've taken the Sample Endpoint App from here.

I've then followed the steps under Creating the Service here

In fact, when I added the XPC service via the template, Xcode automatically added an Embed XPC Services phase to the container app.

I can confirm that in the built container app I see the xpc service: SampleEndpointApp.app/Contents/XPCServices/Service.xpc

If I initiate an NSXPCConnection from the container app then I can both connect and make RPCs. Furthermore I see the service process running via ps and also launchtl.

If however I try to initiate an NSXPCConnection from the extension then I see nothing. RPC doesn't work and I don't see the service being launched. I've tried this with and without the connection in the main app.

What am I missing here? What needs to be done to allow both processes to talk to each other? Is there some permissions issue here?

Note that my plist for the service is as follows:

  • It's entirely possible I'm missing the point of what XPC is for. Doc imply maybe it's only for (single client)-(XPC)server model. It sounded to me like XPC was designed to be a MITM, not hold business logic. I want to use it for a (multiple client)-server model. Also, it's unclear to me whether XPC can be used for any 2 apps to communicate or just ones int the same bundle.

  • Note furthermore that I've found the Simple Firewall example. Interestingly this is designed differently to what the docs tell you to do. Specifically, docs tell you to make your XPC service a separate binary and put it in a specific location in the bundle. However, this example seems to just embed the XPC service inside the Sys Ext. 🤷

Add a Comment

Replies

I've then followed the steps under Creating the Service

You are confusing an XPC service with a named XPC endpoint. An XPC service is a bundled chunk of code that you can embed within your app. A named XPC endpoint is an XPC listener that you can look up by name and communicate with. An XPC service publishes a named XPC endpoint, but other code can as well. Specifically to do this in your ES sysex:

  1. Edit its Info.plist to list the name in the NSEndpointSecurityMachServiceName property.

  2. On start, set up a listener for that name using NSXPCListener.

However, this example seems to just embed the XPC service inside the Sys Ext.

Right. While an NE sysex uses a different property in step 1, it’s otherwise the same.

Share and Enjoy

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

  • @eskimo If I want to use the XPC Service as a message forwarder between the Container App and the System Extension. So in the context of this topic, I would make a xpc connection from com.example.endpoint-test.Extension (system extension) to com.example.endpoint-test.Service (XPC Service). Is that able to work?

    I write my own test, I got the same result as @HormyAJP , the XPC Service process is not launched, really hope that could work, is there any way to make it happen?

Add a Comment

I've tried what you suggested. That seems to have changed behaviour but I still get an error when connecting from the client.

Specifically, when I called remoteObjectProxyWithErrorHandler previously I got this error:

Error: Error Domain=NSCocoaErrorDomain Code=4099 "The connection to service named com.example.endpoint-test.Service was invalidated: failed at lookup with error 3 - No such process." UserInfo={NSDebugDescription=The connection to service named com.example.endpoint-test.Service was invalidated: failed at lookup with error 3 - No such process.}

Now, after adding the NSEndpointSecurityMachServiceName property, I get the following vague error:

Error: Error Domain=NSCocoaErrorDomain Code=4097 "connection to service named com.example.endpoint-test.Service" UserInfo={NSDebugDescription=connection to service named com.example.endpoint-test.Service}

N.B. that in order to even get this far I had to add com.apple.security.temporary-exception.mach-lookup.global-name to my container app entitlements with value com.example.endpoint-test.Service. If I remove that then I get this error:

  • Do you know what the issue is?
  • Are there restrictions on the mach service name?
  • Are there some other permissions/entitlements needed?
  • In addition, I'm a bit uncomfortable about adding the temporary exception. According to the docs that really means I'm doing something that's not supported and thus should submit a feature request 🤔

Thanks

Error: Error Domain=NSCocoaErrorDomain Code=4099 "The connection to service named com.example.endpoint-test.Service was invalidated: failed at lookup with error 159 - Sandbox restriction." UserInfo={NSDebugDescription=The connection to service named com.example.endpoint-test.Service was invalidated: failed at lookup with error 159 - Sandbox restriction.}

I'm a bit uncomfortable about adding the temporary exception.

Don’t worry about that. The temporary in temporary exception entitlement is only relevant to App Store apps. For products that ship outside of the Mac App Store, which is a requirement for ES clients, it’s fine to use temporary exception entitlements.

Having said that, you can avoid that problem by prefixing your service name with an app group ID.

Having said that, I don’t recommend that you start down this path yet. Rather, let’s first see if the server side of this XPC connection is working. If you run this command:

% sudo launchctl list

does your service name show up?

Share and Enjoy

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

Thanks for getting back (particularly at the weekend!)

My service shows up as: XXXXXXXXX.com.example.endpoint-test.Extension with status 0 (where XXXXXXXXX is my team identifier). In addition

  • I see the corresponding process running as root using ps: /Library/SystemExtensions/1049F8B0-4BEC-44AD-B0D0-AE8EBCBC431A/com.example.endpoint-test.Extension.systemextension/Contents/MacOS/com.example.endpoint-test.Extension.
  • systemextensionsctl list shows my extension as [activated enabled]

I have also looked at the output from sudo launchctl procinfo $PID and I see: XPC_SERVICE_NAME => XXXXXXXXX.com.example.endpoint-test.Extension (not sure if this is relevant).

In order to help debug this, I'll give you a dump of any key information I can think of which might be useful:

  • I have given the extension permission to run and Full Disk Access via System Preferences->Security & Privacy
  • My container app Bundle ID is com.example.endpoint-test
  • My Extension Bundle ID is com.example.endpoint-test.Extension
  • I have distinct provisioning profiles for both the app and the extension. Each has the System Extension entitlement. N.B. My developer account has been granted the right to use Endpoint Security for development only.
  • Both bundles have App Groups capabilities enabled with group $(TeamIdentifierPrefix)com.example.mygroup
  • In the container app entitlements plist I have: com.apple.security.temporary-exception.mach-lookup.global-name = com.example.endpoint-test.Extension
  • In the extension entitlements plist I have: com.apple.developer.endpoint-security.client=1
  • In the extension Info.plist I have: NSEndpointSecurityMachServiceName= com.example.endpoint-test.Extension
  • I am debugging the app from the Xcode DerivedData directory. The documentation says I need to copy the app to /Applications in order for System Extensions to work, however, I've not found any behavioural changes if I do that. The only downside is that if I delete the app from DerivedData then the system doesn't uninstall the extension for me and it gets "stuck". In order to delete I just rebuild the app, copy to /Applications and delete from there.

My code roughly looks like the following

  • Container app requests user interaction to install the extension
  • System extension creates and resumes an NSXPCListener on startup using initWithMachServiceName with "com.example.endpoint-test.Extension"
  • Container app tries to connect a few seconds after the system extension API callback returns success
  • Container app creates an NSXPCListener on startup using initWithMachServiceName with "com.example.endpoint-test.Extension", sets the remoteObjectInterface and resumes the connection.
  • Container app calls remoteObjectProxyWithErrorHandler to get the remote interface
  • Container app calls a method on the remote interface. N.B. This call then causes the error handler passed to remoteObjectProxyWithErrorHandler to fire. Without this call the error handler doesn't fire.
  • Note that the shouldAcceptNewConnection method in the extension's listener delegate never fires during this.

Actual code

(Sorry about the objC - don't ask)

Server listen:

NSXPCListener *listener;
void extensionListen(void) {
  NSLog(@"Extension Service Started");
  ServiceDelegate *delegate = [ServiceDelegate new];
  listener = [[NSXPCListener alloc] initWithMachServiceName: @"com.example.endpoint-test.Extension"];
  listener.delegate = delegate;
  [listener resume];
}

App Connect

void appConnect(void) {
  NSLog(@"appConnect");
  _connectionToService = [[NSXPCConnection alloc] initWithMachServiceName:@"com.example.endpoint-test.Extension" options:NSXPCConnectionPrivileged];
  _connectionToService.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(ServiceProtocol)];
  [_connectionToService resume];
   
  id rop = [_connectionToService remoteObjectProxyWithErrorHandler:^(NSError* error) {
    NSLog(@"Error: %@", error);
  }];
   
  [rop upperCaseString:@"hello" withReply:^(NSString *aString) {
    NSLog(@"Result string was: %@", aString);
  }];
//  [_connectionToService invalidate];
}

One particular area where I lack confidence that what I'm doing is correct is around the various strings I've put into plists and initWithMachServiceName. There are so many possible combinations, but none seem to work. Quite possibly there's a bug there.

In the extension Info.plist I have: NSEndpointSecurityMachServiceName= com.example.endpoint-test.Extension

That’s going to create some ambiguity. I recommend that you do the following:

  • Add your Team ID as a prefix.

  • Add a suffix to distinguish this from your sysex, so something like .xpc-qqq.

Once your sysex is loaded, run:

% sudo launchctl list

and see if the now very recognisable NSEndpointSecurityMachServiceName string shows up.

Share and Enjoy

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

I've done as you suggested but no dice I'm afraid. I still see the sysext in the output from sudo launchctl list: XXXXXXXX.com.example.endpoint-test.Extension, but nothing else.

To be clear:

  • I'm now calling initWithMachServiceName, in both the app and the sysext, with @"XXXXXXX.com.example.endpoint-test.Extension.xpc-qqq"
  • I've updated NSEndpointSecurityMachServiceName to $(TeamIdentifierPrefix)com.example.endpoint-test.Extension.xpc-qqq
  • I've updated com.apple.security.temporary-exception.mach-lookup.global-name to $(TeamIdentifierPrefix)com.example.endpoint-test.Extension.xpc-qqq

I'm a little confused about your expectation from sudo launchctl list TBH. That would imply to me that launchd would be starting yet another process. However, my XPC is embedded in the sysext. Does launchd create some sort of broker process to facilitate communication between the app and the sysext?

Does launchd create some sort of broker process to facilitate communication between the app and the sysext?

No, that’s just me being confused )-: Sorry.

If your ES sysex starts then you can check the services its advertises using:

% sudo launchctl procinfo PPP

replacing PPP with its process ID.

Share and Enjoy

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

No problems.

I see the following from procinfo

	endpoints = {
		"XXXXXXXX.com.example.endpoint-test.Extension.xpc-qqq" = {
			port = 0x9c2df
			active = 1
			managed = 1
			reset = 0
			hide = 0
			watching = 0
		}
	}

I guess that's good news at least :)

I'm poking around a bit more. One interesting thing I found. If I set an interruptionHandler on the NSXPCConnection in the client, then that handler is triggered by calling the upperCaseString method on the remote interface.

The docs state that "An interruption handler that is called if the remote process exits or crashes".

However, if I put a breakpoint before the call to the remote interface look at the proc id of the service, then it doesn't change after the interruption handler is called. So it doesn't look like the sysext process has exited.

Few other random things I've checked/tried:

  • Running the app as root doesn't change anything
  • Moving the app to /Applications doesn't change anything
  • I checked my remote object proxy in the client with respondsToSelector and the object does conform to the remote protocol.
  • If I don't start listening in the sysext, then the output of procinfo shows active = 0 (as opposed to active = 1)

I guess that's good news at least :)

Yes!

At this point I’m going to recommend two things. To start, use the technique described in TN3113 Testing and debugging XPC code with an anonymous listener to get your XPC code up and running in a simple test app.

Once you do that you know that your XPC code is good, and so the only remaining problem is to set up the listener and connection for your named endpoint. To do that:

  • On the sysex side, call -[NSXPCListener initWithMachServiceName:] with the endpoint name printed by procinfo.

  • On the client side, if you have the App Sandbox enabled then disable it. If you need to bring it back in the future that’s fine, but for the initial bring up just get it out of the way.

  • Then, still on the client side call -[NSXPCConnection initWithMachServiceName:options:], passing in the endpoint name and NSXPCConnectionPrivileged.

Share and Enjoy

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

  • Thanks. I'll give it a go. Expect a bit of radio silence for a few days. I'm travelling a lot and don't think I'll have time to come back to this.

Add a Comment

Okay, I'm finally back. Apologies for the long hiatus on this. I was travelling then got Covid and actually got quite sick from it. So much for the "it's just a bad cold these days".

I believe I've fixed the problem via a circuitous route. I was following your advice about debugging and creating a new app from the ground up. Since TN3113 was written in Swift, I also re-wrote in Swift. When I transferred my code over I got a warning from the Swift compiler that the ObjC compiler couldn't detect. Turns out that there's a bug in my code with a weak property being deallocated immediately after creation. Specifically, in the code snippet I posted above, the NSXPCListenerDelegate property of the NSXPCListener is weak:

NSXPCListener *listener;
void extensionListen(void) {
  NSLog(@"Extension Service Started");
  ServiceDelegate *delegate = [ServiceDelegate new];
  listener = [[NSXPCListener alloc] initWithMachServiceName: @"com.example.endpoint-test.Extension"]; 
  // THIS IS THE PROBLEM LINE
  listener.delegate = delegate;
  [listener resume];
}

The ServiceDelegate is deallocated immediately after return, which essentially means I have no delegate. As per the docs: "If no delegate is set, all new connections will be rejected.".

If I retain the delegate then my XPC now works as expected [facepalm].

Thanks for all the help on this. We got there in the end. Once I get my brain back into this space properly, I'll likely write up some sort of guide on this.

Clear message to me is to ditch ObjC once and for all. If I'd done this in Swift from the outset I'd have never written this bug!

Add a Comment