SMJobSubmit works in user domain, but cannot be submitted in system domain

Hi, I'm in the process of creating an App + Helper Tool combo application, and depending on the necessity of root privileges, I'm setting up two paths in the app:

  1. If root privileges are not necessary, I'm using SMJobSubmit rather directly:

    var submissionError: Unmanaged<CFError>?
    let submissionResult = SMJobSubmit(kSMDomainUserLaunchd, plist, nil, &submissionError)
    

    where plist contains these items:

    • Label=com.***.redactedApp.redacted,
    • ProgramArguments=[path/to/helper-tool, commandName, commandArg1, commandArg2]
    • RunAtLoad=1,
    • KeepAlive=0

    and it works as necessary, and performs the operations.

  2. Now, in the case of privilege escalation being necessary, this call becomes a bit more complex:

    let authorization = SFAuthorization()
    var authRef: AuthorizationRef?
    do {
             try authorization?.obtain(withRight: kSMRightModifySystemDaemons,
                                       flags: [.extendRights, .interactionAllowed])
             
             authRef = authorization?.authorizationRef()
    } catch let error {
             // Logging error
    }
    
    var submissionError: Unmanaged<CFError>?
    let submissionResult = SMJobSubmit(kSMDomainSystemLaunchd, plist, authRef, &submissionError)
    

    while using the same plist, same executable at the same path, same Label.

However, when using the second path, suddenly SMJobSubmit fails:

Error Domain=CFErrorDomainLaunchd Code=2 "(null)"

Now, naturally I headed over to system logs in Console.app, and this is the weirdest - there is nothing suspicious near the log item I submit with the above error from the main application.

The tool is embedded in the Contents/MacOS folder. However, my problem is that anything that I can think of seems to lead to the same thought: it should be a problem in both cases, not just the privileged one.

Is there something extra that must be taken care of when using SMJobSubmit with privileged helper tools?

So, we actually need to stop right here:

I'm using SMJobSubmit

Stop using SMJobSubmit. That API was deprecated in 10.10 (seven years ago) and I believe we'd been recommending against it for several years.

The modern replacement is SMAppService, introduced in macOS 13.0. Note that this is a "modern" replacement, in that it specifically supports privileged helper tools embedded inside app bundles. Keep in mind that doing this:

The tool is embedded in the Contents/MacOS folder.

...is not safe with SMJobSubmit and never has been. SMJobSubmit is "hard coding" the executable path, which means the user renaming your app (or any other manipulation) will both break your job and create an "opening" which could allow an attacker to "insert" their executable in place of your job.

If you need to support older systems, then the recommended approach would be to use SMJobBless as shown in "EvenBetterAuthorization" to install a privileged helper tool. The helper can then be used as the target for a launchd plist, which the privileged helper can install itself and/or configure using the launchctl command line tool.

Covering a few specific details:

Error Domain=CFErrorDomainLaunchd Code=2 "(null)"

Unfortunately, this error "2" is service managements generic "catch all" code for errors that weren't mapped to other, more specific values. That makes it very difficult to find the underlying error source.

Now, naturally I headed over to system logs in Console.app, and this is the weirdest - there is nothing suspicious near the log item I submit with the above error from the main application.

If you want to look at this from the log side, then I would:

  • Install the "Sysdiagnose (Unredacted)" profile, just to ensure you're not missing any data.

  • Add log message to your code before and after SMJobSubmit and make sure it's reaching the console log.

  • Reproduce the issue then capture the sysdiagnose.

...the search the log between those to log messages to see if you can find anything. Note that I'd look at "all" message, not just smd or launchd. For example, codesigning validation occurs in other daemon's, so you might see a failure there and not the directly involved daemons.

However, my problem is that anything that I can think of seems to lead to the same thought: it should be a problem in both cases, not just the privileged one.

Actually, "kSMDomainSystemLaunchd" has it's own implementation path that's split off from all other domains. I don't know what's causing the ultimate failure but I'm not surprised to see the domains failing differently.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Thank you Kevin for your detailed answer, however there are a few points I don't fully understand so I would appreciate some more clarity around them.

Stop using SMJobSubmit. That API was deprecated in 10.10 (seven years ago) and I believe we'd been recommending against it for several years.

To my knowledge SMJobSubmit is the only way other than AuthorizationExecuteWithPrivileges (an even older API) that allows one to get one-time throwaway root permissions to run a single executable. I'm saying this based on your colleague's answer here, and his very extensive and educational post here.

Your recommended replacement, SMAppService, is vastly different from SMJobSubmit:

  • It's not possible to gain one-time privileges, other than a very awkward solution of registering a daemon and having the daemon clean up itself in some way.

  • Registering a daemon is a very unfriendly user experience: the user sees an extremely small notification in the top right corner (if they even see it), where they must hover to even reveal the in-notification button to approve the daemon or have to directly go to system settings and approve the daemon.

    I trust we can agree how much better of an API SMJobSubmit is from user experience sense.

Sadly, the first point above is also true for SMJobBless - it's a long shot for such a simple thing as running an executable once. Not to mention, to this day, this is the documentation header for SMJobSubmit, copied from the macOS 15.2 SDK:

@discussion
This routine is deprecated and will be removed in a future release. A replacement will be provided by libxpc.

Said replacement hasn't been provided yet to my knowledge, I checked the XPC framework's functions and objects.

I hope you understand my reasons behind having SMJobSubmit as the ideal and easiest solution here.

About

SMJobSubmit is "hard coding" the executable path, which means the user renaming your app (or any other manipulation) will both break your job and create an "opening" which could allow an attacker to "insert" their executable in place of your job.

