Apple asked for the question as a Developer Forums post, then a reply to the
case email with the post URL. Nothing below has been posted or sent.
Post at https://developer.apple.com/forums/ (signed in, "Ask a question").
Suggested tags: XPC, App Sandbox, Endpoint Security
Attach: fixtures/egress_guard/hp04/apple-descendant-sample/main.c, renamed
to main.txt if the forum refuses .c.
Keeping every descendant of a sandboxed XPC service's worker inside its lifetime (fork, double-fork, setsid)
We are designing a macOS app that runs an unmodified third-party command-line tool for one task at a time. The plan is an app-bundled, sandboxed XPC service that launches the tool as a child process. The tool is not ours, so we have to assume it may fork without exec, double-fork, and call setsid().
For each run we need one guarantee: after the run ends, every process the tool created has stopped. "Ends" includes the XPC service crashing and the host app crashing, not only a clean cancel. If we cannot prove that, we cannot report the run as finished.
What we have looked at so far:
- Killing the process group. A child that calls
setsid()is no longer in the group. - launchd's process-group cleanup for the job. As we read
launchd.plist(5), that also covers the process group only. - Tracking pids from fork and exit notifications, including Endpoint Security. Notifications can be dropped, and pids can be reused, so a list rebuilt from events is not proof that nothing is left.
The attached sample shows the process shape without any sandbox or XPC. It starts a worker, the worker double-forks, and the grandchild calls setsid() and keeps writing to a log after the original process has exited. On macOS 15.6.1 the descendant shows parent pid 1 and its own session, and it keeps writing until it finishes on its own:
// worker: fork twice, let the intermediate processes exit
if (fork() > 0) _exit(0);
if (fork() > 0) _exit(0);
setsid(); // leaves the original process group and session
for (int i = 0; i < 8; i++) {
// append "tick i pid ppid" to the log, ppid is now 1
usleep(200000);
}
Build with cc -Wall -Wextra -Werror -o DescendantOutlivesParent main.c and run ./DescendantOutlivesParent /tmp/descendant-log /tmp/descendant-ready.
Questions:
- Is there a supported mechanism that ties every descendant of a process started by an XPC service, including ones that called
setsid(), to the lifetime of that service, so that they are terminated when the service exits or crashes? - If the service is sandboxed, does sandbox inheritance (children inherit the parent's sandbox) also give us a way to find or terminate those descendants, or does it only restrict what they can do?
- If neither, what does Apple recommend for running an untrusted command-line tool where leftover descendants must be ruled out? For example, is a separate launchd job per run, a different process-management API, or Endpoint Security authorization the intended route?
- Can that mechanism be combined with per-run file access (one input folder and one scratch folder per run), without giving the tool a shared container or network access?
We would rather learn that a guarantee is not available than build on an assumption, so a "this is not supported" answer is also useful to us.
Reply to Apple's case email (keep the subject) after the post is live:
Case-ID: 22893354
Hello,
Thank you. I posted the question on the Developer Forums here:
<FORUM POST URL>The sample project is attached to the post. There is nothing private in it.
Best regards,
<NAME>