SMAppService Sample Code seems broken

I abandoned Mac development back around 10.4 when I departed Apple and am playing catch-up, trying to figure out how to register a privileged helper tool that can execute commands as root in the new world order. I am developing on 13.1 and since some of these APIs debuted in 13, I'm wondering if that's ultimately the root of my problem.

Starting off with the example code provided here:

https://developer.apple.com/documentation/servicemanagement/updating-your-app-package-installer-to-use-the-new-service-management-api

Following all build/run instructions in the README to the letter, I've not been successful in getting any part of it to work as documented. When I invoke the register command the test app briefly appears in System Settings for me to enable, but once I slide the switch over, it disappears. Subsequent attempts to invoke the register command are met only with the error message:

`Unable to register Error Domain=SMAppServiceErrorDomain Code=1 "Operation not permitted" UserInfo={NSLocalizedFailureReason=Operation not permitted}

The app does not re-appear in System Settings on these subsequent invocations. When I invoke the status command the result mysteriously equates to SMAppService.Status.notFound.

The plist is in the right place with the right name and it is using the BundleProgram key exactly as supplied in the sample code project. The executable is also in the right place at Contents/Resources/SampleLaunchAgent relative to the app root.

The error messaging here is extremely disappointing and I'm not seeing any way for me to dig any further without access to the underlying Objective-C (which the Swift header docs reference almost exclusively, making it fairly clear that this was a... Swift... Port... [Pun intended]).

Answered by DTS Engineer in 857788022
I have noticed that launchd is changing my status to 78

This is EX_CONFIG. In this context it usually means that there’s something broken in your launchd property list. A common example of this is that the path to the executable is incorrect.

I only update the OS when a machine dies

Fair enough. But it does limit my ability to help you, because it’s not easy for me to test things on such an old OS release. I have macOS 14, 15, and 26 (RC) VMs lying around. I can set up a macOS 13 VM, but it’s hard to do because I’m travelling right now.

Which is actually relevant here. I believe you can virtualise macOS 15 on top of macOS 13, and that’s a good option in this case, and also a good thing to have handy anyway. After all, your users are going to run your product on macOS 15, so you wanna make sure it works there.

Anyway, lemme pass along some snippets from a working test project I have here in my office. First, here’s the embedded launchd property list:

% plutil -p XPCDaemonsAndAgents2.app/Contents/Library/LaunchDaemons/com.example.apple-samplecode.XPCDaemonsAndAgents2.DaemonSM.plist 
{
  "BundleProgram" => "Contents/MacOS/DaemonSM"
  "Label" => "com.example.apple-samplecode.XPCDaemonsAndAgents2.DaemonSM"
  "MachServices" => {
    "com.example.apple-samplecode.XPCDaemonsAndAgents2.DaemonSM" => 1
  }
}

Here’s where the daemon’s executable lives:

% file XPCDaemonsAndAgents2.app/Contents/MacOS/DaemonSM 
…
XPCDaemonsAndAgents2.app/Contents/MacOS/DaemonSM (for architecture x86_64):	Mach-O 64-bit executable x86_64
XPCDaemonsAndAgents2.app/Contents/MacOS/DaemonSM (for architecture arm64):	Mach-O 64-bit executable arm64

Note that this path matches the BundleProgram property.

Here’s how I set up the listener in that daemon:

let xpcListener = NSXPCListener(machServiceName: "com.example.apple-samplecode.XPCDaemonsAndAgents2.DaemonSM")

Note that the machServiceName parameter matches the MachServices property.

Here’s how I register the daemon:

let service = SMAppService.daemon(plistName: "com.example.apple-samplecode.XPCDaemonsAndAgents2.DaemonSM.plist")
try service.register()

Note that the plistName paremeter matches the name of the launchd property list file.

And here’s how I set up the XPC connection:

let connection = NSXPCConnection(machServiceName: "com.example.apple-samplecode.XPCDaemonsAndAgents2.DaemonSM", options: [.privileged])

Again, the machServiceName parameter matches the MachServices property.

Share and Enjoy

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

After changing literally nothing, I was eventually able to get a register invocation to succeed and an enabled response to come back from status. Symptomatically, it seems like some kind of non-deterministic race condition, but I sincerely have no clue what is going on with that given the limited visibility I have into the execution.

Once registered, invoking the test option successfully initializes the XPC session, but the synchronous message send deadlocks, waiting for a response that never comes. I do not see any evidence that com.xpc.example.agent is actually running at any point no matter what my registration status is but I could be looking in the wrong place. How are you supposed to debug that handoff? Console filters?

Additionally, it seems as though I lose my right to execute in the background on every restart and have to re-visit the deeply-nested System Settings panel horrorshow over and over. The documentation vaguely alludes to something like this, but I can't really tell what it's trying to say. I'd like to know if this is indeed the expected behavior so I can stick to the deprecated bless API that's more predictable. This new stuff looks great on paper but I would not feel comfortable shipping anything that uses it given what I've experienced so far.

Is there a specific reason you’re doing this development on macOS 13?

It’d be easier if you started on something recent, say macOS 15.x. There are a couple of reasons for that:

  • It’s more likely that SMAppService has stabilised by that point.
  • It’s a lot easier for me to replicate issues on modern systems. I can run tests on macOS 13, but it takes me longer to set them up.

Also, is your final goal to create an installer package? Or do you want ship an app that just happens to use a privileged helper?

The reason I ask is that the sample code you referenced is somewhat specific to installer packages, and if your ultimate goal is to create an app then you don’t have to deal with that complexity.

it seems as though I lose my right to execute in the background on every restart

No, that’s not right. If you’re seeing that it’s possibly because your code isn’t signed correctly. Specifically, you should be signing both your embedded helper and your app with the same Apple-issued code-signing identity. If, for example, you are using ad hoc signing (Sign to Run Locally in Xcode parlance) then you will see problems like this.

How are you supposed to debug that handoff?

I generally recommend that you do you initial XPC bring up, and also your unit testing, using loopback. See TN3113 Testing and debugging XPC code with an anonymous listener. Once you have that working, you can then start integrating it into your app and privileged helper. At that point you’ll want to ensure that you have appropriate logging so that you can use that to investigate any integration failures.

Share and Enjoy

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

My reasoning is difficult to phrase in a way that will not come across as combative or insulting, but suffice to say I explicitly object to the existence of most--if not all--of the features that Apple has shipped in more recent releases and prefer to keep them all out of my house so to speak. I only update the OS when a machine dies and I have to buy a new one.

If there's a solid guarantee that all of this is better in 15 then I'll absolutely bite that bullet anyway, but right now I'm essentially just doing a feasibility study and cannot destabilize everything else in my world just to find out whether or not maturity has crept in.

My goal is simply an app with an overly-privileged helper that will never appear in any app store or be run on any machine that doesn't belong to a person who immediately disables SIP as a matter of principle. I've already removed all the PackageBuilder stuff as it was very much in the way of testing--that was the only sample project I could find that was close to what I wanted but Google's pretty terrible these days. If there's a better starting point I'll happily give that a shot instead.

You are very correct on the code signing thing. Xcode mysteriously lost my settings for one target in total silence at some point--probably during one of its many, many crashes that left bizarre patterns all over my video RAM requiring all the restarts in the first place. I've now corrected that setting but the service is still acting mute. I am just using a free developer profile if that has anything to do with it.

Last bit of info I can dredge up is that sudo launchctl list does indicate that the service is known to exist and has a zero status; it is simply not responding to the "client" app upon invocation and I have thus far been unable to find any output from it anywhere in Console.app:

- 0 com.xpc.example.agent

Tried adding the .private flag to xpc_session_create_mach_service just in case and there was no change in behavior with that, either

Whatever all is going on here, there really needs to be a timeout option for these XPC calls. I know that GCD has mechanisms to implement this and I'm truly gobsmacked to see them not being exercised on something so pervasive and evangelized. It's like it was all designed to guarantee that deadlocks are a common failure mode rather than a freak occurrence.

I will look through that "anonymous listener" approach with fresher eyes tomorrow but the first thing that jumps out at me is the big yellow box that says:

...if you’re developing a launchd daemon that performs privileged operations on behalf of your app, you can’t use this technique to debug your privileged code because it’s not running in a privileged process.

Seems like a cul-de-sac. Levin's XPoCe looks nice but seems to want a PID I don't believe I have to feed it.

I have noticed that launchd is changing my status to 78 at times which I vaguely remember is a permissions issue but I can't find the relevant documentation anymore.

I'm not actually attempting to do anything even remotely privileged in the code yet; the example service is unchanged save for a liberal blanketing of print() calls to try and see if anything is ever running. Nothing ever shows up on the console so I have no clue what would lack permission to run if I'm getting SMAppService.Status.enabled back from the service object.

Does the nested launchd job not inherit the parent app's entitlements or something?

I have noticed that launchd is changing my status to 78

This is EX_CONFIG. In this context it usually means that there’s something broken in your launchd property list. A common example of this is that the path to the executable is incorrect.

I only update the OS when a machine dies

Fair enough. But it does limit my ability to help you, because it’s not easy for me to test things on such an old OS release. I have macOS 14, 15, and 26 (RC) VMs lying around. I can set up a macOS 13 VM, but it’s hard to do because I’m travelling right now.

Which is actually relevant here. I believe you can virtualise macOS 15 on top of macOS 13, and that’s a good option in this case, and also a good thing to have handy anyway. After all, your users are going to run your product on macOS 15, so you wanna make sure it works there.

Anyway, lemme pass along some snippets from a working test project I have here in my office. First, here’s the embedded launchd property list:

% plutil -p XPCDaemonsAndAgents2.app/Contents/Library/LaunchDaemons/com.example.apple-samplecode.XPCDaemonsAndAgents2.DaemonSM.plist 
{
  "BundleProgram" => "Contents/MacOS/DaemonSM"
  "Label" => "com.example.apple-samplecode.XPCDaemonsAndAgents2.DaemonSM"
  "MachServices" => {
    "com.example.apple-samplecode.XPCDaemonsAndAgents2.DaemonSM" => 1
  }
}

Here’s where the daemon’s executable lives:

% file XPCDaemonsAndAgents2.app/Contents/MacOS/DaemonSM 
…
XPCDaemonsAndAgents2.app/Contents/MacOS/DaemonSM (for architecture x86_64):	Mach-O 64-bit executable x86_64
XPCDaemonsAndAgents2.app/Contents/MacOS/DaemonSM (for architecture arm64):	Mach-O 64-bit executable arm64

Note that this path matches the BundleProgram property.

Here’s how I set up the listener in that daemon:

let xpcListener = NSXPCListener(machServiceName: "com.example.apple-samplecode.XPCDaemonsAndAgents2.DaemonSM")

Note that the machServiceName parameter matches the MachServices property.

Here’s how I register the daemon:

let service = SMAppService.daemon(plistName: "com.example.apple-samplecode.XPCDaemonsAndAgents2.DaemonSM.plist")
try service.register()

Note that the plistName paremeter matches the name of the launchd property list file.

And here’s how I set up the XPC connection:

let connection = NSXPCConnection(machServiceName: "com.example.apple-samplecode.XPCDaemonsAndAgents2.DaemonSM", options: [.privileged])

Again, the machServiceName parameter matches the MachServices property.

Share and Enjoy

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

SMAppService Sample Code seems broken
 
 
Q