How an internal /proc/self/cwd file descriptor leak in libcontainer/init_linux.go allowed container processes to break out to the host filesystem across Docker and Kubernetes.
Imagine you are sent to a prison cell. The guard walks in, closes the heavy steel door, but forgets his master keychain hanging on the inside keyhole! You simply turn the key, walk through the door, and now you have the keys to every single cell and door in the entire prison fortress.
The attacker crafts a container image setting WORKDIR /proc/self/fd/7.
When runc initializes the container, internal file descriptor 7 still points to host /sys/fs/cgroup.
The container's entrypoint process uses ../../../../ relative traversal through the leaked handle.
The container process lands directly in the host root directory (`/`), overwriting host cron jobs and binaries.
Представлено на понятном высокоуровневом исходном коде (без ассемблера и бинарных дампов).
// VULNERABLE: libcontainer/init_linux.go before patch
func (l *linuxStandardInit) Init() error {
// ROOT CAUSE:
// runc opened /sys/fs/cgroup and internal procfs directories during setup,
// but did not close them before invoking execve() into the user container!
// File descriptors remained open and accessible inside the container!
return syscall.Exec(l.config.Args[0], l.config.Args, os.Environ())
}
// SECURE: libcontainer/init_linux.go patch
func (l *linuxStandardInit) Init() error {
// 1. Unconditionally verify and close ALL internal file descriptors before exec
if err := finalizeNamespace(l.config); err != nil {
return err
}
// 2. Explicitly close any leaked descriptor indices (> 2)
closeExecFds(l.config.PassedFiles)
// 3. Verify current working directory is strictly inside container rootfs
if err := verifyCwdInsideRootfs(l.config.Rootfs); err != nil {
return fmt.Errorf("working directory escaped container rootfs: %w", err)
}
return syscall.Exec(l.config.Args[0], l.config.Args, os.Environ())
}