feat: support running Sessions as a jobRunAsUser on macOS#335
feat: support running Sessions as a jobRunAsUser on macOS#335andychoquette wants to merge 3 commits into
Conversation
51dbf6e to
a3128e6
Compare
openjd-sessions could not run a Session's actions as a separate user
(jobRunAsUser) on macOS. Three problems in the POSIX cross-user path:
- setsid(1) does not exist on macOS, so the cross-user command
'sudo -u <user> -i setsid -w <cmd>' failed at spawn (exit 127), breaking all
jobRunAsUser execution on macOS.
- find_child_process_id_pgrep treated pgrep's exit code 1 (no match yet) as a
fatal error, which broke out of the caller's retry loop. Since Linux uses
procfs and only other POSIX platforms use pgrep, signal-target discovery never
actually retried off Linux.
- There was no is_macos() helper (the MACOS constant was unused).
Changes:
- _os_checker.py: add is_macos().
- _subprocess.py: on darwin, launch the workload via a pure-Python setsid shim
('sudo -u <user> -i /usr/bin/python3 -I -c <shim> <cmd>') that makes the
workload a new session/process-group leader before exec, reproducing the
new-session behavior 'setsid -w' provides on Linux. Runs under /usr/bin/python3
(so the job user needs no venv access) with -I (isolated mode, so the working
directory is not on sys.path).
- _linux/_sudo.py: treat pgrep exit code 1 as "no match yet -> return None" so
the retry loop polls as intended; exit >1 is still fatal. Fixes discovery and
cancellation on all non-Linux POSIX hosts.
- Tests for is_macos(), the pgrep exit-code handling, and the macOS command
construction (plus a POSIX check that the shim creates a new process group).
- README: document the macOS Command Line Tools prerequisite for impersonation.
Linux and Windows behavior is unchanged.
Validated end-to-end on macOS 26.5 (arm64) via the AWS Deadline Cloud worker
agent against a live customer-managed fleet: running an action as a jobRunAsUser
and cancellation reaping the workload's process group with no orphans.
Signed-off-by: Andy Choquette <apcho@amazon.com>
a3128e6 to
fcfd47d
Compare
The impersonation tests already exist but xfail everywhere the OPENJD_TEST_SUDO_* environment variables are unset; on Linux they run inside a purpose-built Docker container, and nothing runs them on macOS. macOS runners have passwordless sudo, so this workflow provisions the same user/group layout with Directory Services (sysadminctl/dseditgroup) and runs the existing tests for real, covering the macOS setsid-shim launch, signalling, and process-tree termination paths end to end. A guard step fails the job if the tests regress to xfail (e.g. the provisioning breaks), instead of silently passing an empty run. Signed-off-by: Andy Choquette <apcho@amazon.com>
leongdl
left a comment
There was a problem hiding this comment.
Please also port this to OpenJD-RS.
We're in the middle of migrating from Python to the Rust implementation.
| _MACOS_SETSID_SHIM = ( | ||
| "import os,sys;os.getpgrp()==os.getpid() or os.setsid();os.execvp(sys.argv[1],sys.argv[1:])" | ||
| ) |
There was a problem hiding this comment.
Flagging this line, but really I'd like to learn more in general process management in macOS and what research we've done to know this is the correct solution.
From what I understand, macOS makes sids/pgids pretty hidden. Like you've discovered they don't even provide a utility to assign those values even though there's a syscall. Even when inspecting processes they don't really expose these values via ps -o sess= though a getsid syscall may produce the session id?
Basically, I want to know we are using the right tools provided to us for the OS to properly signal, inspect, manage, isolate, and terminate processes.
There was a problem hiding this comment.
haven't forgotten about this comment. doing some additional research this week to answer whether these are the right os-provided tools.
Will do - I opened issue OpenJobDescription/openjd-rs#263 to track the work |
What was the problem/requirement? (What/Why)
openjd-sessionscould not run a Session's actions as a separate user (a"jobRunAsUser") on macOS. Three problems in the POSIX cross-user path:
setsidis missing on macOS. The cross-user command issudo -u <user> -i setsid -w <cmd>, butsetsid(1)is a Linux/util-linuxtool that does not exist on macOS. Every impersonated action failed at spawn
with
command not found(exit 127), so running actions as a jobRunAsUser wascompletely broken on macOS.
find_child_process_id_pgrepnever retried. On non-Linux POSIX hosts,signal-target discovery uses
pgrep -P <sudo_pid>.pgrepexits1when itfinds no match yet (the child hasn't spawned), but the code treated any
non-zero exit as a fatal
FindSignalTargetError, which broke out of thecaller's retry loop. Because Linux uses procfs and only other POSIX platforms
use
pgrep, this latent bug meant signal-target discovery never actuallyretried off Linux.
No
is_macos()helper. TheMACOS = "darwin"constant existed in_os_checker.pybut was unused, so there was no way to branch on macOS.What was the solution? (How)
Add
is_macos()to_os_checker.py.Reproduce
setsidbehavior on macOS with a pure-Python shim. On darwin,the workload is launched via
sudo -u <user> -i /usr/bin/python3 -I -c '<shim>' <cmd>, where the shimmakes the process a new session/process-group leader and then
execs thereal command:
os.getpgrp() == os.getpid() or os.setsid()callssetsid()only when theprocess is not already a group leader, avoiding the
EPERMthatos.setsid()raises for an existing group leader.
sudo -iargv without shell-quotingfragility.
/usr/bin/python3(the OS interpreter) rather thansys.executable, so thejobRunAsUser can execute it without traverse/read permission on the agent's
virtual environment.
-I(isolated mode) so the session working directory is not onsys.path,preventing a file such as
os.pyin that directory from being imported aheadof the standard library before
os.execvp()runs.This produces the same topology
setsid -wgives on Linux — the workload issudo's direct child in a process group distinct fromsudo's — so theexisting
find_sudo_child_process_group_iddiscovery works unchanged.Treat
pgrepexit code1as "no match yet" (returnNoneso the callerretries) rather than fatal. Exit
>1is still a genuine error. This fixes thediscovery/cancellation path on all non-Linux POSIX hosts, not just macOS.
The Linux and Windows code paths are unchanged.
What is the impact of this change?
macOS hosts can now run Session actions as a jobRunAsUser via
sudo, matchingthe existing behavior on Linux and Windows. On Linux and Windows there is no
behavioral change. The
pgrepfix additionally repairs signal-target discovery(and therefore process cancellation/cleanup) for any non-Linux POSIX platform.
macOS prerequisite: because the shim runs via
/usr/bin/python3(the CommandLine Tools interpreter), impersonated Sessions on macOS require the Xcode Command
Line Tools (
xcode-select --install). This is documented in the README.How was this change tested?
is_macos()(test_os_checker.py); thepgrepexit-codehandling — exit 1 ->
None/retry, exit >1 -> raise, single/multiple/emptymatches (
test_sudo.py, new); the macOS cross-user command construction and aPOSIX check that the shim creates a new process group (
test_subprocess.py).test/openjd/sessions_v0unit suite: 550 passed, 33 skipped,16 xfailed, no failures.
agent against a live customer-managed fleet: install -> worker registration ->
running an action as a jobRunAsUser -> cancellation reaping the workload's
process group with no orphaned processes.
Was this change documented?
(Command Line Tools requirement) and expanded the code comments in
_subprocess.pyexplaining the shim, the-Iisolation, and the discoveryassumption. The
is_macos()/pgrepbehavior is covered by the new tests.Is this a breaking change?
No. No public interface changes; Linux and Windows behavior is unchanged.
Does this change impact security?
This touches the cross-user (jobRunAsUser) impersonation path, which is a
security boundary. The shim is constructed as an argv list (no shell, not
injectable), uses a fixed absolute interpreter path, and
-Iprevents importingattacker-placed modules from the working directory before exec. All work after
sudo -u <user>runs as the target job user (same as the existing Linux path),so control of the command only ever lets the job run as itself. Flagging with the
"security" label so maintainers can review the boundary; happy to work through a
threat-model discussion.
Cross-port to openjd-rs
openjd-rsto port this change (link here):By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.