macOS: Reliable custody of a sandboxed worker if its controller dies during spawn

Hello,

I'm investigating the supported macOS mechanisms for supervising a single unprivileged, sandboxed worker process.

The target environment is macOS 15.6 (24G84), Apple Silicon (arm64).

The worker is intended to operate under a separate Standard user identity, with tightly restricted capabilities and no requirement to create child processes.

The main challenge is the startup failure window:

A controller initiates worker creation, but exits unexpectedly before spawn returns or before the worker's identity and authenticated handshake can be independently recorded.

In this situation, we need a supported approach to prevent the worker from becoming an unaccounted-for process, or to recover sufficiently trustworthy custody to determine its lifecycle and cleanup state.

I have reviewed direct child-process supervision, POSIX process groups, launchd job behaviour, sandbox inheritance, resource limits and process identification mechanisms.

I would appreciate guidance on the following:

  1. Is there a public, supported macOS API or system service that provides recoverable process custody from initial worker creation, including controller failure during startup?
  2. Could an application-associated XPC service or launchd mechanism provide the required lifecycle relationship without granting the worker independent ongoing execution authority?
  3. Is generation-safe, audit-token-based process signaling available through a supported public interface, and what are its privilege and acquisition requirements?
  4. Is setting a hard RLIMIT_NPROC limit before worker operations a supported way to prevent further fork/spawn activity by an unprivileged worker? What limitations or exceptions should be considered?
  5. What can macOS guarantee about process termination and cleanup after controller failure, and what must remain application-level verification?

I'm particularly interested in documented guarantees and supported APIs rather than behaviour observed only in tests or private interfaces.

Any recommendations or references to Apple documentation would be greatly appreciated.

Thank you.

Answered by DTS Engineer in 910395022

Kevin and I have have answered a bunch of questions like this recently. I recommend that you review the recent threads in App & System Services > Processes & Concurrency subtopic, think about how our advice applies to your situation, and then come back with your specific questions.

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

Kevin and I have have answered a bunch of questions like this recently. I recommend that you review the recent threads in App & System Services > Processes & Concurrency subtopic, think about how our advice applies to your situation, and then come back with your specific questions.

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

Thank you, Quinn. I reviewed the relevant XPC, launchd and process-lifecycle discussions, including your answers and Kevin's recent replies.

I understand that embedded XPC covers application death during service startup, launchd performs reaping, and operation replay prevention belongs in the application protocol.

We're comparing embedded XPC with running an existing controller under launchd. Our deployment target is macOS 15.6 on Apple silicon.

Three remaining questions would help us choose the appropriate supported architecture:

If a controller runs as a launchd job with AbandonProcessGroup=false, does job-death cleanup cover a worker created while the controller dies inside posix_spawn, before the call returns, assuming the worker remains in the same process group and creates no descendants?

For embedded XPC, what supported mechanism or design would you recommend for termination control and independently attributable completion evidence if the containing application remains alive but unresponsive? We distinguish connection interruption, process exit, reaping and filesystem cleanup, and are not seeking hard-real-time guarantees during kernel failure.

We are also considering running the application and its service under the same dedicated Standard account, instead of switching users during launch. On macOS 15.6, what supported login/bootstrap context should that arrangement use? Is prior interactive login necessary?

Our aim is to avoid PID-only recovery and treating timeout or connection loss as proof of termination.

Thanks again for your guidance.

If a controller runs as a launchd job with AbandonProcessGroup=false, does job-death cleanup cover a worker …

The documented behaviour for that is pretty clear. The launchd.plist man page says:

AbandonProcessGroup <boolean>

When a job dies, launchd kills any remaining processes with the same process group ID as the job. Setting this key to true disables that behavior.

You specifically asked about a potential race condition, and my take on that is that it should work as documented and, if it’s not, then that’s a bug and you should file it as such.

Keep in mind that, if your launchd job spawns a child process, there’s nothing stopping that from changing its process group, meaning that it won’t be killed via this mechanism. If your goal is to guard against that, you’ll need something much more heavy-handed, like an es_new_descendants_client ES client.


