Service isolation, cgroups, and user managers

Analyze manager scope, resource controls and service sandbox settings.

14 min readChapter 109 of 168Intermediate → advanced
In this chapter

Before you start#

Review service state, credentials and capabilities, namespaces and cgroup v2, and system-call filtering. The source baseline is systemd v256 commit 5c79cdec10a547a866764a66e1e14898112a00cd; Linux v6.12 is a separate historical kernel teaching baseline. Neither identifies the host's installed version, enabled features or security posture.

Learning goals#

Trace how service execution parameters reach the executor, distinguish identity, filesystem, syscall and resource restrictions, and explain how inherited descriptors retain authority. Analyze a delegated cgroup and a user manager without assuming every controller or sandbox mechanism is available. A private configuration model is evidence of reasoning, not an actual sandbox test.

Source walkthrough#

Execution policy and inherited authority
  1. Manager

    Prepare execution context, parameters and cgroup settings.

  2. Executor

    Apply applicable credentials, namespaces, limits and filters.

  3. Application

    Executes with effective credentials and any inherited descriptors.

  4. Failure

    Setup errors return through executor or manager error paths.

Conceptual model. A conceptual v256 source map, not a captured sandbox; support and enforcement depend on configuration and kernel.

A systemd service definition can request execution settings and cgroup resource controls. The manager parses configuration into structures including an ExecContext for execution policy, ExecParameters for a particular invocation, and CGroupContext for resource settings. These are systemd userspace objects, not kernel security principals. A service's actual authority is determined by the credentials, descriptors, namespaces, kernel features, policies and other resources it has when it runs. The system manager and a per-user manager do not have the same privilege or control over cgroups.

The following exact excerpt from systemd v256 src/core/execute.c, exec_spawn, is LGPL-2.1-or-later under its SPDX notice. Assertions and subsequent spawn setup are omitted:

c
        r = exec_context_load_environment(unit, context, &params->files_env);
        if (r < 0)
                return log_unit_error_errno(unit, r, "Failed to load environment files: %m");

The unit and execution context identify the configuration, while params->files_env receives the environment-file result for this invocation. An error is logged and returned before the subsequent spawn steps proceed. That failure is not a service program returning an error after it started: the application may never have executed. Environment files can contain sensitive values and have their own permission and provenance concerns; this chapter does not read host service environments. The surrounding function owns temporary state such as an FDSet, files and allocated strings through cleanup annotations, and passes appropriate data toward the executor under the selected path.

The following exact excerpt from v256 src/core/exec-invoke.c, helper context_has_no_new_privileges, is also LGPL-2.1-or-later. The surrounding function signature and subsequent conditional are omitted:

c
        if (c->no_new_privileges)
                return true;

c is the execution context; an explicitly requested no-new-privileges setting makes this helper report that it is needed. The remainder of the helper also accounts for applicable seccomp use when built with that support. It is a decision in one execution path, not a complete sandbox guarantee. If a later privilege, namespace, resource or syscall-filter setup operation fails, the executor has its own error and exit-status path; a manager must not treat the service as successfully started merely because parsing the unit succeeded.

Manager, executor and conditional ordering#

In this version exec_spawn prepares invocation state, environment and descriptors and starts the execution machinery. The separate systemd executor calls exec_invoke with the command, contexts, runtime and exit-status output. It carries out the applicable setup before executing the requested program. Exact steps and order depend on configuration, build options, architecture, privilege and kernel support; there is no universal sequence in which every directive is applied for every service. The source includes conditional namespace, credential, capability, resource-limit, seccomp and security-module handling.

The child can fail before its executable runs. For example, loading an environment file can fail in the manager-side preparation; a mount namespace, user identity, resource limit or filter setup can fail later in the executor. The manager observes an execution failure through its process and service state machinery and may clean up resources or apply restart policy. Some external work already performed by earlier commands is not automatically rolled back. Temporary descriptors, allocated vectors and process references have cleanup paths, but those paths do not promise that all arbitrary service side effects are reversible.

A namespace gives a process a scoped view of some resource; it is not a virtual machine. A mount namespace can present a restricted filesystem view, but the process still shares a kernel with the host. A user namespace changes credential interpretation within a particular scope, with its own mapping and policy limitations. A service may also hold file descriptors opened before a namespace change or received over a socket. A path becoming inaccessible by name does not automatically revoke access through an already open descriptor. The service designer must account for descriptors passed by socket activation, standard input/output, inherited files and any IPC channel.

