ADR-016, from docs/DECISIONS.md in the QuantyKube repository.
Status In force
src/image.rs applies OCI image layers to a directory, with QuantyCore's
streaming gzip and tar readers underneath (its ADR-007). A layer is
input from a registry, and the classic way to break out of a container
is a layer that escapes the directory it is unpacked into.
.. in a path is the easy half, refused outright. The hard half is
symlinks: a lower layer creates a -> /etc, a higher one writes
a/passwd, and an unpacker that lets the kernel follow the link writes
the host's file. Refusing to write through symlinks is not an option,
because real images depend on it: on any distribution with a merged
/usr, /lib is a symlink and later layers write through it. So every
parent component is resolved by this code, inside the root: an absolute
target starts over at the root, .. stops at it, and more than 40 links
is an error, the kernel's own limit. Only the last component is created
by a syscall, never followed. A test builds exactly that attack with GNU
tar and fails if a single file lands outside; it was shown to fail when
resolution was handed back to the kernel.
Whiteouts follow the OCI image spec: .wh.name removes name from the
layers below, and .wh..wh..opq empties a directory of what lower
layers put there but not of what the same layer already wrote. Device
nodes are skipped: the runtime builds /dev itself.
Checked against real images, alpine, busybox and nginx with eight
layers, all fetched with verified digests: the unpacked alpine is
identical, file for file, to what GNU tar unpacks, and all three run in
quantykube sandbox.
The cost: a lookup per path component per entry, and modification times are set on files but not on directories, which no runtime reads.