Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

3 cats sit in 3 cardboard boxes. Every box is marked number 1, and each cat is sure it is the first. An owl on a branch above can see all 3.

Namespaces

A namespace is a kernel object that gives a group of processes their own instance of a global resource: their own process numbers, their own host name, their own mounted file systems, or their own network interfaces. Processes inside a namespace see and change the instance of that namespace, and processes outside are unaffected. Namespaces are the isolation half of a container. Chapter 12 covers the other half, limits.

In this chapter, we will learn how a process enters a new namespace and what changes for it. We will first measure a child in new user, UTS and PID namespaces with nsfacts. Then we will write nsmodel, a model of nested PID namespaces and of the UTS namespace.

This chapter covers

  • the kinds of namespace and what each isolates
  • how to see the namespaces of a process under /proc
  • the kernel structures: struct nsproxy, struct pid and struct upid
  • how a process is placed in a new namespace: clone flags, unshare and setns
  • the user namespace: why an unprivileged user can be user 0 inside
  • the PID namespace: a process has a number at every level
  • which processes a PID namespace can see
  • the UTS namespace and the host name
  • what the mount and network namespaces isolate

The kinds of namespace

Linux 5.15 has 8 kinds of namespace. Every process is in exactly 1 namespace of each kind, and a child starts in the namespaces of its parent. On a system without containers all processes share 1 namespace of each kind, and the resources appear global.

The 8 kinds of namespace and what each isolates: pid, process numbers; mnt, mounted file systems; uts, host name; net, interfaces, ports and routes; user, user and group numbers; ipc, shared memory and queues; cgroup, the control group root; time, clock offsets.
Figure 11.1 The kinds of namespace. The name is the entry under /proc/<pid>/ns.
KindIsolatesA process in a new one sees
pidprocess numbersonly the processes of the namespace, numbered from 1
mntthe list of mounted file systemsa copy of the parent’s mounts, which it can change privately (chapter 13)
utsthe host namea copy of the host name, which it can change
netnetwork interfaces, addresses, routes, portsa loopback interface that is down, and nothing else
useruser and group numbers, and capabilitiesits own numbering of users, mapped to the numbers outside
ipcshared memory segments and message queuesnone of the objects outside
cgroupthe root of the control group treeits own group as the root (chapter 12)
timethe offsets of the monotonic and boot clocksclocks that can be shifted

The kernel shows the namespaces of a process as symbolic links:

$ ls -l /proc/self/ns
cgroup -> cgroup:[4026531835]
ipc -> ipc:[4026531839]
mnt -> mnt:[4026531841]
net -> net:[4026531840]
pid -> pid:[4026531836]
pid_for_children -> pid:[4026531836]
time -> time:[4026531834]
user -> user:[4026531837]
uts -> uts:[4026531838]

The number in brackets identifies the namespace. Processes that show the same number for a kind are in the same namespace of that kind. pid_for_children is the PID namespace that children of the process will be created in, which can differ from the one the process itself is in.

The kernel’s structures

A task refers to its namespaces through struct nsproxy. This is the complete definition from include/linux/nsproxy.h of Linux 5.15, with comments added.

struct nsproxy {
        atomic_t count;                        /* tasks that share this nsproxy */
        struct uts_namespace *uts_ns;
        struct ipc_namespace *ipc_ns;
        struct mnt_namespace *mnt_ns;
        struct pid_namespace *pid_ns_for_children;
        struct net *net_ns;
        struct time_namespace *time_ns;
        struct time_namespace *time_ns_for_children;
        struct cgroup_namespace *cgroup_ns;
};

The structure is a set of pointers, 1 for each kind. Processes that are in the same namespaces share a single nsproxy. The user namespace is absent from the list: it is reached through the credentials of the task, task->cred->user_ns, because it decides what the task is permitted to do.

The PID namespace of a task is recorded in a different place, together with its numbers. These are the definitions from include/linux/pid.h and include/linux/pid_namespace.h.

struct upid {
        int nr;                                /* the number in 1 namespace */
        struct pid_namespace *ns;              /* that namespace */
};

struct pid {
        unsigned int level;                    /* depth of the task's PID namespace */
        struct upid numbers[1];                /* 1 entry per level, 0 to level */
        /* ... */
};

struct pid_namespace {
        struct task_struct *child_reaper;      /* its process 1, which adopts orphans */
        unsigned int level;                    /* 0 for the root namespace */
        struct pid_namespace *parent;          /* the enclosing namespace */
        struct user_namespace *user_ns;        /* the user namespace that owns it */
        /* ... */
};
FieldMeaning
struct pidWhat task_struct.thread_pid points to: the identity of a task in every PID namespace that can see it.
numbersAn array with level + 1 entries. numbers[0] is the number in the root namespace, and numbers[level] is the number in the task’s own namespace.
child_reaperThe first process of the namespace. Orphans of the namespace are given to it (chapter 3).
parent, levelPID namespaces form a tree, and level is the depth in it.
user_nsEvery namespace is owned by a user namespace. A process needs privilege in the owner to change the namespace.
How a task refers to its namespaces. struct task_struct has nsproxy, a pointer to its user namespace through cred, and thread_pid. struct nsproxy has a pointer for each of the UTS, IPC, mount, network, cgroup and time namespaces, and pid_ns_for_children. struct pid has the level of its PID namespace and an array numbers with a struct upid for each level, holding the number at that level and the namespace.
Figure 11.2 A task, its nsproxy, and its numbers in each PID namespace.

Entering a namespace

A process gets into a namespace in 3 ways.

CallEffect
clone with flags such as CLONE_NEWUTScreates a child that starts in new namespaces of the given kinds
unshare with the same flagsmoves the calling process into new namespaces
setns with a descriptor for /proc/<pid>/ns/<kind>moves the calling process into an existing namespace; this is how docker exec enters a running container
What clone does with namespace flags. The parent calls clone with CLONE_NEWUTS and CLONE_NEWPID. The kernel checks that the caller has CAP_SYS_ADMIN in its user namespace. It creates new namespace objects; the UTS one starts as a copy of the parent's. It builds a new struct nsproxy with new pointers for those 2 kinds and the old pointers for the rest. The child starts as pid 1 in the new PID namespace. The parent keeps its own nsproxy.
Figure 11.3 clone with namespace flags gives the child a new nsproxy.

A namespace exists for as long as something refers to it: a process that is in it, or an open descriptor for its /proc link. When the last process leaves, the namespace is destroyed.

