myrun is a small Linux container runtime demo written in C. It implements the core lifecycle described in the prompt:
- PID, UTS, mount, and optional network namespaces with
clone(2) - cgroups v2 limits for memory, CPU, and process count
- OverlayFS writable container layers
pivot_root(2)into the merged filesystem- a fresh
/procmount inside the PID namespace - parent-side cleanup after the container exits
This project targets Linux only and must be run as root because it uses namespace, mount, cgroup, and pivot_root operations.
See SECURITY.md for the rootful security model, current protections, and what real user namespace support would require.
Install a Linux C toolchain first if needed, for example build-essential on Debian/Ubuntu or build-base on Alpine.
makeOne easy test image is an Alpine or BusyBox rootfs. For example, using Docker as a bootstrap step:
docker create --name myrun-alpine alpine:latest
docker export myrun-alpine > /tmp/alpine-rootfs.tar
docker rm myrun-alpine
make import-rootfs NAME=alpine TARBALL=/tmp/alpine-rootfs.tar
sudo ./myrun run images/alpine /bin/shThe importer accepts any rootfs tarball and unpacks it into images/<name>.
For the built-in BusyBox demo rootfs:
sudo make smoke-test
sudo make cleanup-test
sudo make net-test
sudo make security-test
sudo make seccomp-test
sudo make cli-test
sudo ./myrun run /tmp/myrun-rootfs /bin/shsudo ./myrun run images/alpine /bin/shWith limits:
sudo ./myrun run --memory 100M --cpu 10000:100000 --pids 64 images/alpine /bin/shOptional isolated network namespace:
sudo ./myrun run --net images/alpine /bin/sh
sudo ./myrun run --net --host-ip 10.201.0.1/24 --container-ip 10.201.0.2 images/alpine /bin/sh--net creates a fresh network namespace, wires a veth pair, assigns 10.200.0.1/24 on the host side and 10.200.0.2/24 inside the container by default, and brings both interfaces up. This gives host-to-container connectivity on that private link. Internet access still needs host-side forwarding/NAT, which is intentionally left as a follow-up.
Because the final container process drops capabilities before exec, tools that need special capabilities, such as raw-socket ping, may fail inside the container even when the interface and route are present.
myrun run [--hostname NAME] [--memory BYTES|K|M|G|max] [--cpu QUOTA:PERIOD|max] [--pids N|max] [--net] [--host-ip CIDR] [--container-ip IP] [--container-netmask MASK] [--seccomp-basic] ROOTFS COMMAND [ARGS...]
Examples:
--memory 100Mwrites104857600tomemory.max--cpu 10000:100000writes10000 100000tocpu.max--pids 64writes64topids.max--host-ip 10.201.0.1/24 --container-ip 10.201.0.2customizes the veth link addresses--seccomp-basicenables a small seccomp filter before the command is executed
The parent process creates the OverlayFS mount and cgroup, then calls clone() with namespace flags. The child sets the hostname, makes mounts private, pivots into the merged root, mounts /proc, brings up loopback when --net is used, and finally execvp()s the requested command.
The parent writes the child's host PID into cgroup.procs, waits for the container's init process, then unmounts and removes temporary state.
Before executing the requested command, the child sets no_new_privs and drops all effective, permitted, and inheritable Linux capabilities. With --seccomp-basic, it also installs a small seccomp-BPF filter that denies mount, umount2, ptrace, reboot, kernel module loading, and kexec_load.
This is still an educational runtime, not a hardened sandbox. User namespaces and a broader Docker-style seccomp profile are good next security upgrades.
myrun is split into a parent-side setup path and a child-side container init path.
user CLI
|
v
parent process
|-- create OverlayFS upper/work/merged dirs
|-- mount lower rootfs + writable upper layer
|-- create cgroup v2 directory and write limits
|-- clone child with PID, UTS, mount, and optional network namespaces
|-- optionally create veth pair and move one end into child netns
|-- add child host PID to cgroup.procs
|-- wait for child exit
'-- cleanup veth, cgroup, overlay mount, and temp dirs
child process
|-- become PID 1 inside the new PID namespace
|-- set UTS hostname
|-- wait for parent network setup when --net is enabled
|-- configure loopback and container veth address
|-- pivot_root into OverlayFS merged root
|-- mount fresh /proc
|-- optionally install seccomp filter
|-- set no_new_privs and drop capabilities
'-- exec requested command
| Area | Implementation |
|---|---|
| PID isolation | CLONE_NEWPID; target command runs as PID 1 inside the container |
| Hostname isolation | CLONE_NEWUTS; configurable with --hostname |
| Filesystem isolation | OverlayFS plus pivot_root |
| Mount isolation | CLONE_NEWNS; private mount propagation before pivot |
| Process view | fresh /proc mounted after entering the PID namespace |
| Resource limits | cgroups v2: memory.max, cpu.max, pids.max |
| Networking | optional --net with veth pair and configurable private IPv4 link |
| Image import | rootfs tarball import into images/<name> |
| Cleanup | success and failure cleanup tests for temp mounts/cgroups |
| Security polish | no_new_privs, capability drop, optional basic seccomp filter |
Run the complete validation suite:
sudo make clean
sudo make smoke-test
sudo make cleanup-test
sudo make net-test
sudo make security-test
sudo make seccomp-test
sudo make cli-testUseful manual demo commands:
sudo make rootfs
sudo ./myrun run --hostname demo --memory 100M --cpu 10000:100000 --pids 64 /tmp/myrun-rootfs /check.sh
sudo ./myrun run --net --host-ip 10.201.0.1/24 --container-ip 10.201.0.2 /tmp/myrun-rootfs /bin/sh
sudo ./myrun run --seccomp-basic /tmp/myrun-rootfs /bin/shInside an interactive shell, useful checks are:
hostname
ps
cat /proc/1/status
cat /proc/net/dev
cat /proc/net/route- Namespaces change what a process can see; they do not make the process disappear from the host. The container's PID 1 is still a normal host PID outside the namespace.
- PID namespaces only take effect for children, so the runtime uses
clone()rather than expecting the current process to become PID 1. /procmust be mounted after entering the PID namespace, otherwise process listings reflect the wrong namespace.- OverlayFS separates the read-only image layer from the writable container layer, which is the same core idea behind layered container images.
- cgroups v2 are configured through filesystem writes, not special container syscalls.
pivot_rootis stronger than plainchrootbecause the old root can be detached from the container's mount namespace.- The runtime performs privileged setup first, then reduces the final process with
no_new_privs, capability dropping, and optional seccomp. - Networking is explicit: a new network namespace starts isolated, so the runtime wires connectivity through a veth pair.
- user namespaces and UID/GID mapping
- host-side NAT/forwarding for internet access from the container
- read-only rootfs mode and masked sensitive paths
- broader Docker-style seccomp profile
- AppArmor or SELinux integration
- CI that runs compile-only checks on Linux