Skip to content

Commit

Permalink
Merge pull request #132 from trilemma-dev/executable-path
Browse files Browse the repository at this point in the history
Fixes #128 - consistently determines location of CLI or app bundle
  • Loading branch information
jakaplan authored May 10, 2023
2 parents c105054 + 68529a9 commit 1cece54
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 4 deletions.
4 changes: 2 additions & 2 deletions Sources/SecureXPC/Server/ClientRequirement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,10 @@ extension XPCServer.ClientRequirement {

private static var parentBundleURL: URL {
get throws {
let components = Bundle.main.bundleURL.pathComponents
let components = currentExecutableOrAppBundleURL().pathComponents
guard let contentsIndex = components.lastIndex(of: "Contents") else {
throw XPCError.misconfiguredServer(description: "This server does not have a parent bundle.\n" +
"Path components: \(components)")
"Components: \(components)")
}

return URL(fileURLWithPath: "/" + components[1..<contentsIndex].joined(separator: "/"))
Expand Down
5 changes: 3 additions & 2 deletions Sources/SecureXPC/Server/MachServiceCriteria.swift
Original file line number Diff line number Diff line change
Expand Up @@ -499,12 +499,12 @@ private func throwIfSandboxedAndThisLoginItemCannotCommunicateOverXPC() throws {
// MARK: SMAppService daemon & agent

private func parentAppURL() throws -> URL {
let components = Bundle.main.bundleURL.pathComponents
let components = currentExecutableOrAppBundleURL().pathComponents
guard let contentsIndex = components.lastIndex(of: "Contents"),
components[components.index(before: contentsIndex)].hasSuffix(".app") else {
throw XPCError.misconfiguredServer(description: """
Parent bundle could not be found.
Path:\(Bundle.main.bundleURL)
Components: \(components)
""")
}

Expand Down Expand Up @@ -565,6 +565,7 @@ private func validateThisProcessIsAnSMAppServiceDaemon() -> ValidationResult {
return .failure("""
An SMAppService daemon must have a property list within its parent bundle's Contents/Library/LaunchDaemons /
directory.
Parent bundle: \(try! parentAppURL())
""")
}

Expand Down
55 changes: 55 additions & 0 deletions Sources/SecureXPC/XPCCommon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,61 @@ func const(_ input: UnsafePointer<CChar>!) -> UnsafePointer<CChar>! {
return UnsafePointer(mutableCopy) // The result should never actually be mutated
}


/// If this running code is the main executable of an app bundle, the URL to the app bundle will be returned, otherwise the URL to the currently running executable
/// will be returned.
///
/// Some servers (such as `SMAppService` daemons & agents) can be either command line tools (single file executables) or app bundles. Distinguishing between
/// these cases is necessary in order to properly identify a parent app bundle and/or whether one exists.
///
/// See https://github.com/trilemma-dev/SecureXPC/issues/128 for why this is needed.
func currentExecutableOrAppBundleURL() -> URL {
// To determine if this currently running executable is the main executable for an app bundle:
// - located in a Contents/MacOS/ directory
// - parent directory of Contents/MacOS/ directory is for an app bundle
// - its name matches the CFBundleExecutable info dictionary value
// Just being in Contents/MacOS/ is insufficient as it's valid for a command line tool to be located there.
let executablePath = currentExecutableURL()
if executablePath.deletingLastPathComponent().pathComponents.suffix(2) == ["Contents", "MacOS"],
executablePath.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent().pathExtension == "app",
executablePath.lastPathComponent == Bundle.main.infoDictionary?["CFBundleExecutable"] as? String {
return Bundle.main.bundleURL
} else {
return executablePath
}
}

/// The path of the currently running executable.
///
/// This works consistently whether or not the executable is part of a bundle. The returned value is not affected by its location within a bundle (if applicable).
private func currentExecutableURL() -> URL {
// Adapted from https://developer.apple.com/forums/thread/709577
var buffer = [CChar](repeating: 0, count: Int(MAXPATHLEN))
var bufferSize = UInt32(buffer.count)
let result = _NSGetExecutablePath(&buffer, &bufferSize)

// From _NSGetExecutablePath's documentation:
// The function returns 0 if the path was successfully copied, and *bufsize is left unchanged. It returns -1 if
// the buffer is not large enough, and *bufsize is set to the size required.
if result == -1 {
buffer = [CChar](repeating: 0, count: Int(bufferSize))
let result2 = _NSGetExecutablePath(&buffer, &bufferSize)
guard result2 == 0 else {
fatalError("_NSGetExecutablePath failed (\(result2)) after increasing buffer size to \(bufferSize)")
}
} else if result != 0 {
fatalError("_NSGetExecutablePath failed (\(result)) with undocumented result code")
}

// From _NSGetExecutablePath's documentation:
// Note that _NSGetExecutablePath will return "a path" to the executable not a "real path" to the executable. That
// is the path may be a symbolic link and not the real file.
return URL(fileURLWithFileSystemRepresentation: buffer, isDirectory: false, relativeTo: nil)
.resolvingSymlinksInPath()
}

/// Creates the static code representation for this running process.
///
/// This is a convenience wrapper around `SecCodeCopySelf` and `SecCodeCopyStaticCode`.
Expand Down

0 comments on commit 1cece54

Please sign in to comment.