User namespaces

Creating a namespace requires a capability. Linux divides the power of the administrator into separate capabilities, and the one needed here is CAP_SYS_ADMIN. There is a single exception: any user may create a user namespace. Inside it, the kernel maps user numbers. The creator can map user 0 inside to its own user number outside, and it then holds all privileges over the resources that the user namespace owns, which include the other namespaces created together with it. Toward everything outside, it remains the unprivileged user it was. This is what lets an ordinary user start a container.

The mapping is written to /proc/<pid>/uid_map, a line of 3 numbers: the first user number inside, the first user number outside, and the count of numbers mapped. In the root namespace every number maps to itself:

$ cat /proc/self/uid_map
         0          0 4294967295

In the namespace that nsfacts creates below, the line is 0 1000 1: user 0 inside is user 1000 outside, and no other number is mapped.

A user namespace with the mapping 0 1000 1. Inside the namespace the process is user 0, root, with all capabilities, and other users are unmapped and shown as 65534. User 0 inside maps to user 1000 outside, an ordinary user on the host.
Figure 11.4 The user mapping of an unprivileged container.

The capabilities a process holds inside a user namespace are checked against the owner of the object it acts on. It may set the host name of a UTS namespace that its user namespace owns. It may not set the clock of the machine, mount a disk, or read a file that user 1000 cannot read, because those belong to the root user namespace.

User 0 inside a user namespace is an ordinary user outside it. The privilege ends at the edge of the namespace.

Measuring namespaces

nsfacts uses golang.org/x/sys/unix in Go and the nix crate with the hostname, sched and user features in Rust.

$ cargo new nsfacts
$ cargo add -p nsfacts nix --features hostname,sched,user

The kernel shows the namespaces of a process as symbolic links under /proc/<pid>/ns. The target of a link, such as uts:[4026531838], names the namespace, and processes with equal targets are in the same namespace.

Listing 11.1 The imports of nsfacts, in Go. code/go/nsfacts/main.go

import (
	"bufio"
	"errors"
	"fmt"
	"log"
	"os"
	"os/exec"
	"strings"
	"syscall"

	"golang.org/x/sys/unix"
)

Listing 11.2 The imports of nsfacts, in Rust. code/rust/nsfacts/src/main.rs

use std::collections::HashMap;
use std::env;
use std::fs;
use std::io;
use std::process::{self, Command};

use nix::sched::{CloneFlags, unshare};
use nix::unistd::{getgid, gethostname, getpid, getuid, sethostname};

Listing 11.3 Reading the namespaces of a process, in Go. code/go/nsfacts/main.go

// kinds are the namespace types of Linux 5.15, as named under /proc/<pid>/ns.
var kinds = []string{"cgroup", "ipc", "mnt", "net", "pid", "time", "user", "uts"}

// namespaces returns the identity of each namespace of a process. The kernel
// shows it as the target of a symbolic link, such as "uts:[4026531838]".
// Processes are in the same namespace when the targets are equal.
func namespaces(pid string) (map[string]string, error) {
	ids := map[string]string{}

	for _, kind := range kinds {
		id, err := os.Readlink("/proc/" + pid + "/ns/" + kind)

		if err != nil {
			return nil, err
		}

		ids[kind] = id
	}

	return ids, nil
}

Listing 11.4 Reading the namespaces of a process, in Rust. code/rust/nsfacts/src/main.rs

/// The namespace types of Linux 5.15, as named under `/proc/<pid>/ns`.
const KINDS: [&str; 8] = ["cgroup", "ipc", "mnt", "net", "pid", "time", "user", "uts"];

/// Returns the identity of each namespace of a process. The kernel shows it
/// as the target of a symbolic link, such as `uts:[4026531838]`.
/// Processes are in the same namespace when the targets are equal.
fn namespaces(pid: &str) -> io::Result<HashMap<&'static str, String>> {
    KINDS
        .iter()
        .map(|&kind| {
            let id = fs::read_link(format!("/proc/{pid}/ns/{kind}"))?;

            Ok((kind, id.to_string_lossy().into_owned()))
        })
        .collect()
}

fn hostname() -> io::Result<String> {
    Ok(gethostname()?.to_string_lossy().into_owned())
}

The program runs itself again as a child with the argument inside. The child tries to set the host name to box and prints what it sees.

Listing 11.5 What the child reports, in Go. code/go/nsfacts/main.go

// name returns the symbolic name of a system call error, such as EPERM.
func name(err error) string {
	var errno unix.Errno

	if errors.As(err, &errno) {
		return unix.ErrnoName(errno)
	}

	return fmt.Sprint(err)
}

// inside runs in the child. It tries to change the host name, and prints
// what it sees as key=value lines for the parent to read.
func inside() error {
	outcome := "succeeds"

	if err := unix.Sethostname([]byte("box")); err != nil {
		outcome = "fails with " + name(err)
	}

	host, err := os.Hostname()

	if err != nil {
		return err
	}

	ids, err := namespaces("self")

	if err != nil {
		return err
	}

	uidMap, err := os.ReadFile("/proc/self/uid_map")

	if err != nil {
		return err
	}

	fmt.Printf("sethostname=%s\nhostname=%s\npid=%d\nuid=%d\n", outcome, host, os.Getpid(), os.Getuid())
	fmt.Printf("map=%s\nuts=%s\npidns=%s\n", strings.Join(strings.Fields(string(uidMap)), " "), ids["uts"], ids["pid"])

	return nil
}

Listing 11.6 What the child reports, in Rust. code/rust/nsfacts/src/main.rs

/// Runs in the child. It tries to change the host name, and prints what it
/// sees as key=value lines for the parent to read.
fn inside() -> io::Result<()> {
    let outcome = match sethostname("box") {
        Ok(()) => "succeeds".to_owned(),
        Err(error) => format!("fails with {error:?}"),
    };

    let ids = namespaces("self")?;
    let uid_map = fs::read_to_string("/proc/self/uid_map")?;
    let uid_map: Vec<&str> = uid_map.split_whitespace().collect();

    println!("sethostname={outcome}");
    println!("hostname={}", hostname()?);
    println!("pid={}", getpid());
    println!("uid={}", getuid());
    println!("map={}", uid_map.join(" "));
    println!("uts={}", ids["uts"]);
    println!("pidns={}", ids["pid"]);

    Ok(())
}

