From 89b91999f4e5cdc8a863bb2941b70e4e12336313 Mon Sep 17 00:00:00 2001 From: Martin van Diemen Date: Tue, 14 Jul 2026 13:50:50 +0200 Subject: [PATCH] Fix LaunchAgent/LaunchDaemon loading on macOS 27 Create launchd plists with PlistBuddy instead of `defaults write`. defaults hands the write to cfprefsd, which adds a com.apple.quarantine extended attribute to every file it creates. Starting with macOS 27 beta 3, launchd refuses to load quarantined property list files. - Remove existing plists before recreation so upgrades clear stale quarantine attributes from previous installs - Add SpawnConstraint (macOS 14+) so launchd only spawns binaries signed with the Root3 Team ID, preventing orphaned plists from launching a substituted binary - Strip com.apple.quarantine as an extra safeguard --- SupportHelper/pkgbuild/scripts/postinstall | 58 ++++++++++++++--- pkgbuild/scripts/postinstall | 60 ++++++++++++++---- .../install_privileged_helper_tool.zsh | 62 ++++++++++++++++--- 3 files changed, 149 insertions(+), 31 deletions(-) diff --git a/SupportHelper/pkgbuild/scripts/postinstall b/SupportHelper/pkgbuild/scripts/postinstall index 351eb45..fc0dc62 100755 --- a/SupportHelper/pkgbuild/scripts/postinstall +++ b/SupportHelper/pkgbuild/scripts/postinstall @@ -21,16 +21,56 @@ launch_daemon="nl.root3.support.helper" # CLI location cli_location="/usr/local/bin/SupportHelper" -# Create the LaunchAgent -defaults write "/Library/LaunchDaemons/${launch_daemon}.plist" Label -string "${launch_daemon}" -defaults write "/Library/LaunchDaemons/${launch_daemon}.plist" ProgramArguments -array -string "${cli_location}" -# Keep the process alive -defaults write "/Library/LaunchDaemons/${launch_daemon}.plist" KeepAlive -boolean yes -# Run at every reboot -defaults write "/Library/LaunchDaemons/${launch_daemon}.plist" RunAtLoad -boolean yes +# Developer Team Identifier used for the SpawnConstraint +team_identifier="98LJ4XBGYK" + +# Path to PlistBuddy +plistbuddy="/usr/libexec/PlistBuddy" + +# Path to the LaunchDaemon property list +launch_daemon_plist="/Library/LaunchDaemons/${launch_daemon}.plist" + +# Load Requirements +autoload is-at-least + +# macOS Version +os_version=$(sw_vers -productVersion) + +# Remove any existing LaunchDaemon plist to avoid stale keys and extended +# attributes. Note: the plist is created with PlistBuddy instead of +# "defaults write", as defaults hands the write to cfprefsd which adds the +# com.apple.quarantine extended attribute to every file it creates. launchd +# refuses to load quarantined property list files on macOS 27 and higher. +rm -f "${launch_daemon_plist}" + +# Create the LaunchDaemon +# - KeepAlive keeps the process alive +# - RunAtLoad runs at every reboot +"${plistbuddy}" \ + -c "Add :Label string ${launch_daemon}" \ + -c "Add :ProgramArguments array" \ + -c "Add :ProgramArguments:0 string ${cli_location}" \ + -c "Add :KeepAlive bool true" \ + -c "Add :RunAtLoad bool true" \ + "${launch_daemon_plist}" > /dev/null + +# Add a SpawnConstraint on macOS 14 and higher so launchd only spawns this +# service when the binary is signed by Root3 with the expected signing +# identifier +if is-at-least 14.0 ${os_version}; then + "${plistbuddy}" \ + -c "Add :SpawnConstraint dict" \ + -c "Add :SpawnConstraint:team-identifier string ${team_identifier}" \ + -c "Add :SpawnConstraint:signing-identifier string nl.root3.support.helper" \ + "${launch_daemon_plist}" > /dev/null +fi + +# Just to be sure, remove the quarantine extended attribute if present +xattr -d com.apple.quarantine "${launch_daemon_plist}" &> /dev/null + # Set permissions -chown root:wheel "/Library/LaunchDaemons/${launch_daemon}.plist" -chmod 644 "/Library/LaunchDaemons/${launch_daemon}.plist" +chown root:wheel "${launch_daemon_plist}" +chmod 644 "${launch_daemon_plist}" # Unload the LaunchDaemon launchctl bootout system "/Library/LaunchDaemons/${launch_daemon}.plist" &> /dev/null diff --git a/pkgbuild/scripts/postinstall b/pkgbuild/scripts/postinstall index 1f4b1c1..98131b0 100755 --- a/pkgbuild/scripts/postinstall +++ b/pkgbuild/scripts/postinstall @@ -22,6 +22,12 @@ launch_agent="nl.root3.support" # LaunchDaemon label launch_daemon="nl.root3.support.helper" +# Developer Team Identifier used for the SpawnConstraint +team_identifier="98LJ4XBGYK" + +# Path to PlistBuddy +plistbuddy="/usr/libexec/PlistBuddy" + # Install location install_location="/Applications/Support.app" @@ -47,19 +53,49 @@ fi # ------------------ LaunchAgent ------------------ -# Add AssociatedBundleIdentifiers to show app name in Login Items on -# macOS 13 and higher instead of developer name -defaults write "/Library/LaunchAgents/${launch_agent}.plist" AssociatedBundleIdentifiers -array -string "nl.root3.support" -# Set the Label and ProgramArguments -defaults write "/Library/LaunchAgents/${launch_agent}.plist" Label -string "${launch_agent}" -defaults write "/Library/LaunchAgents/${launch_agent}.plist" ProgramArguments -array -string "/Applications/Support.app/Contents/MacOS/Support" -# Run every reboot -defaults write "/Library/LaunchAgents/${launch_agent}.plist" KeepAlive -boolean yes -# Set ProcessType to Interactive -defaults write "/Library/LaunchAgents/${launch_agent}.plist" ProcessType -string "Interactive" +# Path to the LaunchAgent property list +launch_agent_plist="/Library/LaunchAgents/${launch_agent}.plist" + +# Remove any existing LaunchAgent plist to avoid stale keys and extended +# attributes. Note: the plist is created with PlistBuddy instead of +# "defaults write", as defaults hands the write to cfprefsd which adds the +# com.apple.quarantine extended attribute to every file it creates. launchd +# refuses to load quarantined property list files on macOS 27 and higher. +rm -f "${launch_agent_plist}" + +# Create the LaunchAgent +# - AssociatedBundleIdentifiers shows the app name in Login Items on +# macOS 13 and higher instead of the developer name +# - KeepAlive runs the app at every reboot and relaunches it when quit +# - ProcessType Interactive gives the app UI process priority +"${plistbuddy}" \ + -c "Add :Label string ${launch_agent}" \ + -c "Add :ProgramArguments array" \ + -c "Add :ProgramArguments:0 string ${install_location}/Contents/MacOS/Support" \ + -c "Add :KeepAlive bool true" \ + -c "Add :ProcessType string Interactive" \ + -c "Add :AssociatedBundleIdentifiers array" \ + -c "Add :AssociatedBundleIdentifiers:0 string nl.root3.support" \ + "${launch_agent_plist}" > /dev/null + +# Add a SpawnConstraint on macOS 14 and higher so launchd only spawns this +# service when the binary is signed by Root3 with the expected signing +# identifier. This prevents an orphaned plist from launching a malicious +# binary placed at the same path. +if is-at-least 14.0 ${os_version}; then + "${plistbuddy}" \ + -c "Add :SpawnConstraint dict" \ + -c "Add :SpawnConstraint:team-identifier string ${team_identifier}" \ + -c "Add :SpawnConstraint:signing-identifier string nl.root3.support" \ + "${launch_agent_plist}" > /dev/null +fi + +# Just to be sure, remove the quarantine extended attribute if present +xattr -d com.apple.quarantine "${launch_agent_plist}" &> /dev/null + # Set permissions -chown root:wheel "/Library/LaunchAgents/${launch_agent}.plist" -chmod 644 "/Library/LaunchAgents/${launch_agent}.plist" +chown root:wheel "${launch_agent_plist}" +chmod 644 "${launch_agent_plist}" # Reload the LaunchAgent if [[ -n "${username}" ]]; then diff --git a/src/Support/Scripts/install_privileged_helper_tool.zsh b/src/Support/Scripts/install_privileged_helper_tool.zsh index be7b5ce..506df0f 100755 --- a/src/Support/Scripts/install_privileged_helper_tool.zsh +++ b/src/Support/Scripts/install_privileged_helper_tool.zsh @@ -25,6 +25,18 @@ launch_daemon="nl.root3.support.helper" # Install location install_location="/Applications/Support.app" +# Developer Team Identifier used for the SpawnConstraint +team_identifier="98LJ4XBGYK" + +# Path to PlistBuddy +plistbuddy="/usr/libexec/PlistBuddy" + +# Load Requirements +autoload is-at-least + +# macOS Version +os_version=$(sw_vers -productVersion) + # ------------------ PrivilegedHelperTool ------------------ # Create "/Library/PrivilegedHelperTools/" if not present @@ -40,17 +52,47 @@ chmod 544 "${privileged_helper_tool}" # ------------------ LaunchDaemon PrivilegedHelperTool ------------------ -# Add AssociatedBundleIdentifiers to show app name in Login Items on -# macOS 13 and higher instead of developer name -defaults write "/Library/LaunchDaemons/${launch_daemon}.plist" AssociatedBundleIdentifiers -array -string "nl.root3.support" -# Set the Label and ProgramArguments -defaults write "/Library/LaunchDaemons/${launch_daemon}.plist" Label -string "${launch_daemon}" -defaults write "/Library/LaunchDaemons/${launch_daemon}.plist" ProgramArguments -array -string "${privileged_helper_tool}" -# Set MachServices -defaults write "/Library/LaunchDaemons/${launch_daemon}.plist" MachServices -dict -string "nl.root3.support.helper" -bool true +# Path to the LaunchDaemon property list +launch_daemon_plist="/Library/LaunchDaemons/${launch_daemon}.plist" + +# Remove any existing LaunchDaemon plist to avoid stale keys and extended +# attributes. Note: the plist is created with PlistBuddy instead of +# "defaults write", as defaults hands the write to cfprefsd which adds the +# com.apple.quarantine extended attribute to every file it creates. launchd +# refuses to load quarantined property list files on macOS 27 and higher. +rm -f "${launch_daemon_plist}" + +# Create the LaunchDaemon +# - AssociatedBundleIdentifiers shows the app name in Login Items on +# macOS 13 and higher instead of the developer name +"${plistbuddy}" \ + -c "Add :Label string ${launch_daemon}" \ + -c "Add :ProgramArguments array" \ + -c "Add :ProgramArguments:0 string ${privileged_helper_tool}" \ + -c "Add :MachServices dict" \ + -c "Add :MachServices:nl.root3.support.helper bool true" \ + -c "Add :AssociatedBundleIdentifiers array" \ + -c "Add :AssociatedBundleIdentifiers:0 string nl.root3.support" \ + "${launch_daemon_plist}" > /dev/null + +# Add a SpawnConstraint on macOS 14 and higher so launchd only spawns this +# service when the binary is signed by Root3 with the expected signing +# identifier. This prevents an orphaned plist from launching a malicious +# binary placed at the same path. +if is-at-least 14.0 ${os_version}; then + "${plistbuddy}" \ + -c "Add :SpawnConstraint dict" \ + -c "Add :SpawnConstraint:team-identifier string ${team_identifier}" \ + -c "Add :SpawnConstraint:signing-identifier string nl.root3.support.helper" \ + "${launch_daemon_plist}" > /dev/null +fi + +# Just to be sure, remove the quarantine extended attribute if present +xattr -d com.apple.quarantine "${launch_daemon_plist}" &> /dev/null + # Set permissions -chown root:wheel "/Library/LaunchDaemons/${launch_daemon}.plist" -chmod 644 "/Library/LaunchDaemons/${launch_daemon}.plist" +chown root:wheel "${launch_daemon_plist}" +chmod 644 "${launch_daemon_plist}" # Unload the LaunchDaemon if launchctl print "system/${launch_daemon}" &> /dev/null ; then