Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion Sources/Blessed/BlessError.swift
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ public struct BlessError: Error {
toolAssessor.infoPropertyListAuthorizedClients(type: .bundled), // 7 & 8 - bundled
toolAssessor.infoPropertyListAuthorizedClients(type: .installed), // 7 & 8 - bundled
toolAssessor.infoPropertyListBundleVersion(), // 9
appAssessor.infoPropertyList(bundledHelperToolLocation: toolAssessor.bundledLocation, label: label) // 10
appAssessor.infoPropertyList(bundledHelperToolLocation: toolAssessor.bundledLocation, label: label), // 10
appAssessor.isNotSandboxed(), // 11
]
}
}
Expand Down Expand Up @@ -528,4 +529,20 @@ fileprivate struct AppAssessor {
""")
}
}

// 11
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).

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?


return .notSatisfied(explanation: """
This \(programType) is sandboxed, which will cause SMJobBless() to always get denied.
Helper tools can only be blessed from a non-sandboxed \(programType).
""")
}

return .satisfied
}
}
2 changes: 1 addition & 1 deletion Sources/Blessed/ProcessInfo+Sandbox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ extension ProcessInfo {
return false
}

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).

// The entitlement value must be a boolean value. If it's not, then it's presumbly not sandboxed.
return false
}
Expand Down