New specs from /kiro-discovery (2026-09-28/29), added to roadmap.md: - job-status-detail-display: show pending reasons and reserved-vs-used resources in REST and xcecli. Requirements generated (7 requirements). - scheduler-alter-pend-reasons: keep a pending reason for every alternative resreq (per-alternative JobPendReason) and fix the obj->reason leak. - controller-job-cleanup: primary-only sweep that removes finished jobs past a configurable retention, via a replicated WAL removal fact. - application-profile: admin-defined application profiles (execution mode, launch type, env, resource defaults) chosen per job, with app:use/app:view RBAC. Queues and profiles stay orthogonal. - job-sandbox-execution / job-sandbox-multinode: run jobs in a systemd DynamicUser sandbox behind a pluggable execution backend. - rbac-persistence: make RBAC changes made through the API survive an apiserver restart (closes the security.md gap). roadmap.md also records the scheduler-sim-diagnostics info job update and a security item: remote-task units appear to run as root (found by code reading). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
9.1 KiB
Brief: job-sandbox-execution
Problem
A job whose execution mode is sandbox (from application-profile) must run on a
worker node:
- without any account belonging to the submitter;
- under an unprivileged, per-job, ephemeral uid;
- isolated from the host and from other jobs, including other jobs by the same submitter;
- with its input and output directories, or a temporary workspace, mounted in.
Today the worker can only run a job as a named account. There is no isolation, and a jailed process could not even get its output back to the user.
This is the worker half of the job-sandbox initiative. It covers single-node
execution, including single-node MPI in one unit. Multi-node is handled by
job-sandbox-multinode.
Current State
Code exploration, 2026-09-29.
Launch
xcecdmust run as root (worker/src/main.c:300-303).- In the production
DBUS_MANAGERbuild, each job starts as a systemd transient unitxcecd-worker-<jobid>via D-BusStartTransientUnit(xcecd_cgroups.cstart_systemd_job,xcecd_dbus_manager.c:73-76). - The unit runs
ExecStart=/bin/sh -c "<path>/xcecd_worker ..."withRemainAfterExit=1,StandardInput=null,KillMode=control-groupandUser=<execuser>(xcecd_cgroups.c:300-316). Optional cgroup limits come from exdata. - The identity switch happens only through
User=. There is nosetuid/initgroupsanywhere insrc/. - No sandboxing properties are set: no
DynamicUser,Protect*,PrivateTmp,NoNewPrivileges,WorkingDirectoryorBindPaths. The job runs in/.
Job helper
xcecd_workerreceives the command over/var/run/.xcecd.sock, which is mode 0666 (main.c:641).- It
uv_spawns/bin/sh -c cmd(uv_proc.c:270-288). exechome/execcwdare copied but never applied (xcecd_worker_utils.c:146-157).
Output
- Written to
/tmp/job_<id>_task<N>.out(job_output_path.h:24-41), createdO_EXCL|O_NOFOLLOW0600 by the helper. - Root
xcecdserves it throughhandle_fetchfile, which accepts only that exact path (bug-1508). See also bug-1868, the/tmpsymlink hazard.
Execution backends
- There is no abstraction; the launch code is tied to systemd, with a
#ifndef DBUS_MANAGERprocess-manager fallback. - The user wants to support systems without systemd in future, so the backend must be pluggable.
Known bug (fix first, separately)
- Remote-task units start with
limits == NULL, so noUser=is set and they run as root (controller_connector.c:1204,:1388;xcecd_cgroups.c:296-312).
Desired Outcome
-
The worker has a pluggable execution-backend interface:
direct, today's behaviour, refactored behind the interface;systemd-sandbox, new.
A backend without systemd (for example namespaces) can be added later without touching callers. The interface stays usable for future
container/vmmodes. -
A sandbox job runs in its transient unit under a systemd
DynamicUserephemeral uid:- The unit's user name is a new, short, per-job name (for example
xj<jobid>, 31 characters or fewer). It must never be the submitter's name. If a static account with that name existed, systemd would silently use it, and jobs from the same submitter would share a uid. ProtectSystem=strict,ProtectHome,PrivateTmpand an explicitNoNewPrivileges=yes.HOMEandWorkingDirectoryare set to the workspace.- One unit per job per node, so that MPI shared memory works within a node.
- The unit's user name is a new, short, per-job name (for example
-
Directories:
- Job-specified input directories are mounted read-only
(
BindReadOnlyPaths), and output directories read-write (BindPaths). - If none are specified, the worker creates a private temporary per-job workspace and mounts it.
- The workspace can't be handed to the uid in advance, because the uid does
not exist until the unit starts. The worker learns the uid when the helper
connects (
SO_PEERCRED) and hands the workspace to it. It must never be made world-writable.
- Job-specified input directories are mounted read-only
(
-
Output:
- stdout and stderr are opened by systemd outside the sandbox
(
StandardOutput=file:<host path>). Avoid PrivateTmp invisibility and ownership problems. - Existing output retrieval (
handle_fetchfile, thejobs outputAPI) serves sandbox job output, including files left in the workspace.
- stdout and stderr are opened by systemd outside the sandbox
(
-
Helper authentication:
xcecdidentifies a genuine job helper by its unit or cgroup, not by uid. A sandboxed job must not be able to pose as a helper over the 0666 socket. -
Mandatory cleanup after the job:
- Remove the workspace, or re-own it (policy decides).
- Normalise ownership of everything the job's uid wrote in any writable mount.
Dynamic uids are reused, and systemd does not check who owns files on disk. Leftovers would otherwise let a later job that gets the same uid read or change them.
-
Capability:
- The worker detects its sandbox capability (systemd 242 or later,
systemdinnsswitch.confpasswd/group, and D-Bus property support) and advertises it toapplication-profile's placement. - It refuses, with a clear error, a sandbox job it cannot honour.
- The worker detects its sandbox capability (systemd 242 or later,
-
Direct mode is unchanged.
Approach
Chosen: approach A, systemd sandbox properties on the existing transient unit, behind a pluggable backend interface (user decision, 2026-09-29).
Rejected alternatives:
- B, XCE-built namespaces. Amounts to writing a container runtime in C as root; a possible later backend.
- C, container runtime (Apptainer or OCI). Adds a dependency and image
management; a possible later
containermode.
Viability check (2026-09-29, systemd NEWS, systemd.exec(5), v239 sources):
-
All properties can be set on transient units on 239 and later.
-
BindPathshas D-Bus signaturea(ssbt), with flags 0 orMS_REC.ProtectSystem/ProtectHomeare strings. -
The minimum is systemd 242 (user decision). From 242,
RestrictSUIDSGIDandNoNewPrivilegesare implied, which closes CVE-2019-3843/3844-style takeover of a reused uid through SUID binaries. This excludes stock RHEL 8 (239). -
Uid capacity is about 4,336 per node, shared with other DynamicUser services.
pick_uidgives up after 100 tries. StaleRemainAfterExitunits hold their uid, so units must be stopped reliably. -
There is no
idmapoption for BindPaths or BindReadOnlyPaths. Ownership of mounted directories is XCE's problem (handover plus cleanup). -
DynamicUser is not a full sandbox:
- the host network is shared (MPI needs it);
/procstill shows other jobs (ProtectProcneeds 247+);- there is no syscall filter.
The requirements should decide whether to add
ProtectProcwhere available,PrivateDevices, or a baselineSystemCallFilter, and must document the remaining exposure. -
Tmpfs pages count against the job's
MemoryMax. RDMA needsLimitMEMLOCK=infinity.
Scope
- In:
- The backend interface, with
directrefactored behind it andsystemd-sandboxadded. - DynamicUser unit properties.
- The workspace lifecycle: create, hand over, mount, clean up or re-own.
- Input/output mounts.
StandardOutput=file:output and retrieval.- Helper authentication by unit or cgroup.
- Capability detection and advertisement.
- Single-node MPI in one unit.
- Tests: unit tests plus a live test on a systemd 242+ node.
- The backend interface, with
- Out:
- The submission side: mode field, policy, placement (
application-profile). - Multi-node sandbox launch (
job-sandbox-multinode). - Non-systemd backends and container/vm modes (the interface only, not the implementations).
- The remote-task root bug, which is fixed separately, first.
- The submission side: mode field, policy, placement (
Boundary Candidates
- The execution-backend interface and
directrefactor. - Building the systemd-sandbox unit (properties, naming, capability probe).
- The workspace and mount lifecycle (create, hand over via
SO_PEERCRED, clean up or normalise ownership). - Output capture and retrieval.
- Helper authentication.
Out of Boundary
- Job mode selection and policy.
- Cross-node uid consistency and remote launch.
- Worker cgroup resource limits, which are unchanged and simply apply to sandbox units too.
Upstream / Downstream
- Upstream:
application-profile: mode and directory fields onjob_startinfo_t, and capability-based placement.- The remote-task root fix.
- Downstream:
job-sandbox-multinode.- Future non-systemd or container backends.
Existing Spec Touchpoints
- Extends: none.
- Adjacent:
xce-node-manager(worker node privileges)worker-rejoin-resync(a sandbox job's state across a worker rejoin)- output retrieval (bug-1508 and bug-1868 history)
Constraints
- Nodes need systemd 242 or later, with
nss-systemdinnsswitch.conf. This is a documented deployment prerequisite, and capability detection must enforce it. - Direct-mode behaviour must be byte-for-byte unchanged (regression tests).
- Root code must fail closed. On any failure to set up the sandbox, the job is refused; it never falls back to running unsandboxed or as root.
- Cleanup is mandatory and must be retried and survive a worker crash or restart (for example by sweeping orphaned workspaces at startup).
- TDD:
testsuite/first. ASAN must be clean.