The child is started once in the parent’s namespaces and once in new user, UTS and PID namespaces. The languages create the namespaces differently. Go’s SysProcAttr passes the clone flags and the user mappings to the call that creates the child. unshare of a user namespace is refused in a process with several threads, which a Go program always is. The Rust program is single-threaded, so its helper process calls unshare, writes the mappings to /proc/self/uid_map and /proc/self/gid_map, and then starts the child.

Listing 11.7 Starting the child in new namespaces, in Go. code/go/nsfacts/main.go

// report runs this program again as a child and returns the lines the child
// prints, as a map. With isolate set, the child is created in new user, UTS
// and PID namespaces, and user 0 inside is mapped to the current user.
func report(isolate bool) (map[string]string, error) {
	child := exec.Command("/proc/self/exe", "inside")
	child.Stderr = os.Stderr

	if isolate {
		child.SysProcAttr = &syscall.SysProcAttr{
			Cloneflags:  syscall.CLONE_NEWUSER | syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID,
			UidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: os.Getuid(), Size: 1}},
			GidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: os.Getgid(), Size: 1}},
		}
	}

	output, err := child.Output()

	if err != nil {
		return nil, err
	}

	seen := map[string]string{}

	for lines := bufio.NewScanner(strings.NewReader(string(output))); lines.Scan(); {
		if key, value, ok := strings.Cut(lines.Text(), "="); ok {
			seen[key] = value
		}
	}

	return seen, nil
}

Listing 11.8 Starting the child in new namespaces, in Rust. code/rust/nsfacts/src/main.rs

/// Moves this process into new user and UTS namespaces, maps user 0 inside to
/// the current user, and runs the program again as a child. A new PID
/// namespace applies to the children of the process that asks for it, so the
/// child is the first process of that namespace.
fn isolated() -> io::Result<()> {
    let (uid, gid) = (getuid(), getgid());

    unshare(CloneFlags::CLONE_NEWUSER | CloneFlags::CLONE_NEWUTS | CloneFlags::CLONE_NEWPID)?;

    fs::write("/proc/self/setgroups", "deny")?;
    fs::write("/proc/self/uid_map", format!("0 {uid} 1"))?;
    fs::write("/proc/self/gid_map", format!("0 {gid} 1"))?;

    let status = Command::new("/proc/self/exe").arg("inside").status()?;

    process::exit(if status.success() { 0 } else { 1 });
}

/// Runs this program again as a child and returns the lines the child prints,
/// as a map. With `isolate` set, the child is created in new namespaces.
fn report(isolate: bool) -> io::Result<HashMap<String, String>> {
    let mode = if isolate { "isolated" } else { "inside" };
    let output = Command::new("/proc/self/exe").arg(mode).output()?;

    let seen = String::from_utf8_lossy(&output.stdout)
        .lines()
        .filter_map(|line| line.split_once('='))
        .map(|(key, value)| (key.to_owned(), value.to_owned()))
        .collect();

    Ok(seen)
}

Listing 11.9 Comparing the child with the parent, in Go. code/go/nsfacts/main.go

func run() error {
	mine, err := namespaces("self")

	if err != nil {
		return err
	}

	name, err := os.Hostname()

	if err != nil {
		return err
	}

	for _, isolate := range []bool{false, true} {
		seen, err := report(isolate)

		if err != nil {
			return err
		}

		after, err := os.Hostname()

		if err != nil {
			return err
		}

		fmt.Printf("child in new user, UTS and PID namespaces: %v\n", isolate)
		fmt.Printf("  same UTS namespace as the parent %v, same PID namespace %v\n",
			seen["uts"] == mine["uts"], seen["pidns"] == mine["pid"])
		fmt.Printf("  sethostname %s; host name inside is \"box\" %v, outside unchanged %v\n",
			seen["sethostname"], seen["hostname"] == "box", after == name)
		fmt.Printf("  pid inside is 1 %v; uid inside is 0 %v\n", seen["pid"] == "1", seen["uid"] == "0")

		if fields := strings.Fields(seen["map"]); isolate && len(fields) == 3 {
			fmt.Printf("  uid map: user 0 inside is user %s outside\n", fields[1])
		}
	}

	return nil
}

Listing 11.10 Comparing the child with the parent, in Rust. code/rust/nsfacts/src/main.rs

fn run() -> io::Result<()> {
    let mine = namespaces("self")?;
    let name = hostname()?;

    for isolate in [false, true] {
        let seen = report(isolate)?;
        let get = |key: &str| seen.get(key).map_or("", String::as_str);

        println!("child in new user, UTS and PID namespaces: {isolate}");
        println!(
            "  same UTS namespace as the parent {}, same PID namespace {}",
            get("uts") == mine["uts"],
            get("pidns") == mine["pid"]
        );
        println!(
            "  sethostname {}; host name inside is \"box\" {}, outside unchanged {}",
            get("sethostname"),
            get("hostname") == "box",
            hostname()? == name
        );
        println!(
            "  pid inside is 1 {}; uid inside is 0 {}",
            get("pid") == "1",
            get("uid") == "0"
        );

        let map: Vec<&str> = get("map").split_whitespace().collect();

        if let (true, [_, outside, _]) = (isolate, map.as_slice()) {
            println!("  uid map: user 0 inside is user {outside} outside");
        }
    }

    Ok(())
}
child in new user, UTS and PID namespaces: false
  same UTS namespace as the parent true, same PID namespace true
  sethostname fails with EPERM; host name inside is "box" false, outside unchanged true
  pid inside is 1 false; uid inside is 0 false
child in new user, UTS and PID namespaces: true
  same UTS namespace as the parent false, same PID namespace false
  sethostname succeeds; host name inside is "box" true, outside unchanged true
  pid inside is 1 true; uid inside is 0 true
  uid map: user 0 inside is user 1000 outside
OutputConclusion
ordinary child: same namespacesA child inherits the namespaces of its parent.
ordinary child: sethostname fails with EPERMChanging the host name requires privilege, and the user has none.
new namespaces: different UTS and PID namespaceThe flags gave the child its own instances.
sethostname succeeds, and the host name outside is unchangedThe child is privileged over its own UTS namespace, and the change stays inside it.
pid inside is 1The first process of a PID namespace has number 1 there.
uid inside is 0, mapped to 1000 outsideUser 0 inside the user namespace is the ordinary user outside.

The measurement is clearer with the 2 points of view side by side. Nothing about the child process itself differs between the columns. What differs is the answer the kernel gives when it is asked.

