How to publish an XPC Service in a global daemon that employs EndpointSecurity framework?

I have a global daemon managed by launchd, whose .plist is installed in /Library/LaunchDaemons).

To be correctly entitled and code-signed so it can communicate with EndpointSecurity framework, its executable resides in a normal Mac App bundle (main() will run as minimal UI when launched from UI, and as a daemon when launched by launchd).

This means that the ProgramArguments.0 in its .plist looks something like

Code Block plist
/Library/PrivilegedHelperTools/MyDaemonApp.app/Contents/MacOS/MyDaemonApp


Now I need this daemon to publish an XPC Service (with few control commands) so that other components of our system (UI app, a user-context launchd-daemon and another launchd global-daemon) will be able to connect to The XPC Service and control it via the published protocol.

I read some answers here, and also found a working order sample code that does just this here

But when I apply its content to my global daemon, word for word - it doesn't work - meaning, clients cannot create a connection to The XPC Service.

The daemon is up and running, and functional. its .plist is quite simple and looks like this:

Code Block <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.mycompany.itm.service</string>
<key>KeepAlive</key>
<true/>
<key>RunAtLoad</key>
<true/>
<key>MachServices</key>
<dict>
<key>com.mycompany.itm.service</key>
<true/>
</dict>
<key>ProgramArguments</key>
<array>
<string>/Library/PrivilegedHelperTools/IMyDaemonApp.app/Contents/MacOS/MyDaemonApp</string>
<string>-monitor</string>
<string>-protectDeviceProtocol</string>
<string>USB</string>
</array>
</dict>
</plist>


It creates and starts an XPC listener in MYXPCListener.h like thus:

Code Block Objective-C
#import <Foundation/Foundation.h>
#import "MYXPCProtocol.h"
NS_ASSUME_NONNULL_BEGIN
@interface OITPreventionXPCService : NSObject
- (instancetype) init;
- (void) start; /* Begin listening for incoming XPC connections */
- (void) stop; /* Stop listening for incoming XPC connections */
@end
NS_ASSUME_NONNULL_END


and the implementation is:

Code Block Objective-C
/* AppDelegate.m */
@interface MYXPCService () <NSXPCListenerDelegate, OITPreventionXPCProtocol>
@property (nonatomic, strong, readwrite) NSXPCListener *listener;
@property (nonatomic, readwrite) BOOL started;
@end
@implementation OITPreventionXPCService
- (instancetype) init {
    if ((self = [super init]) != nil) {
        _listener = [[NSXPCListener alloc] initWithMachServiceName:@"com.mycompany.itm.service"];
        _listener.delegate = self;
        if (_listener == nil) {
            os_log_error(myLog, "XPCListener failed to initialize");
        }
        _started = NO;
    }
    return self;
}
- (void) start {
    assert(_started == NO);
    [_listener resume];
    os_log_info(myLog, "XPCListener resumed");
    _started = YES;
}
- (void) stop {
    assert(_started == YES);
    [_listener suspend];
    os_log_info(myLog, "XPCListener suspended");
    _started = NO;
}
/* NSXPCListenerDelegate implementation */
- (BOOL) listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection {
    os_log_info(myLog, "Prevention XPCListener is bequsted a new connection");
    assert(listener == _listener);
    assert(newConnection != nil);
    newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(MYXPCProtocol)];
    newConnection.exportedObject = self;
[newConnection resume];
    return YES;
}
/* Further down this implementation, I have implementations to all the methods in MYXPCProtocol. */
@end


Now the client code (and I tried EVERY kind of client, signed unsigned, daemon, UI, root privileged, or user-scoped - whatever). For example, in the AppDelegate of a UI app:


Code Block Objective-C
#import "AppDelegate.h"
#import "MYXPCProtocol.h"
@interface AppDelegate ()
@property (strong) IBOutlet NSWindow *window;
@property (nonatomic, strong, readwrite) NSXPCConnection *connection; /* lazy initialized */
@end
@implementation AppDelegate
- (NSXPCConnection *) connection
{
    if (_connection == nil) {
        _connection = [[NSXPCConnection alloc] initWithMachServiceName:daemonLabel options:NSXPCConnectionPrivileged];
        _connection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(MYXPCProtocol)];
        _connection.invalidationHandler =  ^{
            self->_connection = nil;
            NSLog(@"connection has been invalidated");
        };
        [_connection resume];         /* New connections always start suspended */
    }
    return _connection;
}
- (IBAction) getServiceStatus:(id)sender
{
    [self.connection.remoteObjectProxy getStatus:^(NSString * _Nonnull status) {
        NSLog(@"MY XPC Service status is: %@", status);
    }];
}
@end


