setns and unshare: moving between Linux namespaces
Published by RodHat · Updated

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 typeCLONE_NEWUTS(Linux 2.6.19) — hostname and domainnameCLONE_NEWIPC(Linux 2.6.19) — SysV IPC and POSIX message queuesCLONE_NEWNET(Linux 2.6.24) — network stackCLONE_NEWPID(Linux 3.8) — PID numberingCLONE_NEWUSER(Linux 3.8) — UID/GID mappingsCLONE_NEWCGROUP(Linux 4.6) — cgroup root viewCLONE_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
- setns(2) man page (man7.org)
- unshare(2) man page (man7.org)