A process in new namespaces seen from outside and from inside. Its process number is 565534 on the host and 1 inside. Its user is 1000 on the host and 0 inside. The host name is yahboom outside and box inside. It may set the host name inside and gets EPERM outside.
Figure 11.5 The child of nsfacts, as the host sees it and as it sees itself.

PID namespaces

PID namespaces are nested. A new PID namespace is a child of the namespace in which it was created, and a process in it has a process number in its own namespace and in every namespace above it, up to the root. The numbers are independent of each other.

Visibility follows the nesting. A namespace sees the processes in itself and in the namespaces below it. It has no number for a process in a namespace above it or beside it, so it cannot name such a process in kill or find it under /proc.

The first process of a PID namespace takes the role that process 1 has on the whole machine: it adopts the orphans of its namespace, and when it ends, the kernel ends every other process in the namespace.

The root PID namespace contains the first process, pid 1, and a shell, pid 2. It encloses namespace A and namespace B. In A, the first process has pid 1 inside and pid 3 in the root, and its child has pid 2 inside and pid 4 in the root. A namespace nested in A has a first process with pid 1 inside, pid 3 in A, and pid 6 in the root. In B, the first process has pid 1 inside and pid 5 in the root.
Figure 11.6 The processes of the model's trace, with their number at each level.

Low-level design: nsmodel

TypeFieldsMeaning
pidNamespaceparent, level, lasta namespace, its depth below the root, and the last number it gave out
utsNamespacehostnamethe host name of its processes
processpid, uts, numbersits namespaces; its number in each PID namespace from the root down
OperationBehaviour
fork(parent, newPID, newUTS)creates a child in the parent’s namespaces or in new ones; allocates a number at every level
numberIn(namespace)returns the number of a process in a namespace, when the namespace can see it

Modelling namespaces

The Go version links namespaces with pointers. The Rust version keeps them in vectors owned by a Kernel value and refers to them by index.

Listing 11.11 The imports of nsmodel, in Go. code/go/nsmodel/main.go

import "fmt"

Listing 11.12 PID namespaces, UTS namespaces and processes, in Go. code/go/nsmodel/main.go

// A pidNamespace hands out process numbers. level is its depth below the
// root namespace, which has level 0.
type pidNamespace struct {
	parent *pidNamespace
	level  int
	last   int
}

// allocate returns the next unused number of the namespace.
func (ns *pidNamespace) allocate() int {
	ns.last++

	return ns.last
}

// A utsNamespace holds the host name that its processes see.
type utsNamespace struct {
	hostname string
}

// A process belongs to one namespace of each kind. numbers holds its process
// number in every PID namespace from the root, at index 0, down to its own.
type process struct {
	pid     *pidNamespace
	uts     *utsNamespace
	numbers []int
}

Listing 11.13 PID namespaces, UTS namespaces and processes, in Rust. code/rust/nsmodel/src/main.rs

/// Hands out process numbers. `level` is its depth below the root namespace,
/// which has level 0. `parent` is the index of the enclosing namespace.
struct PidNamespace {
    parent: Option<usize>,
    level: usize,
    last: u32,
}

/// A process belongs to one namespace of each kind, named here by its index
/// in the kernel's tables. `numbers` holds its process number in every PID
/// namespace from the root, at index 0, down to its own.
struct Process {
    pid: usize,
    uts: usize,
    numbers: Vec<u32>,
}

/// The namespaces of a machine. A UTS namespace is the host name that its
/// processes see.
struct Kernel {
    pid_namespaces: Vec<PidNamespace>,
    hostnames: Vec<String>,
}

impl Kernel {
    /// Returns the next unused number of a PID namespace.
    fn allocate(&mut self, namespace: usize) -> u32 {
        self.pid_namespaces[namespace].last += 1;

        self.pid_namespaces[namespace].last
    }
}

fork walks from the child’s PID namespace up to the root and allocates a number in each. A new UTS namespace starts as a copy of the parent’s, so the child initially sees the same host name.

Listing 11.14 Creating a process, with or without new namespaces, in Go. code/go/nsmodel/main.go

// fork creates a child. By default the child is in its parent's namespaces.
// With newPID it is the first process of a new PID namespace below the
// parent's. With newUTS it gets a copy of the parent's UTS namespace.
func fork(parent *process, newPID, newUTS bool) *process {
	child := &process{pid: parent.pid, uts: parent.uts}

	if newPID {
		child.pid = &pidNamespace{parent: parent.pid, level: parent.pid.level + 1}
	}

	if newUTS {
		child.uts = &utsNamespace{hostname: parent.uts.hostname}
	}

	child.numbers = make([]int, child.pid.level+1)

	for ns := child.pid; ns != nil; ns = ns.parent {
		child.numbers[ns.level] = ns.allocate()
	}

	return child
}

Listing 11.15 Creating a process, with or without new namespaces, in Rust. code/rust/nsmodel/src/main.rs

impl Kernel {
    /// Creates a child. By default the child is in its parent's namespaces.
    /// With `new_pid` it is the first process of a new PID namespace below
    /// the parent's. With `new_uts` it gets a copy of the parent's UTS
    /// namespace.
    fn fork(&mut self, parent: &Process, new_pid: bool, new_uts: bool) -> Process {
        let (mut pid, mut uts) = (parent.pid, parent.uts);

        if new_pid {
            self.pid_namespaces.push(PidNamespace {
                parent: Some(parent.pid),
                level: self.pid_namespaces[parent.pid].level + 1,
                last: 0,
            });
            pid = self.pid_namespaces.len() - 1;
        }

        if new_uts {
            self.hostnames.push(self.hostnames[parent.uts].clone());
            uts = self.hostnames.len() - 1;
        }

        let mut numbers = vec![0; self.pid_namespaces[pid].level + 1];
        let mut namespace = Some(pid);

        while let Some(current) = namespace {
            numbers[self.pid_namespaces[current].level] = self.allocate(current);
            namespace = self.pid_namespaces[current].parent;
        }

        Process { pid, uts, numbers }
    }
}

numberIn decides visibility. It climbs from the process’s own namespace to the level of the asking namespace and checks that it arrives at that namespace.

Listing 11.16 The number of a process in a namespace, in Go. code/go/nsmodel/main.go

// numberIn returns the number by which a process is known in a namespace. A
// process is visible in its own PID namespace and in every namespace that
// encloses it, and in no other.
func (p *process) numberIn(ns *pidNamespace) (int, bool) {
	own := p.pid

	for own != nil && own.level > ns.level {
		own = own.parent
	}

	if own != ns {
		return 0, false
	}

	return p.numbers[ns.level], true
}