In src/core/exec-invoke.c, exec_invoke, v256, LGPL-2.1-or-later under its SPDX notice (earlier setup and the descriptor-packing helper omitted):

c
        /* We repeat the fd closing here, to make sure that nothing is leaked from the PAM modules. Note that
         * we are more aggressive this time, since we don't need socket_fd and the netns and ipcns fds any
         * more. We do keep exec_fd and handoff_timestamp_fd however, if we have it, since we need to keep
         * them open until the final execve(). But first, close the remaining sockets in the context
         * objects. */

        exec_runtime_close(runtime);
        exec_params_close(params);

        r = close_all_fds(keep_fds, n_keep_fds);
        if (r >= 0)
                r = pack_fds(params->fds, n_fds);
        if (r >= 0)
                r = flag_fds(params->fds, n_socket_fds, n_fds, context->non_blocking);
        if (r < 0) {
                *exit_status = EXIT_FDS;
                return log_exec_error_errno(context, params, r, "Failed to adjust passed file descriptors: %m");
        }

Before execve, the executor closes every descriptor that is not on its keep list, then packs and flags the descriptors the program is meant to receive. The comment says the close is repeated after PAM module setup so that nothing those modules opened leaks. The failure path sets EXIT_FDS. The excerpt shows which descriptors the executor keeps; it does not show what an already passed descriptor can do after a path or namespace change.

Filesystem and identity controls#

PrivateTmp= gives a service a private view of temporary directories under the documented conditions; it is not a guarantee that every writable location is private. ProtectSystem= can make selected filesystem hierarchy portions read-only in the service's mount view. ReadWritePaths= can grant specified paths writable access within that arrangement, subject to the underlying mount and permissions. A read-only mount view does not necessarily prevent communication with an already accessible Unix socket or a service that performs writes on behalf of a client. ProtectHome= changes access to home-related paths according to its configured mode and platform support. Review the v256 execution manual for the exact values and caveats.

DynamicUser= requests a dynamically allocated service identity under systemd's policy. StateDirectory= and related directory options can arrange paths and ownership for service state, but the actual host filesystem, mount setup, ownership mapping and service privileges determine access. Do not treat a numeric UID as a permanent identity after a dynamic allocation has ended and a number might be reused. A persistent directory and its files have lifetimes beyond one process; ownership, cleanup and upgrade policy must be planned deliberately. User manager features and availability can differ from the system manager's capabilities.

NoNewPrivileges= restricts gaining additional privilege through subsequent execution under Linux's no-new-privileges mechanism. It does not close existing file descriptors, revoke an already mapped object, erase existing capabilities on its own, or prevent a peer from exercising its own authority. Capability bounding, ambient capabilities, UID/GID changes and security-module policy are separate controls with their own ordering and limitations. A syscall filter can deny selected system calls, but it must account for the service's real ABI, architecture, runtime and required error handling. A broad syscall allowlist is not a proof that the service cannot cause harm through permitted interfaces.

In the same src/core/exec-invoke.c, exec_invoke, after the secure-bits setup and before the address-family and seccomp steps (other lines omitted):

c
                if (context_has_no_new_privileges(context))
                        if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) {
                                *exit_status = EXIT_NO_NEW_PRIVILEGES;
                                return log_exec_error_errno(context, params, errno, "Failed to disable new privileges: %m");
                        }

The order is part of the source: PR_SET_NO_NEW_PRIVS is set in this branch before apply_address_families runs. The excerpt does not show whether the seccomp filter installs successfully. A failure here is reported with EXIT_NO_NEW_PRIVILEGES and is not a successful start.

RestrictAddressFamilies= constrains selected socket address families on applicable paths; it does not necessarily invalidate a socket inherited before the restriction. SystemCallFilter= configures a seccomp-based filter when available and applicable. The v256 source includes a skip_seccomp_unavailable path, so support and privilege must be checked rather than assumed. A directive may be unsupported or ineffective in some container or user-manager configurations, fail at setup, or have documented exceptions. A successful unit parse alone cannot prove any particular kernel restriction was installed.

A worked authority example#

Suppose a synthetic service is configured with a read-only system view, a private temporary directory, no-new-privileges and a syscall filter. Before the program begins, the manager passes descriptor 3 for a writable file opened elsewhere. At T0 the service cannot open the file's path for writing through its restricted mount view under our assumptions. At T1 it calls write(3, ...); whether that succeeds depends on the descriptor's access mode, underlying object and other policy, but the path restriction alone does not revoke the already open reference. At T2 the service passes that descriptor to another cooperating process through a permitted IPC channel. The receiver obtains its own descriptor reference and must close it independently. This example illustrates possible retained authority, not a universal bypass promise or a measured systemd run.

