Namespace lifecycles and the cgroup v2 model

Compare namespace-scoped views with cgroup v2 resource organization.

14 min readChapter 78 of 168Advanced
In this chapter

Before you start#

Review task creation, mount namespaces, network namespaces, and credentials and user namespaces. All source paths are pinned to upstream v6.12. A namespace changes selected views and ownership relationships; it does not create a separate kernel or a complete security boundary.

Learning goals#

Identify namespace families and their reference lifetimes, trace a failed namespace construction and cgroup migration, and distinguish a cgroup namespace's view from actual controller membership and resource limits.

Source walkthrough#

Namespace lifecycles and the cgroup v2 model
  1. Namespace selection

    Per-family views and user-namespace authority

  2. Reference construction

    nsproxy obtains references and unwinds failures

  3. Cgroup migration

    Permissions and controller callbacks precede commit

  4. Membership and view

    CSS sets represent membership; cgroup namespace changes path view

Conceptual model. Source-based conceptual overview, not captured execution; actual architecture, configuration and policy determine behavior.

A namespace groups a particular category of kernel resources or identifiers into a view. Linux has UTS namespaces for hostname and domain name, IPC namespaces for selected System V IPC and POSIX message-queue resources, PID namespaces for process-ID visibility, mount namespaces for mount trees, network namespaces for network stacks and devices, user namespaces for identity mappings and scoped capabilities, cgroup namespaces for cgroup path views, and time namespaces for selected clock offsets (namespace overview; kernel cgroup v2 documentation). These are separate mechanisms: a new mount namespace does not automatically create a new network namespace. All share the same running kernel, whose configuration, bugs and global resources remain relevant.

Many namespace references for a task are collected in struct nsproxy: its fields include mount, UTS, IPC, network, cgroup, time and PID-for-children namespace pointers where configured. User namespace membership is associated with credentials; PID membership and the PID namespace used for future children require their own distinctions. ns_common provides common namespace identity and operations used by nsfs. Namespace file descriptors under /proc/PID/ns/ can pin relevant namespace objects and serve as handles for supported setns operations. Holding such a descriptor can extend a namespace's lifetime after a task exits; it does not automatically confer permission to enter it.

Creation and error unwind

clone, unshare and setns have different effects and flag restrictions. They check credentials, namespace ownership and operation-specific conditions; creating or entering a namespace is not simply copying a pointer. In kernel/nsproxy.c, create_new_namespaces allocates a new nsproxy, obtains or creates selected namespace references in order, and returns it to its caller for attachment under proper locking. This exact v6.12 GPL-2.0-only excerpt, copyright IBM and contributors, shows the network step; earlier and later code are omitted:

c
	new_nsp->net_ns = copy_net_ns(flags, user_ns, tsk->nsproxy->net_ns);
	if (IS_ERR(new_nsp->net_ns)) {
		err = PTR_ERR(new_nsp->net_ns);
		goto out_net;
	}

The first statement obtains the network namespace according to the requested flags and target user namespace. On an encoded error, the branch saves the error and jumps to cleanup; it does not attach a partially initialized nsproxy. The out_net and subsequent labels release earlier cgroup, PID, IPC, UTS and mount namespace references as applicable, then free the nsproxy. A later time-namespace failure first releases the already obtained network namespace as well. On success the caller receives a reference-bearing nsproxy and is responsible for attachment and later release. Namespace creation can fail for allocation, capability, configuration or resource-limit reasons; an unsuccessful operation is not proof that namespaces are conceptually unavailable on the host.

A namespace fd refers to a particular namespace object, not an independent copy of its configuration. Entering it with setns is subject to target and caller user-namespace capability checks and other restrictions; some namespace types impose additional rules. Sharing a user namespace with a target is not a general right to join all of its namespaces. A namespace's user namespace controls important authority checks, but DAC, LSMs, other namespaces and resource limits remain separate. Namespace lifetime is reference-based and type-specific: a bind mount of an nsfs entry, an open fd or a task can retain a reference even after the creator exits.

When clone or unshare asks for a cgroup namespace, copy_cgroup_ns first decides whether a new object is needed. In that file, v6.12, SPDX GPL-2.0 (surrounding code omitted):

c
	if (!(flags & CLONE_NEWCGROUP)) {
		get_cgroup_ns(old_ns);
		return old_ns;
	}

	/* Allow only sysadmin to create cgroup namespace. */
	if (!ns_capable(user_ns, CAP_SYS_ADMIN))
		return ERR_PTR(-EPERM);

Without CLONE_NEWCGROUP, the code only takes a reference to the existing namespace. With it, the CAP_SYS_ADMIN check runs before the per-user count is charged and the object is allocated, so a denied request creates nothing. The excerpt does not show the css_set that the new namespace records as its root, and it does not move any task.

