Sandboxed helper keeps running after the app is turned off in Background App Activity

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 bootstrap returns 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/LaunchDaemons job, not yet SMAppService): 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

  1. Is this setup supported for shipping, including the sandbox starting before anyone logs in?
  2. Does the approval for daemon-bundled helpers cover a helper bootstrapped into another account's domain?
  3. Our plan: when the daemon gets SIGTERM, it runs bootout on the helper and its domain, and it treats bootstrap exit 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?
  4. 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.

You’re on very shaky compatibility ground here. What not doing anything wrong per se, but it’s quite unusual and thus I’m not surprised you’ve encountered some sharp edges.

In your setup, where on disk is <fixed-agent-plist>?

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

Thanks, Quinn. It depends on the setup:

  • In the reproducer from the post, it's a fixed file inside the app bundle: /Applications/<App>.app/Contents/Library/LaunchAgents/<label>.plist, bootstrapped by that absolute path.
  • In our current build, the daemon writes it at each start to /var/run/<label>.plist (owned by root, mode 0600) and bootstraps that file. Its ProgramArguments point at the nested helper inside the app bundle: /Applications/<App>.app/Contents/Helpers/<Helper>.app/Contents/MacOS/<Helper>. We generate it because it passes the app's build number as an argument; that could move into the helper itself.

We can use whichever location you'd consider least fragile.

What we're really after is both App Sandbox and a dedicated non-root identity for the helper, before login, since it parses untrusted input. Is there a supported way to get both?

If not, we'd pick one of two configurations that already work for us. Which would you consider on firmer ground?

  1. SMAppService.daemon running as root, with App Sandbox.
  2. SMAppService.daemon with UserName set to the service account, without App Sandbox.
Sandboxed helper keeps running after the app is turned off in Background App Activity
 
 
Q