Daemon has reduced permissions after migrating from SMJobBless to SMAppService

Hello,

I am working on updating an app to see if we can remove deprecated API usage, and am running into an issue after migrating from SMJobBless to SMAppService. If there is no current solution, I know that SMJobBless still works, but I wish to move to non-deprecated APIs whenever possible.

The app is a text editor that installs a privileged helper for when users need to edit text files with root privileges. The example I'll use here is /etc/ssh/sshd_config. When using SMJobBless, the privileged helper was able to write to this location. When using SMAppService.daemon, the daemon is not able to write to this location.

Neither the app nor the daemon are sandboxed. Both use the hardened runtime, and the daemon does not have any hardened runtime exceptions.

I'm not sure how to attach a debugger to the daemon, but I was able to add logging to the daemon to confirm that getuid() and geteuid() are both 0, so the daemon appears to be running as root.

However, the daemon is returning permission errors when attempting to replace the file.

{Error Domain=NSPOSIXErrorDomain Code=1 "Operation not permitted"}

I've tried both atomic saving and writing directly to the file. When this code is run by the privileged helper installed with SMJobBless, it works without permissions problems.

Here is some simplified code I tried for atomic saving.

do {
    let fileManager = FileManager.default
    try? fileManager.createDirectory(at: originalItemURL.deletingLastPathComponent(), withIntermediateDirectories: true)
    _ = try fileManager.replaceItemAt(originalItemURL, withItemAt: newItemURL, options: options)
    completionHandler(nil)
}
catch {
    completionHandler(error)
}

And the code for writing directly to the file

do {
    try data.write(to: url)
    completionHandler(nil)
}
catch {
    completionHandler(error)
}

One thing I should note is that the privileged helper tool had a launchd plist embedded in the binary. When moving to SMAppService, I removed it from the build settings and added BundleProgram to it. It gets placed in my app bundle in Contents/Library/LaunchDaemons, while the daemon itself gets put in Contents/MacOS. The plist only contains the following keys: BundleProgram, Label, MachServices, and AssociatedBundleIdentifiers.

is there anything additional I can do to give my daemon permission to edit these files, or do I need to stick with SMJobBless for the time being?

First up, I’m gonna have you read On File System Permissions, because the following assumes terms from there.

The EPERM error you’re seeing suggests you’re hitting a MAC check. It’s not clear to me why you’re hitting that [1]. Before we go further, I’d like to check one thing: Are you able to open the target file with write privileges?

The code snippets you posted don’t actually do that, but rather replace the file. So, for testing purposes only, please add code like this:

let fh = try FileHandle(forWritingTo: url)
fh.close()

Does that throw an error when you point it at /etc/ssh/sshd_config?

Share and Enjoy

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

[1] The most obvious reason is that SMAppService creates a strong ‘responsible code’ link between the daemon and your app, but I wouldn’t have thought that the specific file you’re trying to overwrite is MAC protected.

Hello Quinn, thank you as always. I have read and understand the On File System Permissions link. It is very useful, and I'm going to share it around the office. Thanks!

I did as suggested and found that I could open a writing file handle, and even write data. However, I noticed that I could not open a reading handle. So I restored the original code but changed the originalItemURL to be hard-coded to a file in my home directory, and the call to fileManager.replaceItemAt succeeded.

The problem wasn't writing to /etc/ssh/sshd_config, it was reading from the temporary file location.

(So why was the function that called data.write failing? Because it wasn't actually getting called. Due to a bug, we were always calling the replaceItemAt function.)

Looking through the code, I found that the URL was created (in Objective-C this time) using

[fileManager URLForDirectory:NSItemReplacementDirectory inDomain:NSUserDomainMask appropriateForURL:fileURL create:YES error:&error]

which resulted in a path within

/private/var/folders/yg/2j4m93b50d7bwj9wd4r8cww00000gn/T/TemporaryItems

And, as it turns out, you can't list that path, even with sudo, so your guess about a MAC check appears to be correct.

So for some reason, the privileged helper installed using SMJobBless is allowed to access files within TemporaryItems, but the daemon installed using SMAppService.daemon is not.

I attempted to send bookmarked URLs, both security-scoped and normal to the daemon, but that did not grant permission for fileManager to move the file. I suspect we can work around the problem by saving the temporary file elsewhere. That's a shame because NSItemReplacementDirectory seems like the "correct" location for the temporary file, but I suppose storing it to the Caches directory is probably a safe option, unless you think there's a more appropriate location.

Thanks again.

Daemon has reduced permissions after migrating from SMJobBless to SMAppService
 
 
Q