Joining an existing cgroup namespace with setns takes a different path through cgroupns_install, in the same v6.12 GPL-2.0 file (surrounding code omitted):

c
	if (!ns_capable(nsset->cred->user_ns, CAP_SYS_ADMIN) ||
	    !ns_capable(cgroup_ns->user_ns, CAP_SYS_ADMIN))
		return -EPERM;

	/* Don't need to do anything if we are attaching to our own cgroupns. */
	if (cgroup_ns == nsproxy->cgroup_ns)
		return 0;

	get_cgroup_ns(cgroup_ns);
	put_cgroup_ns(nsproxy->cgroup_ns);
	nsproxy->cgroup_ns = cgroup_ns;

The install path requires CAP_SYS_ADMIN in the caller's credential user namespace and in the user namespace that owns the target cgroup namespace. A caller already in that namespace returns early; otherwise the reference is swapped. The excerpt does not show the surrounding setns checks or locking, so it is not a complete permission rule.

PID, user and mount details#

A new PID namespace gives processes different PID numbers at different nesting levels. The first process created in that namespace is PID 1 there and has special responsibility for reaping orphaned children. If that namespace's init exits, the kernel handles the remaining tasks according to PID namespace lifecycle rules; one cannot treat it as an ordinary interchangeable worker. A task can see PID numbers in ancestor contexts while descendants have more limited visibility. A procfs mount exposes processes according to its associated PID namespace, so simply entering a PID namespace for future children does not automatically make an existing procfs mount show the desired view. The read-only lab below does not mount procfs.

A user namespace maps ranges of its UIDs and GIDs to parent-namespace IDs. /proc/PID/uid_map and gid_map show mappings subject to visibility and permission rules. Writing maps has specific one-time, privilege and ordering constraints; unprivileged group mapping commonly requires handling the setgroups control first. A numeric zero in the child namespace can map to an ordinary parent UID, so it is not host root. Identity mapping is distinct from ownership of a network, mount or cgroup namespace and from whether the caller has capability over that resource's governing user namespace.

Mount propagation determines whether mount events travel among related mount points. A cloned mount namespace begins from a copy of mount relationships, not necessarily an isolated private tree: shared propagation can carry later mount events across namespaces. A container setup must consider propagation explicitly in a safe environment. In the synthetic example below, no mounts are created or altered. A new namespace object alone is not proof that the existing propagation topology prevents all mount-event sharing.

Cgroup v2 objects and membership#

Cgroup v2 organizes tasks in a unified hierarchy, exposed through cgroupfs and backed by kernfs nodes. A struct cgroup represents a node; each enabled controller can have a cgroup_subsys_state (CSS) for the relevant group. A css_set connects a task's membership to the applicable controller states and cgroup association. References, RCU and locking allow readers and migrations to coexist; a pathname printed in /proc/PID/cgroup is a view of membership, not a pointer whose lifetime userspace controls. The cgroup core implements these objects and the v2 documentation defines the interface.

A parent exposes available controllers in cgroup.controllers; enabling one in cgroup.subtree_control makes it available to eligible children and is subject to hierarchy rules and permissions. Resource distribution is hierarchical: a child's settings cannot grant resources beyond constraints imposed by ancestors. The no-internal-process rule for domain controllers prevents certain non-root domain cgroups from both hosting processes and distributing those domain resources to children. It has root and threaded-subtree qualifications; saying “no cgroup can ever have both tasks and children” is incorrect. Threaded mode allows supported threaded controllers and thread-level placement under its rules, while domain controllers retain their domain semantics.

cgroup.procs moves a process as a thread group under the documented conditions; cgroup.threads supports thread-level placement in the appropriate threaded subtree. Migration also requires the relevant permissions at source and destination and any delegation containment checks. Delegation grants a subtree's manager controlled authority over that subtree, not authority to change ancestor limits or move arbitrary host tasks. A cgroup namespace changes how cgroup paths are presented relative to its namespace root. It does not itself move tasks, enable controllers or impose CPU and memory limits.

Migration validation and commit

In cgroup_migrate_execute, controller callbacks validate a prepared task set before the commit. This exact excerpt is from v6.12; the file's copyright and GPL notice apply, and surrounding loop and cancellation code are omitted:

c
				ret = ss->can_attach(tset);
				if (ret) {
					failed_ssid = ssid;
					goto out_cancel_attach;
				}