// seen describes a process as a namespace sees it.
func seen(p *process, ns *pidNamespace) string {
	if number, ok := p.numberIn(ns); ok {
		return fmt.Sprintf("pid %d", number)
	}

	return "not visible"
}

Listing 11.17 The number of a process in a namespace, in Rust. code/rust/nsmodel/src/main.rs

impl Kernel {
    /// Returns the number by which a process is known in a namespace. A
    /// process is visible in its own PID namespace and in every namespace
    /// that encloses it, and in no other.
    fn number_in(&self, process: &Process, namespace: usize) -> Option<u32> {
        let level = self.pid_namespaces[namespace].level;
        let mut own = Some(process.pid);

        while let Some(current) = own
            && self.pid_namespaces[current].level > level
        {
            own = self.pid_namespaces[current].parent;
        }

        if own != Some(namespace) {
            return None;
        }

        process.numbers.get(level).copied()
    }

    /// Describes a process as a namespace sees it.
    fn seen(&self, process: &Process, namespace: usize) -> String {
        match self.number_in(process, namespace) {
            Some(number) => format!("pid {number}"),
            None => "not visible".to_owned(),
        }
    }
}

Listing 11.18 The trace, in Go. code/go/nsmodel/main.go

func main() {
	root := &pidNamespace{}
	host := &utsNamespace{hostname: "host"}

	first := &process{pid: root, uts: host, numbers: []int{root.allocate()}}
	shell := fork(first, false, false)
	a := fork(shell, true, true)
	child := fork(a, false, false)
	b := fork(shell, true, true)
	nested := fork(child, true, false)

	rows := []struct {
		name string
		p    *process
	}{{"first process", first}, {"shell", shell}, {"first process of A", a}, {"its child", child},
		{"first process of B", b}, {"nested below A", nested}}

	for _, row := range rows {
		fmt.Printf("%-20s numbers from the root namespace down: %v\n", row.name, row.p.numbers)
	}

	fmt.Printf("the first process of B, seen from the root namespace: %s\n", seen(b, root))
	fmt.Printf("the first process of B, seen from A: %s\n", seen(b, a.pid))
	fmt.Printf("the shell, seen from A: %s\n", seen(shell, a.pid))
	fmt.Printf("the nested process, seen from A: %s\n", seen(nested, a.pid))

	a.uts.hostname = "box"
	fmt.Printf("host name set to \"box\" in A: A sees %q, its child %q, B %q, the shell %q\n",
		a.uts.hostname, child.uts.hostname, b.uts.hostname, shell.uts.hostname)
}

Listing 11.19 The trace, in Rust. code/rust/nsmodel/src/main.rs

/// Formats numbers the way Go prints a slice.
fn list(numbers: &[u32]) -> String {
    let words: Vec<String> = numbers.iter().map(u32::to_string).collect();

    format!("[{}]", words.join(" "))
}

fn main() {
    let root = PidNamespace {
        parent: None,
        level: 0,
        last: 0,
    };

    let mut kernel = Kernel {
        pid_namespaces: vec![root],
        hostnames: vec!["host".to_owned()],
    };

    let first = Process {
        pid: 0,
        uts: 0,
        numbers: vec![kernel.allocate(0)],
    };

    let shell = kernel.fork(&first, false, false);
    let a = kernel.fork(&shell, true, true);
    let child = kernel.fork(&a, false, false);
    let b = kernel.fork(&shell, true, true);
    let nested = kernel.fork(&child, true, false);

    let rows = [
        ("first process", &first),
        ("shell", &shell),
        ("first process of A", &a),
        ("its child", &child),
        ("first process of B", &b),
        ("nested below A", &nested),
    ];

    for (name, process) in rows {
        println!(
            "{name:<20} numbers from the root namespace down: {}",
            list(&process.numbers)
        );
    }

    println!(
        "the first process of B, seen from the root namespace: {}",
        kernel.seen(&b, 0)
    );
    println!(
        "the first process of B, seen from A: {}",
        kernel.seen(&b, a.pid)
    );
    println!("the shell, seen from A: {}", kernel.seen(&shell, a.pid));
    println!(
        "the nested process, seen from A: {}",
        kernel.seen(&nested, a.pid)
    );

    kernel.hostnames[a.uts] = "box".to_owned();
    println!(
        "host name set to \"box\" in A: A sees {:?}, its child {:?}, B {:?}, the shell {:?}",
        kernel.hostnames[a.uts],
        kernel.hostnames[child.uts],
        kernel.hostnames[b.uts],
        kernel.hostnames[shell.uts]
    );
}
first process        numbers from the root namespace down: [1]
shell                numbers from the root namespace down: [2]
first process of A   numbers from the root namespace down: [3 1]
its child            numbers from the root namespace down: [4 2]
first process of B   numbers from the root namespace down: [5 1]
nested below A       numbers from the root namespace down: [6 3 1]
the first process of B, seen from the root namespace: pid 5
the first process of B, seen from A: not visible
the shell, seen from A: not visible
the nested process, seen from A: pid 3
host name set to "box" in A: A sees "box", its child "box", B "host", the shell "host"
LineWhat it shows
first process of A: [3 1]Number 3 in the root namespace and 1 in its own, as nsfacts measured.
first process of B: [5 1]A and B both have a process 1. The numbers of a namespace are its own.
nested below A: [6 3 1]A process 2 levels down has 3 numbers.
B seen from the root: pid 5The root namespace sees every process.
B seen from A: not visibleA namespace cannot see a namespace beside it.
the shell seen from A: not visibleA namespace cannot see the namespace above it.
the nested process seen from A: pid 3A namespace sees the namespaces below it.
the host name lineThe change in A is seen by A and by its child, which shares A’s UTS namespace. B and the shell are unaffected.

Correspondence with the kernel source. A process refers to its namespaces through struct nsproxy (include/linux/nsproxy.h), with a pointer for each kind. The numbers of a process are in struct pid (include/linux/pid.h), whose array numbers has an entry for each level, as in the model. pid_nr_ns is the kernel’s numberIn. copy_namespaces in kernel/nsproxy.c handles the clone flags, and zap_pid_ns_processes ends a namespace when its first process ends.

Inspecting namespaces on a live system

