Skip to content
Menu

Virtualization8 min read

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)
  • POSIX message queues (/dev/mqueue)
Host IPC Namespace
console
console

          $ 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
        
Container IPC Namespace
console
console

          $ 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

Host

UIDs: 0 (real root), 1000 (your user), 100000-165535 (mapped range)

User Namespace

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:

console
# 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
console
console

    # 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_ADMIN inside 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.

Without Cgroup NS (host view leaked)
console
console

          # 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
        
With Cgroup NS (isolated view)
console
console

          # 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

console
console

        # 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:

  1. clone() with ALL namespace flags

    Runtime calls clone() with CLONE_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.

  2. 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.

  3. 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.

  4. 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.

  5. 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).

  6. 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.

  7. 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 docker0 or cni0). IP addresses are assigned, routes are set up. In Kubernetes, this is where CNI plugins run (Calico, Cilium, Flannel).

  8. 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 like CAP_NET_BIND_SERVICE is retained.

  9. 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

  1. clone()

    New namespaces created

  2. Cgroups

    Resource limits applied

  3. pivot_root

    Rootfs from image

  4. Mount /proc /sys

    Essential pseudo-fs

  5. Network + Security

    veth, seccomp, caps

  6. exec(entrypoint)

    Container is running

What a "Container" Actually Is

Hardware (CPU, RAM, Disk, NIC)
Linux Kernel (shared by all containers)
Container = Namespace Bundle
PID NS

PID 1: app

NET NS

eth0: 10.0.0.2

MNT NS

/ from image

UTS NS

hostname: app-1

Cgroup

512Mi mem, 0.5 CPU

Seccomp + Caps

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:

console
console

    # 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.

Solidnines — solidnines.com