Why Read-Only Containers Do Not Stop Binary Execution

Hardening containerized workloads often starts with immutability. Engineers pass --read-only to the Docker daemon or set readOnlyRootFilesystem: true inside a Kubernetes Pod security context. The logic seems clear: if an attacker manages to exploit an application vulnerability, they find an immutable filesystem where write operations fail with EROFS (Read-only file system). Without the ability to write to disk, conventional wisdom says an attacker cannot stage or execute external binaries.

Myth

The prevailing belief is that a read-only root filesystem acts as a hard execution barrier against staging unauthorized binaries. Under this assumption, an attacker gaining code execution inside the container cannot run custom ELF executables because they cannot save files to the disk. They are assumed to be confined strictly to the binaries and interpreter environments baked into the container image.

Immutability on the disk layer does not restrict the kernel from allocating executable memory structures in RAM.

Treating disk immutability as an execution boundary overlooks how the Linux kernel loads and executes process memory.

Reality

Linux does not require physical disk persistence or writable block storage to execute arbitrary binaries. When an attacker achieves code execution inside a container with a read-only root filesystem, two primary vectors allow running arbitrary ELF binaries without touching the read-only storage layer.

First, almost every functional application requires temporary storage for runtime scratch files, PID locks, or inter-process communication. Container configurations routinely mount tmpfs instances at /tmp or leave default POSIX shared memory available at /dev/shm. If these temporary filesystems lack explicit mount options, they default to allowing execution. An attacker writes their compiled binary directly to /dev/shm/payload, sets execution bits with chmod +x, and executes the file directly.

Second, even when all mounted filesystems are strictly read-only, the Linux kernel provides the memfd_create() system call. This system call creates an anonymous file descriptor residing entirely in RAM without mapping to any filesystem directory tree. An attacker streams an ELF executable directly over a network socket into a memory descriptor, then invokes the binary via fexecve() or by referencing the path in the proc pseudo-filesystem:

# Compile and execute an in-memory binary without disk writes
python3 -c '
import ctypes, os

libc = ctypes.CDLL(None)
SYS_memfd_create = 319 # x86_64 syscall number

# Create anonymous in-memory file descriptor
fd = libc.syscall(SYS_memfd_create, b"mem_bin", 1) # MFD_CLOEXEC = 1

# Write payload bytes directly into the memory descriptor
with open("/proc/self/fd/" + str(fd), "wb") as f:
    f.write(b"\x7fELF...") # Binary bytes from network stream

# Execute the anonymous file descriptor directly from memory
os.execv("/proc/self/fd/" + str(fd), ["mem_bin"])
'

Because the binary executes straight out of kernel-allocated memory, traditional host integrity monitoring and file integrity tools that watch the container storage graph detect nothing. The root filesystem remains untouched.

How to protect

Closing memory execution pathways requires combining mount flags, system call filtering, and capability restrictions across the container lifecycle.

  • Mount temporary paths with execution blocks: Explicitly pass noexec,nosuid,nodev to all tmpfs mounts, including /tmp, /run, and /dev/shm. In Docker, specify --tmpfs /tmp:noexec,nosuid,nodev and --tmpfs /dev/shm:noexec,nosuid,nodev.
  • Restrict memfd_create via Seccomp: Block or audit the memfd_create syscall using custom seccomp profiles unless the application specifically depends on anonymous memory sharing.
  • Drop Linux capabilities and prevent privilege escalation: Always enforce --cap-drop=ALL and --security-opt=no-new-privileges:true. Dropping CAP_SYS_ADMIN and CAP_PTRACE prevents processes from debugging neighboring processes or tampering with memory mappings.
  • Inspect runtime execution using eBPF: Deploy runtime monitoring through eBPF probes such as Tetragon or Falco. Set audit rules to alert whenever execve or execveat targets paths matching /proc/self/fd/* or /dev/shm/*.

Enforcing a read-only root filesystem remains a sound practice for reducing the attack surface. However, true workload isolation requires treating process execution boundaries and memory management with the same rigor applied to disk storage.

Press Cmd K to search برای جستجوی سایت از Cmd+K استفاده کنید