Skip to content

Conversation

@amomchilov
Copy link

Hey Josh, it's been a while!

I had stopped working on my person project for a while, and I had given up on my goal to keep my main app sandboxed (while still having an unsandboxed XPC service to install an privileged helper, like Apple's EvenBetterAuthorizationSample).

My XPC service was attempting to call SMJobBless(), always got denied, and I just couldn't figure out why. After coming back to with fresh eyes (for the third time!) and scouring the barrage of Console.app logs, I finally found this message:

Sandbox denied authorizing right com.apple.ServiceManagement.blesshelper for authorization created by /Users/Alex/MyApp.app/Contents/XPCServices/MyService.xpc [49158] (engine 2754) com.apple.Authorization

My XPC service was also sandboxed, unintentionally 🙃

For some reason I had OTHER_CODE_SIGN_FLAGS = "--deep" set on my main app's Xcode target, which made its XPC services inherit the com.apple.security.app-sandbox entitlement. Took ages, but I finally found the culprit.

To save others the frustration, I'm contributing back this new assessment, to detect this condition. You already had an isSandboxed extension (which isn't used elsewhere today), which worked perfectly here.

func isNotSandboxed() -> Assessment {
if ProcessInfo.processInfo.isSandboxed {
// XPC services are usually found in a path like ".../MyApp.app/Contents/XPCServices/Bar.xpc/Contents/MacOS/MyService"
let isXPCService = Bundle.main.executableURL?.deletingLastPathComponent().path.hasSuffix(".xpc/Contents/MacOS") ?? false
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kinda janky, but it's the best signal I could find for detecting if this code is running an App or XPC service.

I tried looking at the ProcessInfo.processInfo.environment, but interestingly, even main Apps have the XPC_SERVICE key set.

Another idea was to check the parent process ID. GUI apps are always owned by launchd (pid 1), whereas XPC services are owned by their GUI apps. But that isn't true for programs running from Xcode, which will cause both the App and XPC service to run under two different debugserver processes (kernel_task > launchd > Xcode > lldb-rpc-server > debugserver).

}

guard CFGetTypeID(entitlement) == CFBooleanGetTypeID(), let boolValue = (entitlement as? Bool) else {
guard let boolValue = entitlement as? Bool else {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The explicit CFGetTypeID check wasn't necessary, because the as? already checks that:

import Foundation

// `as? Bool` already rejects non-numeric `CFTypeRef` values, like a CFString,
// so we don't need to do our own `CFGetTypeID()` check
let cfString: CFTypeRef = "abc" as CFString
print(cfString as? Bool as Any) // => nil

// Bools cast successfully, as expected
let cfFalse: CFTypeRef = kCFBooleanFalse
let cfTrue:  CFTypeRef = kCFBooleanTrue
print(cfFalse as? Bool as Any) // => Optional(false)
print(cfTrue  as? Bool as Any) // => Optional(true)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spoke to soon. I forgot how Core Foundation and Foundation are rather loose around the distinction between numbers and actual booleans. I guess that comes from the C heritage.

import Foundation

func createCFNumberInt(_ value: Int) -> CFNumber {
  var value = value
  return CFNumberCreate(kCFAllocatorDefault, .intType, &value)
}

let zero: CFTypeRef = createCFNumberInt(0)
let one:  CFTypeRef = createCFNumberInt(1)

print(CFGetTypeID(zero) == CFNumberGetTypeID()) // => true, obviously
print(CFGetTypeID(zero) == CFBooleanGetTypeID()) // => false

// Surprisingly, 0/1 can be treated as booleans:
print(zero as? Bool as Any) // => Optional(false)
print(one  as? Bool as Any) // => Optional(true)

Still, I think being more permissive and accepting 0/1 as false/true is probably more consistent with the rest of the system (which wouldn't have been written in Swift, and didn't expect a strong distinction between the two).

if ProcessInfo.processInfo.isSandboxed {
// XPC services are usually found in a path like ".../MyApp.app/Contents/XPCServices/Bar.xpc/Contents/MacOS/MyService"
let isXPCService = Bundle.main.executableURL?.deletingLastPathComponent().path.hasSuffix(".xpc/Contents/MacOS") ?? false
let programType = isXPCService ? "XPC service" : "App"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think of applying this language change more broadly, to make more sense in the context of someone following the EvenBetterAuthorizationSample, who's trying to use Blessed from an XPC helper?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant