Skip to content
Menu

Virtualization9 min read

Namespace Theory & Core Types

The isolation primitive — giving each process its own view of the system

What Namespaces Are

Namespaces are a Linux kernel feature that partitions kernel resources so that one set of processes sees one set of resources, while another set of processes sees a completely different set. Each process lives in exactly one namespace of each type.

Key Properties

  • Namespaces isolate views, not resources themselves
  • Each process belongs to exactly one namespace per type
  • Child processes inherit their parent's namespaces by default
  • Modern Linux has 8 namespace types
  • Each namespace is visible as a file descriptor in /proc/[pid]/ns/

What They Are NOT

  • Not security boundaries on their own — they isolate views, not access
  • Not VMs — all namespaces share the same kernel
  • Not cgroups — namespaces isolate visibility, cgroups limit usage
  • Not containers — a container is namespaces + cgroups + filesystem + security policies combined

Warning

Security warning: A namespace alone does not prevent a process from escaping. A process in a new PID namespace but still in the host mount namespace can read /proc on the host. You need multiple namespaces + seccomp + capabilities + cgroups together for real isolation.

Conceptual Model: Same Kernel, Different Views

Hardware
Linux Kernel (shared)
Namespace Set A

PID 1: nginx
Hostname: web-01
Mounts: /app, /etc/nginx
Network: 10.0.0.2/24

Namespace Set B

PID 1: postgres
Hostname: db-01
Mounts: /var/lib/postgres
Network: 10.0.0.3/24

Namespace Set C (Host)

PID 1: systemd
Hostname: prod-server
Mounts: / (full tree)
Network: 192.168.1.10/24

The Three Syscalls

All namespace operations boil down to three system calls. Everything else (docker run, unshare CLI, ip netns) is a wrapper around these.

clone()

Creates a new process in new namespace(s). Pass CLONE_NEW* flags to specify which namespaces to create.

bash
// Create child in new PID + mount NS
clone(child_fn, stack,
  CLONE_NEWPID | CLONE_NEWNS | SIGCHLD,
  arg);

This is what container runtimes use under the hood.

unshare()

Moves the current process into new namespace(s). No new process is created.

bash
// Current process gets new mount NS
unshare(CLONE_NEWNS);

The unshare command-line tool wraps this syscall.

setns()

Joins an existing namespace by opening its fd from /proc/[pid]/ns/xxx.

c
// Join PID 1234's network NS
int fd = open(
  "/proc/1234/ns/net", O_RDONLY);
setns(fd, CLONE_NEWNET);

nsenter and docker exec use this.

unshare CLI Example

console
console

    # Create new PID + mount namespaces and fork into them

    $ sudo unshare --pid --mount --fork bash


    # --pid:   new PID namespace

    # --mount: new mount namespace (needed to remount /proc)

    # --fork:  fork so the new shell is PID 1 in the new PID NS


    # Inside the new namespace, mount a fresh /proc

    $ mount -t proc proc /proc


    # Now ps only shows processes in this namespace

    $ ps aux

    USER  PID %CPU %MEM   VSZ  RSS TTY STAT START TIME COMMAND

    root    1  0.0  0.0  7236 3968 pts/0 S 12:00 0:00 bash

    root    2  0.0  0.0 10072 3312 pts/0 R+ 12:00 0:00 ps aux
  

/proc/[pid]/ns/ — Namespace File Descriptors

Every namespace instance is represented as a pseudo-file (actually a symlink) in /proc/[pid]/ns/. The symlink target includes the namespace type and an inode number that uniquely identifies the namespace instance.