I'm aware this can be a problem, but after all, SMJobSubmit is a launchd interface (its job CFDictionary is a launchd.plist AFAIK), and for launchd.plist-s Spawn Constraints have been introduced in macOS 14.0. My idea has been to include a Spawn Constraints key in the job plist according to Apple Documentation however I haven't gotten that far yet.

Also, it doesn't really matter whether the user renames the app I'm creating, since the executable to submit is inside the app bundle and I'm submitting a path computed by Bundle.path and co. I don't think it's possible for that to break, unless the user tampers with the bundle itself, which is protected in macOS by default with the App Management privacy feature, as far as I understood its purposes based on this WWDC video.

When I attempt to modify another app's bundle from Terminal for example, I get operation not permitted, unless Terminal was given App Management rights.

These above put confidence in me that SMJobSubmit can be used in a secure way. I wouldn't have started this project with SMJobSubmit if I hadn't been convinced that it's compatible with the security hardening interfaces macOS provides by its latest SDKs (environment/launch/spawn constraints, XPC connection validation, etc.)


I'll now go ahead and try your suggested debugging methods, thanks for the Profile especially, I wasn't aware this profile existed.

Here are the logs with the profile installed, it indeed revealed a single extra line related to Service Management before both my SMJobRemove and SMJobSubmit calls:

authd	default	com.apple.Authorization	2024-12-20 00:40:35.015306 -0800	authd	Succeeded authorizing right 'com.apple.ServiceManagement.daemons.modify' by client ‘redacted/path/tp/RedactedAppName.app' [874] for authorization created by ‘redacted/path/to/RedactedAppName.app' [874] (3,0) (engine 31)
	error	com.apple.os_debug_log	2024-12-20 00:40:35.018568 -0800	RedactedAppName	assertion failed: 23G93: ServiceManagement + 24216 [A64F7B28-7DFF-3137-A74B-BEB900AE33A2]: 0xffffffffffff159e
app	error	com.redacted.redactedAppName	2024-12-20 00:40:35.018699 -0800	RedactedAppName	Failed to remove job! [Error Domain=CFErrorDomainLaunchd Code=2 "(null)"]
app	debug	com.redacted.redactedAppName	2024-12-20 00:40:35.018888 -0800	RedactedAppName	Job's property list is [["ProgramArguments": <__NSArrayI 0x6000002a5880>(
/redacted/path/to/RedactedAppName.app/Contents/MacOS/redactedDaemonName,
—redactedFlagPassedToExecutable
)
, "KeepAlive": 0, "Label": com.redacted.redactedAppName.redactedDaemonName, "RunAtLoad": 1]].
app	default	com.redacted.redactedAppName	2024-12-20 00:40:35.018913 -0800	RedactedAppName	Will not re-request authorization right, already got it.
	error	com.apple.os_debug_log	2024-12-20 00:40:35.019021 -0800	RedactedAppName	assertion failed: 23G93: ServiceManagement + 24216 [A64F7B28-7DFF-3137-A74B-BEB900AE33A2]: 0xffffffffffff159e
app	error	com.redacted.redactedAppName	2024-12-20 00:40:35.019047 -0800	RedactedAppName	Failed to submit job! [Error Domain=CFErrorDomainLaunchd Code=2 "(null)"]
logging	info	com.apple.libspindump	2024-12-20 00:40:35.019144 -0800	RedactedAppName	Reporting HID response delay 4657124658-4743569411
connection	default	com.apple.xpc	2024-12-20 00:40:35.019155 -0800	RedactedAppName	[0x600003dc1c20] activating connection: mach=true listener=false peer=false name=com.apple.spindump
logging	info	com.apple.spindump	2024-12-20 00:40:35.019840 -0800	spindump	RedactedAppName [874]: slow hid response: start, <private> 4657124658-4743569411: 3.6s (threshold 0.5s)
logging	default	com.apple.spindump	2024-12-20 00:40:35.021972 -0800	spindump	RedactedAppName [874]: slow hid response: not sampling due to conditions 0x8002
logging	info	com.apple.spindump	2024-12-20 00:40:35.025126 -0800	spindump	RedactedAppName [874]: spin stop
logging	info	com.apple.spindump	2024-12-20 00:40:35.025181 -0800	spindump	RedactedAppName [874]: Didn't receive spin start notification when we want to see spins, turning spin notifications on
cas	info	com.apple.launchservices	2024-12-20 00:40:35.026494 -0800	launchservicesd	Moving App:"RedactedAppName" asn:0x0-41041 pid:874 refs=9 @ 0x12c7605d0 to front of visible list.
cas	info	com.apple.launchservices	2024-12-20 00:40:35.026702 -0800	launchservicesd	SetFrontReservationExists(), newValue=NO, app=App:"RedactedAppName" asn:0x0-41041 pid:874 refs=9 @ 0x12c7605d0 ( was YES)

particularly these two lines:

assertion failed: 23G93: ServiceManagement + 24216 [A64F7B28-7DFF-3137-A74B-BEB900AE33A2]: 0xffffffffffff159e

and

assertion failed: 23G93: ServiceManagement + 24216 [A64F7B28-7DFF-3137-A74B-BEB900AE33A2]: 0xffffffffffff159e

seem to be interesting, however they provide no immediate extra information to me (other than 23G93 is the macOS version I'm running, 14.6.1). I'm hoping it might make more sense to you.

SMJobSubmit works in user domain, but cannot be submitted in system domain
 
 
Q