Restrictions and the authority they touch
Restrictions and the authority they touchProtectSystem=: Changes the mount view that path lookups see. → New path lookup: Resolved in the service mount view. (restricts names). NoNewPrivileges=: Stops later privilege gains through execution. → Later setuid or file-capability gain: Limited on execve. (limits gains). SystemCallFilter=: Denies selected syscalls when installed and supported. → Later syscall: Subject to the filter result. (filters calls). Inherited fd 3: Refers to an open file object that already exists. → I/O on fd 3: Depends on access mode and object policy. (not revoked by path rules alone)restricts nameslimits gainsfilters callsnot revoked by pathrules aloneProtectSystem=Changes the mount view thatpath lookups see.NoNewPrivileges=Stops later privilege gainsthrough execution.SystemCallFilter=Denies selected syscallswhen installed andsupported.Inherited fd 3Refers to an open fileobject that already exists.New path lookupResolved in the servicemount view.Later setuid or file-capability gainLimited on execve.Later syscallSubject to the filterresult.I/O on fd 3Depends on access mode andobject policy.
ProtectSystem=Changes the mount view that path lookups see.
New path lookupResolved in the service mount view.
NoNewPrivileges=Stops later privilege gains through execution.
Later setuid or file-capability gainLimited on execve.
SystemCallFilter=Denies selected syscalls when installed and supported.
Later syscallSubject to the filter result.
Inherited fd 3Refers to an open file object that already exists.
I/O on fd 3Depends on access mode and object policy.
Read the text version
  • ProtectSystem=: Changes the mount view that path lookups see. → New path lookup: Resolved in the service mount view. (restricts names)
  • NoNewPrivileges=: Stops later privilege gains through execution. → Later setuid or file-capability gain: Limited on execve. (limits gains)
  • SystemCallFilter=: Denies selected syscalls when installed and supported. → Later syscall: Subject to the filter result. (filters calls)
  • Inherited fd 3: Refers to an open file object that already exists. → I/O on fd 3: Depends on access mode and object policy. (not revoked by path rules alone)
Conceptual model. A synthetic configuration model, not a sandbox test; each directive has its own scope, support requirements and exceptions.

A second case concerns a cgroup. Imagine a delegated subtree where the parent has not made a desired controller available. A child manager requests a resource limit that requires that controller. The request may fail, be unsupported or not achieve the intended enforcement depending on the interface and environment; it is incorrect to infer enforcement from a unit file alone. Check the controller availability, effective cgroup hierarchy, actual errors and permission boundary in a disposable environment. The model requires no host cgroup changes.

Resource limits and the execution boundary#

A resource limit and a cgroup controller constrain different scopes. A per-process limit such as an open-file limit is inherited and interpreted through the relevant process and credential rules; a cgroup memory limit applies to a hierarchy under the controller's accounting and enforcement rules. A process can hit an FD limit while the cgroup has spare memory, or a group can hit a memory limit while a particular process remains below its own per-process limits. A unit's configured value, the value the manager attempted to apply and the effective kernel state are three distinct observations. Permission failures and parent-imposed bounds can prevent the result a reader expected.

Three observations of one limit
Configured
The unit file states the request, such as a memory or file-descriptor limit.
Manager attempt
The manager tries to apply it and may log or report a failure.
Effective state
The kernel hierarchy, controller availability and permissions decide what is enforced.
Offline check
Unit verification checks syntax and files, not kernel enforcement.
Conceptual model. A teaching model of three separate observations, not a captured cgroup state; enforcement needs a check on a disposable host.

A useful execution timeline is manager preparation, executor setup, application exec, application work and teardown. At each point record the owner of the error and the process that can still report it. An environment-file error in the quoted manager path occurs before executor setup. A failure to establish an execution namespace can be reported by the executor's status path. A successful exec can still be followed by an application-level failure opening its data directory. These events have different causes and should not all be labeled “the sandbox denied it” without evidence.

Suppose a service receives a listening descriptor from socket activation. If RestrictAddressFamilies= prevents creating a new socket in an address family, that does not retroactively change the fact that a descriptor was already passed to the service. Whether using the descriptor is permitted depends on the operation and other policy. A review must inventory preopened descriptors, filesystem access, IPC peers and capabilities in addition to listing directives. This is a threat-model exercise; the result is not a promise that a particular bypass will work on an untested system.

