Short version: we run a sandboxed helper as a hidden service account, started at boot by an SMAppService daemon. It works, even before login. But when the user turns our app off in Background App Activity, only the daemon stops. The helper keeps running. Is this setup supported, and what's the right way to manage the helper?
What we want
A Developer ID signed, notarized app (not Mac App Store) with a helper that parses untrusted input. The helper should:
- run as a dedicated, hidden, non-login local account;
- use App Sandbox, with its own container;
- be available before anyone logs in (after FileVault unlock).
What we built
An unsandboxed root LaunchDaemon, registered with SMAppService.daemon, runs this at boot:
launchctl bootstrap user/<serviceUID> <fixed-agent-plist>
The agent plist uses LimitLoadToSessionType=Background. The helper is a nested app in the same bundle, with com.apple.security.app-sandbox=true. We don't create a GUI session, change UID after the sandbox starts, or use private APIs.
What we measured
macOS 27.0 (26A428), arm64, dummy data only:
- Register and approve: the daemon starts. The helper starts as UID 60000, its container works, and reads outside it are denied.
- Turn the app off in Background App Activity: the daemon gets SIGTERM and stops. The helper keeps running (same PID).
- Turn it back on: the daemon starts again. Its
bootstrapreturns exit 5, because the old helper is still loaded. - Call
unregister(): the daemon stops. The helper keeps running. - Cold boot (tested with a plain
/Library/LaunchDaemonsjob, not yetSMAppService): the helper started and worked before login finished.
For comparison, running the same sandboxed helper as a system daemon with UserName set to this account fails before main: Incoming message euid:60000 does not match secinitd uid:0.
Questions
- Is this setup supported for shipping, including the sandbox starting before anyone logs in?
- Does the approval for daemon-bundled helpers cover a helper bootstrapped into another account's domain?
- Our plan: when the daemon gets SIGTERM, it runs
bootouton the helper and its domain, and it treatsbootstrapexit 5 as "already loaded". Is that the intended pattern, or is there a supported way for the helper to follow the app's Background App Activity setting? - If this setup isn't supported, what public mechanism gives a sandboxed helper its own non-root identity before login?
I can share the plists, entitlements and logs from a minimal reproducer.
Is this static plist plus kickstart arrangement within the intended … model … ?
Your overall goal isn’t well supported by macOS as things currently stand, and this approach is my best response to that. Specifically, you have the bits in place such that, if the background task management subsystem starts to notice your agent, it should understand that the agent is associated with your app.
However, you’re gonna need to do some testing, both to ensure compatibility with any older releases you support and to confirm that things still work as new OS releases are seeded.
Then again, you should be doing that anyway (-:
On disable or unregister, should our daemon leave the agent job loaded and let the helper exit on socket EOF … ?
That’s what I’d recommend. It keeps the system configuration static, which is kind of the launchd way. And the cost of a loaded job that’s not actually started is very small.
We can file Feedback for system-wide SMAppService agent registration if useful.
Please do. This doesn’t have to be complicated. You can just ask that it be dup’d to the existing bug (r. 92457638). That acts to both register your interest and get you notified if things change.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"