Skip to content

getpid · getppid

A namespace is a view, not a boundary

A process does not live inside a namespace. It holds one per kind, and every question it asks is answered through them — which is why a container is a process whose views have been narrowed rather than a thing with walls.

Five views, held independently

There are five kinds that matter here: pid, mount, net, user and uts. A process holds one of each, always — there is no such thing as being in no namespace, only being in the host's.

They are independent. A process can have its own pid namespace and share the host's network, or have its own mounts and the host's pids, in any combination. Every container runtime picks a combination, and almost every surprise about containers is a combination somebody did not realise they had chosen.

Nothing is walled off. A namespace narrows what a question returns; it does not put the process anywhere.

Two processes share a namespace exactly when /proc/<pid>/ns/pid reads the same string. That is how nsenter works, and how every container tool decides what to join.

Try this
#include <sched.h>
#include <unistd.h>

int main(void) {
  char before[64];
  char after[64];

  gethostname(before, 64);
  printf("before %s\n", before);

  unshare(CLONE_NEWUTS);
  sethostname("container", 9);

  gethostname(after, 64);
  printf("after %s\n", after);
  return 0;
}

Watch: The hostname changes here and nowhere else. The smallest namespace there is, doing exactly what all five do.

a-container-is-not-a-box

Before you run it

A process in a new pid namespace calls getpid and gets 1.

Predict

Is it really pid 1?

a-container-is-not-a-box

`unshare --pid` on its own does nothing

unshare moves the calling process into the new namespaces it makes — except for the pid namespace, which it cannot, because a process cannot change its own pid.

So the new pid namespace is what the next child is born into, and a program that unshares one and does not fork has changed nothing it can observe. That single exception is why unshare --pid without --fork appears to be broken, and why every container runtime forks immediately after.

Try this
#include <sched.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void) {
  unshare(CLONE_NEWPID);
  printf("parent %d\n", getpid());

  pid_t pid = fork();
  if (pid == 0) {
    printf("child %d\n", getpid());
    _exit(0);
  }

  int status;
  waitpid(pid, &status, 0);
  return 0;
}

Watch: Which of the two processes ended up in the new namespace, and which did not.

a-container-is-not-a-box

Visibility runs one way

A parent pid namespace sees every process in its children. A child sees nothing above it. That asymmetry is the whole of the isolation: a container cannot signal the host because it cannot name anything on it, and the host can signal anything in the container because it can name all of it.

It is also why kill 1 means two completely different things depending on where you type it. Inside, it is the container's own init. Outside, it is the machine's.

a-container-is-not-a-box

Root in the container is not root on the host

A user namespace carries a mapping: uid 0 inside might be uid 100000 outside. The process holds every capability against its own namespace's resources and none at all against the host's, and that sentence is the whole of what makes an unprivileged container possible.

A uid with no mapping is not zero — it is nobody, and it can do nothing. Files owned by an unmapped user are why a bind-mounted directory in a rootless container so often turns up owned by a number nobody recognises.

a-container-is-not-a-box

Trace

Run one of the programs on the left.

Everything you do here stays in this browser.Part of liter8.sh