cgroup v2 and user managers#

A cgroup organizes processes in a hierarchy and supports resource accounting and controls when the relevant controller is available and enabled. A parent controls which controllers can be delegated to descendants under kernel and manager rules. Resource settings for CPU, memory or I/O operate through the cgroup interfaces and have their own units, hierarchy and error behavior. A service's cgroup can retain processes after its main PID exits; process tracking and cleanup are therefore broader than waiting for one PID. Delegation permits a suitable subordinate manager to manage an allowed subtree; it is not ownership of the host root cgroup or a way to enable unavailable controllers.

A per-user manager generally runs for a particular UID and manages user units under that user's authority. user@UID.service, user bus availability, default target and login session lifetime depend on distribution configuration and PAM/logind integration. Lingering can keep a user manager available beyond an interactive login when configured, but it is not the same as granting root privileges. A user manager may lack permission to create namespaces, change certain credentials, delegate controllers or access devices. A service that works in a system-manager sandbox can fail or have different effective restrictions as a user unit.

When investigating, separate configured intent, manager setup result and kernel-enforced state. A hypothetical ProtectSystem=strict line is a request; a successful offline verifier only checks selected syntax and files. To claim that a process is actually constrained, a prepared disposable environment and carefully scoped observation would be needed. The old chapter 08 VM has not been boot verified, so this chapter does not claim such a result.

Practice#

This model uses a private file and a descriptor opened by the same ordinary Python process. It demonstrates descriptor lifetime, not a mount namespace or systemd sandbox. No host service or cgroup is modified:

sh
python3 - <<'PY'
import os, tempfile
with tempfile.TemporaryDirectory() as directory:
    path = os.path.join(directory, 'data')
    with open(path, 'w+') as handle:
        handle.write('first')
        handle.flush()
        os.unlink(path)
        handle.write('-still-open')
        handle.seek(0)
        print('open descriptor:', handle.read())
        print('path exists:', os.path.exists(path))
PY

Expected output includes first-still-open and path exists: False on an ordinary Unix filesystem. Removing a pathname differs from a read-only mount, but the example shows why a live descriptor and a pathname have separate lifetimes. If unlink is unavailable or denied, record the error and use the T0–T2 source-only case. The temporary directory is removed automatically; no service, security setting or host unit is changed.

As a source-only alternative, draw the manager, executor, ExecContext, descriptor 3 and target file. Mark the environment-file error before spawn, then explain why no-new-privileges and a path restriction do not automatically close descriptor 3. Add the unavailable-controller cgroup case and identify which claim still requires an actual disposable-guest observation. Deliver the diagram and failure table.

Knowledge check#

  1. What happens when exec_context_load_environment returns a negative result in the quoted path?
  2. Why can a read-only path view and no-new-privileges leave authority through an existing descriptor?
  3. What does a delegated cgroup subtree not guarantee about its parent controllers?
  4. Why does a successful private unit parse not prove that seccomp or a mount namespace was installed?
  5. How does a per-user manager differ from system PID 1 in authority?

Answers#

  1. exec_spawn logs and returns the error before the subsequent spawn setup. The configured service executable may never have run, so this must be distinguished from an application exit status.
  2. A descriptor references an already opened kernel object independently of a later pathname lookup. No-new-privileges restricts future privilege gain through execution; it does not itself close descriptors or revoke all existing authority.
  3. Delegation is bounded by the hierarchy and permissions. A controller not available or enabled from the parent cannot be assumed usable merely because a child unit requests a limit.
  4. Parsing checks configuration under the verifier's environment; applying namespaces, filters and credentials occurs later and depends on kernel support, privileges and execution results. Enforcement needs separate evidence.
  5. The user manager normally runs under a particular user's authority and manages a constrained user scope. System PID 1 has different responsibilities and potentially greater authority; a user manager is not automatically privileged by being a service manager.

Checkpoint#

Submit the execution-context and inherited-descriptor diagram, the cgroup availability counterexample, and either the private file output or the source-only explanation. Ready means distinguishing configuration from enforcement, identifying the early environment-file failure and explaining why a descriptor reference survives pathname removal. Retry with only the descriptor case before adding namespace and controller assumptions.

Sources#

A little further beneath the surface.

Check your understanding with the checkpoint. Marking a chapter as read saves a bookmark, not a proficiency assessment.