Step 1 of 5 · Reading · ~4 min
Read
Linux Namespaces
Mount Namespace
The second lie: what the filesystem looks like. A container sees an Ubuntu or Alpine root at / — while the host's real / is untouched and invisible. The mount namespace is how one kernel serves completely different filesystem views to different processes, and it's the oldest namespace in Linux (2002 — predating containers by a decade).
Mounts are a table, and the table can be private
Unix builds its single directory tree by mounting filesystems onto directories: / from one disk, /proc from the kernel, a USB stick at /media/usb. The kernel tracks these grafts in a mount table. A mount namespace (CLONE_NEWNS) gives a process group a private copy of that table: mounts and unmounts inside it edit the copy, not the original.
host mount table: container's table (a namespace copy):
/ ← disk / ← overlay of image layers
/proc ← procfs /proc ← a FRESH procfs (its own PIDs!)
/home ← disk2 /tmp ← tmpfs
Note /proc: last lesson promised this connection. Mounting a new procfs inside the container's mount+PID namespaces is what makes ps show the container's processes — the two namespaces cooperating to keep the story consistent. Container setup is largely this: enter fresh namespaces, then build a mount table that matches the lie you're telling.
chroot vs pivot_root: changing the floor
Giving the container its own / needs more than mounting — the process's idea of "root directory" must move into the image filesystem. The old tool is chroot ("pretend this directory is /") — and it's famously escapable: the real root is still in the mount table underneath, and privileged processes have documented tricks to climb back out. Real container runtimes use pivot_root: swap the mount table's root — new rootfs becomes /, the old root is moved to a subdirectory, and then — the crucial move — the old root is unmounted entirely. Nothing to escape to: the host filesystem isn't hidden from the container, it's absent from the container's table. That distinction — hidden vs absent — is the same security philosophy as PID namespaces, applied to files.
Bind mounts: deliberate holes in the wall
Total isolation is the default; sharing is opt-in. A bind mount grafts an existing directory to a second location — and across namespaces it's how docker run -v /host/data:/data works: the host directory appears in the container's mount table by explicit invitation. (Docker "volumes" are the same mechanism pointed at Docker-managed directories.) The security model reads directly off the table: a container can touch exactly the host paths that were bind-mounted in — the rest isn't locked, it's unlisted.
The kernel also tracks propagation between related mount tables (whether a new mount in one namespace echoes into another — private vs shared). File the term; it's the answer to advanced puzzles like "why did my new host mount not appear in the running container."
Why the exercise is a simulator
CLONE_NEWNS needs CAP_SYS_ADMIN, and so does mount(2). The sandbox that runs your submissions has neither, so nothing here can create a real mount namespace — the attempt returns EPERM immediately. What you can do is build the thing the kernel actually keeps: a list of mounts per namespace, copied on creation and edited independently afterwards. Every property this lesson describes — private views, bind mounts, pivot_root making the host absent — is a consequence of that list and nothing else.
If the exercise feels a long way from docker run, follow one command back: docker run -v /host/data:/data appends exactly one (source, target) row to exactly one of these tables, and the container's whole filesystem reality is the table you get after that append. You are not writing a toy next to Docker; you are writing the part of Docker that is not the CLI.
Your exercise: Mount Namespace Simulator
| The kernel does | Your simulator does |
|---|---|
clone(CLONE_NEWNS) calls copy_mnt_ns() — a copy of the parent's mount list | NEWNS — snapshot ns 0's rows into a new table; emit the id |
mount(src, dst, ...) inserts a mount; mounting onto an occupied target shadows what was there | MOUNT <ns> <src> <dst> — the row for <dst> is replaced, not duplicated |
umount(dst) detaches the mount at that target | UMOUNT <ns> <dst> — drop the row for <dst> |
reading /proc/self/mountinfo — what df and mount print | LISTMOUNTS <ns> — that table, sorted by target |
Two behaviours carry the whole idea. Copy, not reference: a mount added to ns 0 after NEWNS must not appear in the child, and a UMOUNT in ns 0 must not remove it from the child — if your tables share a list object the namespace has no meaning. Deepest/last wins: two mounts on the same target leave one visible mount, the newer one, exactly as the kernel stacks and hides. Get both and df inside a container reads as a dump of your own data structure.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…