CommandShows
ls -l /proc/<pid>/nsthe namespaces of a process
lsnsevery namespace of the system, with the number of processes in it
cat /proc/<pid>/uid_mapthe user mapping of the user namespace of a process
unshare --user --map-root-user --uts --pid --fork shstarts a shell in new namespaces, as nsfacts does
nsenter --target <pid> --uts --pid shstarts a shell in the namespaces of another process, with setns
cat /proc/sys/user/max_user_namespaceshow many user namespaces a user may create: 61507 here

Comparing the links of 2 processes is the quickest way to learn whether they are in the same container.

The complete programs

Listing 11.20 The complete nsfacts, in Go. code/go/nsfacts/main.go

// Command nsfacts measures namespaces. It compares the namespaces of a
// process with those of an ordinary child, and then starts a child in new
// user, UTS and PID namespaces and reports what that child sees.
//
// Run it from code/go with:
//
//	go run ./nsfacts
package main

import (
	"bufio"
	"errors"
	"fmt"
	"log"
	"os"
	"os/exec"
	"strings"
	"syscall"

	"golang.org/x/sys/unix"
)


// kinds are the namespace types of Linux 5.15, as named under /proc/<pid>/ns.
var kinds = []string{"cgroup", "ipc", "mnt", "net", "pid", "time", "user", "uts"}

// namespaces returns the identity of each namespace of a process. The kernel
// shows it as the target of a symbolic link, such as "uts:[4026531838]".
// Processes are in the same namespace when the targets are equal.
func namespaces(pid string) (map[string]string, error) {
	ids := map[string]string{}

	for _, kind := range kinds {
		id, err := os.Readlink("/proc/" + pid + "/ns/" + kind)

		if err != nil {
			return nil, err
		}

		ids[kind] = id
	}

	return ids, nil
}


// name returns the symbolic name of a system call error, such as EPERM.
func name(err error) string {
	var errno unix.Errno

	if errors.As(err, &errno) {
		return unix.ErrnoName(errno)
	}

	return fmt.Sprint(err)
}

// inside runs in the child. It tries to change the host name, and prints
// what it sees as key=value lines for the parent to read.
func inside() error {
	outcome := "succeeds"

	if err := unix.Sethostname([]byte("box")); err != nil {
		outcome = "fails with " + name(err)
	}

	host, err := os.Hostname()

	if err != nil {
		return err
	}

	ids, err := namespaces("self")

	if err != nil {
		return err
	}

	uidMap, err := os.ReadFile("/proc/self/uid_map")

	if err != nil {
		return err
	}

	fmt.Printf("sethostname=%s\nhostname=%s\npid=%d\nuid=%d\n", outcome, host, os.Getpid(), os.Getuid())
	fmt.Printf("map=%s\nuts=%s\npidns=%s\n", strings.Join(strings.Fields(string(uidMap)), " "), ids["uts"], ids["pid"])

	return nil
}


// report runs this program again as a child and returns the lines the child
// prints, as a map. With isolate set, the child is created in new user, UTS
// and PID namespaces, and user 0 inside is mapped to the current user.
func report(isolate bool) (map[string]string, error) {
	child := exec.Command("/proc/self/exe", "inside")
	child.Stderr = os.Stderr

	if isolate {
		child.SysProcAttr = &syscall.SysProcAttr{
			Cloneflags:  syscall.CLONE_NEWUSER | syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID,
			UidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: os.Getuid(), Size: 1}},
			GidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: os.Getgid(), Size: 1}},
		}
	}

	output, err := child.Output()

	if err != nil {
		return nil, err
	}

	seen := map[string]string{}

	for lines := bufio.NewScanner(strings.NewReader(string(output))); lines.Scan(); {
		if key, value, ok := strings.Cut(lines.Text(), "="); ok {
			seen[key] = value
		}
	}

	return seen, nil
}


func run() error {
	mine, err := namespaces("self")

	if err != nil {
		return err
	}

	name, err := os.Hostname()

	if err != nil {
		return err
	}

	for _, isolate := range []bool{false, true} {
		seen, err := report(isolate)

		if err != nil {
			return err
		}

		after, err := os.Hostname()

		if err != nil {
			return err
		}

		fmt.Printf("child in new user, UTS and PID namespaces: %v\n", isolate)
		fmt.Printf("  same UTS namespace as the parent %v, same PID namespace %v\n",
			seen["uts"] == mine["uts"], seen["pidns"] == mine["pid"])
		fmt.Printf("  sethostname %s; host name inside is \"box\" %v, outside unchanged %v\n",
			seen["sethostname"], seen["hostname"] == "box", after == name)
		fmt.Printf("  pid inside is 1 %v; uid inside is 0 %v\n", seen["pid"] == "1", seen["uid"] == "0")

		if fields := strings.Fields(seen["map"]); isolate && len(fields) == 3 {
			fmt.Printf("  uid map: user 0 inside is user %s outside\n", fields[1])
		}
	}

	return nil
}


func main() {
	if len(os.Args) > 1 && os.Args[1] == "inside" {
		if err := inside(); err != nil {
			log.Fatal(err)
		}

		return
	}

	if err := run(); err != nil {
		log.Fatal(err)
	}
}

Listing 11.21 The complete nsfacts, in Rust. code/rust/nsfacts/src/main.rs

//! Measures namespaces. It compares the namespaces of a process with those of
//! an ordinary child, and then starts a child in new user, UTS and PID
//! namespaces and reports what that child sees.
//!
//! Run it from code/rust with:
//!
//! ```text
//! cargo run -p nsfacts
//! ```

use std::collections::HashMap;
use std::env;
use std::fs;
use std::io;
use std::process::{self, Command};

use nix::sched::{CloneFlags, unshare};
use nix::unistd::{getgid, gethostname, getpid, getuid, sethostname};

/// The namespace types of Linux 5.15, as named under `/proc/<pid>/ns`.
const KINDS: [&str; 8] = ["cgroup", "ipc", "mnt", "net", "pid", "time", "user", "uts"];

/// Returns the identity of each namespace of a process. The kernel shows it
/// as the target of a symbolic link, such as `uts:[4026531838]`.
/// Processes are in the same namespace when the targets are equal.
fn namespaces(pid: &str) -> io::Result<HashMap<&'static str, String>> {
    KINDS
        .iter()
        .map(|&kind| {
            let id = fs::read_link(format!("/proc/{pid}/ns/{kind}"))?;

            Ok((kind, id.to_string_lossy().into_owned()))
        })
        .collect()
}

