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 pidandstruct upid - how a process is placed in a new namespace:
cloneflags,unshareandsetns - 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.
/proc/<pid>/ns.| Kind | Isolates | A process in a new one sees |
|---|---|---|
pid | process numbers | only the processes of the namespace, numbered from 1 |
mnt | the list of mounted file systems | a copy of the parent’s mounts, which it can change privately (chapter 13) |
uts | the host name | a copy of the host name, which it can change |
net | network interfaces, addresses, routes, ports | a loopback interface that is down, and nothing else |
user | user and group numbers, and capabilities | its own numbering of users, mapped to the numbers outside |
ipc | shared memory segments and message queues | none of the objects outside |
cgroup | the root of the control group tree | its own group as the root (chapter 12) |
time | the offsets of the monotonic and boot clocks | clocks 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 */
/* ... */
};
| Field | Meaning |
|---|---|
struct pid | What task_struct.thread_pid points to: the identity of a task in every PID namespace that can see it. |
numbers | An 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_reaper | The first process of the namespace. Orphans of the namespace are given to it (chapter 3). |
parent, level | PID namespaces form a tree, and level is the depth in it. |
user_ns | Every namespace is owned by a user namespace. A process needs privilege in the owner to change the namespace. |
nsproxy, and its numbers in each PID namespace.Entering a namespace
A process gets into a namespace in 3 ways.
| Call | Effect |
|---|---|
clone with flags such as CLONE_NEWUTS | creates a child that starts in new namespaces of the given kinds |
unshare with the same flags | moves 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 |
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.
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
| Output | Conclusion |
|---|---|
| ordinary child: same namespaces | A child inherits the namespaces of its parent. |
ordinary child: sethostname fails with EPERM | Changing the host name requires privilege, and the user has none. |
| new namespaces: different UTS and PID namespace | The flags gave the child its own instances. |
sethostname succeeds, and the host name outside is unchanged | The child is privileged over its own UTS namespace, and the change stays inside it. |
| pid inside is 1 | The first process of a PID namespace has number 1 there. |
| uid inside is 0, mapped to 1000 outside | User 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.
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.
Low-level design: nsmodel
| Type | Fields | Meaning |
|---|---|---|
pidNamespace | parent, level, last | a namespace, its depth below the root, and the last number it gave out |
utsNamespace | hostname | the host name of its processes |
process | pid, uts, numbers | its namespaces; its number in each PID namespace from the root down |
| Operation | Behaviour |
|---|---|
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"
| Line | What 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 5 | The root namespace sees every process. |
| B seen from A: not visible | A namespace cannot see a namespace beside it. |
| the shell seen from A: not visible | A namespace cannot see the namespace above it. |
| the nested process seen from A: pid 3 | A namespace sees the namespaces below it. |
| the host name line | The 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
| Command | Shows |
|---|---|
ls -l /proc/<pid>/ns | the namespaces of a process |
lsns | every namespace of the system, with the number of processes in it |
cat /proc/<pid>/uid_map | the user mapping of the user namespace of a process |
unshare --user --map-root-user --uts --pid --fork sh | starts a shell in new namespaces, as nsfacts does |
nsenter --target <pid> --uts --pid sh | starts a shell in the namespaces of another process, with setns |
cat /proc/sys/user/max_user_namespaces | how 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.
cloneflags andunsharecreate 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
namespaces(7),user_namespaces(7),pid_namespaces(7)andunshare(2), Linux manual pages: https://man7.org/linux/man-pages/man7/namespaces.7.html- M. Kerrisk, Namespaces in operation, LWN.net, 2013: https://lwn.net/Articles/531114/
kernel/nsproxy.c,kernel/pid_namespace.candkernel/user_namespace.c, Linux 5.15: https://elixir.bootlin.com/linux/v5.15/source/kernel/nsproxy.c