Skip to content
Merged
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
21 changes: 0 additions & 21 deletions VirtualBuddy.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -808,7 +808,6 @@
isa = PBXNativeTarget;
buildConfigurationList = F4C18A4E28491B8500335EC7 /* Build configuration list for PBXNativeTarget "VirtualBuddyGuest" */;
buildPhases = (
F4552F4729BA60C7002A21D8 /* Set VBGuestBuildID in Info.plist */,
F4C18A3E28491B8500335EC7 /* Sources */,
F4C18A3F28491B8500335EC7 /* Frameworks */,
F4C18A4028491B8500335EC7 /* Resources */,
Expand Down Expand Up @@ -1100,26 +1099,6 @@
shellPath = /bin/sh;
shellScript = "# Go into the VirtualBuddy.app/Contents/MacOS path\ncd \"$BUILT_PRODUCTS_DIR/$CONTENTS_FOLDER_PATH/MacOS\"\n\n# Symlink VirtualBuddy as vctool for VirtualCatalog command-line tool\nln -fs $EXECUTABLE_NAME vctool\n";
};
F4552F4729BA60C7002A21D8 /* Set VBGuestBuildID in Info.plist */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Set VBGuestBuildID in Info.plist";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/VBGenerated-Info.plist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# Ensures VirtualBuddyGuest has a VBGuestBuildID entry in its Info.plist file\n# This entry is used by the app itself when running in a VM to determine when it needs to be updated.\nTEMPLATE=\"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?><!DOCTYPE plist PUBLIC \\\"-//Apple//DTD PLIST 1.0//EN\\\" \\\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\\\"><plist version=\\\"1.0\\\"><dict/></plist>\"\nPLISTPATH=\"$DERIVED_FILE_DIR/VBGenerated-Info.plist\"\n\necho $TEMPLATE > \"$PLISTPATH\"\n\n/usr/libexec/PlistBuddy -c \"Add :VBGuestBuildID string xxxxxx\" \"$PLISTPATH\" 2>/dev/null || echo \"\"\n\n/usr/libexec/PlistBuddy -c \"Set :VBGuestBuildID `uuidgen`\" \"$PLISTPATH\"\n";
};
F4A277142BF51C480011B626 /* Strip Preview Content in Release Builds */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
Expand Down
2 changes: 1 addition & 1 deletion VirtualBuddy/Config/Versions.xcconfig
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
MARKETING_VERSION = 2.2
CURRENT_PROJECT_VERSION = 348
CURRENT_PROJECT_VERSION = 400
VERSIONING_SYSTEM = apple-generic
MACOSX_DEPLOYMENT_TARGET = 14.0

Expand Down
40 changes: 22 additions & 18 deletions VirtualBuddyGuest/GuestAppInstaller.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ final class GuestAppInstaller {

/// `true` if there's a VirtualBuddyGuest image mounted at `/Volumes/Guest`
/// for which the following conditions are true:
/// 1 - The guest in the volume has a different `VBGuestBuildID` from this process
/// 1 - The guest executable in the volume has a different CDHash from this process
/// 2 - The guest in the volume has a `CFBundleVersion` that's **greater than or equal to** the `CFBundleVersion` of this process
private var mountedGuestImageNeedsInstall: Bool {
guard FileManager.default.fileExists(atPath: "/Volumes/Guest") else { return false }
Expand All @@ -88,51 +88,55 @@ final class GuestAppInstaller {
return false
}

guard let imageBuildID = imageBundle.vbGuestBuildID else {
logger.error("Couldn't find VBGuestBuildID in image bundle")
return false
/// Fail open when trying to read CDHashes fails. Reinstalling the same build is better than
/// potentially skipping install of a different build just because this check has failed.
if let imageHash = cdHash(for: imageBundle),
let currentHash = cdHash(for: .main)
{
guard currentHash != imageHash else {
logger.debug("Image CDHash is same as current CDHash")
return false
}

logger.notice("Image CDHash differs from current CDHash (image: \(imageHash.hexString.quoted, privacy: .public); current: \(currentHash.hexString.quoted, privacy: .public))")
}

guard let imageBundleVersion = imageBundle.bundleVersion else {
logger.error("Couldn't find CFBundleVersion in image bundle")
return false
}

guard let currentBuildID = Bundle.main.vbGuestBuildID else {
logger.error("Couldn't find VBGuestBuildID in current bundle")
return false
}

guard let currentBundleVersion = Bundle.main.bundleVersion else {
logger.error("Couldn't find CFBundleVersion in current bundle")
return false
}

guard imageBuildID != currentBuildID else {
logger.debug("Image build ID is same as current build ID (\(currentBuildID, privacy: .public)), update won't be performed")
return false
}

guard imageBundleVersion >= currentBundleVersion else {
logger.debug("Image build ID differs from current build ID, but image has a lower CFBundleVersion (\(imageBundleVersion, privacy: .public)), ignoring")
return false
}

logger.notice("Mounted image qualifies for update with CFBundleVersion \(imageBundleVersion, privacy: .public), VBGuestBuildID \(imageBuildID, privacy: .public)")
logger.notice("Mounted image qualifies for update with CFBundleVersion \(imageBundleVersion, privacy: .public)")

return true
}

private func cdHash(for bundle: Bundle) -> Data? {
do {
return try bundle.executableCDHash()
} catch {
logger.error("Error reading CDHash for bundle at \(bundle.bundleURL.path(percentEncoded: false), privacy: .public) - \(error, privacy: .public)")
return nil
}
}

}

extension Bundle {
var bundleVersion: Int? {
guard let str = infoDictionary?[kCFBundleVersionKey as String] as? String else { return nil }
return Int(str)
}
var vbGuestBuildID: String? {
infoDictionary?["VBGuestBuildID"] as? String
}
}

extension NSApplication {
Expand Down
59 changes: 59 additions & 0 deletions VirtualCore/Source/Utilities/Bundle+CDHash.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Foundation
import Security

public extension Bundle {
/// Returns the CDHash from the code signature of the bundle's executable.
func executableCDHash() throws -> Data {
guard let executableURL else {
throw NSError(
domain: NSCocoaErrorDomain,
code: NSFileNoSuchFileError,
userInfo: [NSURLErrorKey: bundleURL]
)
}

var staticCode: SecStaticCode?
var status = SecStaticCodeCreateWithPath(
executableURL as CFURL,
SecCSFlags(),
&staticCode
)

guard status == errSecSuccess, let staticCode else {
throw NSError(
domain: NSOSStatusErrorDomain,
code: Int(status),
userInfo: [NSURLErrorKey: executableURL]
)
}

var signingInformation: CFDictionary?
status = SecCodeCopySigningInformation(
staticCode,
SecCSFlags(),
&signingInformation
)

guard status == errSecSuccess else {
throw NSError(
domain: NSOSStatusErrorDomain,
code: Int(status),
userInfo: [NSURLErrorKey: executableURL]
)
}

guard
let signingInformation,
let cdHash = (signingInformation as NSDictionary)[kSecCodeInfoUnique] as? Data
else {
/// The executable exists but has no code signature/CDHash.
throw NSError(
domain: NSOSStatusErrorDomain,
code: Int(errSecCSUnsigned),
userInfo: [NSURLErrorKey: executableURL]
)
}

return cdHash
}
}