Advanced Types & Container Birth
IPC, User, Cgroup, Time namespaces — and how they combine to create a container
IPC Namespace — CLONE_NEWIPC
IPC namespaces isolate inter-process communication resources. Processes in different IPC namespaces cannot see or interact with each other's IPC objects.
What It Isolates
- System V IPC objects:
- Shared memory segments (
shmget,shmat) - Semaphore sets (
semget,semop) - Message queues (
msgget,msgsnd,msgrcv)
- Shared memory segments (
- POSIX message queues (
/dev/mqueue)
$ ipcs
------ Shared Memory Segments ------
key shmid owner perms bytes
0x00000000 0 root 644 80
0x00000000 1 root 644 16384
0x00000000 2 www 600 65536
------ Semaphore Arrays ------
key semid owner perms nsems
0x0052e2c1 0 postgres 600 17
$ ipcs
------ Shared Memory Segments ------
key shmid owner perms bytes
------ Semaphore Arrays ------
key semid owner perms nsems
# Empty! Container can't see
# host IPC objects
Note
Kubernetes pod-level IPC: By default, containers in the same pod share an IPC namespace. This allows PostgreSQL shared memory, semaphores between sidecars, etc. You can disable this with shareProcessNamespace: false in the pod spec.
User Namespace — CLONE_NEWUSER (the game-changer)
User namespaces are the most security-critical namespace type. They enable UID/GID remapping: a process can appear to be root (UID 0) inside the namespace while actually running as an unprivileged user (e.g., UID 100000) on the host.
The Core Idea: UID Mapping
UIDs: 0 (real root), 1000 (your user), 100000-165535 (mapped range)
UID 0 (root inside) → maps to UID 100000 (nobody outside)
UID 1 (inside) → maps to UID 100001 (outside)
...
UID 65535 (inside) → maps to UID 165535 (outside)
The Mapping Files
UID and GID mappings are defined in /proc/[pid]/uid_map and /proc/[pid]/gid_map. The format is:
# Format: <inside-start> <outside-start> <count>
# /proc/[pid]/uid_map
0 100000 65536
# UID 0 inside = UID 100000 outside
# UID 1 inside = UID 100001 outside
# ...up to 65536 UIDs mapped
# /proc/[pid]/gid_map — same format for GIDs
0 100000 65536
# Check a container's UID mapping
$ cat /proc/12345/uid_map
0 100000 65536
# Inside the container
$ id
uid=0(root) gid=0(root) groups=0(root)
# But on the host, this process runs as UID 100000
$ ps -o user,pid,comm -p 12345
USER PID COMMAND
100000 12345 bash
Why This Is a Game-Changer: Rootless Containers
Without User Namespaces (traditional Docker)
- Docker daemon runs as real root on the host
- Container processes run as real root (UID 0)
- A container escape = full root access to the host
- Must trust the container image completely
With User Namespaces (Podman, rootless Docker)
- Container runtime runs as your regular user
- Container root (UID 0) maps to unprivileged UID (e.g., 100000)
- A container escape = unprivileged user access
- No SUID binaries, no real root — much smaller attack surface
Tip
Podman: Runs containers rootless by default using user namespaces. No daemon, no root required. The podman unshare command lets you enter the user namespace to debug UID mapping issues. /etc/subuid and /etc/subgid define the allocated UID/GID ranges per user.
Security Properties
Namespace Ownership & Capabilities
- User namespaces must be created first — other namespaces are then owned by the user namespace
- A process with
CAP_SYS_ADMINinside a user NS has that capability only within the namespace - Root inside the user NS cannot:
- Load kernel modules
- Access host devices (unless explicitly mapped)
- Modify host files owned by other UIDs
- Change host network configuration
- Capabilities are relative to the namespace — not absolute on the host
Cgroup Namespace — CLONE_NEWCGROUP (Linux 4.6, 2016)
Cgroup namespaces give a process a relative view of its cgroup hierarchy, hiding the host's cgroup tree structure.
# Inside container, full path visible
$ cat /proc/self/cgroup
0::/system.slice/docker-abc123def456.scope
# Container can see:
# - It's running under Docker
# - Its container ID
# - The host's cgroup hierarchy
# Inside container, relative path
$ cat /proc/self/cgroup
0::/
# Container sees / as its cgroup root
# Can't navigate up to host cgroups
# Can't discover other containers
Key Points
- Prevents the container from seeing or navigating the host's cgroup tree
- The container's cgroup root appears as
/rather than its real path - Created automatically by Docker and containerd for all containers
- Important for multi-tenant environments — one container can't enumerate other containers via cgroup paths
- Works with both cgroups v1 and cgroups v2
Time Namespace — CLONE_NEWTIME (Linux 5.6, 2020)
The newest namespace type. It allows a container to have a different system uptime than the host by adding offsets to monotonic and boot-time clocks.
What It Isolates
CLOCK_MONOTONIC— time since some arbitrary point (used for measuring intervals)CLOCK_BOOTTIME— time since boot (including time spent suspended)- Does NOT affect
CLOCK_REALTIME(wall clock / date — that's too dangerous to namespace)
Use Case: Live Migration
When a container is live-migrated from Host A (up 30 days) to Host B (up 2 days), without time namespace the container would see uptime jump from 30 days to 2 days. With time NS, the offset preserves the container's perceived uptime.
Offset Configuration
# Offsets file for a process
$ cat /proc/[pid]/timens_offsets
monotonic 86400 0
boottime 86400 0
# 86400 seconds = 1 day offset
# Container sees 1 day more uptime
All 8 Linux Namespace Types
| Namespace | Flag | Isolates | Since |
|---|---|---|---|
| Mount | CLONE_NEWNS |
Mount points, filesystem tree | 2.4.19 (2002) |
| UTS | CLONE_NEWUTS |
Hostname, NIS domain name | 2.6.19 (2006) |
| IPC | CLONE_NEWIPC |
SysV IPC, POSIX message queues | 2.6.19 (2006) |
| PID | CLONE_NEWPID |
Process IDs | 2.6.24 (2008) |
| Network | CLONE_NEWNET |
Network stack (interfaces, routes, iptables, sockets) | 2.6.29 (2009) |
| User | CLONE_NEWUSER |
UIDs, GIDs, capabilities | 3.8 (2013) |
| Cgroup | CLONE_NEWCGROUP |
Cgroup root directory view | 4.6 (2016) |
| Time | CLONE_NEWTIME |
CLOCK_MONOTONIC, CLOCK_BOOTTIME offsets | 5.6 (2020) |
Note
Timeline observation: It took 18 years (2002-2020) to complete all 8 namespace types. Mount came first as part of early Linux isolation experiments. User namespaces took the longest to stabilize (first patches in 2.6.23, but not considered safe until 3.8). Time was the last addition, primarily motivated by container live migration.
How a Container Is Born — Putting It All Together
A container is not a kernel primitive — it's a combination of namespaces, cgroups, filesystem isolation, and security policies. Here's the exact sequence that a container runtime (like runc) follows:
- clone() with ALL namespace flags
Runtime calls
clone()withCLONE_NEWNS | CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWUSER | CLONE_NEWCGROUP. The child process is now in fresh namespaces but still has the host's filesystem. - Child process is in new namespaces
At this point the child can see its own PID as 1, has an empty network stack, a fresh IPC space, its own hostname — but the filesystem is still the host's.
- Set up cgroups for resource limits
The parent process places the child into a cgroup with configured limits: CPU shares/quota, memory limit, PIDs limit, block I/O weight. This happens via writing the child's PID to
/sys/fs/cgroup/.../cgroup.procs. - pivot_root to the container's rootfs
The container image (pulled from a registry) is unpacked to a directory. The child calls
pivot_root()to make this directory the new root, then unmounts the old root. The host filesystem is no longer accessible. - Mount /proc, /sys, /dev inside the container
Essential pseudo-filesystems are mounted:
/proc(process info, respects PID NS),/sys(kernel/device info, read-only),/dev(device nodes, minimal set: null, zero, random, urandom, tty). - Set hostname (UTS NS)
The container's hostname is set via
sethostname(). This only affects the container's UTS namespace — the host hostname is untouched. - Configure networking (Net NS)
A veth pair is created: one end goes into the container's network namespace, the other stays in the host (usually attached to a bridge like
docker0orcni0). IP addresses are assigned, routes are set up. In Kubernetes, this is where CNI plugins run (Calico, Cilium, Flannel). - Apply seccomp filters and drop capabilities
A seccomp BPF profile is loaded to restrict which syscalls the process can make (e.g., block
mount,reboot,kexec_load). Most Linux capabilities are dropped — only a minimal set likeCAP_NET_BIND_SERVICEis retained. - exec() the container's entrypoint
Finally, the child process calls
execve()to replace itself with the container's entrypoint (e.g.,/bin/sh,nginx,python app.py). This process becomes PID 1 in the container's PID namespace. The container is now running.
Visual: Container Lifecycle Flow
clone()
New namespaces created
Cgroups
Resource limits applied
pivot_root
Rootfs from image
Mount /proc /sys
Essential pseudo-fs
Network + Security
veth, seccomp, caps
exec(entrypoint)
Container is running
What a "Container" Actually Is
PID 1: app
eth0: 10.0.0.2
/ from image
hostname: app-1
512Mi mem, 0.5 CPU
Restricted syscalls, minimal caps
Note
This is essentially what runc create + runc start does. runc reads an OCI runtime spec (config.json) that declares all namespaces, cgroup limits, mounts, and security settings, then executes the steps above. We'll see the OCI spec in detail in Virt 06 — OCI Runtime Spec.
Practical: Creating a "Container" by Hand
To solidify the concept, here's how you'd manually create something container-like using just unshare and standard Linux tools:
# Step 1: Create all namespaces (except network, which needs extra setup)
$ sudo unshare --pid --mount --uts --ipc --fork bash
# Step 2: Set a hostname (UTS namespace)
$ hostname my-container
$ hostname
my-container
# Step 3: Remount /proc for the new PID namespace
$ mount -t proc proc /proc
# Step 4: Verify isolation
$ 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 3 0.0 0.0 10072 3312 pts/0 R+ 12:00 0:00 ps aux
$ ipcs
------ Shared Memory Segments ------
(empty)
# This is ~80% of what Docker does. Missing: rootfs,
# network namespace + veth, cgroups, seccomp, user NS
Tip
Key takeaway: A container is not a single kernel feature. It's a bundle of kernel features (8 namespace types + cgroups + seccomp + capabilities + filesystem isolation) orchestrated by a runtime. Understanding each piece individually makes debugging containers dramatically easier — you can nsenter into specific namespaces, inspect cgroup limits, check seccomp profiles, etc.