console
console

    # List all namespace fds for PID 1 (init/systemd)

    $ ls -la /proc/1/ns/

    lrwxrwxrwx 1 root root 0 Mar 31 12:00 cgroup -> 'cgroup:[4026531835]'

    lrwxrwxrwx 1 root root 0 Mar 31 12:00 ipc -> 'ipc:[4026531839]'

    lrwxrwxrwx 1 root root 0 Mar 31 12:00 mnt -> 'mnt:[4026531840]'

    lrwxrwxrwx 1 root root 0 Mar 31 12:00 net -> 'net:[4026531992]'

    lrwxrwxrwx 1 root root 0 Mar 31 12:00 pid -> 'pid:[4026531836]'

    lrwxrwxrwx 1 root root 0 Mar 31 12:00 pid_for_children -> 'pid_for_children:[4026531836]'

    lrwxrwxrwx 1 root root 0 Mar 31 12:00 time -> 'time:[4026531834]'

    lrwxrwxrwx 1 root root 0 Mar 31 12:00 user -> 'user:[4026531837]'

    lrwxrwxrwx 1 root root 0 Mar 31 12:00 uts -> 'uts:[4026531838]'
  

Checking if Two Processes Share a Namespace

console
console

        # Same inode = same namespace

        $ readlink /proc/1/ns/net

        net:[4026531992]


        $ readlink /proc/1234/ns/net

        net:[4026531992]

        # Same! Both in host network NS


        $ readlink /proc/5678/ns/net

        net:[4026532456]

        # Different! PID 5678 is in a container
      

nsenter: Joining a Process's Namespaces

console
console

        # Enter ALL namespaces of PID 1234

        $ nsenter -t 1234 -a bash


        # Enter only network namespace

        $ nsenter -t 1234 -n ip addr


        # Enter network + mount namespaces

        $ nsenter -t 1234 -n -m bash


        # This is what "docker exec" does

        # internally (via setns syscall)
      

Mount Namespace (mnt) — CLONE_NEWNS

The first namespace type ever added to Linux (2.4.19, 2002). It was so new the flag is just CLONE_NEWNS ("new namespace") rather than CLONE_NEWMNT because nobody imagined there'd be more types.

What It Isolates

Each mount namespace has its own mount table — an independent list of mount points. Mounting or unmounting filesystems in one namespace has no effect on other namespaces (assuming private propagation).

  • Independent /, /tmp, /proc, /sys, /dev
  • Container gets its own rootfs (from the image) without affecting the host
  • Bind mounts, tmpfs mounts, overlay mounts — all per-namespace

pivot_root vs chroot

chroot (weak)
  • Changes the apparent root directory for a process
  • Original root is still accessible — easy to escape
  • A process with CAP_SYS_CHROOT can break out by creating a new chroot and using .. traversal
  • Does NOT change the mount table
  • Meant for build environments, not security
console
console

          # Classic chroot escape

          $ chroot /newroot /bin/bash

          # Process can still access

          # host root via fd tricks
        