fn hostname() -> io::Result<String> {
    Ok(gethostname()?.to_string_lossy().into_owned())
}

/// Runs in the child. It tries to change the host name, and prints what it
/// sees as key=value lines for the parent to read.
fn inside() -> io::Result<()> {
    let outcome = match sethostname("box") {
        Ok(()) => "succeeds".to_owned(),
        Err(error) => format!("fails with {error:?}"),
    };

    let ids = namespaces("self")?;
    let uid_map = fs::read_to_string("/proc/self/uid_map")?;
    let uid_map: Vec<&str> = uid_map.split_whitespace().collect();

    println!("sethostname={outcome}");
    println!("hostname={}", hostname()?);
    println!("pid={}", getpid());
    println!("uid={}", getuid());
    println!("map={}", uid_map.join(" "));
    println!("uts={}", ids["uts"]);
    println!("pidns={}", ids["pid"]);

    Ok(())
}

/// Moves this process into new user and UTS namespaces, maps user 0 inside to
/// the current user, and runs the program again as a child. A new PID
/// namespace applies to the children of the process that asks for it, so the
/// child is the first process of that namespace.
fn isolated() -> io::Result<()> {
    let (uid, gid) = (getuid(), getgid());

    unshare(CloneFlags::CLONE_NEWUSER | CloneFlags::CLONE_NEWUTS | CloneFlags::CLONE_NEWPID)?;

    fs::write("/proc/self/setgroups", "deny")?;
    fs::write("/proc/self/uid_map", format!("0 {uid} 1"))?;
    fs::write("/proc/self/gid_map", format!("0 {gid} 1"))?;

    let status = Command::new("/proc/self/exe").arg("inside").status()?;

    process::exit(if status.success() { 0 } else { 1 });
}

/// Runs this program again as a child and returns the lines the child prints,
/// as a map. With `isolate` set, the child is created in new namespaces.
fn report(isolate: bool) -> io::Result<HashMap<String, String>> {
    let mode = if isolate { "isolated" } else { "inside" };
    let output = Command::new("/proc/self/exe").arg(mode).output()?;

    let seen = String::from_utf8_lossy(&output.stdout)
        .lines()
        .filter_map(|line| line.split_once('='))
        .map(|(key, value)| (key.to_owned(), value.to_owned()))
        .collect();

    Ok(seen)
}

fn run() -> io::Result<()> {
    let mine = namespaces("self")?;
    let name = hostname()?;

    for isolate in [false, true] {
        let seen = report(isolate)?;
        let get = |key: &str| seen.get(key).map_or("", String::as_str);

        println!("child in new user, UTS and PID namespaces: {isolate}");
        println!(
            "  same UTS namespace as the parent {}, same PID namespace {}",
            get("uts") == mine["uts"],
            get("pidns") == mine["pid"]
        );
        println!(
            "  sethostname {}; host name inside is \"box\" {}, outside unchanged {}",
            get("sethostname"),
            get("hostname") == "box",
            hostname()? == name
        );
        println!(
            "  pid inside is 1 {}; uid inside is 0 {}",
            get("pid") == "1",
            get("uid") == "0"
        );

        let map: Vec<&str> = get("map").split_whitespace().collect();

        if let (true, [_, outside, _]) = (isolate, map.as_slice()) {
            println!("  uid map: user 0 inside is user {outside} outside");
        }
    }

    Ok(())
}

fn main() -> io::Result<()> {
    match env::args().nth(1).as_deref() {
        Some("inside") => inside(),
        Some("isolated") => isolated(),
        _ => run(),
    }
}

Listing 11.22 The complete nsmodel, in Go. code/go/nsmodel/main.go

// Command nsmodel models 2 kinds of namespace. A PID namespace gives its
// processes their own process numbers, and every process also has a number
// in each enclosing namespace. A UTS namespace holds a host name.
//
// Run it from code/go with:
//
//	go run ./nsmodel
package main

import "fmt"


// A pidNamespace hands out process numbers. level is its depth below the
// root namespace, which has level 0.
type pidNamespace struct {
	parent *pidNamespace
	level  int
	last   int
}

// allocate returns the next unused number of the namespace.
func (ns *pidNamespace) allocate() int {
	ns.last++

	return ns.last
}

// A utsNamespace holds the host name that its processes see.
type utsNamespace struct {
	hostname string
}

// A process belongs to one namespace of each kind. numbers holds its process
// number in every PID namespace from the root, at index 0, down to its own.
type process struct {
	pid     *pidNamespace
	uts     *utsNamespace
	numbers []int
}


// fork creates a child. By default the child is in its parent's namespaces.
// With newPID it is the first process of a new PID namespace below the
// parent's. With newUTS it gets a copy of the parent's UTS namespace.
func fork(parent *process, newPID, newUTS bool) *process {
	child := &process{pid: parent.pid, uts: parent.uts}

	if newPID {
		child.pid = &pidNamespace{parent: parent.pid, level: parent.pid.level + 1}
	}

	if newUTS {
		child.uts = &utsNamespace{hostname: parent.uts.hostname}
	}

	child.numbers = make([]int, child.pid.level+1)

	for ns := child.pid; ns != nil; ns = ns.parent {
		child.numbers[ns.level] = ns.allocate()
	}

	return child
}


// numberIn returns the number by which a process is known in a namespace. A
// process is visible in its own PID namespace and in every namespace that
// encloses it, and in no other.
func (p *process) numberIn(ns *pidNamespace) (int, bool) {
	own := p.pid

	for own != nil && own.level > ns.level {
		own = own.parent
	}

	if own != ns {
		return 0, false
	}

	return p.numbers[ns.level], true
}

// seen describes a process as a namespace sees it.
func seen(p *process, ns *pidNamespace) string {
	if number, ok := p.numberIn(ns); ok {
		return fmt.Sprintf("pid %d", number)
	}

	return "not visible"
}


