launchctl fails with "Bootstrap failed: 5: Input/output error", if the app is signed with sandbox

I am implementing parental control app via python 3.9 for macOS. Therefore I want to use launch agent to keep my application always alive (app reopened automatically after reboot pc and protected against to be closed via activity monitor.) I want to give keep alive enabling and disabling option to parents that they can deactivate or activate it in the app GUI.

I achieved to keep alive my app standalone signed app without sandboxing(or via Terminal command) but if I sign it with sandbox then I get following error for "launchctl load" and "launchctl bootstrap" commands which are executed in the application "Load failed: 5: Input/output error"

My steps without Sandbox which works fine:

1- Create a com.test_gui.macos.plist file under /Library/LaunchAgents with following content:

`<?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>
    <key>KeepAlive</key>
    <true/>
    <key>Label</key>
    <string>test_gui</string>
    <key>ProgramArguments</key>
    <array>
        <string>open</string>
        <string>-g</string>
        <string>-a</string>
        <string>/Applications/test_gui.app</string>
    </array>
</dict>
</plist>`

2- create a python file for launch agent activation (I know either launchctl load or bootstrap should be used but I used both of them to test both.):

import subprocess
cmd = "launchctl enable gui/501/test_gui"
response = subprocess.call(cmd, shell=True)
time.sleep(2)
cmd = "launchctl load -w /Library/LaunchAgents/com.test_gui.macos.plist"
response = subprocess.call(cmd, shell=True)
time.sleep(2)
cmd = "launchctl bootstrap system /Library/LaunchAgents/com.test_gui.macos.plist"
response = subprocess.call(cmd, shell=True)
time.sleep(50)

3- Create standalone app via nutika:

python3.9 -m nuitka --run --standalone --macos-disable-console --macos-create-app-bundle
\--macos-app-mode=ui-element --enable-plugin=pyside6 --macos-app-icon=/Users/emre/Documents/MrProtect/icons/app_icon.png
\--include-data-dir=icons=icons test_gui.py

4-Create a .sh file to sign app without sandboxing:

#!/bin/sh


APP_PATH="/Users/emre/Documents/tests/Deployment/test_gui.app"
SIGNING_IDENTITY_APP="Apple Development: Emre Guenay (***)"
PASSWORD="***"
codesign -s "$SIGNING_IDENTITY_APP" -f \
"$APP_PATH/Contents/MacOS/Python"
codesign -s "$SIGNING_IDENTITY_APP" -f \
"$APP_PATH/Contents/MacOS/test_gui"

exit 0

5-execute sh file and see following output: /Users/emre/Documents/MrProtect/tests/Deployment/test_gui.app/Contents/MacOS/Python: replacing existing signature /Users/emre/Documents/MrProtect/tests/Deployment/test_gui.app/Contents/MacOS/test_gui: replacing existing signature

6-copy paste signed standalone app(without sandbox) file under /Applications/

7-execute app and try to close app via activity monitor and observe that the app is reopened automatically. so the launch agent works fine

Failed Steps(Sign the same app with sandbox and observe that both launchctl load and bootstrap returns "Load failed: 5: Input/output error")

8- create an app.entitlements file with following content:

\<?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\>
\<key\>com.apple.security.app-sandbox\</key\>
\<true/\>
\</dict\>
\</plist\>

9-Create a .sh file to sign app with sandboxing:

#!/bin/sh

# 


APP_PATH="/Users/emre/Documents/tests/Deployment/test_gui.app"
SIGNING_IDENTITY_APP="Apple Development: Emre Guenay (***)"
PASSWORD="***"
codesign -s "$SIGNING_IDENTITY_APP" -f \
\--entitlements app.entitlements \
"$APP_PATH/Contents/MacOS/Python"
codesign -s "$SIGNING_IDENTITY_APP" -f \
\--entitlements app.entitlements \
"$APP_PATH/Contents/MacOS/test_gui"


exit 0

10-execute sh file and see following output: /Users/emre/Documents/MrProtect/tests/Deployment/test_gui.app/Contents/MacOS/Python: replacing existing signature /Users/emre/Documents/MrProtect/tests/Deployment/test_gui.app/Contents/MacOS/test_gui: replacing existing signature

11-execute app and try to close app via activity monitor and observe that the app is closed although app started the launch agent.

12-you can re-execute app under /Applications/test_gui.app/Contents/MacOS folder with "./test_gui" or even with sudo "sudo ./test_gui" commands you would see following error message for both launchctl load and bootstrap returns: "Load failed: 5: Input/output error". So if you close the app, it will not be re-opened Moreover console tool launchd.log output shows also (1: Operation not permitted) error for launchctl load and bootstrap commands

My questions:

1-Is there any other method different than launch agent for keep alive? I researched it in internet but I could not find any other method unfortunately. Moreover launch agent is not user friendly anyway, with the reason that sandboxed apps cannot copy paste any files under LaunchAgents folder automatically. I am planning to provide my plist file to users that they can copy paste the file themself into launch agent folder out of sandbox.

2-How can i get rid of "Load failed: 5: Input/output error", thereby I can proceed at least with lanuchctl?

What I have tried additionally:

1-I have also tried to use these linux commands, before executing my sandboxed standalone app file:

sudo -S chown 600 /Library/LaunchAgents/com.test_gui.macos.plist sudo -S chown root:wheel /Library/LaunchAgents/com.test_gui.macos.plist

2-I have already given security fulldisk access to my test_gui app, but it also did not work

My Requirements:

Nuitka: 1.9rc5 Commercial: None Python: 3.9.12 (v3.9.12:b28265d7e6, Mar 23 2022, 18:22:40) Flavor: CPython Official Executable: /Library/Frameworks/Python.framework/Versions/3.9/bin/python3.9 OS: Darwin Arch: x86_64 Version C compiler: /usr/bin/clang (clang).

macOS Sonoma: 14.2.1

Replies

I’m going to ignore the Python aspects of this for the moment. Python makes this problem harder, because of its unique packaging requirements, but it doesn’t change the fundamental story.

It is possible to install a launchd agent from a sandboxed app. However, you shouldn’t try to this to do this using launchctl. Rather, use the SMAppService API.

IMPORTANT If the app is sandboxed then the agent must also be sandboxed. Sandboxing your agent is a bit tricky because the App Sandbox requires that the agent have a bundle ID. There are two ways you can give your agent a bundle ID:

  • Embed the Info.plist in the agent directly. In Xcode, use the Create Info.plist Section in Binary build setting for this.

  • Put it in an app-like wrapper. Once you do that, you set the bundle ID in the Info.plist like you would a normal app.

Note Signing a daemon with a restricted entitlement discuss the concept of an app-like wrapper, albeit for a very different reason.

Having said that, I want to be clear about this:

protected against to be closed via activity monitor

Using a launchd agent helps with this, but it doesn’t make your product immune from tampering. If the user has access to Terminal, they can unload your agent using launchctl. Also, macOS 13 and later give the user explicit control over background execution in System Settings > General > Login Items.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"