Namespaces

On this page

A container showing PID 1 does not mean the host also sees PID 1; creating a new mount namespace does not copy file contents. Namespaces establish views and permission boundaries for various resources, while cgroups manage resource usage.

Containers also need to combine root file systems, capabilities, seccomp, and security policies; there is no single namespace object that handles all isolation.

ObjectiveReading Location
Object scope and referencesTypes, creation, and entry
Identity and file viewsPID, User, Mount
Recyclable experiments and other boundariesIsolation examples, network, and time
Containers combine multiple namespaces; processes can share some of them Process A Independent mount / PID Process B Another set of mount / PID Same network ns Shared ports and network device views Namespace membership is chosen per type; it is not an indivisible "container identity".

What each namespace manages

TypeFlagScope and Boundaries
MountCLONE_NEWNSMount tree view; file contents may still be shared, events may propagate
PIDCLONE_NEWPIDPID numbers and visible hierarchy; not every member becomes PID 1
UserCLONE_NEWUSERUID/GID mapping and capability scope
NetworkCLONE_NEWNETNetwork devices, routing, protocol stack state, ports, etc.
IPCCLONE_NEWIPCSysV IPC, POSIX message queues, etc.
UTSCLONE_NEWUTSHostname and NIS domainname; not an independent DNS system
CgroupCLONE_NEWCGROUPCgroup path view; does not create resource quotas
TimeCLONE_NEWTIMEClock offsets for monotonic/boottime, etc.; does not virtualize all clocks

Specific capabilities depend on kernel configuration, and creation and entry are subject to permission restrictions. Namespace overview

Creation, entry, and holding references

clone/clone3 allows new tasks to enter new namespaces; unshare detaches the caller from certain shared relationships; setns enters an existing namespace specified by a file descriptor. Each type has special rules; the three cannot be treated as completely equivalent "container switching."

Links under /proc/PID/ns can be used to compare membership; opening the corresponding fd or retaining a bind mount can maintain namespace references. Process exit does not necessarily immediately destroy namespace objects that are still held.

nsproxy holds references to multiple types of namespaces, but user namespaces are associated with process credentials, and the current PID namespace is related to the PID hierarchy; nsproxy cannot be simplified into a complete snapshot containing everything. pid_for_children and time_for_children specifically highlight the difference between "currently belonging" and "subsequent child processes belonging."

PID namespace: numbers are relative

The first process in a new PID namespace becomes PID 1 within it, and subsequent members have their own numbers. The same task has other PIDs in ancestor PID namespaces; ancestors can see descendants, but child namespaces do not gain the ability to see all ancestor processes through this.

unshare(CLONE_NEWPID) does not change the caller's own PID namespace, but affects child processes created subsequently; setns also has corresponding child process semantics for PID namespaces. This explains why the unshare command usually requires --fork for the executed program to enter the new PID view. PID namespace

Create a child process first, then unshare and create subsequent child processes. Switch views between two observers, comparing the identity and visible numbers of the same task, rather than treating the namespace as if it copied a process.

Preparing the visual
Why didn't the parent process's PID change after unshare?

unshare(CLONE_NEWPID) changes pid_for_children, not migrating the caller's current PID namespace. The same child task can have different PIDs in the ancestor and its own space, without copying two processes.

PID 1 has special signal rules: other members within the namespace can usually only send signals for which it has installed handlers; SIGKILL/SIGSTOP cannot take effect internally like ordinary processes; SIGKILL/SIGSTOP from the ancestor namespace has specific exceptions. Therefore, "cannot kill internally, but can SIGSTOP" is an incorrect generalization.

When PID 1 exits, the kernel kills the remaining processes in that namespace, and subsequent process creation to this namespace is restricted; objects may still be held by fds, so it cannot be equated to immediately releasing all references.

Verifying "current space unchanged, subsequent child processes changed"

The following uses the Linux os.unshare() interface in Python 3.12+ to directly observe these two membership relationships. First, create a temporary user namespace from the command line to give the program scope permissions needed to create its own PID namespace; the program retains the outer proc mount, allowing the parent process to check child processes by outer PID.

Save as pid_view.py, and run with unshare --user --map-root-user python3 pid_view.py. The program creates only one child process and controls its exit timing using a pipe.

import errno
import os

def ns_identity(path):
    info = os.stat(path)
    return info.st_dev, info.st_ino

before_pid = os.getpid()
before_ns = ns_identity("/proc/self/ns/pid")
os.unshare(os.CLONE_NEWPID)
assert os.getpid() == before_pid
assert ns_identity("/proc/self/ns/pid") == before_ns

read_fd, write_fd = os.pipe()
child = os.fork()
if child == 0:
    os.close(write_fd)
    try:
        assert os.getpid() == 1 and os.getppid() == 0
        assert os.read(read_fd, 1) == b"x"
    except BaseException:
        os._exit(1)
    os._exit(0)

os.close(read_fd)
try:
    child_ns = ns_identity(f"/proc/{child}/ns/pid")
    assert child_ns != before_ns
    assert ns_identity("/proc/self/ns/pid_for_children") == child_ns
    assert ns_identity("/proc/self/ns/pid") == before_ns
    assert os.write(write_fd, b"x") == 1
finally:
    os.close(write_fd)
    _, status = os.waitpid(child, 0)
assert os.waitstatus_to_exitcode(status) == 0
print(f"parent PID unchanged: {before_pid}; child seen outside: {child}; inside: 1")

# The init of the new space has exited; continuing to fork to that space should fail.
try:
    unexpected = os.fork()
except OSError as error:
    assert error.errno == errno.ENOMEM
else:
    if unexpected == 0:
        os._exit(1)
    os.waitpid(unexpected, 0)
    raise AssertionError("PID namespace with exited init should not accept new processes")

