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

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

  • 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_spawns /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.