func main() {
	root := &pidNamespace{}
	host := &utsNamespace{hostname: "host"}

	first := &process{pid: root, uts: host, numbers: []int{root.allocate()}}
	shell := fork(first, false, false)
	a := fork(shell, true, true)
	child := fork(a, false, false)
	b := fork(shell, true, true)
	nested := fork(child, true, false)

	rows := []struct {
		name string
		p    *process
	}{{"first process", first}, {"shell", shell}, {"first process of A", a}, {"its child", child},
		{"first process of B", b}, {"nested below A", nested}}

	for _, row := range rows {
		fmt.Printf("%-20s numbers from the root namespace down: %v\n", row.name, row.p.numbers)
	}

	fmt.Printf("the first process of B, seen from the root namespace: %s\n", seen(b, root))
	fmt.Printf("the first process of B, seen from A: %s\n", seen(b, a.pid))
	fmt.Printf("the shell, seen from A: %s\n", seen(shell, a.pid))
	fmt.Printf("the nested process, seen from A: %s\n", seen(nested, a.pid))

	a.uts.hostname = "box"
	fmt.Printf("host name set to \"box\" in A: A sees %q, its child %q, B %q, the shell %q\n",
		a.uts.hostname, child.uts.hostname, b.uts.hostname, shell.uts.hostname)
}

Listing 11.23 The complete nsmodel, in Rust. code/rust/nsmodel/src/main.rs

//! Models 2 kinds of namespace. A PID namespace gives its processes their own
//! process numbers, and every process also has a number in each enclosing
//! namespace. A UTS namespace holds a host name.
//!
//! Run it from code/rust with:
//!
//! ```text
//! cargo run -p nsmodel
//! ```

/// Hands out process numbers. `level` is its depth below the root namespace,
/// which has level 0. `parent` is the index of the enclosing namespace.
struct PidNamespace {
    parent: Option<usize>,
    level: usize,
    last: u32,
}

/// A process belongs to one namespace of each kind, named here by its index
/// in the kernel's tables. `numbers` holds its process number in every PID
/// namespace from the root, at index 0, down to its own.
struct Process {
    pid: usize,
    uts: usize,
    numbers: Vec<u32>,
}

/// The namespaces of a machine. A UTS namespace is the host name that its
/// processes see.
struct Kernel {
    pid_namespaces: Vec<PidNamespace>,
    hostnames: Vec<String>,
}

impl Kernel {
    /// Returns the next unused number of a PID namespace.
    fn allocate(&mut self, namespace: usize) -> u32 {
        self.pid_namespaces[namespace].last += 1;

        self.pid_namespaces[namespace].last
    }
}

impl Kernel {
    /// Creates a child. By default the child is in its parent's namespaces.
    /// With `new_pid` it is the first process of a new PID namespace below
    /// the parent's. With `new_uts` it gets a copy of the parent's UTS
    /// namespace.
    fn fork(&mut self, parent: &Process, new_pid: bool, new_uts: bool) -> Process {
        let (mut pid, mut uts) = (parent.pid, parent.uts);

        if new_pid {
            self.pid_namespaces.push(PidNamespace {
                parent: Some(parent.pid),
                level: self.pid_namespaces[parent.pid].level + 1,
                last: 0,
            });
            pid = self.pid_namespaces.len() - 1;
        }

        if new_uts {
            self.hostnames.push(self.hostnames[parent.uts].clone());
            uts = self.hostnames.len() - 1;
        }

        let mut numbers = vec![0; self.pid_namespaces[pid].level + 1];
        let mut namespace = Some(pid);

        while let Some(current) = namespace {
            numbers[self.pid_namespaces[current].level] = self.allocate(current);
            namespace = self.pid_namespaces[current].parent;
        }

        Process { pid, uts, numbers }
    }
}

impl Kernel {
    /// Returns the number by which a process is known in a namespace. A
    /// process is visible in its own PID namespace and in every namespace
    /// that encloses it, and in no other.
    fn number_in(&self, process: &Process, namespace: usize) -> Option<u32> {
        let level = self.pid_namespaces[namespace].level;
        let mut own = Some(process.pid);

        while let Some(current) = own
            && self.pid_namespaces[current].level > level
        {
            own = self.pid_namespaces[current].parent;
        }

        if own != Some(namespace) {
            return None;
        }

        process.numbers.get(level).copied()
    }

    /// Describes a process as a namespace sees it.
    fn seen(&self, process: &Process, namespace: usize) -> String {
        match self.number_in(process, namespace) {
            Some(number) => format!("pid {number}"),
            None => "not visible".to_owned(),
        }
    }
}

/// Formats numbers the way Go prints a slice.
fn list(numbers: &[u32]) -> String {
    let words: Vec<String> = numbers.iter().map(u32::to_string).collect();

    format!("[{}]", words.join(" "))
}

fn main() {
    let root = PidNamespace {
        parent: None,
        level: 0,
        last: 0,
    };

    let mut kernel = Kernel {
        pid_namespaces: vec![root],
        hostnames: vec!["host".to_owned()],
    };

    let first = Process {
        pid: 0,
        uts: 0,
        numbers: vec![kernel.allocate(0)],
    };

    let shell = kernel.fork(&first, false, false);
    let a = kernel.fork(&shell, true, true);
    let child = kernel.fork(&a, false, false);
    let b = kernel.fork(&shell, true, true);
    let nested = kernel.fork(&child, true, false);

    let rows = [
        ("first process", &first),
        ("shell", &shell),
        ("first process of A", &a),
        ("its child", &child),
        ("first process of B", &b),
        ("nested below A", &nested),
    ];

    for (name, process) in rows {
        println!(
            "{name:<20} numbers from the root namespace down: {}",
            list(&process.numbers)
        );
    }

    println!(
        "the first process of B, seen from the root namespace: {}",
        kernel.seen(&b, 0)
    );
    println!(
        "the first process of B, seen from A: {}",
        kernel.seen(&b, a.pid)
    );
    println!("the shell, seen from A: {}", kernel.seen(&shell, a.pid));
    println!(
        "the nested process, seen from A: {}",
        kernel.seen(&nested, a.pid)
    );

    kernel.hostnames[a.uts] = "box".to_owned();
    println!(
        "host name set to \"box\" in A: A sees {:?}, its child {:?}, B {:?}, the shell {:?}",
        kernel.hostnames[a.uts],
        kernel.hostnames[child.uts],
        kernel.hostnames[b.uts],
        kernel.hostnames[shell.uts]
    );
}

Summary

  • A namespace gives a group of processes their own instance of a resource. Linux 5.15 has 8 kinds, and a process is in 1 of each.
  • A child inherits its parent’s namespaces. clone flags and unshare create new ones.
  • An unprivileged user may create a user namespace and be user 0 inside it, with privilege over the namespaces created with it and none outside.
  • A process has a number in its own PID namespace and in every enclosing one. The first process of a PID namespace has number 1 there.
  • A PID namespace sees the processes in itself and below it.
  • A change to the host name stays inside the UTS namespace in which it was made.

References