pivot_root (proper isolation)
  • Swaps the root mount — old root becomes a subdirectory
  • Old root can be unmounted — truly inaccessible
  • Requires a mount namespace (can't affect host mounts)
  • This is what container runtimes actually use
  • No escape path once old root is unmounted
console
console

          # pivot_root swaps root mount

          $ pivot_root /newroot /newroot/.old

          $ umount -l /.old

          # Old root is gone. No escape.
        

Mount Propagation

When namespaces share mount points, mount propagation controls whether mounts in one namespace appear in another. There are four modes:

Mode Behavior Use Case
shared Mounts propagate both ways between namespaces Host systemd mounts, initial setup
private Mounts never propagate — fully independent Default for containers
slave Mounts propagate one way (host → container, not back) Container sees new host mounts but can't leak its own
unbindable Like private, but also can't be bind-mounted Prevent recursive bind mount explosions
console
console

    # Check propagation type of a mount

    $ findmnt -o TARGET,PROPAGATION

    TARGET         PROPAGATION

    /              shared

    /tmp           private

    /run/user/1000 shared


    # Make a mount private

    $ mount --make-private /mnt/data


    # Make a mount shared

    $ mount --make-shared /mnt/data
  

PID Namespace (pid) — CLONE_NEWPID

PID namespaces give each container its own process ID numbering starting at 1. The first process in a PID namespace becomes PID 1 and has special responsibilities.

Nested PID View

A process has a PID in every ancestor namespace. The host always sees the "real" PID; the container sees its own numbering:

Host PID Namespace

Host view (ps aux on host):

console
bash

          PID   COMMAND

            1   systemd

          856   containerd

          12345 nginx: master

          12346 nginx: worker

          12400 postgres
        
Container A PID NS

Container view (ps aux inside):

console
bash

          PID COMMAND

            1 nginx: master

            2 nginx: worker
        

PID 1 inside = PID 12345 outside

Container B PID NS

Container view (ps aux inside):

console
bash

          PID COMMAND

            1 postgres
        

PID 1 inside = PID 12400 outside

The PID 1 Problem: Zombie Reaping

Why PID 1 Matters in Containers

In Linux, PID 1 has a special kernel-level responsibility: it adopts orphaned child processes and must call wait() on them. If it doesn't, zombie processes accumulate.

  • On a normal host, systemd/init handles this — it's designed for it
  • In a container, your app (nginx, node, python) is often PID 1 — but most apps don't reap children
  • Zombies consume PID space. Enough of them and the container can't spawn new processes
  • PID 1 also gets different signal handling: SIGTERM and SIGINT are not delivered unless the process explicitly registers a handler
Without Init (zombie risk)
console
# Dockerfile
CMD ["node", "server.js"]

# node is PID 1
# If it spawns child processes
# (e.g., child_process.exec),
# orphans become zombies
With tini / dumb-init
console
# Dockerfile
RUN apt-get install -y tini
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["node", "server.js"]

# tini is PID 1, reaps zombies
# node runs as PID 2
# Signals are forwarded properly

Tip

Docker --init: You can also use docker run --init myimage which injects tini as PID 1 automatically, without modifying the Dockerfile. Kubernetes doesn't have a built-in equivalent — bake tini into the image or use the shareProcessNamespace feature (where the kubelet becomes PID 1).

Demo: PID Namespace in Action

console
console

    # Create new PID + mount namespaces

    $ sudo unshare --pid --fork --mount-proc bash


    # --mount-proc remounts /proc so ps works correctly

    # We are now PID 1 inside the new namespace


    $ ps aux

    USER  PID %CPU %MEM   VSZ  RSS TTY STAT START TIME COMMAND

    root    1  0.0  0.0  7236 3968 pts/0 S 12:00 0:00 bash

    root    2  0.0  0.0 10072 3312 pts/0 R+ 12:00 0:00 ps aux


    $ echo $$

    1


    # Meanwhile, on the host, this bash has a normal PID (e.g., 54321)
  

UTS Namespace (uts) — CLONE_NEWUTS

The simplest namespace type. UTS stands for "UNIX Time-Sharing" — named after the struct utsname in the kernel (returned by the uname() syscall).

What It Isolates

  • Hostname (nodename) — each container has its own hostname
  • NIS domain name (domainname) — rarely used in containers
  • Setting hostname inside a container does not affect the host
  • This is why docker run --hostname myapp works — it creates a new UTS namespace
console
console

    # Create a new UTS namespace

    $ sudo unshare --uts bash


    # Change hostname inside the namespace

    $ hostname container-web-01

    $ hostname

    container-web-01


    # Open another terminal on the host

    $ hostname

    prod-server

    # Host hostname is unaffected
  

Tip

Kubernetes note: Each pod gets its own UTS namespace. The pod's hostname defaults to the pod name. You can override it with spec.hostname and spec.subdomain in the pod spec.

Note

Network namespaces are covered in detail in Net 09 — Namespaces & Virtual Interfaces. That page covers CLONE_NEWNET, veth pairs, bridges, and how container networking is built on top of network namespaces.

Summary: Core Namespace Lifecycle

  1. clone() / unshare()

    Create new namespace(s) with CLONE_NEW* flags

  2. Process runs isolated

    Own PID tree, mount table, hostname, etc.

  3. /proc/[pid]/ns/*

    NS persists as long as any process or fd references it

  4. setns() / nsenter

    Other processes can join via fd

Solidnines — solidnines.com