PID numbers change with each run; what truly needs checking are four relationships: the parent process's own number has not changed; the child process is 1 internally; the child process sees the external parent's PPID as 0; the parent's pid_for_children is the same as the child's current PID namespace. pid_for_children has special visibility before the first child process is created, so the code waits until after fork to compare it.

unshare(PID) changes subsequent child process space, caller stays in place Parent process: current PID namespace A getpid() keeps original number Child process: current PID namespace B Internal PID=1, external has another number fork Parent's pid_for_children → B; parent's ns/pid → A The child's father is outside B, so its getppid() returns 0; after PID 1 exits, B no longer accepts fork.

The final ENOMEM in this controlled trajectory comes from the namespace init having exited, and cannot be generally explained as physical memory shortage. Actual programs should still retain errno and diagnose based on preceding operations. The experiment does not remount proc, so it does not demonstrate the complete view of ps inside a container; when that view is needed, combine it with an independent mount namespace and a new proc instance. PID namespace members and proc semantics

Running as PID 1 also requires assuming signal and reaping responsibilities

Orphans are adopted by appropriate subreapers or namespace inits, and the responsible party should wait to reap. When an application acts as PID 1, it needs to properly handle termination signals and child process reaping, and cannot rely solely on the default behavior of ordinary interactive programs.

User namespace: the meaning of root depends on mapping

Reading your own /proc/self/uid_map in a non-initial user namespace, if you see 0 1000 1, it can be interpreted by the three fields:

FieldValueMeaning
Internal starting UID0Identity within the new space
Parent space starting UID1000Corresponding external identity
Range length1Maps only this one UID

It does not automatically create other user mappings, nor can internal root be simply converted to host root.

Internal root does not equal host nobody; nor does it automatically mean inability to read any host files. If the mount view exposes host files, the mapped identity may still have ordinary access permissions. Capabilities within the user namespace mainly take effect on resources within their permission scope, and do not automatically gain global permissions of the initial user namespace. User namespace

Multiple UID/GID mappings involve subuid/subgid, mapping helpers, and setgroups restrictions. Whether unprivileged user namespaces can be created may also be affected by distribution security policies, LSM, and quantity limits; kernel.unprivileged_userns_clone is not a switch that exists uniformly on all Linux systems.

Nested mappings must be synthesized layer by layer, and the reader's perspective must be recorded

Assume the range from the first layer namespace to the initial namespace is 0 100000 65536, and the second layer maps only 0 1000 1 to the first layer. Then second layer UID 0 first maps to first layer UID 1000, then to initial namespace UID 101000; first layer's own UID 0 corresponds to initial UID 100000. Both layers being called root does not mean they represent the same external identity.

def mapped(uid, ranges):
    for inside, outside, count in ranges:
        if inside <= uid < inside + count:
            return outside + uid - inside
    return None

first_to_initial = [(0, 100000, 65536)]
second_to_first = [(0, 1000, 1)]
intermediate = mapped(0, second_to_first)
assert intermediate == 1000
assert mapped(intermediate, first_to_initial) == 101000
assert mapped(0, first_to_initial) == 100000
assert mapped(65535, first_to_initial) == 165535
assert mapped(65536, first_to_initial) is None
assert mapped(1, second_to_first) is None

None in the model indicates no corresponding mapping, not UID 0, nor does it equal automatically having nobody's permissions. The kernel may display an overflow UID for owners that cannot be represented in different interfaces, or reject operations requiring valid mappings; display results cannot replace authorization judgments. The above ranges are only examples of mapping calculations; actually creating them still requires mapping write permissions and parent layer mappable ranges.

Another easy-to-miss detail: if the reader and the target process are in the same user namespace, the second column of uid_map is interpreted according to the target's parent namespace; if they are in different user namespaces, the second column is interpreted according to the reader's namespace that opened the file. Therefore, when collecting mappings, the reader's location should be recorded simultaneously; the second column of any /proc/PID/uid_map cannot be labeled as "host UID" or "parent UID". User namespace: reading rules for UID/GID mapping

Mount namespace: independent mount table does not equal independent file contents

New spaces are usually copied from existing mount views, and the underlying file systems may still be the same. Propagation attributes such as shared/slave/private for shared subtrees determine how mount events propagate across views; therefore, "mounting in one space never affects another" is not true. Mount namespace

Root directory isolation also requires appropriate mount and root switching designs. chroot or a new mount namespace itself does not copy all files, nor does it automatically revoke access capabilities for already opened fds.

A local example that does not change the host hostname

The following command creates user, UTS, PID, and mount spaces in an environment that allows unprivileged user namespaces, and ends the experiment after the child process exits:

unshare --user --map-root-user --uts --pid --fork --mount --mount-proc   sh -c 'hostname kb-demo; printf "pid=%s hostname=%s\n" "$$" "$(hostname)"; cat /proc/self/uid_map'

The hostname modification occurs in the new UTS namespace; --mount-proc mounts proc for the new PID view, preventing tools like ps from still reading the external proc instance. The output UID mapping depends on the executor and cannot be preset to necessarily be host 1000. When permission is denied, first identify the environment policy; do not relax the entire machine's security configuration directly to run teaching examples.

Common misconceptions about network and time spaces

Containers can share network namespaces, or even use the host network, so not every container must have an independent netns. New network spaces do not automatically get available external networks; devices, addresses, routing, or user-space network backends need to be configured according to the plan.

The time namespace provides offsets for the corresponding monotonic/boottime clocks, suitable for maintaining the time view after migration; CLOCK_REALTIME does not thereby gain a per-container independent wall clock. There are also specific restrictions on the timing of creating subsequent child processes and offset configuration. Time namespace

For further understanding of resource limits and delegation, see cgroup.