For embedded XPC, what supported mechanism or design would you recommend for termination control … if the containing application remains alive but unresponsive?

Termination control of what? The app? Or the XPC service?

And who exactly is doing the terminating? Clearly it’s not the app, because it’s “alive but unresponsive”. Are you expecting the XPC service to terminate the app? Or is this termination happening from ‘outside’ the app / XPC service pair?


Is prior interactive login necessary?

Yes, assuming you’re using “application” is the standard meaning we use on Apple platforms, that is, a program with a GUI that the user launches from the Finder (Home Screen on iOS, and so on). macOS has no supported API for spinning up a GUI login context.

Running programs in a different existing GUI login context is possible with current versions of launchctl, but it’s not something I recommend. Rather, I recommend that you install a global launchd agent, one that runs in every GUI login context. Then, if if you want to run an app in a specific context, message the corresponding agent and have it launch the app using NSWorkspace. This causes the app to launch pretty much like it would if the user double clicked it, which helps avoid a world of potential weirdness.

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

Thank you, Quinn. Your explanation resolves my startup-race question. I understand that launchd's cleanup depends on the worker remaining in the job's process group.

To clarify the XPC question: I meant terminating the worker/service, not having it terminate the application. My existing controller normally performs worker termination. If that controller becomes wholly unresponsive, it cannot do so; there is no independent watchdog in my current design. I therefore distinguish controller death from controller hang and am prioritising the launchd-managed controller route.

The qualification worker I am evaluating is fixed-purpose, sandboxed, and does not need to create descendants. My target remains macOS 15.6 on Apple silicon. Four remaining platform points would help me avoid choosing an unsupported arrangement:

  1. Completion after controller loss

What supported observation can I use in a later verification step to establish that the worker from a particular job execution has exited, including when the controller died before recording its identity?

Is there a public completion interface, with particular privilege or registration requirements, or only the documented group-cleanup assurance?

I would not treat PID/PGID disappearance or parsing launchctl print as proof.

  1. A living but hung controller

I understand that launchd has no general hard wall-clock runtime limit.

Does macOS 15.6 offer an OS-managed liveness or lease mechanism that can initiate termination when an ordinary job stops participating, without another application-created watchdog?

I mean initiating termination, not guaranteeing completion within a deadline.

  1. Enforced confinement on macOS 15.6

Following your group-escape warning, is there a supported unprivileged restriction that prevents this worker from changing its inherited process group/session or creating descendants, without Endpoint Security or custom sandbox profiles?

I distinguish that from merely reviewing my code to ensure it does not perform those operations.

  1. Non-GUI launch context

Your login answer addressed a conventional GUI application.

As a prospective alternative, could a genuinely non-GUI, signed App Sandbox worker run under a dedicated Standard account before its first GUI login?

If supported, which launchd domain/session and sandbox-container initialization prerequisites apply?

A reference to the applicable public interface, or confirmation that a particular facility is unavailable, would be sufficient.

I understand that filesystem effects and cleanup verification would remain my responsibility.

Thank you.

Completion after controller loss

…

I would not treat PID/PGID disappearance … as proof.

I don’t understand this. If you know the work was being done by a specific process and you know that process has terminated, what other guarantees are you looking for?

including when the controller died before recording its identity?

You can avoid that concern by having the controller spawn the worker in the suspended state, the record the audit token, then resume the worker. If controller dies before it records the worker’s audit token, the worker will never run.


A living but hung controller

Does macOS 15.6 offer an OS-managed liveness or lease mechanism that can initiate termination when an ordinary job stops participating … ?

No. If you need that, you’ll have to build it yourself.


Enforced confinement on macOS 15.6

… is there a supported unprivileged restriction that prevents this worker from changing its inherited process group/session or creating descendants, without Endpoint Security or custom sandbox profiles?

No.


Non-GUI launch context

I recommend that you start a new thread for this question. Login session management is a whole different kettle of fish.

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