A controller's can_attach examines the task set and can reject it. The failure records which controller rejected and goes to a cancellation path so earlier controllers with cancel_attach can undo their preparation. It does not proceed to move the task set. After all applicable checks succeed, the source marks a commit point and holds css_set_lock with interrupts disabled while moving tasks between CSS sets and updating references and task counts. Afterwards it invokes controller attach callbacks. Preparation, permission checks and destination setup occur in surrounding paths; the commit-point statement does not mean every userspace migration request must succeed.

Suppose a task in group A is to move to group B. Permission checks and the destination's validity must succeed, and each relevant controller must accept the transition. If one can_attach rejects, earlier preparations are cancelled and the commit loop is not executed. If all checks pass, the core acquires a destination CSS-set reference, moves the task and drops the old set reference under the lock. A concurrent fork or exit and freezer state require the core's synchronization; a hand-written list update would not preserve these invariants. Controller-specific failures and membership visibility must be interpreted from the actual paths, not inferred from the group name alone.

Migration checks before commit
Migration checks before commit1. Migration caller to cgroup core: Request move of a prepared task set. 2. cgroup core to Controller callbacks: can_attach(tset) for each controller. 3. Controller callbacks to cgroup core (return): Rejected: failed controller recorded. 4. cgroup core to Controller callbacks: Rejected: cancel_attach for earlier controllers. 5. cgroup core to Task set: Accepted: commit under css_set_lock. 6. cgroup core to Controller callbacks: Accepted: attach callbacks after commit.Migration callercgroup coreController callbacksTask setRequest move of aprepared task setcan_attach(tset) foreach controllerRejected: failedcontroller recordedRejected:cancel_attach forearlier controllersAccepted: commit under css_set_lockAccepted: attachcallbacks aftercommit
  1. Migration callercgroup core

    Request move of a prepared task set

  2. cgroup coreController callbacks

    can_attach(tset) for each controller

  3. Controller callbackscgroup core

    Return. Rejected: failed controller recorded

  4. cgroup coreController callbacks

    Rejected: cancel_attach for earlier controllers

  5. cgroup coreTask set

    Accepted: commit under css_set_lock

  6. cgroup coreController callbacks

    Accepted: attach callbacks after commit

Time runs downward. Dashed arrows show returns.

Read the text version
  1. Migration caller → cgroup core: Request move of a prepared task set
  2. cgroup core → Controller callbacks: can_attach(tset) for each controller
  3. Controller callbacks → cgroup core (return): Rejected: failed controller recorded
  4. cgroup core → Controller callbacks: Rejected: cancel_attach for earlier controllers
  5. cgroup core → Task set: Accepted: commit under css_set_lock
  6. cgroup core → Controller callbacks: Accepted: attach callbacks after commit
Source map. Teaching model of the cgroup_migrate_execute ordering in Linux v6.12; the two branches are labelled, and permission checks and destination setup in surrounding code are not shown.

Two separate-view examples#

Consider tasks A and B that share one network namespace but have different mount namespaces. They see the same network interfaces and routing state belonging to that network namespace, subject to permissions, while their mount trees can differ. A file visible through A's mount tree need not be reachable at the same pathname through B's. Shared mount propagation can still connect selected mount events, so “different mount namespaces” alone does not prove complete mount independence. Neither task has its own kernel, and a network namespace alone does not restrict filesystem access.

For cgroups, assume actual host hierarchy /service/team/job and a cgroup namespace rooted at /service/team. The job can be displayed relative to that namespace as /job, while a host observer can see a longer path. Both views can refer to the same underlying membership. Moving the namespace root view does not create a new resource controller or alter a limit. This is a synthetic illustration; the exact displayed path depends on procfs and cgroup namespace context. Confirm actual mount and namespace roots before using a printed path to locate files.

Namespace-relative path, same membership
Namespace-relative path, same membershipHost hierarchy: /service/team/job in the host view → Displayed path: /job seen from inside the namespace (path relative to namespace root). Namespace root: /service/team, fixed when the namespace was created → Displayed path: /job seen from inside the namespace (root removed from displayed path). Host hierarchy: /service/team/job in the host view → Membership and limits: Same cgroup; the view change alters no limit (same underlying membership)path relative tonamespace rootroot removed fromdisplayed pathsame underlyingmembershipHost hierarchy/service/team/job in thehost viewNamespace root/service/team, fixed whenthe namespace was createdDisplayed path/job seen from inside thenamespaceMembership and limitsSame cgroup; the view changealters no limit
Host hierarchy/service/team/job in the host view
Displayed path/job seen from inside the namespace
Namespace root/service/team, fixed when the namespace was created
Displayed path/job seen from inside the namespace
Host hierarchy/service/team/job in the host view
Membership and limitsSame cgroup; the view change alters no limit
Read the text version
  • Host hierarchy: /service/team/job in the host view → Displayed path: /job seen from inside the namespace (path relative to namespace root)
  • Namespace root: /service/team, fixed when the namespace was created → Displayed path: /job seen from inside the namespace (root removed from displayed path)
  • Host hierarchy: /service/team/job in the host view → Membership and limits: Same cgroup; the view change alters no limit (same underlying membership)