but no matter what I do - I always get the "connection invalidated".

The sample launchDaemon that works - is not code-signed at all!!! but mine, which is both signed and checking of which yields

Code Block bash
$ spctl --assess --verbose IMyDaemonApp.app
IMyDaemonApp.app: accepted
source=Notarized Developer ID
 


I'm at a loss - and would like to get any advice, or any helpful documentation (I've been reading TN2083, and man launchctl and man launchd.plist and many other pages - to no avail. There seems to be no real "programming guide" for XPC and no reasonable sample code on Apple developer site to fit my needs.

Last - this is MacOS 10.15.7, and latest Xcode 12.3
Does your daemon actually start? If you do a sudo launchctl start and then a sudo launchctl list, does the list show the daemons pid?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@apple.com"
My daemon is up and running at all times, and is doing its work (some kind of monitoring and blocking of specific authorization events from EndpointSecurity framework). I started this endeavor with a working-order global daemon. My goal is to keep it small fast and safe, but still receive "control commands" from external process, and for that I want it to publish an XPC Service.

Of course it does appear in the sudo launchctl list, and I can also launchctl print its state and properties without problem.

I saved and compared the output of launchctl print for both the working sample global-daemon and mine. They both look benign and identical, except for the expected differences (label of the daemon, path of executable, PID and port number of the endpoint). They also both have this
Code Block
environment = {
XPC_SERVICE_NAME => com.mycompany.itm.service
}

entry, which says launchd KNOWS my global daemon should publish an XPC Service by this name.

I managed to make another tiny progress, by trying to connect to my global daemon from the UI app of the sample-code I'm using - the one that succeeds in communicating with the sample daemon.

I duplicated the code there, and try to talk to the two daemons - each with its own label and protocol.

Now, the UI client app no longer calls my connection invalidation handler immediately as I try to get to the remote object proxy and send it a message, like thus:
Code Block
    id ro = [self.myServiceConnection remoteObjectProxyWithErrorHandler:^(NSError * _Nonnull error) {
        NSLog(@"Error obtaining remote object: %@", error);  }];
    if (myServiceRemote) {
        [preventionRemote getStatus: ^(NSString * _Nonnull status) {  NSLog(@"Service status: %@", status);  }];
    }

I get this NSError.

2021-01-17 20:20:38.895040+0200 Client[18858:420994] Error obtaining remote object: Error Domain=NSCocoaErrorDomain Code=4097 "connection to service on pid 0 named com.mycompany.itm.service" UserInfo={NSDebugDescription=connection to service on pid 0 named com.mycompany.itm.service}

Now what? I can't figure out how to debug this thing effectively - these are caveman's methods...
I’m out of straightforward suggestions, alas. I recommend you open a DTS tech support incident so that I can allocate the time to dig into this in more depth.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@apple.com"
Apple dev forum's usability and documentation is in par with the other parts of the OS and frameworks... that is... again I find myself disappointed.

Anyway, Yet another hint about this issue (still unresolved, dear Eskimo - I would be so grateful for any ideas...)
When my client is sandboxed - I get the "connection invalidation" handler called. when it's NOT sandboxed, I get the NSError 4097 trying to obtain the remoteObjectProxy from the connection. Go figure.

The listener on my global daemon is never called for a new connection attemp (so my logs show) so I guess the connection attempt is blocked at the launchd level, or at some security stage - however, I just can't find anything in the logs, and I don't know how WHICH processes and frameworks are involved.

The more I read documentation, the more I become confused - There seem to be .plist configurations For XPC Services (that are bundled as a "plugin" within an application) and whose entries may (or not?) be relevant to my case - but where and how would I stick those in my scenario?

It's been almost 2 weeks of struggle already, and I don't know where to go next.

Any updates on that matter?

IIRC suMac did not open a DTS tech support incident about this.

However, I’ve helped a bunch of developers with:

  • Creating an ES client in a system extension

  • Creating an ES client in a launchd daemon

  • Sending XPC requests from a client process to a listener hosted in a launchd daemon

They all have their challenges, but nothing unsurmountable.

If you’re having problems with any of these above, I recommend that you start a new thread with the details. Tag it with the appropriate combination of Endpoint Security, System Extensions, and XPC so that I see it.

Share and Enjoy

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

How to publish an XPC Service in a global daemon that employs EndpointSecurity framework?
 
 
Q