myxce/.kiro/specs/job-sandbox-execution/brief.md
Rongsong Shen 7fe9b22381
docs(spec): discovery briefs for job status detail, job cleanup, sandbox execution and RBAC persistence
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>
2026-09-29 08:57:31 +00:00

202 lines
9.1 KiB
Markdown

# 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**
- `xcecd` must run as root (`worker/src/main.c:300-303`).
- In the production `DBUS_MANAGER` build, each job starts as a systemd
**transient unit** `xcecd-worker-<jobid>` via D-Bus `StartTransientUnit`
(`xcecd_cgroups.c` `start_systemd_job`, `xcecd_dbus_manager.c:73-76`).
- The unit runs `ExecStart=/bin/sh -c "<path>/xcecd_worker ..."` with
`RemainAfterExit=1`, `StandardInput=null`, `KillMode=control-group` and
`User=<execuser>` (`xcecd_cgroups.c:300-316`). Optional cgroup limits come from
exdata.
- The identity switch happens only through `User=`. There is no
`setuid`/`initgroups` anywhere in `src/`.
- No sandboxing properties are set: no `DynamicUser`, `Protect*`, `PrivateTmp`,
`NoNewPrivileges`, `WorkingDirectory` or `BindPaths`. The job runs in `/`.
**Job helper**
- `xcecd_worker` receives the command over `/var/run/.xcecd.sock`, which is mode
**0666** (`main.c:641`).
- It `uv_spawn`s `/bin/sh -c cmd` (`uv_proc.c:270-288`).
- `exechome`/`execcwd` are 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`), created
`O_EXCL|O_NOFOLLOW` 0600 by the helper.
- Root `xcecd` serves it through `handle_fetchfile`, which accepts only that exact
path (bug-1508). See also bug-1868, the `/tmp` symlink hazard.
**Execution backends**
- There is no abstraction; the launch code is tied to systemd, with a
`#ifndef DBUS_MANAGER` process-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 no `User=` 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`/`vm`
modes.
- A sandbox job runs in its transient unit under a **systemd `DynamicUser`
ephemeral 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`, `PrivateTmp` and an explicit
`NoNewPrivileges=yes`.
- `HOME` and `WorkingDirectory` are set to the workspace.
- One unit per job per node, so that MPI shared memory works within a node.
- **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.
- **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`, the `jobs output` API) serves
sandbox job output, including files left in the workspace.
- **Helper authentication**: `xcecd` identifies 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, `systemd`
in `nsswitch.conf` passwd/group, and D-Bus property support) and advertises
it to `application-profile`'s placement.
- It refuses, with a clear error, a sandbox job it cannot honour.
- **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 `container` mode.
Viability check (2026-09-29, systemd NEWS, systemd.exec(5), v239 sources):
- All properties can be set on transient units on 239 and later.
- `BindPaths` has D-Bus signature `a(ssbt)`, with flags 0 or `MS_REC`.
`ProtectSystem`/`ProtectHome` are strings.
- **The minimum is systemd 242** (user decision). From 242, `RestrictSUIDSGID`
and `NoNewPrivileges` are 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_uid` gives up after 100 tries. Stale `RemainAfterExit` units hold their
uid, so units must be stopped reliably.
- There is no `idmap` option 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);
- `/proc` still shows other jobs (`ProtectProc` needs 247+);
- there is no syscall filter.
The requirements should decide whether to add `ProtectProc` where available,
`PrivateDevices`, or a baseline `SystemCallFilter`, and must document the
remaining exposure.
- Tmpfs pages count against the job's `MemoryMax`. RDMA needs
`LimitMEMLOCK=infinity`.
## Scope
- **In**:
- The backend interface, with `direct` refactored behind it and
`systemd-sandbox` added.
- 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.
- **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.
## Boundary Candidates
- The execution-backend interface and `direct` refactor.
- 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 on `job_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-systemd` in `nsswitch.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.