Conceptual model. Synthetic illustration of the cgroup_show_path view in Linux v6.12; the printed path depends on procfs, mounts and namespace context.

Thread groups, references and delegated boundaries#

The membership interface makes a practical distinction between a process leader identifier and an individual thread identifier. In a domain cgroup, moving a process through the process interface is intended to keep the thread group together under the documented rules. A threaded subtree permits selected thread-level placement, but the domain root and supported threaded controllers still constrain that layout. A service manager's permission to write a delegated subtree is not permission to change the parent controller settings: the parent decides what resources can be distributed, and the delegate can organize permitted descendants within that boundary.

A manager can keep an fd for a cgroup directory while tasks migrate or exit. That reference is useful for addressing an object but does not make it permanently populated or grant authority to move any unrelated process into it. The kernel validates source and destination relationships, ownership and permissions; a task can also exit during an attempted migration. Source and destination CSS sets and task-set lists therefore need references and synchronization throughout preparation. A successful controller precheck is not the commit itself, and a failure before the commit must cancel prior controller-specific preparation. After the commit point, controller notification happens for the moved task set.

For namespace lifetimes, consider an otherwise empty network namespace whose creator has exited. If an authorized process still holds an nsfs fd referring to it, that reference can keep the namespace object alive. Closing the fd eventually releases that reference, but other references may remain, and destruction can involve deferred work. Conversely, merely knowing a printed namespace inode-like identifier does not create a reference or ensure the object remains available. In an investigation, distinguish a link string, an open fd and the process's actual namespace membership; each supports a different claim. None of these observations establishes a complete container security policy.

Practice#

Read-only namespace and hierarchy inventory

On an authorized host or disposable guest, inspect only the shell's own namespace links, mappings and cgroup information:

sh
uname -r
for f in /proc/$$/ns/*; do [ -e "$f" ] && printf '%s -> %s\n' "${f##*/}" "$(readlink "$f")"; done
cat /proc/$$/uid_map /proc/$$/gid_map /proc/$$/cgroup
awk '$0 ~ / - cgroup2 / {print}' /proc/$$/mountinfo
if [ -r /sys/fs/cgroup/cgroup.controllers ]; then cat /sys/fs/cgroup/cgroup.controllers; else echo 'controller file unavailable at this mount path'; fi

Namespace link identifiers allow comparison with another authorized process; they do not describe full policy or prove isolation. The mountinfo row contains a mount root and mount point, so /sys/fs/cgroup need not be the correct path on every system. A cgroup namespace may present paths relative to its own root. Missing files, hidden procfs, denied reads, a cgroup v1 system or a nonstandard mount are valid observations; do not mount a filesystem, create a namespace, move tasks or change controller files to fix them.

Source-only alternative: complete a table with A/B network and mount relationships from the synthetic example, then trace the copy_net_ns error through the cleanup labels. For cgroup migration, record controller rejection before commit and the lock-protected successful move as separate rows. Deliver either those source tables or your observed links and cgroup view, noting that identical link text is evidence of a shared namespace object but does not reveal all permissions or resource limits.

Knowledge check#

  1. What does a network namespace share or separate independently of a mount namespace?
  2. What happens to already acquired namespace references if copy_net_ns fails?
  3. Why is PID 1 inside a child PID namespace special?
  4. What does a rejecting can_attach callback prevent in the shown migration path?
  5. Does a shorter cgroup path in a cgroup namespace mean the task received a new limit?

Answers#

  1. Network stack resources are scoped by the network namespace; mount-tree views are scoped separately. Tasks can share one and differ in the other, with propagation and permission caveats.
  2. The error is saved and the cleanup labels put previously acquired namespace references and free the new nsproxy. The partially built object is not attached as a successful result.
  3. It is the namespace's init process and must handle orphan reaping; its exit has namespace-wide consequences distinct from an ordinary worker exit.
  4. The source goes to cancellation before the commit loop, allowing earlier controller preparations to be undone; the proposed task movement is not committed by that path.
  5. No. It can be a namespace-relative view of the same underlying cgroup. Membership and enabled controller limits must be examined separately.

Checkpoint#

Submit both synthetic view tables and a namespace-creation or migration cleanup timeline. Ready means each namespace family is distinguished, PID 1 and propagation are accounted for, and cgroup path view is not confused with membership or limits. Retry the pinned source labels if any acquired reference lacks a cleanup owner. This is not certification.

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.