The setns() Syscall: How docker exec Enters an Already-Running Container's Namespaces

2026-09-06

When you run docker exec -it mycontainer bash, a brand-new process on the host somehow ends up seeing the container's PID 1, its filesystem, its network interfaces, and none of the host's. It didn't fork from inside the container. It was spawned by dockerd on the host. The magic is setns(2) — a syscall that lets a process join an existing namespace instead of creating a new one.

Every namespace in Linux is represented as an inode under /proc/<pid>/ns/. Open one and you have a file descriptor that refers to that specific namespace instance. Pass that fd to setns() and the calling thread swaps its own namespace pointer for that one:

Concrete example. Suppose container init lives at host PID 4217:

int fd = open("/proc/4217/ns/net", O_RDONLY);
setns(fd, CLONE_NEWNET);   // this thread now sees container's interfaces
close(fd);
system("ip addr");         // shows eth0 inside container, not host's

One critical subtlety: setns() on a PID namespace only affects children created after the call, not the caller itself. The PID namespace is fixed at process creation because your PID is assigned at fork/clone time. So docker exec does setns() for mnt/net/ipc/uts, then fork()s, and the child inherits the PID namespace membership.

Order matters too. If you setns() into the target mount namespace before the user namespace, path resolution for /proc/<pid>/ns/user can fail because the path no longer exists in the new mount view. The convention: user ns first, then everything else, then exec into the target.

Rule of thumb: a namespace file descriptor keeps that namespace alive even if every process inside it exits. That's how ip netns add foo works — it bind-mounts /proc/self/ns/net to /var/run/netns/foo, pinning the namespace so you can enter it later with no processes in it.

Cost: setns() is roughly a few microseconds per namespace — it's just pointer swaps in the task_struct's nsproxy, no TLB flush, no page table change. Cheap enough that container tooling does it on every exec.

Key Takeaway: setns() turns any namespace into a file descriptor you can join — but the PID namespace switch only takes effect for children you fork after the call, which is why docker exec always ends in a fork before your shell.

All newsletters