$RodHat_
Console Tips

setns and unshare: moving between Linux namespaces

Published by · Updated

setns and unshare: moving between Linux namespaces
Photo: AI-generated — no human photographer / RodHat AI Cover

Linux namespaces are the kernel feature underneath every container runtime you have ever run, cursed at, or pretended to understand. Each namespace type wraps one resource view: mount points, PID trees, network stacks, UTS (hostname/domainname), IPC objects, cgroups, clocks. A process lives in one of each. Two syscalls control how it gets there: unshare(2) carves off a private copy for the calling process, and setns(2) moves a thread into a namespace that already exists somewhere else.

unshare: fork your current namespace

unshare(int flags) accepts CLONE_NEW* flags and creates a new namespace of each requested type, moving the calling thread into the fresh copy. The original namespace still exists; other processes in it are unaffected. You need CAP_SYS_ADMIN for most namespace types, with one important exception covered below.

#define _GNU_SOURCE
#include <sched.h>
#include <err.h>

/* new mount and UTS namespaces for this process */
if (unshare(CLONE_NEWNS | CLONE_NEWUTS) < 0)
    err(1, "unshare");

/* from here: mounts and hostname changes are private */

The flags map to namespace types:

  • CLONE_NEWNS (mount, Linux 2.4.19) — the original namespace type
  • CLONE_NEWUTS (Linux 2.6.19) — hostname and domainname
  • CLONE_NEWIPC (Linux 2.6.19) — SysV IPC and POSIX message queues
  • CLONE_NEWNET (Linux 2.6.24) — network stack
  • CLONE_NEWPID (Linux 3.8) — PID numbering
  • CLONE_NEWUSER (Linux 3.8) — UID/GID mappings
  • CLONE_NEWCGROUP (Linux 4.6) — cgroup root view
  • CLONE_NEWTIME (Linux 5.6) — monotonic and boot clocks

The PID namespace gotcha that will bite you once

CLONE_NEWPID does not move the calling process into the new PID namespace. The caller stays in the old one. The next child it forks becomes PID 1 in the new namespace. This surprises everyone, including me, the first time.

if (unshare(CLONE_NEWPID) < 0)
    err(1, "unshare CLONE_NEWPID");

/* this process is still in the parent PID namespace */

pid_t child = fork();
if (child == 0) {
    /* this child is PID 1 in the new namespace */
    /* mount a new procfs or ps(1) will lie */
    if (mount("proc", "/proc", "proc", 0, NULL) < 0)
        err(1, "mount /proc");
    execvp(argv[0], argv);
}

If you forget to mount a new /proc before running anything that reads it, you will get PID numbers from the parent namespace, which defeats the purpose. setns(2) to a PID namespace has the same behavior: the calling thread stays put, only future children land in the target namespace.

setns: join an existing namespace

setns(int fd, int nstype) moves the calling thread into the namespace that fd refers to. Pass 0 for nstype to skip the type check; pass a specific CLONE_NEW* flag to require it. The fd comes from one of two places:

/proc/<pid>/ns/<type> for any running process whose namespace you can open. The types are mnt, uts, ipc, net, pid, user, cgroup, time. Hold the fd open and the namespace stays alive even if every process in it exits.

pidfd_getfd(2) if you have a pidfd for the target. This needs ptrace-attach rights over the target process; see pidfd_getfd.

#include <fcntl.h>
#include <sched.h>
#include <err.h>
#include <stdio.h>

/* join a container's network namespace by PID */
char path[64];
snprintf(path, sizeof path, "/proc/%d/ns/net", target_pid);

int nsfd = open(path, O_RDONLY | O_CLOEXEC);
if (nsfd < 0)
    err(1, "open ns/net");

if (setns(nsfd, CLONE_NEWNET) < 0)
    err(1, "setns");

close(nsfd);

/* now in target_pid's network namespace */

One requirement people hit: you cannot join a PID namespace if your thread has already created threads since unshare(CLONE_NEWPID). The kernel refuses with EINVAL. Single-threaded is safe. For multithreaded programs, do namespace setup before spawning worker threads, or fork a single-threaded helper.

User namespaces: unprivileged isolation

CLONE_NEWUSER is the exception to the CAP_SYS_ADMIN requirement. Unprivileged processes can call unshare(CLONE_NEWUSER), write UID and GID mappings to /proc/self/uid_map and /proc/self/gid_map, and then own root inside the new user namespace. This is the mechanism under rootless containers and newuidmap/newgidmap.

The catch: you can only write one uid_map entry from inside the new namespace unless you have CAP_SETUID in the parent, which is why the setuid helpers exist. Distros ship them at /usr/bin/newuidmap and /usr/bin/newgidmap.

Security note: user namespaces have historically produced a steady supply of local privilege escalation CVEs. Many distributions allow CLONE_NEWUSER for unprivileged users (you can check sysctl kernel.unprivileged_userns_clone), but hardened deployments disable it.

Keeping a namespace alive without a process

Open the /proc/<pid>/ns/<type> file and keep the fd. The namespace lives as long as the fd is open, regardless of what happens to the processes inside. This is how container runtimes hold network namespaces across exec. Bind-mount the fd to a path (mount --bind /proc/$$/ns/net /run/netns/foo) and it survives until explicitly unmounted, which is exactly what ip netns add does.

BSD comparison

FreeBSD has jail(2) (FreeBSD 4.0), which wraps filesystem, networking, and process visibility in a single coarser-grained call. You get a jail ID, not a per-resource namespace. More opinionated, less composable. OpenBSD has neither jails nor namespaces; process isolation there is pledge/unveil and chroot. The composable-namespaces approach is a Linux invention, for better or worse.

The “for worse” part: assembling seven separate namespaces, writing UID maps, bind-mounting /proc, and handling the PID 1 requirement is why container runtimes exist. It is not something you want to do manually more than once. unshare(1) (util-linux) and nsenter(1) wrap the syscalls for shell use. The kernel primitives are unshare(2) and setns(2). Now you know which layer broke.

Sources

  1. setns(2) man page (man7.org)
  2. unshare(2) man page (man7.org)