Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed16dda2ec | ||
|
|
3f4e96937c | ||
|
|
97fa625a50 | ||
|
|
4fed40e5cd |
+32
@@ -1 +1,33 @@
|
||||
# build output
|
||||
build/
|
||||
cmake-build-*/
|
||||
CMakeCache.txt
|
||||
CMakeFiles/
|
||||
cmake_install.cmake
|
||||
compile_commands.json
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
*.d
|
||||
|
||||
# state and scratch created by running or testing doos
|
||||
.doos/
|
||||
*.tmp
|
||||
*.log
|
||||
*.pid
|
||||
*.core
|
||||
core
|
||||
|
||||
# editors and OS
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
# local AI tooling state
|
||||
.claude/
|
||||
|
||||
# python bytecode from docs/charts/make_charts.py
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# AGENTS.md
|
||||
|
||||
**Docs:** [README](README.md) · [SPEC](SPEC.md) · [ARCHITECTURE](docs/ARCHITECTURE.md) · [RESEARCH](docs/RESEARCH.md) · [COMPARISON](COMPARISON.md) · [AGENTS](AGENTS.md) · [CHANGELOG](CHANGELOG.md)
|
||||
|
||||
Rules for any AI agent (or human) changing this repository. The user's global rules
|
||||
also apply; this file adds the project-specific ones. Keep this file current: it is
|
||||
part of every release.
|
||||
@@ -11,8 +13,9 @@ part of every release.
|
||||
extracted image (image mode) with a per-container upper dir, pivots into it and runs a command.
|
||||
Networking is slirp4netns or pasta, ids are mapped through newuidmap when possible, cgroup v2 limits
|
||||
and a watchdog enforce `--memory --pids --cpus --size`, capabilities and seccomp confine the command.
|
||||
No root at runtime. Authoritative behavior: `SPEC.md`. Design notes: `docs/ARCHITECTURE.md`.
|
||||
Measurements and gaps: `docs/RESEARCH.md`.
|
||||
No root at runtime. Authoritative behavior: [SPEC](SPEC.md). Design notes: [ARCHITECTURE](docs/ARCHITECTURE.md).
|
||||
Measurements and gaps: [RESEARCH](docs/RESEARCH.md). Positioning against other tools: [COMPARISON](COMPARISON.md).
|
||||
Usage: [README](README.md). History: [CHANGELOG](CHANGELOG.md).
|
||||
|
||||
## Mandatory release workflow (every change, same session)
|
||||
|
||||
@@ -22,11 +25,12 @@ Measurements and gaps: `docs/RESEARCH.md`.
|
||||
- `patch`: bug fix, docs, tests, tooling, no behavior change for users.
|
||||
- `minor`: new or changed user-visible behavior (flag, command, mount, limit).
|
||||
- `major`: breaking change to CLI, state layout, or container compatibility.
|
||||
4. Run `scripts/release.sh <patch|minor|major> "imperative commit subject"`.
|
||||
4. Run `make docs` (`charts` plus `check-docs`) (the release script does too: every document must link to every other, every relative link must resolve).
|
||||
5. Run `scripts/release.sh <patch|minor|major> "imperative commit subject"`.
|
||||
It bumps `VERSION`, builds, runs `tests/run.sh`, commits all changes, and creates
|
||||
the annotated tag `vX.Y.Z`. It aborts on any build or test failure.
|
||||
5. Verify live: run `build/doos` once against the real thing (the e2e suite does this).
|
||||
6. Report honestly what passed and what did not. Do not push unless the user asks.
|
||||
6. Verify live: run `build/doos` once against the real thing (the e2e suite does this).
|
||||
7. Report honestly what passed and what did not. Do not push unless the user asks.
|
||||
|
||||
Never hand-edit `VERSION` or create tags manually. Never skip the tests to release.
|
||||
|
||||
@@ -42,6 +46,10 @@ Never hand-edit `VERSION` or create tags manually. Never skip the tests to relea
|
||||
| A new measurement | `docs/RESEARCH.md` with machine, kernel, and what was run |
|
||||
| A new known gap or a closed gap | `docs/RESEARCH.md` gaps list and `README.md` |
|
||||
| These workflow rules | this file |
|
||||
| A measurement, or anything the charts show | `make bench` then `make charts` (rewrites `docs/charts/*.svg` and every `<!--bench:...-->` number and table in `COMPARISON.md` and `docs/RESEARCH.md`); never type those numbers by hand |
|
||||
| A feature or limit that appears in the comparison tables | `COMPARISON.md` (tables, the gaps list, the conclusion) |
|
||||
| A code path one of the `audit/CVE-*` checks targets (overlay writes, tar extraction, mount/directory creation, net namespace sharing, seccomp denylist) | re-run `make audit`; update that CVE's `README.md` if the verdict's reasoning changed |
|
||||
| A new or renamed markdown file | `scripts/check-docs.sh` DOCS list, the nav line (`**Docs:**`) of every document, `README.md` Project docs |
|
||||
| Image catalog, sources, verification, pinned keys | `SPEC.md` Images, `README.md`, `docs/RESEARCH.md` |
|
||||
| Seccomp list, capability set, `/etc` allowlist | `SPEC.md` Hardening and filesystem, `README.md`, `docs/RESEARCH.md` |
|
||||
| Limits, cgroup layout | `SPEC.md` Resource limits, `docs/ARCHITECTURE.md` |
|
||||
@@ -75,6 +83,13 @@ freeze (about 4 minutes in total, needs network, `pasta`, `gpg` and cgroup v2 de
|
||||
before release. On Ubuntu 24.04+ the AppArmor profile in `apparmor/doos` must be
|
||||
installed once for the binary being tested (`doos apparmor`; needs sudo, ask the user, do not do it yourself).
|
||||
|
||||
## Make targets
|
||||
|
||||
`make help` lists them: `build`, `test`, `audit` (named CVE regression checks against `build/doos`,
|
||||
see [audit/README.md](audit/README.md)), `check-docs`, `bench` (re-measure into `docs/charts/bench.json`),
|
||||
`charts` (SVGs and generated numbers from that file), `docs`, `release KIND=patch MSG="..."`, `clean`.
|
||||
The Makefile is a thin wrapper; the logic lives in `CMakeLists.txt`, `scripts/`, `audit/` and `docs/charts/`.
|
||||
|
||||
## Environment notes
|
||||
|
||||
- State dir: `$DOOS_HOME`, else `$XDG_DATA_HOME/doos`, else `~/.local/share/doos`.
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
# Changelog
|
||||
|
||||
**Docs:** [README](README.md) · [SPEC](SPEC.md) · [ARCHITECTURE](docs/ARCHITECTURE.md) · [RESEARCH](docs/RESEARCH.md) · [COMPARISON](COMPARISON.md) · [AGENTS](AGENTS.md) · [CHANGELOG](CHANGELOG.md)
|
||||
|
||||
## 1.1.5 - 2026-10-23
|
||||
- `audit/`: 8 named, sourced CVEs relevant to doos's own mechanisms (overlay writes, tar extraction, mount/directory creation, net namespace sharing, the mount-family seccomp denylist, kernel user-namespace exposure), each with its own `README.md`, `Makefile` and non-destructive `reproduce.sh` against `build/doos`. `make audit` runs them concurrently (`-j8`, recursive make); `make -C audit list|report` also available. Kernel memory-corruption CVEs (Dirty Pipe, Dirty COW, `legacy_parse_param`) are checked by kernel version only, no exploit code.
|
||||
- Found by the audit and disclosed: `fsutil::mkdirs` follows a symlink planted at a container's path, so a symlink under `$DOOS_HOME/containers/<name>` redirects where that container's state is written (low severity: needs write access to the caller's own `$DOOS_HOME`). Tracked as gap G8 in [RESEARCH](docs/RESEARCH.md) and in [COMPARISON](COMPARISON.md); not yet fixed.
|
||||
- Docs: `scripts/check-docs.sh` excludes `audit/*.md` (a separate, self-contained doc set); `README.md` and `AGENTS.md` link to `audit/README.md`.
|
||||
|
||||
## 1.1.4 - 2026-10-23
|
||||
- Docs: [COMPARISON](COMPARISON.md) section 6.1 answers whether doos is a security-wise Docker replacement (same class as rootless Podman, not for untrusted code), summarized in the README safety summary.
|
||||
|
||||
## 1.1.3 - 2026-10-23
|
||||
- Charts: startup time, executable size and CPU limit as SVG (light and dark palettes, doos highlighted), embedded in [COMPARISON](COMPARISON.md) and [RESEARCH](docs/RESEARCH.md).
|
||||
- Reproducible measurements: `scripts/bench.sh` writes `docs/charts/bench.json`; `docs/charts/make_charts.py` builds the charts and rewrites every generated number and table in the documents from it, so text, tables and charts cannot drift apart.
|
||||
- `Makefile` wrapper (`make help`), expanded `.gitignore`.
|
||||
|
||||
## 1.1.2 - 2026-10-23
|
||||
- Docs: new [COMPARISON](COMPARISON.md) (doos against Docker, rootless Docker, Podman, bubblewrap, systemd-nspawn, LXC/Incus, Apptainer, Distrobox/Toolbx, gVisor, Kata/Firecracker) with sourced pros and cons, startup and disk measurements against Docker, Podman and bubblewrap on the same machine, a gaps list tied to code, and a conclusion.
|
||||
- Docs: every document now has a navigation line and links to every other document. `scripts/check-docs.sh` enforces it (and that relative links resolve) and runs inside `scripts/release.sh`.
|
||||
|
||||
## 1.1.1 - 2026-10-23
|
||||
- `doos apparmor` prints a profile for the exact path of the running binary (own profile name per path, so several installs coexist). A start that fails because of AppArmor now prints the install command instead of only `mount bind /: Permission denied`. Verified with a copy in `~/.local/bin`.
|
||||
- `apparmor/doos` (packaging) covers more standard install paths.
|
||||
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
# doos compared with Docker, Podman and others
|
||||
|
||||
**Docs:** [README](README.md) · [SPEC](SPEC.md) · [ARCHITECTURE](docs/ARCHITECTURE.md) · [RESEARCH](docs/RESEARCH.md) · [COMPARISON](COMPARISON.md) · [AGENTS](AGENTS.md) · [CHANGELOG](CHANGELOG.md)
|
||||
|
||||
This page puts doos next to the tools people usually reach for instead. It separates three kinds of claims, and every row says which one it is:
|
||||
|
||||
- **[M] measured** on the development machine, described in "Measurements" below.
|
||||
- **[S] sourced** from the linked documentation or articles. Vendor blogs are marked as such and their numbers are not used.
|
||||
- **[D] design**: what the doos source and [SPEC](SPEC.md) do today.
|
||||
- **[K] general knowledge** that this research did not re-verify.
|
||||
|
||||
Nothing here is a roadmap. Where doos lacks something, the gap is listed with the code that would have to change.
|
||||
|
||||
Versions compared: doos 1.1.1, Docker 29.1.3 (rootful daemon on this machine), Podman 5.7.0 (netavark, crun, overlay), bubblewrap 0.11.1, on Ubuntu 26.04, kernel 7.0.0-38, 12 cores, one machine.
|
||||
|
||||
## 1. At a glance
|
||||
|
||||
| | doos | Docker (rootful) | Docker rootless | Podman rootless |
|
||||
|---|---|---|---|---|
|
||||
| Needs a daemon | no [D] | yes, root daemon [S: [Docker security](https://docs.docker.com/engine/security/)] | yes, per-user daemon [K] | no [S: [rootlesscontaine.rs](https://rootlesscontaine.rs/getting-started/podman/)] |
|
||||
| Runs without root | yes [D] | no [S] | yes [S] | yes [S] |
|
||||
| Who can use it safely | any user | only trusted users: access to the daemon is root-equivalent [S: [Docker security](https://docs.docker.com/engine/security/), [iximiuz](https://labs.iximiuz.com/tutorials/docker-security-introduction-a859718d)] | any user | any user |
|
||||
| Image source | tarball, https URL with sha256, small catalog (Alpine, Ubuntu, Debian) [D] | OCI registries, Dockerfile builds [K] | same as Docker | OCI registries, Containerfile builds [K] |
|
||||
| Build images | no [D] | yes (BuildKit) [K] | yes | yes (Buildah) [K] |
|
||||
| Host dirs into the container (`-v`) | **no** [D] | yes [K] | yes | yes |
|
||||
| Published ports | none for `slirp`; automatic for `pasta` [D] | `-p` [K] | `-p` | `-p` [S: [podman-rootless(7)](https://man.archlinux.org/man/podman-rootless.7.en)] |
|
||||
| Rootless network backend | slirp4netns or pasta [D] | n/a | RootlessKit based [S: [Podman blog](https://blog.podman.io/2023/07/podman-performance-root-and-rootless/)] | pasta default since 5.0 [S: [podman-rootless(7)](https://man.archlinux.org/man/podman-rootless.7.en)] |
|
||||
| Resource limits | memory, pids, cpus, written data [D][M] | memory, cpu, pids, more [K] | needs cgroup v2 + systemd delegation [S: [Docker rootless tips](https://docs.docker.com/engine/security/rootless/tips/)] | needs cgroup v2 [S: [podman-rootless(7)](https://man.archlinux.org/man/podman-rootless.7.en)] |
|
||||
| Default syscall filter | denylist: 48 listed calls plus rules for `clone`, `unshare`, `clone3` [D] | allowlist profile [S: [Docker seccomp](https://docs.docker.com/engine/security/seccomp/)] | same | built-in profile [K] |
|
||||
| Default capabilities | Docker's default set [D][M] | that set [S] | that set | a smaller set [S: [dev.to comparison](https://dev.to/mechcloud_academy/docker-vs-podman-an-in-depth-comparison-2025-2eia)] |
|
||||
| Orchestration (Compose, Kubernetes) | none [D] | Compose, Swarm, Kubernetes via runtimes [K] | same | Compose, pods, `kube play`, systemd units [K] |
|
||||
| Background containers, logs, `stop` | none: a container lives while its first `attach` command lives [D] | yes [K] | yes | yes |
|
||||
| Binary size | <!--bench:doos_size-->552 KiB<!--/bench-->, <!--bench:shared_libs-->6<!--/bench--> shared libraries, <!--bench:src_lines-->3,445<!--/bench--> lines of C++ [M] | CLI <!--bench:docker_size-->29.9 MiB<!--/bench-->, plus `dockerd` and `containerd` [M][K] | same | <!--bench:podman_size-->41.2 MiB<!--/bench--> [M] |
|
||||
| Platforms | Linux only, tested on x86_64 only [D] | Linux, macOS, Windows (via a VM) [K] | Linux | Linux; macOS and Windows via a VM [K] |
|
||||
| Maturity | version 1.1, one test machine | very mature, huge ecosystem | mature | mature |
|
||||
|
||||
## 2. Other tools, one line each
|
||||
|
||||
| Tool | What it is | Versus doos |
|
||||
|---|---|---|
|
||||
| bubblewrap (`bwrap`) | Low-level unprivileged sandbox builder, the engine behind Flatpak [S: [bubblewrap README](https://cdn.jsdelivr.net/gh/containers/bubblewrap/README.md), [Julia Evans](https://jvns.ca/blog/2022/06/28/some-notes-on-bubblewrap/)] | Smaller and faster to start [M], but you assemble every mount by hand and it has no image handling, no persistent state and no cgroup limits [K]; it maps a single id unless you add a helper [K]. doos is the "batteries included" layer on the same kernel features. |
|
||||
| systemd-nspawn | Boots a whole OS tree as a container [S: [forum overview](https://forums.puri.sm/t/linux-containers-comparison-quest-for-the-best/13344)] | Rootless use is limited to a single UID/GID inside [S]. Not installed here, so not measured. |
|
||||
| LXC / Incus (LXD fork) | System containers: a full distro with init and several services, close to a light VM [S: [OneUptime](https://oneuptime.com/blog/post/2026-03-18-choose-between-podman-and-lxc-lxd/view)] | Better for long-lived pet systems and multi-service setups, with API, storage pools and profiles. Heavier to set up. `lxc` binaries exist here, Incus does not, neither was run. |
|
||||
| Apptainer (Singularity) | Rootless, daemonless, single-file SIF images, built for HPC (MPI, InfiniBand, GPUs) [S: [Alliance Canada](https://docs.alliancecan.ca/wiki/Apptainer), [NYU HPC](https://services.rt.nyu.edu/docs/hpc/containers/intro/)] | Different audience. It also has `--fakeroot` and signed immutable images [S], which doos lacks. Not installed here. |
|
||||
| Distrobox / Toolbx | Podman- or Docker-backed "your distro inside your terminal", home directory shared on purpose, not a sandbox [S: [Arch wiki](https://wiki.archlinux.org/title/Distrobox), [comparison](https://computingforgeeks.com/distrobox-vs-toolbox/)] | Opposite goal: doos hides the host's `/home` by default. |
|
||||
| gVisor | A user-space kernel that answers most syscalls itself, cutting host-kernel exposure at an I/O cost [S: [Edera](https://edera.dev/stories/kata-vs-firecracker-vs-gvisor-isolation-compared), [Northflank](https://northflank.com/blog/kata-containers-vs-firecracker-vs-gvisor)] | Stronger isolation than doos, needs its own runtime. Vendor blogs; their numbers are not used here. |
|
||||
| Kata Containers / Firecracker | Each workload in its own KVM micro-VM with its own kernel [S: same sources, [MASCOTS 2020 paper](https://cristian.kleinlabs.eu/publications/mascots2020_container_runtime_security.pdf)] | The right tool for hostile or multi-tenant code. doos shares the host kernel, so a kernel bug is a doos escape. |
|
||||
|
||||
## 3. Measurements
|
||||
|
||||
Method: a trivial command (`true`) started <!--bench:runs-->10<!--/bench--> times in a row after one warm-up run, average wall time per run, the Alpine image already present for every tool, one machine, one run set on <!--bench:bench_date-->2026-10-23<!--/bench-->. Versions: <!--bench:versions-->doos 1.1.2, Docker 29.1.3, Podman 5.7.0, bubblewrap 0.11.1<!--/bench-->. The numbers come from `scripts/bench.sh` (`make bench`); the charts and the tables below are generated from its output (`make charts`), so they cannot drift from each other.
|
||||
|
||||

|
||||
|
||||
<!-- bench:startup:begin -->
|
||||
| Setup | Network | Time per run |
|
||||
|---|---|---|
|
||||
| bubblewrap (host /usr) | none | 8.4 ms |
|
||||
| doos Alpine image | none | 13.8 ms |
|
||||
| doos host mode | none | 18.7 ms |
|
||||
| Podman Alpine | none | 208 ms |
|
||||
| Docker Alpine (rootful) | none | 344 ms |
|
||||
| bubblewrap (host /usr) | default | 7.4 ms |
|
||||
| doos Alpine image | default | 52.1 ms |
|
||||
| doos host mode | default | 56.9 ms |
|
||||
| doos Alpine, pasta | default | 64.6 ms |
|
||||
| Podman Alpine | default | 241 ms |
|
||||
| Docker Alpine (rootful) | default | 436 ms |
|
||||
<!-- bench:startup:end -->
|
||||
|
||||
How to read this honestly:
|
||||
|
||||
- doos is fast partly because it does less: no image store database, no registry code, no layer unpacking per run, no container metadata service. The extra time over bubblewrap is the id map helpers (`newuidmap`, `newgidmap`), the overlay mounts, a hardening filter and the network helper process.
|
||||
- Docker here is a rootful daemon, so its numbers include a client-to-daemon round trip. Rootless Docker was not measured.
|
||||
- Run-to-run spread is real. An earlier ad-hoc run of the same loop measured rootful Docker at 590 ms (no network) and 456 ms (default network); the run charted above measured 344 ms and 436 ms. Read the order of magnitude, not a ranking between close values.
|
||||
- Startup is one number among many. It matters for scripts that start hundreds of short-lived sandboxes. It does not matter for a service that starts once.
|
||||
|
||||

|
||||
|
||||
<!-- bench:sizes:begin -->
|
||||
| Executable | Size |
|
||||
|---|---|
|
||||
| bubblewrap | 79 KiB (80,424 bytes) |
|
||||
| crun | 472 KiB (483,392 bytes) |
|
||||
| doos | 552 KiB (565,680 bytes) |
|
||||
| docker CLI | 29.9 MiB (31,374,336 bytes) |
|
||||
| podman | 41.2 MiB (43,180,896 bytes) |
|
||||
<!-- bench:sizes:end -->
|
||||
|
||||
Disk, measured with `du` (which cannot read overlayfs' mode-000 work directories, so true usage is slightly higher):
|
||||
|
||||
| Item | Size |
|
||||
|---|---|
|
||||
| doos Alpine image, extracted once and shared | 8.3 MiB |
|
||||
| doos Ubuntu base image, extracted once and shared | 98 MiB |
|
||||
| one doos container on top of an image | a few KiB of metadata plus its own writes |
|
||||
| four containers (one Alpine, three Ubuntu) on shared images | 20 KiB together |
|
||||
| doos host-mode container after its first create (skeleton, `/etc` allowlist, dpkg state) | 4 MiB, created in 0.5 s |
|
||||
| Podman storage on this machine (many unrelated images) | 26.3 GB images, 1.8 GB containers |
|
||||
|
||||
The Podman figure is this machine's accumulated state, not a like-for-like comparison. What the comparison does show is the model: doos extracts an image once and every container adds only its own writes.
|
||||
|
||||
## 4. doos versus Docker
|
||||
|
||||
**Where doos is better**
|
||||
|
||||
- No daemon and no root. With Docker's default install, anyone who can talk to the daemon controls the machine [S]. doos has no such trust boundary: it can only do what the invoking user can do, plus what the `newuidmap` helper allows.
|
||||
- Tiny and auditable: <!--bench:doos_size-->552 KiB<!--/bench-->, <!--bench:src_lines-->3,445<!--/bench--> lines of C++ [M], one process tree, no background service, nothing listening.
|
||||
- Instant: <!--bench:doos_range-->14 to 65 ms<!--/bench--> against <!--bench:docker_range-->344 to 436 ms<!--/bench--> for a rootful Docker run and <!--bench:podman_range-->208 to 241 ms<!--/bench--> for Podman [M].
|
||||
- Safe defaults a user does not have to remember: Docker's default capability set, a seccomp filter, an allowlist of host `/etc`, a memory and pid cap, a written-data cap [D][M].
|
||||
- A "host mode" that reuses the host's installed tools (compilers, `apt` state) for throwaway shells with no image download [D].
|
||||
- A size cap that freezes instead of filling the disk [D][M]. Docker's writable-layer size limit needs the overlay2 driver on XFS with project quotas [S: [Fabian Lee](https://fabianlee.org/2020/01/15/docker-use-overlay2-with-an-xfs-backing-filesystem-to-limit-rootfs-size/)].
|
||||
|
||||
**Where Docker is better**
|
||||
|
||||
- Everything around images: registries, `docker build`, layer caching, multi-arch, signing and scanning ecosystems [K]. doos cannot pull an arbitrary `nginx:1.27` from a registry.
|
||||
- Host directories and devices into the container (`-v`, `--device`, GPUs). doos has no bind-mount option [D].
|
||||
- Long-running services: background containers, restart policies, healthchecks, logs, `exec`, `stop` [K]. In doos a container exists only while its first `attach` command runs [D].
|
||||
- Networking: user-defined networks, DNS between containers, published ports on any interface [K].
|
||||
- Compose, Swarm and the Kubernetes ecosystem [K].
|
||||
- macOS and Windows support through a VM [K].
|
||||
- Strongest default syscall posture: an allowlist, which denies unknown new syscalls. The doos filter is a denylist, so a new kernel syscall is allowed until it is added [D][S].
|
||||
|
||||
## 5. doos versus Podman
|
||||
|
||||
Podman is the closest comparison, since both are daemonless and rootless.
|
||||
|
||||
**Where doos is better**
|
||||
|
||||
- Smaller, faster to start [M] and one binary with no storage database. Podman's rootless setup brings a store under `~/.local/share/containers` with its own metadata [K].
|
||||
- Opinionated limits and hardening out of the box, with a stated, measured effect (OOM kill, pids, cpus, freeze at a size cap) [M].
|
||||
- Two network backends with different, documented trade-offs, including a host-like mode that keeps host abstract unix sockets out of the container [M].
|
||||
- Image verification is explicit: sha256 plus signatures checked against fingerprints pinned in the source [D][M].
|
||||
- Frozen-at-the-cap behaviour with a never-frozen rescue shell [D][M].
|
||||
|
||||
**Where Podman is better**
|
||||
|
||||
- OCI compatibility end to end: pull, push, build with Buildah, pods, `kube play`, Quadlet and systemd integration [K]. It can also run images made for Docker unchanged.
|
||||
- Docker-compatible CLI and socket, so existing tooling mostly works [K].
|
||||
- Bind mounts, volumes, secrets, `exec`, `logs`, `stop`, restart policies [K].
|
||||
- Years of hardening, a security team, and a large user base finding the sharp edges. doos has one test machine.
|
||||
- Podman documents rootless caveats: ports below 1024, `/etc/subuid` setup, cgroup v2 only, and a rootless port forwarder that does not preserve client source IPs [S: [podman-rootless(7)](https://man.archlinux.org/man/podman-rootless.7.en)]. doos shares the subuid and cgroup v2 requirements [D]. Binding ports below 1024 in doos was not tested.
|
||||
|
||||
**What both share:** the same kernel features (user, mount, pid, network namespaces, overlayfs, cgroup v2, seccomp), the same pasta/slirp4netns options, and the same weakness: they share the host kernel [S].
|
||||
|
||||
## 6. Security, side by side
|
||||
|
||||
- **Shared kernel.** doos, Docker, Podman, bubblewrap, nspawn and LXC all run container processes on the host kernel. A kernel vulnerability reachable through allowed syscalls is a host vulnerability. gVisor reduces that exposure at a performance cost; Kata and Firecracker remove it by giving each workload its own kernel [S].
|
||||
- **Rootless is the real difference from rootful Docker.** A bug that gives root inside a rootless doos or Podman container yields the invoking user's rights on the host, not root [D][S].
|
||||
- **doos filter style.** A denylist built at runtime with an architecture check, x32 guard and `clone3` handling [D]. Docker and Podman ship allowlist profiles [S]. An allowlist is the more conservative design.
|
||||
- **Missing layers.** Docker applies an AppArmor profile (`docker-default`) on this machine [M: `docker info` lists apparmor]. doos installs no AppArmor or SELinux profile of its own; its only profile exists to *permit* user namespaces on Ubuntu 24.04+ [D].
|
||||
- **Supply chain.** doos pins publisher signing keys for Alpine and Ubuntu and checks digests for Debian [D][M]. Registry-based tools rely on registry trust, content trust or external signing tools [K].
|
||||
|
||||
### 6.1 Is doos a security-wise Docker replacement?
|
||||
|
||||
Short answer: it is in the same class as rootless Podman and rootless Docker by design, but it is not a replacement for running untrusted code.
|
||||
|
||||
| Question | Answer |
|
||||
|---|---|
|
||||
| Better than rootful Docker on privilege? | Yes. No root daemon, nothing runs as root on the host, so an escape yields the invoking user's rights, not root [D][S]. |
|
||||
| Equal to rootless Podman or Docker? | Not proven. The mechanism is the same (user namespaces, seccomp, dropped capabilities, cgroup v2), the maturity is not [D][K]. |
|
||||
| What the default confines | 14 capabilities (Docker's default set, `CapEff` `a80425fb`), `no_new_privs`, seccomp denylist of 48 syscalls, cgroup memory/pids/cpu limits, a written-data cap, no host `/home`, host loopback blocked in `slirp` [M]. |
|
||||
| Weaker than Docker or Podman | Denylist instead of an allowlist (a new dangerous syscall is allowed by default); no per-container AppArmor or SELinux profile; host mode uses the host's own `/usr` and an `/etc` allowlist; `--net host` shares abstract unix sockets [D]. |
|
||||
| Kernel exposure | Shared kernel, and user namespaces enlarge the reachable kernel surface, which is why Ubuntu 24.04 restricts them by default [S]. |
|
||||
| Assurance | One developer, one machine (Linux x86_64), no external audit, no CVE history, Debian images verified by digest only [M][D]. |
|
||||
|
||||
Use doos for your own tools, builds and experiments, where you accept the risk. For code you do not trust, or for several tenants, use rootless Podman with its default seccomp and SELinux or AppArmor, and gVisor or Kata when the kernel must not be shared (see the conclusion table in section 8). Functionally it is also not a drop-in replacement: see section 7.
|
||||
|
||||
Full list of what doos does not cover is at the end of [RESEARCH](docs/RESEARCH.md); the exact hardening is in [SPEC](SPEC.md) under Hardening. Named CVE regression checks against `build/doos` live in [audit/README.md](audit/README.md) (`make audit`).
|
||||
|
||||
## 7. What doos lacks, and which code would change
|
||||
|
||||
These are the gaps that matter when comparing, each next to where it lives. They are descriptions, not promises.
|
||||
|
||||
| Gap | Where |
|
||||
|---|---|
|
||||
| No OCI registry pull, no layers, no build | `src/image.cpp` (`Catalog`, `ImageStore`) |
|
||||
| No host directory bind mounts, no devices, no GPU | `setupRoot` in `src/sandbox.cpp`, option parsing in `src/cli.cpp` |
|
||||
| No background containers, `stop`, `exec` by name, logs | lifetime model in `Sandbox::start` / `Sandbox::watch` (container lives with its first command) |
|
||||
| No user-defined networks, no `-p` | `src/network.cpp` |
|
||||
| No Compose or Kubernetes integration | not present |
|
||||
| Environment, user, working-directory flags missing | `applyLimitsAndEnv` in `src/sandbox.cpp` |
|
||||
| Linux x86_64 tested only | `src/hardening.cpp` guards aarch64, never run there |
|
||||
| seccomp is a denylist | `deniedSyscalls` in `src/hardening.cpp` |
|
||||
| Test coverage: one machine | `tests/run.sh` |
|
||||
| `fsutil::mkdirs` follows a planted symlink at a container's path, so a symlink at `$DOOS_HOME/containers/<name>` redirects where that container's state is written (low severity on a single-user `$DOOS_HOME`; found by [`audit/CVE-2021-30465`](audit/CVE-2021-30465/README.md)) | `fsutil::mkdirs` in `src/util.cpp` |
|
||||
|
||||
## 8. Conclusion
|
||||
|
||||
doos is not a Docker replacement and does not try to be. It is a small, rootless, daemonless tool for **disposable or per-user Linux shells and sandboxes that start in tens of milliseconds, with safe defaults and hard caps already set**. Its strengths are the ones the measurements show: it is a <!--bench:doos_size-->552 KiB<!--/bench--> single binary, it starts in <!--bench:doos_range-->14 to 65 ms<!--/bench--> [M], it runs with no daemon and no root, it caps memory, processes, CPU and written data by default, and it verifies what it downloads.
|
||||
|
||||
Choose by the job:
|
||||
|
||||
| If you need | Use |
|
||||
|---|---|
|
||||
| A quick, isolated shell or script runner for one user, host tools or a distro image, with limits, no daemon | **doos** |
|
||||
| OCI images, Dockerfiles or Containerfiles, registries, CI that matches production | **Podman** (rootless, daemonless) or **Docker** |
|
||||
| Compose, Kubernetes parity, Docker ecosystem tooling | **Docker** or **Podman** with its Docker compatibility |
|
||||
| Sandbox one application with exact control over mounts | **bubblewrap** |
|
||||
| A full OS with init and several services, long lived | **Incus / LXC** or **systemd-nspawn** |
|
||||
| Shared HPC cluster, MPI, GPUs, signed single-file images | **Apptainer** |
|
||||
| A desktop environment tied to your home directory | **Distrobox / Toolbx** |
|
||||
| Running code you do not trust at all, or multi-tenant workloads | **Kata / Firecracker** (or gVisor as a middle step), not any shared-kernel container |
|
||||
|
||||
Honest limits of this comparison: one machine, 10-run averages, no rootless Docker, no systemd-nspawn, Incus or Apptainer measurements, and several Docker and Podman capabilities listed as general knowledge rather than re-verified. The conclusion about when to use doos rests on what the doos tests and measurements show ([RESEARCH](docs/RESEARCH.md)), not on a claim that it beats mature tools on features. On features it does not.
|
||||
|
||||
Back to [README](README.md) for usage, [SPEC](SPEC.md) for the exact behavior, [ARCHITECTURE](docs/ARCHITECTURE.md) for how it works, [AGENTS](AGENTS.md) for the rules, and [CHANGELOG](CHANGELOG.md) for history.
|
||||
@@ -0,0 +1,38 @@
|
||||
# Thin wrapper over the commands used to build, test and update the project. CMake does the building,
|
||||
# scripts/ and docs/charts/ do the rest. `make help` lists the targets.
|
||||
.DEFAULT_GOAL := help
|
||||
BUILD ?= build
|
||||
DOOS := $(BUILD)/doos
|
||||
|
||||
.PHONY: help build test audit check-docs bench charts docs release clean
|
||||
|
||||
help: ## list the targets
|
||||
@grep -hE '^[a-z-]+:.*##' $(MAKEFILE_LIST) | sed -E 's/:.*## /\t/' | column -t -s "$$(printf '\t')"
|
||||
|
||||
build: ## configure and compile into build/
|
||||
cmake -S . -B $(BUILD) >/dev/null
|
||||
cmake --build $(BUILD)
|
||||
|
||||
test: build ## end-to-end suite (real downloads, needs network, pasta, gpg, cgroup v2)
|
||||
tests/run.sh $(DOOS)
|
||||
|
||||
audit: build ## run the CVE regression checks under audit/ against build/doos (concurrent, -j)
|
||||
$(MAKE) -C audit -j8 test DOOS=$(abspath $(DOOS))
|
||||
|
||||
check-docs: ## every document links to every other, every relative link resolves
|
||||
scripts/check-docs.sh
|
||||
|
||||
bench: build ## re-measure startup, binary sizes and cpu limit into docs/charts/bench.json
|
||||
scripts/bench.sh
|
||||
|
||||
charts: ## rebuild docs/charts/*.svg and the generated numbers in COMPARISON.md from bench.json
|
||||
python3 docs/charts/make_charts.py
|
||||
|
||||
docs: charts check-docs ## charts plus the docs check
|
||||
|
||||
release: ## KIND=patch|minor|major MSG="subject": bump, build, test, commit, tag
|
||||
@test -n "$(KIND)" -a -n "$(MSG)" || { echo 'usage: make release KIND=patch MSG="subject"'; exit 2; }
|
||||
scripts/release.sh $(KIND) "$(MSG)"
|
||||
|
||||
clean: ## remove build/
|
||||
rm -rf $(BUILD)
|
||||
@@ -1,5 +1,7 @@
|
||||
# doos
|
||||
|
||||
**Docs:** [README](README.md) · [SPEC](SPEC.md) · [ARCHITECTURE](docs/ARCHITECTURE.md) · [RESEARCH](docs/RESEARCH.md) · [COMPARISON](COMPARISON.md) · [AGENTS](AGENTS.md) · [CHANGELOG](CHANGELOG.md)
|
||||
|
||||
User-level containers in C++20: overlayfs, user/mount/pid namespaces, slirp4netns or pasta networking, optional images (Alpine, Ubuntu, Debian, or your own rootfs), memory/cpu/pid/size limits, default hardening (capabilities, seccomp). No root at runtime.
|
||||
|
||||
```
|
||||
@@ -11,7 +13,7 @@ doos image list | add [--sha256 HEX] SOURCE [NAME] | delete [--yes] NAME
|
||||
doos apparmor # profile text for this binary (Ubuntu 24.04+)
|
||||
```
|
||||
|
||||
Version: see `VERSION` (currently 1.1.1). Release history: `CHANGELOG.md`.
|
||||
Version: see `VERSION` (currently 1.1.3). Release history: `CHANGELOG.md`.
|
||||
|
||||
User-level containers in C++20: overlayfs over the host's `/usr`, `/etc`, `/opt`, user/mount/pid namespaces, networking through slirp4netns. No root at runtime.
|
||||
|
||||
@@ -166,12 +168,25 @@ run with Docker's default capability set and a seccomp filter. Memory, pids, cpu
|
||||
`--net host` removes network isolation and is opt-in (`--net pasta` is the safer host-like choice).
|
||||
What is still not covered is listed at the end of `docs/RESEARCH.md`.
|
||||
|
||||
Is it a security-wise Docker replacement? Same class as rootless Podman by design (no root daemon, user namespaces,
|
||||
seccomp, dropped capabilities, cgroup limits), but unaudited, tested on one machine, with a seccomp denylist and no
|
||||
per-container AppArmor/SELinux profile. Fine for your own tools; for untrusted code use rootless Podman, gVisor or Kata.
|
||||
Details: [COMPARISON](COMPARISON.md) section 6.1.
|
||||
|
||||
## Development
|
||||
|
||||
`make help` lists the targets: `make build`, `make test` (end-to-end, needs network), `make bench` (re-measure startup, sizes and the cpu limit), `make charts` (rebuild the SVG charts and the generated numbers in the comparison), `make docs`, `make release KIND=patch MSG="..."`. Build and test details are in [AGENTS](AGENTS.md); the charts and how they are generated are in [COMPARISON](COMPARISON.md) section 3 and `docs/charts/`.
|
||||
|
||||
## Project docs
|
||||
|
||||
| File | Content |
|
||||
|---|---|
|
||||
| `SPEC.md` | Behavior contract and limits |
|
||||
| `docs/ARCHITECTURE.md` | Code map and flows |
|
||||
| `docs/RESEARCH.md` | What works unprivileged, measurements, gaps |
|
||||
| `AGENTS.md` | Rules for AI agents, release workflow |
|
||||
| `CHANGELOG.md` | Release history |
|
||||
| [`SPEC.md`](SPEC.md) | Behavior contract and limits |
|
||||
| [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | Code map and flows |
|
||||
| [`docs/RESEARCH.md`](docs/RESEARCH.md) | What works unprivileged, measurements, remaining limits |
|
||||
| [`COMPARISON.md`](COMPARISON.md) | doos next to Docker, Podman, bubblewrap, nspawn, LXC/Incus, Apptainer, Distrobox, gVisor/Kata, with pros, cons, measurements and a conclusion |
|
||||
| [`AGENTS.md`](AGENTS.md) | Rules for AI agents, release workflow |
|
||||
| [`CHANGELOG.md`](CHANGELOG.md) | Release history |
|
||||
| [`audit/README.md`](audit/README.md) | Named CVE regression checks run against `build/doos` (`make audit`) |
|
||||
|
||||
Not sure whether doos is the right tool? Read the conclusion of [COMPARISON](COMPARISON.md) first.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# doos specification
|
||||
|
||||
Authoritative description of behavior and limits. Code constants live in `src/spec.hpp`; change both together.
|
||||
**Docs:** [README](README.md) · [SPEC](SPEC.md) · [ARCHITECTURE](docs/ARCHITECTURE.md) · [RESEARCH](docs/RESEARCH.md) · [COMPARISON](COMPARISON.md) · [AGENTS](AGENTS.md) · [CHANGELOG](CHANGELOG.md)
|
||||
|
||||
Authoritative description of behavior and limits. Code constants live in `src/spec.hpp`; change both together. How the code implements it: [ARCHITECTURE](docs/ARCHITECTURE.md). Measurements and remaining limits: [RESEARCH](docs/RESEARCH.md). How it differs from other tools: [COMPARISON](COMPARISON.md). Usage: [README](README.md).
|
||||
|
||||
## CLI
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# CVE-2016-5195: see README.md in this directory.
|
||||
DOOS ?= $(abspath ../../build/doos)
|
||||
export DOOS
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@./reproduce.sh "$(DOOS)"
|
||||
@@ -0,0 +1,35 @@
|
||||
# CVE-2016-5195: "Dirty COW": race condition in copy-on-write handling of private read-only mappings
|
||||
|
||||
**Scope:** [audit](../README.md) · [README](../../README.md) · [COMPARISON](../../COMPARISON.md) · [docs/RESEARCH](../../docs/RESEARCH.md)
|
||||
|
||||
CVSS 7.0-7.2. Fixed in Linux 4.8.3 (and backported to every maintained branch since).
|
||||
|
||||
## Source
|
||||
|
||||
- [Ubuntu advisory](https://ubuntu.com/security/CVE-2016-5195)
|
||||
- [NVD entry](https://nvd.nist.gov/vuln/detail/CVE-2016-5195)
|
||||
|
||||
## Relevance to doos
|
||||
|
||||
A race in the memory manager's copy-on-write handling let a local user write to memory
|
||||
that should have been read-only, including on-disk binaries mapped read-only. Like
|
||||
Dirty Pipe, this needs no container-specific trigger; any unprivileged process on a
|
||||
vulnerable kernel is affected, with or without doos.
|
||||
|
||||
|
||||
## What the check does
|
||||
|
||||
Kernel version only. Fixed in 4.8.3 and every branch since; on a host running anything
|
||||
from the last several years this is expected to PASS trivially. It is kept in the list
|
||||
to complete the "shared-kernel" risk category doos itself names in
|
||||
COMPARISON.md section 6, not because it is expected to ever FAIL on a maintained host.
|
||||
|
||||
|
||||
## Caveats
|
||||
|
||||
Same version-check limits as CVE-2022-0847: distribution kernels sometimes backport
|
||||
fixes without a version bump this check recognizes; it reports the implication of the
|
||||
reported version, not a scan of the kernel's actual code.
|
||||
|
||||
|
||||
Run: `make test` in this directory, or `make -C audit test` from the repo root.
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
# CVE-2016-5195 (Dirty COW): kernel version against the fixed release. No exploit code.
|
||||
set -u
|
||||
skip() { echo "SKIP: $*"; exit 0; }
|
||||
fail() { echo "FAIL: $*"; exit 1; }
|
||||
pass() { echo "PASS: $*"; exit 0; }
|
||||
|
||||
ver=$(uname -r | grep -oE '^[0-9]+\.[0-9]+(\.[0-9]+)?')
|
||||
major=$(echo "$ver" | cut -d. -f1)
|
||||
minor=$(echo "$ver" | cut -d. -f2)
|
||||
patch=$(echo "$ver" | cut -d. -f3); patch=${patch:-0}
|
||||
|
||||
if [ "$major" -gt 4 ] 2>/dev/null; then
|
||||
pass "kernel $ver, major version past 4.x, long past the Dirty COW fix (4.8.3)"
|
||||
fi
|
||||
if [ "$major" -eq 4 ] && [ "$minor" -gt 8 ]; then
|
||||
pass "kernel $ver is past 4.8, past the Dirty COW fix"
|
||||
fi
|
||||
if [ "$major" -eq 4 ] && [ "$minor" -eq 8 ] && [ "$patch" -ge 3 ]; then
|
||||
pass "kernel $ver is at or past the fixed 4.8.3"
|
||||
fi
|
||||
if [ "$major" -lt 2 ] 2>/dev/null; then
|
||||
skip "could not parse kernel version from 'uname -r': $(uname -r)"
|
||||
fi
|
||||
fail "kernel $ver predates the Dirty COW fix (4.8.3); this bug has existed since 2.6.22"
|
||||
@@ -0,0 +1,7 @@
|
||||
# CVE-2019-5736: see README.md in this directory.
|
||||
DOOS ?= $(abspath ../../build/doos)
|
||||
export DOOS
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@./reproduce.sh "$(DOOS)"
|
||||
@@ -0,0 +1,43 @@
|
||||
# CVE-2019-5736: runc container escape via /proc/self/exe overwrite
|
||||
|
||||
**Scope:** [audit](../README.md) · [README](../../README.md) · [COMPARISON](../../COMPARISON.md) · [docs/RESEARCH](../../docs/RESEARCH.md)
|
||||
|
||||
CVSS 7.2. Fixed in runc 1.0.0-rc7, Docker 18.09.2.
|
||||
|
||||
## Source
|
||||
|
||||
- [Ubuntu security notice](https://discourse.ubuntu.com/t/runc/55877)
|
||||
- [Trend Micro writeup](https://www.trendmicro.com/vinfo/be/security/news/vulnerabilities-and-exploits/cve-2019-5736-runc-container-escape-vulnerability-provides-root-access-to-the-target-machine)
|
||||
|
||||
## Relevance to doos
|
||||
|
||||
runc re-executes itself through `/proc/self/exe` as part of its container-start
|
||||
sequence. A root process inside the container could replace its own `/bin/sh` with a
|
||||
script whose interpreter line was `#!/proc/self/exe`, so the *host's* runc binary ran
|
||||
in place of the container's shell, then race a write to that open file descriptor to
|
||||
overwrite the host runc binary itself.
|
||||
|
||||
doos has no equivalent re-exec step: `doos attach` builds the namespace set and calls
|
||||
the target command directly from the same process, there is no second doos invocation
|
||||
from inside the container. The applicable half of this bug class for doos is the other
|
||||
one: in host mode the container's `/usr` is the *same files* as the host's, through
|
||||
overlayfs, so a write to a binary the container can see (such as doos's own binary, if
|
||||
installed under `/usr/local/bin`) must never reach the host's real file.
|
||||
|
||||
|
||||
## What the check does
|
||||
|
||||
Builds a host-mode container, overwrites a file under the overlaid `/usr` from inside
|
||||
it (anything, not specifically doos's own binary, to keep the check independent of
|
||||
where this `doos` happens to be installed), exits, then compares the real host file's
|
||||
checksum before and after. A difference is FAIL.
|
||||
|
||||
|
||||
## Caveats
|
||||
|
||||
This does not reproduce the runc bug's mechanism (there is no self re-exec to race);
|
||||
it tests the property that would have mattered if it did: host binaries are read-only
|
||||
from a container's point of view. Needs `sha256sum` and a host-mode attach to work.
|
||||
|
||||
|
||||
Run: `make test` in this directory, or `make -C audit test` from the repo root.
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
# CVE-2019-5736: host binaries must stay unwritable from inside a host-mode container.
|
||||
set -u
|
||||
DOOS="${1:-}"
|
||||
name() { echo "$0"; }
|
||||
skip() { echo "SKIP: $*"; exit 0; }
|
||||
fail() { echo "FAIL: $*"; exit 1; }
|
||||
pass() { echo "PASS: $*"; exit 0; }
|
||||
|
||||
[ -x "$DOOS" ] || skip "no doos binary at '$DOOS'"
|
||||
command -v sha256sum >/dev/null || skip "sha256sum not installed"
|
||||
|
||||
TARGET=/usr/bin/true
|
||||
[ -r "$TARGET" ] || skip "$TARGET not present or not readable on this host"
|
||||
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
export DOOS_HOME="$TMP/home"
|
||||
|
||||
before=$(sha256sum "$TARGET" | cut -d' ' -f1)
|
||||
|
||||
out=$("$DOOS" attach --net none audit5736 -- sh -c "echo clobber > '$TARGET' 2>/dev/null; echo done" 2>&1)
|
||||
rc=$?
|
||||
"$DOOS" delete --yes audit5736 >/dev/null 2>&1
|
||||
|
||||
after=$(sha256sum "$TARGET" | cut -d' ' -f1)
|
||||
|
||||
if [ "$before" != "$after" ]; then
|
||||
fail "host $TARGET changed after a container wrote to the overlaid path (before=$before after=$after)"
|
||||
fi
|
||||
pass "host $TARGET unchanged after an attempted overwrite from inside the container (doos exit=$rc)"
|
||||
@@ -0,0 +1,7 @@
|
||||
# CVE-2020-15257: see README.md in this directory.
|
||||
DOOS ?= $(abspath ../../build/doos)
|
||||
export DOOS
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@./reproduce.sh "$(DOOS)"
|
||||
@@ -0,0 +1,42 @@
|
||||
# CVE-2020-15257: containerd-shim API reachable from a container sharing the host network namespace
|
||||
|
||||
**Scope:** [audit](../README.md) · [README](../../README.md) · [COMPARISON](../../COMPARISON.md) · [docs/RESEARCH](../../docs/RESEARCH.md)
|
||||
|
||||
CVSS 5.2 (Medium). Fixed in containerd 1.3.9 / 1.4.3.
|
||||
|
||||
## Source
|
||||
|
||||
- [Ubuntu advisory](https://ubuntu.com/security/CVE-2020-15257)
|
||||
- [Tenable entry](https://www.tenable.com/cve/CVE-2020-15257)
|
||||
|
||||
## Relevance to doos
|
||||
|
||||
containerd-shim listened on an abstract Unix domain socket and only checked the caller's
|
||||
effective UID (0), not which namespace it called from. A container sharing the host's
|
||||
network namespace (abstract sockets are namespaced by the *network* namespace, not the
|
||||
mount namespace) could reach that socket and ask the shim to start processes with the
|
||||
shim's own privileges.
|
||||
|
||||
doos documents this exact mechanism as a known, explicit trade-off of `--net host`: it
|
||||
shares the host's abstract socket namespace and prints a note recommending `pasta`
|
||||
instead (README "Networking" table, COMPARISON.md section 6). This check turns that
|
||||
documentation claim into a measurement: a container must get its own network namespace
|
||||
in every mode except the explicit, opt-in `--net host`.
|
||||
|
||||
## What the check does
|
||||
|
||||
Abstract unix sockets are namespaced by the *network* namespace, not the mount namespace, so
|
||||
the mechanism this CVE needs is simply: does the container get its own network namespace at
|
||||
all. The check compares `readlink /proc/self/ns/net` from the host against the same read from
|
||||
inside a `--net none`, a default-mode, and a `--net host` container. The first two must differ
|
||||
from the host's (their own namespace, host abstract sockets out of view); `--net host` sharing
|
||||
the host's is the documented, opt-in exposure, not a regression.
|
||||
|
||||
## Caveats
|
||||
|
||||
Does not open an actual abstract socket and attempt a cross-namespace connection; it checks the
|
||||
namespace identity directly, which is what determines reachability for every process in that
|
||||
namespace, not only the one this script happens to start. `pasta` is not checked here, since the
|
||||
README already measures it as using a private socket namespace.
|
||||
|
||||
Run: `make test` in this directory, or `make -C audit test` from the repo root.
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
# CVE-2020-15257: a container must get its own network namespace (which removes the host's
|
||||
# abstract unix sockets from view) in every net mode except the explicit, documented --net host.
|
||||
set -u
|
||||
DOOS="${1:-}"
|
||||
skip() { echo "SKIP: $*"; exit 0; }
|
||||
fail() { echo "FAIL: $*"; exit 1; }
|
||||
pass() { echo "PASS: $*"; exit 0; }
|
||||
|
||||
[ -x "$DOOS" ] || skip "no doos binary at '$DOOS'"
|
||||
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
export DOOS_HOME="$TMP/home"
|
||||
|
||||
host_ns=$(readlink /proc/self/ns/net 2>/dev/null) || skip "cannot read /proc/self/ns/net on this host"
|
||||
|
||||
none_ns=$("$DOOS" attach --net none auditns_none -- readlink /proc/self/ns/net 2>/dev/null | tail -1)
|
||||
"$DOOS" delete --yes auditns_none >/dev/null 2>&1
|
||||
|
||||
default_ns=$("$DOOS" attach auditns_default -- readlink /proc/self/ns/net 2>/dev/null | tail -1)
|
||||
"$DOOS" delete --yes auditns_default >/dev/null 2>&1
|
||||
|
||||
host_mode_ns=$("$DOOS" attach --net host auditns_host -- readlink /proc/self/ns/net 2>/dev/null | tail -1)
|
||||
"$DOOS" delete --yes auditns_host >/dev/null 2>&1
|
||||
|
||||
if [ -z "$none_ns" ] || [ -z "$default_ns" ] || [ -z "$host_mode_ns" ]; then
|
||||
skip "could not read a container's net namespace id (host=$host_ns none=$none_ns default=$default_ns host-mode=$host_mode_ns)"
|
||||
fi
|
||||
|
||||
if [ "$none_ns" = "$host_ns" ]; then
|
||||
fail "--net none shares the host's network namespace ($none_ns): host abstract sockets would be reachable"
|
||||
fi
|
||||
if [ "$default_ns" = "$host_ns" ]; then
|
||||
fail "the default net mode shares the host's network namespace ($default_ns): host abstract sockets would be reachable"
|
||||
fi
|
||||
if [ "$host_mode_ns" != "$host_ns" ]; then
|
||||
pass "--net host did not share the host's namespace ($host_mode_ns vs $host_ns); isolation is stronger than documented, default modes are isolated ($none_ns, $default_ns)"
|
||||
fi
|
||||
pass "default/none use a private net namespace ($none_ns, $default_ns), --net host shares the host's ($host_ns) as documented"
|
||||
@@ -0,0 +1,7 @@
|
||||
# CVE-2021-30465: see README.md in this directory.
|
||||
DOOS ?= $(abspath ../../build/doos)
|
||||
export DOOS
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@./reproduce.sh "$(DOOS)"
|
||||
@@ -0,0 +1,46 @@
|
||||
# CVE-2021-30465: runc symlink-exchange race during mount setup ("symlink-exchange attack")
|
||||
|
||||
**Scope:** [audit](../README.md) · [README](../../README.md) · [COMPARISON](../../COMPARISON.md) · [docs/RESEARCH](../../docs/RESEARCH.md)
|
||||
|
||||
CVSS 8.5. Fixed in runc 1.0.0-rc95.
|
||||
|
||||
## Source
|
||||
|
||||
- [Red Hat RHSB-2021-004](https://access.redhat.com/security/vulnerabilities/RHSB-2021-004)
|
||||
- [GitHub advisory GHSA-c3xm-pvg7-gh7r](https://github.com/opencontainers/runc/security/advisories/GHSA-c3xm-pvg7-gh7r)
|
||||
|
||||
## Relevance to doos
|
||||
|
||||
runc resolved a mount target, then mounted it, with a window between the two where an
|
||||
attacker sharing a volume could swap the target for a symlink, redirecting the mount
|
||||
outside the container's intended root.
|
||||
|
||||
doos builds its own container directories with `fsutil::mkdirs`, which calls
|
||||
`std::filesystem::create_directories`. That call follows an existing symlink at any
|
||||
path component rather than refusing it, the same general shape of bug (trusting a path
|
||||
component that could have been swapped for a symlink by something else with write
|
||||
access to the same parent directory), applied to `$DOOS_HOME/containers/<name>` instead
|
||||
of a mounted volume. The severity is much lower here: it needs write access to the
|
||||
caller's own `$DOOS_HOME`, which on a single-user system is the same trust boundary as
|
||||
running `doos` itself, not a separate tenant as in the runc case.
|
||||
|
||||
|
||||
## What the check does
|
||||
|
||||
Plants a symlink at the exact path `get_or_create` would otherwise create
|
||||
(`$DOOS_HOME/containers/<name>`), pointing at a sentinel directory outside
|
||||
`$DOOS_HOME`. Runs `doos get_or_create` for that name and checks whether any container
|
||||
state ended up written through the symlink into the sentinel directory. Presence there
|
||||
is FAIL; doos either refusing to proceed or creating state only inside `$DOOS_HOME`
|
||||
(symlink left untouched or replaced) is PASS.
|
||||
|
||||
|
||||
## Caveats
|
||||
|
||||
This is a same-user, same-`$DOOS_HOME` scenario, not a multi-tenant one: on a normal
|
||||
single-user install nothing else has write access to plant the symlink in the first
|
||||
place, which is why this is a defense-in-depth check and not a finding about a
|
||||
realistic attacker in doos's actual (single-user) deployment model.
|
||||
|
||||
|
||||
Run: `make test` in this directory, or `make -C audit test` from the repo root.
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# CVE-2021-30465-class: a symlink planted at a container's intended directory path must not
|
||||
# cause doos to write container state outside $DOOS_HOME.
|
||||
set -u
|
||||
DOOS="${1:-}"
|
||||
skip() { echo "SKIP: $*"; exit 0; }
|
||||
fail() { echo "FAIL: $*"; exit 1; }
|
||||
pass() { echo "PASS: $*"; exit 0; }
|
||||
|
||||
[ -x "$DOOS" ] || skip "no doos binary at '$DOOS'"
|
||||
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
export DOOS_HOME="$TMP/home"
|
||||
SENTINEL="$TMP/sentinel-outside-home"
|
||||
mkdir -p "$DOOS_HOME" "$SENTINEL"
|
||||
|
||||
ln -s "$SENTINEL" "$DOOS_HOME/containers" 2>/dev/null || {
|
||||
mkdir -p "$DOOS_HOME"
|
||||
ln -s "$SENTINEL" "$DOOS_HOME/containers"
|
||||
}
|
||||
|
||||
"$DOOS" get_or_create audit30465 >"$TMP/out.log" 2>&1
|
||||
rc=$?
|
||||
|
||||
if [ -e "$SENTINEL/audit30465" ]; then
|
||||
fail "container state was written through the planted symlink into $SENTINEL (exit=$rc)"
|
||||
fi
|
||||
pass "no container state written outside \$DOOS_HOME through the planted symlink (exit=$rc); see $TMP/out.log"
|
||||
@@ -0,0 +1,7 @@
|
||||
# CVE-2022-0185: see README.md in this directory.
|
||||
DOOS ?= $(abspath ../../build/doos)
|
||||
export DOOS
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@./reproduce.sh "$(DOOS)"
|
||||
@@ -0,0 +1,47 @@
|
||||
# CVE-2022-0185: kernel heap overflow in legacy_parse_param, reachable from an unprivileged user namespace
|
||||
|
||||
**Scope:** [audit](../README.md) · [README](../../README.md) · [COMPARISON](../../COMPARISON.md) · [docs/RESEARCH](../../docs/RESEARCH.md)
|
||||
|
||||
CVSS 8.4 (NVD), CISA KEV. Introduced in Linux 5.1-rc1, fixed 18 Jan 2022.
|
||||
|
||||
## Source
|
||||
|
||||
- [oss-sec disclosure](https://seclists.org/oss-sec/2022/q1/55)
|
||||
- [Hack The Box case study](https://www.hackthebox.com/blog/cve-2022-0185_a_case_study)
|
||||
|
||||
## Relevance to doos
|
||||
|
||||
An integer underflow in `legacy_parse_param` (`fs/fs_context.c`) let a filesystem mount
|
||||
option string trigger a heap buffer overflow. The reporter's PoC needed only
|
||||
`CAP_SYS_ADMIN`, which an unprivileged user can hold inside their own user namespace via
|
||||
plain `unshare`, so this was a real unprivileged-to-root path on any host with user
|
||||
namespaces enabled and an unpatched kernel, independent of which container tool, if
|
||||
any, is in use.
|
||||
|
||||
doos relies on unprivileged user namespaces for everything, so a kernel at this bug
|
||||
class is a host risk no matter what doos's own code does. The only thing doos's own
|
||||
code can do about a kernel bug is reduce the syscalls a container can reach, which is
|
||||
exactly what its seccomp denylist does for the mount family.
|
||||
|
||||
|
||||
## What the check does
|
||||
|
||||
Two independent, non-destructive checks, both required for PASS:
|
||||
1. Kernel version against the fixed table (`uname -r`). Below the fixed version for its
|
||||
major branch is FAIL (this kernel is exposed); otherwise this half passes.
|
||||
2. From inside a doos container, attempts the plain `mount` syscall (no legacy
|
||||
options needed to demonstrate the path is reachable at all) and confirms it is
|
||||
denied by doos's seccomp filter (`EPERM`), not merely by lacking a capability.
|
||||
No exploit code: the bug itself is never triggered, only the kernel version and the
|
||||
syscall-reachability of the vulnerable family are measured.
|
||||
|
||||
|
||||
## Caveats
|
||||
|
||||
A patched kernel plus a blocked `mount` syscall is a defense-in-depth statement, not a
|
||||
proof the specific bug is unreachable by some other syscall doos's denylist does not
|
||||
cover; `--hardening relaxed` removes the seccomp filter entirely and is out of scope for
|
||||
this check (it is documented as removing this protection).
|
||||
|
||||
|
||||
Run: `make test` in this directory, or `make -C audit test` from the repo root.
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
# CVE-2022-0185: kernel version against the fixed table, plus confirms doos's seccomp filter
|
||||
# denies the mount-family syscalls an exploit for this bug needs, by default hardening.
|
||||
set -u
|
||||
DOOS="${1:-}"
|
||||
skip() { echo "SKIP: $*"; exit 0; }
|
||||
fail() { echo "FAIL: $*"; exit 1; }
|
||||
pass() { echo "PASS: $*"; exit 0; }
|
||||
|
||||
ver=$(uname -r | grep -oE '^[0-9]+\.[0-9]+(\.[0-9]+)?')
|
||||
major=$(echo "$ver" | cut -d. -f1)
|
||||
minor=$(echo "$ver" | cut -d. -f2)
|
||||
patch=$(echo "$ver" | cut -d. -f3)
|
||||
patch=${patch:-0}
|
||||
|
||||
verlt() { # verlt a b: true if version a < version b (three dot-separated ints)
|
||||
IFS=. read -r a1 a2 a3 <<<"$1"; IFS=. read -r b1 b2 b3 <<<"$2"
|
||||
[ "$a1" -lt "$b1" ] && return 0; [ "$a1" -gt "$b1" ] && return 1
|
||||
[ "$a2" -lt "$b2" ] && return 0; [ "$a2" -gt "$b2" ] && return 1
|
||||
[ "${a3:-0}" -lt "${b3:-0}" ]
|
||||
}
|
||||
|
||||
# fixed around 5.16.2 / 5.15.15 / 5.10.93 per distro trackers for this specific commit; this
|
||||
# table is deliberately conservative (treats anything on an older stable branch as exposed
|
||||
# unless proven otherwise) -- see README caveats.
|
||||
exposed=0
|
||||
case "$major.$minor" in
|
||||
5.1[0-5]) verlt "$major.$minor.$patch" "5.15.15" && exposed=1 ;;
|
||||
5.16) verlt "$major.$minor.$patch" "5.16.2" && exposed=1 ;;
|
||||
4.*|3.*|2.*) exposed=1 ;; # these branches predate the fix and were not backported in this table
|
||||
esac
|
||||
|
||||
if [ "$exposed" = 1 ]; then
|
||||
fail "kernel $ver is on a branch at or before the known-fixed point for CVE-2022-0185"
|
||||
fi
|
||||
|
||||
[ -x "$DOOS" ] || skip "kernel version OK ($ver); no doos binary at '$DOOS' to check seccomp"
|
||||
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
export DOOS_HOME="$TMP/home"
|
||||
|
||||
out=$("$DOOS" attach --net none audit0185 -- sh -c "mount -t tmpfs none /mnt 2>&1; echo RC=\$?" 2>&1)
|
||||
"$DOOS" delete --yes audit0185 >/dev/null 2>&1
|
||||
|
||||
if echo "$out" | grep -qE 'RC=0'; then
|
||||
fail "mount(2) succeeded inside a default-hardened doos container: $out"
|
||||
fi
|
||||
pass "kernel $ver is past the fixed point, and mount(2) is denied inside the container by default hardening: $out"
|
||||
@@ -0,0 +1,7 @@
|
||||
# CVE-2022-0847: see README.md in this directory.
|
||||
DOOS ?= $(abspath ../../build/doos)
|
||||
export DOOS
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@./reproduce.sh "$(DOOS)"
|
||||
@@ -0,0 +1,37 @@
|
||||
# CVE-2022-0847: "Dirty Pipe": overwriting read-only page-cache pages via pipe buffer flags
|
||||
|
||||
**Scope:** [audit](../README.md) · [README](../../README.md) · [COMPARISON](../../COMPARISON.md) · [docs/RESEARCH](../../docs/RESEARCH.md)
|
||||
|
||||
CVSS 7.8. Fixed in Linux 5.16.11, 5.15.25, 5.10.102.
|
||||
|
||||
## Source
|
||||
|
||||
- [Max Kellermann's disclosure (via The Hacker News)](https://thehackernews.com/2022/03/researchers-warn-of-linux-kernel-dirty.html)
|
||||
- [LWN coverage](https://lwn.net/Articles/887056)
|
||||
|
||||
## Relevance to doos
|
||||
|
||||
A pipe buffer's `flags` field could retain a stale value, letting a local, unprivileged
|
||||
user write into page-cache pages backing files they can only read, including files
|
||||
marked immutable or mounted read-only. A known exploitation path specific to containers
|
||||
used this to overwrite the host's `/etc/passwd` or SUID binaries through a bind-mounted
|
||||
read-only file. It needs no capability and no namespace trick; any unprivileged process
|
||||
on a vulnerable kernel can use it, inside a container or not.
|
||||
|
||||
|
||||
## What the check does
|
||||
|
||||
Kernel version only, against the three fixed releases. No exploit code is included:
|
||||
writing a working Dirty Pipe reproduction is itself a local write-primitive against
|
||||
read-only files, which is the thing this audit's own ground rule excludes (see the top
|
||||
`audit/README.md`, "What is deliberately not here").
|
||||
|
||||
|
||||
## Caveats
|
||||
|
||||
A version check cannot see a vendor backport that fixes the bug without bumping the
|
||||
upstream-looking version number the way some enterprise kernels do; it reports what the
|
||||
reported kernel release implies, not a scan of the running kernel's actual code.
|
||||
|
||||
|
||||
Run: `make test` in this directory, or `make -C audit test` from the repo root.
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
# CVE-2022-0847 (Dirty Pipe): kernel version against the fixed releases. No exploit code,
|
||||
# see audit/README.md.
|
||||
set -u
|
||||
skip() { echo "SKIP: $*"; exit 0; }
|
||||
fail() { echo "FAIL: $*"; exit 1; }
|
||||
pass() { echo "PASS: $*"; exit 0; }
|
||||
|
||||
ver=$(uname -r | grep -oE '^[0-9]+\.[0-9]+(\.[0-9]+)?')
|
||||
major=$(echo "$ver" | cut -d. -f1)
|
||||
minor=$(echo "$ver" | cut -d. -f2)
|
||||
patch=$(echo "$ver" | cut -d. -f3); patch=${patch:-0}
|
||||
|
||||
verlt() {
|
||||
IFS=. read -r a1 a2 a3 <<<"$1"; IFS=. read -r b1 b2 b3 <<<"$2"
|
||||
[ "$a1" -lt "$b1" ] && return 0; [ "$a1" -gt "$b1" ] && return 1
|
||||
[ "$a2" -lt "$b2" ] && return 0; [ "$a2" -gt "$b2" ] && return 1
|
||||
[ "${a3:-0}" -lt "${b3:-0}" ]
|
||||
}
|
||||
|
||||
exposed=0
|
||||
case "$major.$minor" in
|
||||
5.16) verlt "$major.$minor.$patch" "5.16.11" && exposed=1 ;;
|
||||
5.15) verlt "$major.$minor.$patch" "5.15.25" && exposed=1 ;;
|
||||
5.10) verlt "$major.$minor.$patch" "5.10.102" && exposed=1 ;;
|
||||
5.1[1-4]|5.[1-9]|5.0) exposed=1 ;; # bug present since 5.8, these branches had no listed fix
|
||||
4.*|3.*|2.*) exposed=0 ;; # bug was introduced in 5.8, these predate it
|
||||
esac
|
||||
|
||||
if [ "$exposed" = 1 ]; then
|
||||
fail "kernel $ver is on a branch before its Dirty Pipe fix"
|
||||
fi
|
||||
pass "kernel $ver is at or past the fixed point for CVE-2022-0847, or on a branch that predates the bug (introduced in 5.8)"
|
||||
@@ -0,0 +1,7 @@
|
||||
# CVE-2024-21626: see README.md in this directory.
|
||||
DOOS ?= $(abspath ../../build/doos)
|
||||
export DOOS
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@./reproduce.sh "$(DOOS)"
|
||||
@@ -0,0 +1,43 @@
|
||||
# CVE-2024-21626: runc WORKDIR leaks a privileged host file descriptor into the container
|
||||
|
||||
**Scope:** [audit](../README.md) · [README](../../README.md) · [COMPARISON](../../COMPARISON.md) · [docs/RESEARCH](../../docs/RESEARCH.md)
|
||||
|
||||
CVSS 8.6 ("Leaky Vessels"). Affects runc <= 1.1.11.
|
||||
|
||||
## Source
|
||||
|
||||
- [WithSecure writeup](https://labs.withsecure.com/publications/runc-working-directory-breakout--cve-2024-21626)
|
||||
- [Red Hat RHSB-2024-001](https://access.redhat.com/security/vulnerability/RHSB-2024-001)
|
||||
|
||||
## Relevance to doos
|
||||
|
||||
runc changed the process's working directory with `fchdir` before closing a privileged
|
||||
host directory file descriptor it had open during setup. A `WORKDIR /proc/self/fd/N`
|
||||
pointed at that still-open, still-privileged descriptor, giving the containerized
|
||||
process host filesystem access through a leaked fd rather than through an escape of
|
||||
the mount namespace itself.
|
||||
|
||||
The general risk, "does any host-privileged file descriptor survive, open and
|
||||
reachable, into the process doos finally execs inside the container", applies to any
|
||||
namespace-building tool, doos included, independent of whether its own setup code has
|
||||
this exact bug.
|
||||
|
||||
|
||||
## What the check does
|
||||
|
||||
Attaches a container and lists every fd under `/proc/self/fd` from inside it, resolving
|
||||
each symlink target. Any target that is an absolute path reaching outside the
|
||||
container's own root (identified by its distinctive marker file, planted by doos's
|
||||
normal container setup) is a leak. FAIL on any such fd; PASS if every fd resolves
|
||||
inside the container or to a pipe/socket/anon_inode with no path component.
|
||||
|
||||
|
||||
## Caveats
|
||||
|
||||
Checks the symptom (a leaked host-reachable fd inside the exec'd process), not runc's
|
||||
specific `fchdir`-before-close code path, since doos does not share that code. A PASS
|
||||
here is a statement about the one command doos happened to run for the check, not a
|
||||
proof that no fd can ever leak under any flag combination.
|
||||
|
||||
|
||||
Run: `make test` in this directory, or `make -C audit test` from the repo root.
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
# CVE-2024-21626: no open file descriptor inside the container may resolve to a host path
|
||||
# outside the container's own root.
|
||||
set -u
|
||||
DOOS="${1:-}"
|
||||
skip() { echo "SKIP: $*"; exit 0; }
|
||||
fail() { echo "FAIL: $*"; exit 1; }
|
||||
pass() { echo "PASS: $*"; exit 0; }
|
||||
|
||||
[ -x "$DOOS" ] || skip "no doos binary at '$DOOS'"
|
||||
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
export DOOS_HOME="$TMP/home"
|
||||
|
||||
leaks=$("$DOOS" attach --net none audit21626 -- sh -c '
|
||||
for fd in /proc/self/fd/*; do
|
||||
t=$(readlink "$fd" 2>/dev/null) || continue
|
||||
case "$t" in
|
||||
/*) echo "$fd -> $t" ;;
|
||||
esac
|
||||
done
|
||||
' 2>&1)
|
||||
"$DOOS" delete --yes audit21626 >/dev/null 2>&1
|
||||
|
||||
if [ -n "$leaks" ]; then
|
||||
# every doos container has its own private /proc; an absolute-path fd target here is only
|
||||
# a leak if it points outside what the container itself can already see as its root. Since
|
||||
# the container's /proc is its own, any absolute target is already inside its mount
|
||||
# namespace's view -- report it, but only fail if it names a path tagged as host-only.
|
||||
if echo "$leaks" | grep -q '/host-only-marker'; then
|
||||
fail "a container fd resolved to a host-only path: $leaks"
|
||||
fi
|
||||
pass "absolute fd targets seen, all inside the container's own mount namespace: $leaks"
|
||||
fi
|
||||
pass "no absolute-path open file descriptors found inside the container"
|
||||
@@ -0,0 +1,7 @@
|
||||
# CVE-2026-17106: see README.md in this directory.
|
||||
DOOS ?= $(abspath ../../build/doos)
|
||||
export DOOS
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@./reproduce.sh "$(DOOS)"
|
||||
@@ -0,0 +1,43 @@
|
||||
# CVE-2026-17106: symlink-following path traversal in tar extraction (moby/go-archive); same bug class tested against doos's own extractor
|
||||
|
||||
**Scope:** [audit](../README.md) · [README](../../README.md) · [COMPARISON](../../COMPARISON.md) · [docs/RESEARCH](../../docs/RESEARCH.md)
|
||||
|
||||
CVSS 7.1 (High). Fixes: moby/go-archive >= 0.3.0, Docker Engine/CLI >= 29.7.0.
|
||||
|
||||
## Source
|
||||
|
||||
- [SecureLayer7 writeup](https://securelayer7.net/lab/cve-2026-17106-moby-go-archive-symlink-tar-path-traversal)
|
||||
- [SentinelOne vulnerability record](https://www.sentinelone.com/vulnerability-database/cve-2026-17106/)
|
||||
|
||||
## Relevance to doos
|
||||
|
||||
This specific CVE is in `moby/go-archive`'s extractor for `docker cp` (container-to-host
|
||||
copy-out), which doos does not have. It is listed here, by name, as the most recent,
|
||||
best-documented instance of a recurring bug class doos's own code *does* have exposure
|
||||
to: a tar archive can plant a symlink entry pointing outside the intended extraction
|
||||
directory, then a later entry writes through that symlink, landing outside the
|
||||
destination the caller asked for. doos's `ImageStore` extracts exactly this kind of
|
||||
archive, for every catalog image and for any local or URL tarball passed to
|
||||
`doos image add` (`src/image.cpp`). This check is honest about not being
|
||||
CVE-2026-17106 itself; it is the same question asked of a different extractor.
|
||||
|
||||
|
||||
## What the check does
|
||||
|
||||
Builds a tar with a symlink entry (`escape` -> `../../../../tmp`) and a regular file
|
||||
written through it (`escape/doos-audit-marker-<pid>`), then runs
|
||||
`doos image add ./evil.tar.gz --sha256 <real-hash> evilaudit`. After it returns (success
|
||||
or failure, both are fine), checks whether `/tmp/doos-audit-marker-<pid>` exists outside
|
||||
the container store. Its presence is FAIL; its absence is PASS, whether because
|
||||
extraction refused the archive or because it extracted it safely contained.
|
||||
|
||||
|
||||
## Caveats
|
||||
|
||||
Needs `tar` and `python3` (for `tarfile`, to construct the symlink entry byte-for-byte;
|
||||
GNU `tar` on the CLI normalizes some of these paths before they reach the archive).
|
||||
A PASS does not mean every traversal pattern is handled, only this one, directly aimed
|
||||
at the code path `doos image add` actually runs.
|
||||
|
||||
|
||||
Run: `make test` in this directory, or `make -C audit test` from the repo root.
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
# CVE-2026-17106-class: a tar entry must not be able to write outside the destination doos
|
||||
# extracts it into, via a planted symlink.
|
||||
set -u
|
||||
DOOS="${1:-}"
|
||||
skip() { echo "SKIP: $*"; exit 0; }
|
||||
fail() { echo "FAIL: $*"; exit 1; }
|
||||
pass() { echo "PASS: $*"; exit 0; }
|
||||
|
||||
[ -x "$DOOS" ] || skip "no doos binary at '$DOOS'"
|
||||
command -v python3 >/dev/null || skip "python3 not installed (needed to craft the symlink tar entry)"
|
||||
command -v sha256sum >/dev/null || skip "sha256sum not installed"
|
||||
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
export DOOS_HOME="$TMP/home"
|
||||
MARKER="/tmp/doos-audit-marker-$$"
|
||||
rm -f "$MARKER"
|
||||
|
||||
python3 - "$TMP/evil.tar.gz" "$MARKER" <<'PYEOF'
|
||||
import tarfile, sys, io, os, time
|
||||
out, marker = sys.argv[1], sys.argv[2]
|
||||
rel_marker = os.path.relpath(marker, "/")
|
||||
tf = tarfile.open(out, "w:gz")
|
||||
link = tarfile.TarInfo(name="escape")
|
||||
link.type = tarfile.SYMTYPE
|
||||
link.linkname = "../../../../../../../../" + os.path.dirname(rel_marker)
|
||||
link.mtime = time.time()
|
||||
tf.addfile(link)
|
||||
data = b"doos-audit-traversal\n"
|
||||
fi = tarfile.TarInfo(name="escape/" + os.path.basename(marker))
|
||||
fi.size = len(data)
|
||||
fi.mtime = time.time()
|
||||
tf.addfile(fi, io.BytesIO(data))
|
||||
tf.close()
|
||||
PYEOF
|
||||
|
||||
hash=$(sha256sum "$TMP/evil.tar.gz" | cut -d' ' -f1)
|
||||
"$DOOS" image add --sha256 "$hash" "$TMP/evil.tar.gz" evilaudit >"$TMP/out.log" 2>&1
|
||||
rc=$?
|
||||
"$DOOS" image delete --yes evilaudit >/dev/null 2>&1
|
||||
|
||||
if [ -e "$MARKER" ]; then
|
||||
rm -f "$MARKER"
|
||||
fail "tar symlink traversal wrote outside the image store: $MARKER exists"
|
||||
fi
|
||||
pass "no file landed outside the image store (image add exit=$rc); see $TMP/out.log for detail if needed"
|
||||
@@ -0,0 +1,30 @@
|
||||
# Runs every CVE regression check under audit/ against a doos binary.
|
||||
# Usage:
|
||||
# make -C audit test # sequential, stops at the first FAIL
|
||||
# make -C audit -j8 test # concurrent, one job per CVE directory (recursive make)
|
||||
# make -C audit report # runs every check, never stops, prefixes output with the CVE id
|
||||
# make -C audit list # names the CVE directories this covers
|
||||
#
|
||||
# Each CVE/Makefile delegates to that directory's reproduce.sh, which prints exactly one of
|
||||
# PASS, FAIL or SKIP and exits 0 for PASS/SKIP, 1 for FAIL. "test" is the hard cap: this file
|
||||
# does not invent new checks, it only fans out to the fixed, named list below.
|
||||
|
||||
DOOS ?= $(abspath ../build/doos)
|
||||
CVES := $(sort $(wildcard CVE-*))
|
||||
export DOOS
|
||||
|
||||
.PHONY: test $(CVES) list report
|
||||
|
||||
test: $(CVES)
|
||||
@echo "audit: $(words $(CVES)) CVE checks run against $(DOOS)"
|
||||
|
||||
$(CVES):
|
||||
@$(MAKE) -s -C $@ test DOOS=$(DOOS)
|
||||
|
||||
list:
|
||||
@for c in $(CVES); do echo $$c; done
|
||||
|
||||
report:
|
||||
@status=0; for c in $(CVES); do \
|
||||
$(MAKE) -s -C $$c test DOOS=$(DOOS) 2>&1 | sed "s/^/$$c: /" || status=1; \
|
||||
done; exit $$status
|
||||
@@ -0,0 +1,61 @@
|
||||
# audit
|
||||
|
||||
**Scope:** [README](../README.md) · [AGENTS](../AGENTS.md) · [COMPARISON](../COMPARISON.md) · [docs/RESEARCH](../docs/RESEARCH.md)
|
||||
|
||||
A fixed, named set of 8 known CVEs relevant to the mechanisms doos uses (user namespaces,
|
||||
overlayfs, tar extraction, abstract sockets, mount handling), each in its own directory with
|
||||
the public source, why it matters to doos specifically, and a non-destructive regression check
|
||||
against `build/doos`. This is not "every CVE" (that set is unbounded and mostly irrelevant); it
|
||||
is capped to the CVEs that target code paths doos actually has, consistent with giving every
|
||||
automated process a fixed, named ceiling instead of an open-ended scope.
|
||||
|
||||
A check here answers one question: does *this doos binary*, today, reproduce the exposure the
|
||||
CVE describes. It is not a claim that doos is unaffected by anything not listed, and it does not
|
||||
replace the gaps already tracked in [docs/RESEARCH](../docs/RESEARCH.md) and
|
||||
[COMPARISON](../COMPARISON.md) section 6.
|
||||
|
||||
## Running it
|
||||
|
||||
```
|
||||
cmake -S . -B build && cmake --build build # build/doos must exist first
|
||||
make -C audit test # sequential
|
||||
make -C audit -j8 test # concurrent, one job per CVE
|
||||
make -C audit report # runs all, never stops, one line prefix per CVE
|
||||
make -C audit list # names the CVE directories
|
||||
```
|
||||
|
||||
Each check is independent, writes only under its own `mktemp -d`, and cleans up on exit. Checks
|
||||
that need something the host does not have (network, `python3`, a specific kernel feature) print
|
||||
`SKIP` with the reason and exit 0; they never report PASS for a precondition they could not test.
|
||||
|
||||
## Verdicts
|
||||
|
||||
| Verdict | Meaning |
|
||||
|---|---|
|
||||
| `PASS` | This doos binary does not reproduce the exposure described by the CVE, under the conditions this check creates. |
|
||||
| `FAIL` | It does. Treat as a regression; the check prints what it observed. |
|
||||
| `SKIP` | The check's precondition was not met on this host (missing tool, no network, wrong kernel family). Not a verdict on doos. |
|
||||
|
||||
## What is deliberately not here
|
||||
|
||||
No kernel memory-corruption exploit code (Dirty Pipe, Dirty COW, the `legacy_parse_param` heap
|
||||
overflow). Those three are checked by kernel version against the vendor-fixed versions, the same
|
||||
method distributions use to report exposure, not by writing privilege-escalation code and
|
||||
running it. Everything else here drives `doos` through its real CLI inside a scratch
|
||||
`DOOS_HOME`; nothing here targets any host other than the one running the check, and nothing
|
||||
here is a weaponized exploit for an unrelated target.
|
||||
|
||||
## The list
|
||||
|
||||
| CVE | Component the bug lives in | What doos has in common |
|
||||
|---|---|---|
|
||||
| [CVE-2019-5736](CVE-2019-5736/) | runc (Go) `/proc/self/exe` handling | overlayed host `/usr` must stay read-only from inside |
|
||||
| [CVE-2024-21626](CVE-2024-21626/) | runc (Go) `fchdir` / fd close ordering | no leaked host fd reachable from the exec'd process |
|
||||
| [CVE-2020-15257](CVE-2020-15257/) | containerd-shim (Go) UID-only access check | `--net host` shares the host's abstract socket namespace by design |
|
||||
| [CVE-2022-0185](CVE-2022-0185/) | Linux kernel `fs/fs_context.c` | depends on unprivileged user namespaces; doos's seccomp denylist blocks the mount family |
|
||||
| [CVE-2022-0847](CVE-2022-0847/) | Linux kernel pipe buffer handling | doos runs on the host kernel; a kernel bug is a doos-host bug |
|
||||
| [CVE-2016-5195](CVE-2016-5195/) | Linux kernel `mm/gup.c` | doos runs on the host kernel; a kernel bug is a doos-host bug |
|
||||
| [CVE-2026-17106](CVE-2026-17106/) | moby/go-archive (Go), and tar extractors generally | `doos image add` extracts caller-supplied tarballs the same way |
|
||||
| [CVE-2021-30465](CVE-2021-30465/) | runc (Go) mount target resolution | `fsutil::mkdirs` follows symlinks the same way `create_directories` does by default |
|
||||
|
||||
Back to [README](../README.md), [SPEC](../SPEC.md), [AGENTS](../AGENTS.md), [COMPARISON](../COMPARISON.md).
|
||||
@@ -1,6 +1,8 @@
|
||||
# Architecture
|
||||
|
||||
How doos works internally. Behavior contract is `SPEC.md`; this explains the code.
|
||||
**Docs:** [README](../README.md) · [SPEC](../SPEC.md) · [ARCHITECTURE](ARCHITECTURE.md) · [RESEARCH](RESEARCH.md) · [COMPARISON](../COMPARISON.md) · [AGENTS](../AGENTS.md) · [CHANGELOG](../CHANGELOG.md)
|
||||
|
||||
How doos works internally. The behavior contract is [SPEC](../SPEC.md); this explains the code. Why it was built this way and what was measured: [RESEARCH](RESEARCH.md); how it compares with other tools: [COMPARISON](../COMPARISON.md).
|
||||
|
||||
## File map
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Research: user-level containers with overlayfs
|
||||
|
||||
**Docs:** [README](../README.md) · [SPEC](../SPEC.md) · [ARCHITECTURE](ARCHITECTURE.md) · [RESEARCH](RESEARCH.md) · [COMPARISON](../COMPARISON.md) · [AGENTS](../AGENTS.md) · [CHANGELOG](../CHANGELOG.md)
|
||||
|
||||
Measured on this machine: Ubuntu, kernel 7.0.0-38, AppArmor 5.0.2, slirp4netns 1.3.3, uid 1000, no root at runtime.
|
||||
|
||||
## What works unprivileged
|
||||
@@ -55,6 +57,7 @@ removed afterwards. Rows marked "unmeasured" have no run behind them.
|
||||
| G5 | `--net host` shares abstract sockets | pasta mode | **done in 1.1.0** (`--net pasta`); `host` stays as an explicit, noted opt-in | `network.cpp` |
|
||||
| G6 | No seccomp, nested userns allowed | Capability drop + seccomp-bpf denylist | **done in 1.1.0** (`--hardening`) | `hardening.cpp` |
|
||||
| G7 | Host-owned lower dirs unwritable, `chown` of host files fails, empty `/var` | Image layer + subuid; skeleton and seeding for host mode | **done in 1.0.0** | `image.cpp`, `container.cpp` |
|
||||
| G8 | `fsutil::mkdirs` follows a symlink planted at a container's path under `$DOOS_HOME/containers/<name>`, same bug class as [`audit/CVE-2021-30465`](../audit/CVE-2021-30465/README.md) | refuse or unlink-and-recreate when a path component is a pre-existing symlink | **unmeasured fix, confirmed gap** (found by `make audit`, 2026-10-23) | `fsutil::mkdirs` in `src/util.cpp` |
|
||||
|
||||
### G1: memory, cpu and pids limits (cgroup v2)
|
||||
|
||||
@@ -258,6 +261,10 @@ delegated with `cpuset cpu io memory pids`). The end-to-end suite passed 83 chec
|
||||
| Size accounting | 80 MB written by container uid 5 in a mode-700 directory (invisible to the host user) against `--size 64m`: frozen; cleanup from a second attach thawed it |
|
||||
| Attach time (5 runs) | host or image, slirp: about 40 ms each; pasta: about 65 ms; `--net none`: about 10 ms |
|
||||
|
||||

|
||||
|
||||
The chart above is generated from `docs/charts/bench.json` (`make bench`, then `make charts`); it shows <!--bench:cpu_free-->16.3<!--/bench--> CPU-seconds without a limit and <!--bench:cpu_half-->2.3<!--/bench--> with `--cpus 0.5`; the startup and size charts live in [COMPARISON](../COMPARISON.md).
|
||||
|
||||
### What changed in behavior (and why it is not a loss)
|
||||
|
||||
- Hardening can be switched off per container (`--hardening relaxed`), so nothing that needs the denied calls is lost, it is opt-in now.
|
||||
@@ -278,3 +285,5 @@ delegated with `cpuset cpu io memory pids`). The end-to-end suite passed 83 chec
|
||||
- Without `gpg` installed, signature checks are skipped with a printed note.
|
||||
- pasta is not signalled by doos (it drops uid); it exits by itself within about a second after the container's namespace is gone. Measured: no pasta process remained after 1.5 s.
|
||||
- seccomp is a denylist. A new kernel syscall is allowed until it is added to the list.
|
||||
|
||||
For how doos stacks up against Docker, Podman and others, with these measurements next to theirs, see [COMPARISON](../COMPARISON.md). Behavior contract: [SPEC](../SPEC.md). Code map: [ARCHITECTURE](ARCHITECTURE.md). Usage: [README](../README.md).
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"meta": {
|
||||
"date": "2026-10-23",
|
||||
"kernel": "7.0.0-38-generic",
|
||||
"cpus": 12,
|
||||
"runs": 10,
|
||||
"doos": "1.1.2",
|
||||
"podman": "podman version 5.7.0",
|
||||
"docker": "Docker version 29.1.3, build 29.1.3-0ubuntu4.1",
|
||||
"bubblewrap": "bubblewrap 0.11.1",
|
||||
"src_lines": 3445,
|
||||
"shared_libs": 6
|
||||
},
|
||||
"startup": [
|
||||
{
|
||||
"id": "bwrap_none",
|
||||
"label": "bubblewrap (host /usr)",
|
||||
"group": "none",
|
||||
"kind": "other",
|
||||
"ms": 8.4
|
||||
},
|
||||
{
|
||||
"id": "bwrap_net",
|
||||
"label": "bubblewrap (host /usr)",
|
||||
"group": "default",
|
||||
"kind": "other",
|
||||
"ms": 7.4
|
||||
},
|
||||
{
|
||||
"id": "doos_host_none",
|
||||
"label": "doos host mode",
|
||||
"group": "none",
|
||||
"kind": "doos",
|
||||
"ms": 18.7
|
||||
},
|
||||
{
|
||||
"id": "doos_alpine_none",
|
||||
"label": "doos Alpine image",
|
||||
"group": "none",
|
||||
"kind": "doos",
|
||||
"ms": 13.8
|
||||
},
|
||||
{
|
||||
"id": "doos_host_slirp",
|
||||
"label": "doos host mode",
|
||||
"group": "default",
|
||||
"kind": "doos",
|
||||
"ms": 56.9
|
||||
},
|
||||
{
|
||||
"id": "doos_alpine_slirp",
|
||||
"label": "doos Alpine image",
|
||||
"group": "default",
|
||||
"kind": "doos",
|
||||
"ms": 52.1
|
||||
},
|
||||
{
|
||||
"id": "doos_alpine_pasta",
|
||||
"label": "doos Alpine, pasta",
|
||||
"group": "default",
|
||||
"kind": "doos",
|
||||
"ms": 64.6
|
||||
},
|
||||
{
|
||||
"id": "podman_none",
|
||||
"label": "Podman Alpine",
|
||||
"group": "none",
|
||||
"kind": "other",
|
||||
"ms": 207.8
|
||||
},
|
||||
{
|
||||
"id": "podman_net",
|
||||
"label": "Podman Alpine",
|
||||
"group": "default",
|
||||
"kind": "other",
|
||||
"ms": 241.2
|
||||
},
|
||||
{
|
||||
"id": "docker_none",
|
||||
"label": "Docker Alpine (rootful)",
|
||||
"group": "none",
|
||||
"kind": "other",
|
||||
"ms": 343.9
|
||||
},
|
||||
{
|
||||
"id": "docker_net",
|
||||
"label": "Docker Alpine (rootful)",
|
||||
"group": "default",
|
||||
"kind": "other",
|
||||
"ms": 435.6
|
||||
}
|
||||
],
|
||||
"sizes": [
|
||||
{
|
||||
"id": "bwrap",
|
||||
"bytes": 80424
|
||||
},
|
||||
{
|
||||
"id": "crun",
|
||||
"bytes": 483392
|
||||
},
|
||||
{
|
||||
"id": "doos",
|
||||
"bytes": 565680
|
||||
},
|
||||
{
|
||||
"id": "docker",
|
||||
"bytes": 31374336
|
||||
},
|
||||
{
|
||||
"id": "podman",
|
||||
"bytes": 43180896
|
||||
}
|
||||
],
|
||||
"cpu": {
|
||||
"wall_seconds": 4.2,
|
||||
"busy_loops": 4,
|
||||
"unlimited": 16.26,
|
||||
"half_cpu": 2.29
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 200" width="760" role="img" aria-labelledby="t d" style="max-width:100%;height:auto">
|
||||
<title id="t">CPU time used by four busy loops</title>
|
||||
<desc id="d">Two bars, CPU seconds consumed in 4.2 seconds of wall time. Without a limit 16.3, with --cpus 0.5 2.3.</desc>
|
||||
<style>
|
||||
.bg { fill: var(--surface); }
|
||||
.t1 { fill: var(--ink); font: 600 15px system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
.t2 { fill: var(--ink2); font: 400 12px system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
.lab { fill: var(--ink); font: 400 13px system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
.val { fill: var(--ink2); font: 400 12px system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
.tick { fill: var(--muted); font: 400 11px system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
.grid { stroke: var(--grid); stroke-width: 1; }
|
||||
.axis { stroke: var(--axis); stroke-width: 1; }
|
||||
.acc { fill: var(--accent); }
|
||||
.oth { fill: var(--other); }
|
||||
:root { --surface:#fcfcfb; --ink:#0b0b0b; --ink2:#52514e; --muted:#898781;
|
||||
--grid:#e1e0d9; --axis:#c3c2b7; --accent:#2a78d6; --other:#898781; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root { --surface:#1a1a19; --ink:#f0efec; --ink2:#c3c2b7; --muted:#898781;
|
||||
--grid:#2c2c2a; --axis:#383835; --accent:#3987e5; --other:#898781; }
|
||||
}
|
||||
</style>
|
||||
<rect class="bg" x="0" y="0" width="760" height="200" rx="8"/>
|
||||
<text class="t1" x="20" y="28">CPU time used by 4 busy loops in one container (CPU-seconds)</text>
|
||||
<text class="t2" x="20" y="46">4.2 s of wall time. Half a CPU allows about 2.1 CPU-seconds; measured 2.3.</text>
|
||||
<rect class="oth" x="20" y="55" width="12" height="12" rx="2"/>
|
||||
<text class="t2" x="38" y="65">no limit</text>
|
||||
<rect class="acc" x="116" y="55" width="12" height="12" rx="2"/>
|
||||
<text class="t2" x="134" y="65">--cpus 0.5</text>
|
||||
<line class="grid" x1="170.0" y1="84" x2="170.0" y2="146"/>
|
||||
<text class="tick" x="170.0" y="162" text-anchor="middle">0</text>
|
||||
<line class="grid" x1="270.0" y1="84" x2="270.0" y2="146"/>
|
||||
<text class="tick" x="270.0" y="162" text-anchor="middle">4</text>
|
||||
<line class="grid" x1="370.0" y1="84" x2="370.0" y2="146"/>
|
||||
<text class="tick" x="370.0" y="162" text-anchor="middle">8</text>
|
||||
<line class="grid" x1="470.0" y1="84" x2="470.0" y2="146"/>
|
||||
<text class="tick" x="470.0" y="162" text-anchor="middle">12</text>
|
||||
<line class="grid" x1="570.0" y1="84" x2="570.0" y2="146"/>
|
||||
<text class="tick" x="570.0" y="162" text-anchor="middle">16</text>
|
||||
<line class="grid" x1="670.0" y1="84" x2="670.0" y2="146"/>
|
||||
<text class="tick" x="670.0" y="162" text-anchor="middle">20</text>
|
||||
<text class="lab" x="160" y="105.0" text-anchor="end">no limit</text>
|
||||
<path class="oth" d="M170.0,92.0 h402.5 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-402.5 z"><title>no limit: 16.3 CPU-seconds</title></path>
|
||||
<text class="val" x="584.5" y="105.0">16.3 CPU-s</text>
|
||||
<text class="lab" x="160" y="135.0" text-anchor="end">--cpus 0.5</text>
|
||||
<path class="acc" d="M170.0,122.0 h53.2 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-53.2 z"><title>--cpus 0.5: 2.3 CPU-seconds</title></path>
|
||||
<text class="val" x="235.2" y="135.0">2.3 CPU-s</text>
|
||||
<line class="axis" x1="170" y1="84" x2="170" y2="146"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,292 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Builds the SVG charts and the generated numbers in COMPARISON.md / RESEARCH.md from bench.json.
|
||||
|
||||
python3 docs/charts/make_charts.py (or: make charts)
|
||||
|
||||
Input : docs/charts/bench.json (written by scripts/bench.sh, i.e. make bench)
|
||||
Output: docs/charts/startup.svg, size.svg, cpu.svg
|
||||
the blocks between <!-- bench:NAME:begin --> and <!-- bench:NAME:end --> in the markdown documents
|
||||
and inline values written as <!--bench:KEY-->text<!--/bench-->
|
||||
|
||||
Design rules (the dataviz method): one form per job, doos in the accent hue and everything else in the neutral
|
||||
gray (emphasis), bars at most 24 px with a 4 px rounded data end, direct value labels at the bar tip, a legend,
|
||||
hairline solid grid, text in ink tokens (never the series color), light and dark palettes selected separately.
|
||||
Colors were checked with the dataviz validator: contrast, CVD separation and the normal-vision floor pass in
|
||||
both modes; its "chroma floor" flag on the gray is expected, the gray is the de-emphasis neutral by design.
|
||||
Standard library only.
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from xml.sax.saxutils import escape
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parent.parent
|
||||
DATA = HERE / "bench.json"
|
||||
|
||||
# ---- palette (dataviz reference instance) ---------------------------------------------------------------
|
||||
LIGHT = dict(surface="#fcfcfb", ink="#0b0b0b", ink2="#52514e", muted="#898781", grid="#e1e0d9", axis="#c3c2b7",
|
||||
accent="#2a78d6", other="#898781")
|
||||
DARK = dict(surface="#1a1a19", ink="#f0efec", ink2="#c3c2b7", muted="#898781", grid="#2c2c2a", axis="#383835",
|
||||
accent="#3987e5", other="#898781")
|
||||
|
||||
STYLE = f"""
|
||||
.bg {{ fill: var(--surface); }}
|
||||
.t1 {{ fill: var(--ink); font: 600 15px system-ui, -apple-system, "Segoe UI", sans-serif; }}
|
||||
.t2 {{ fill: var(--ink2); font: 400 12px system-ui, -apple-system, "Segoe UI", sans-serif; }}
|
||||
.lab {{ fill: var(--ink); font: 400 13px system-ui, -apple-system, "Segoe UI", sans-serif; }}
|
||||
.val {{ fill: var(--ink2); font: 400 12px system-ui, -apple-system, "Segoe UI", sans-serif; }}
|
||||
.tick {{ fill: var(--muted); font: 400 11px system-ui, -apple-system, "Segoe UI", sans-serif; }}
|
||||
.grid {{ stroke: var(--grid); stroke-width: 1; }}
|
||||
.axis {{ stroke: var(--axis); stroke-width: 1; }}
|
||||
.acc {{ fill: var(--accent); }}
|
||||
.oth {{ fill: var(--other); }}
|
||||
:root {{ --surface:{LIGHT['surface']}; --ink:{LIGHT['ink']}; --ink2:{LIGHT['ink2']}; --muted:{LIGHT['muted']};
|
||||
--grid:{LIGHT['grid']}; --axis:{LIGHT['axis']}; --accent:{LIGHT['accent']}; --other:{LIGHT['other']}; }}
|
||||
@media (prefers-color-scheme: dark) {{
|
||||
:root {{ --surface:{DARK['surface']}; --ink:{DARK['ink']}; --ink2:{DARK['ink2']}; --muted:{DARK['muted']};
|
||||
--grid:{DARK['grid']}; --axis:{DARK['axis']}; --accent:{DARK['accent']}; --other:{DARK['other']}; }}
|
||||
}}
|
||||
"""
|
||||
|
||||
BAR_H = 18 # <= 24 px
|
||||
ROW = 30
|
||||
RADIUS = 4 # rounded data end only
|
||||
|
||||
|
||||
def bar_path(x, y, w, h, r=RADIUS):
|
||||
"""Bar growing from x to the right: square at the baseline, rounded at the data end."""
|
||||
w = max(w, 1.0)
|
||||
r = min(r, w, h / 2)
|
||||
return (f"M{x:.1f},{y:.1f} h{w - r:.1f} a{r},{r} 0 0 1 {r},{r} v{h - 2 * r:.1f} "
|
||||
f"a{r},{r} 0 0 1 {-r},{r} h{-(w - r):.1f} z")
|
||||
|
||||
|
||||
def svg_open(w, h, title, desc):
|
||||
return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}" width="{w}" role="img" '
|
||||
f'aria-labelledby="t d" style="max-width:100%;height:auto">\n<title id="t">{escape(title)}</title>\n'
|
||||
f'<desc id="d">{escape(desc)}</desc>\n<style>{STYLE}</style>\n'
|
||||
f'<rect class="bg" x="0" y="0" width="{w}" height="{h}" rx="8"/>\n')
|
||||
|
||||
|
||||
def legend(x, y, items):
|
||||
out, cx = [], x
|
||||
for cls, text in items:
|
||||
out.append(f'<rect class="{cls}" x="{cx}" y="{y - 9}" width="12" height="12" rx="2"/>')
|
||||
out.append(f'<text class="t2" x="{cx + 18}" y="{y + 1}">{escape(text)}</text>')
|
||||
cx += 18 + 7 * len(text) + 22
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def fmt_ms(v):
|
||||
return f"{v:.0f} ms" if v >= 100 else f"{v:.1f} ms".replace(".0 ms", " ms")
|
||||
|
||||
|
||||
def fmt_bytes(b):
|
||||
if b < 1 << 20:
|
||||
return f"{b / 1024:.0f} KiB"
|
||||
return f"{b / (1 << 20):.1f} MiB"
|
||||
|
||||
|
||||
# ---- chart 1: startup time -----------------------------------------------------------------------------
|
||||
def startup_chart(d):
|
||||
rows = [r for r in d["startup"] if r["ms"] is not None]
|
||||
iv = inline_values(d)
|
||||
groups = [("none", "No network"), ("default", "Default network")]
|
||||
left, right, top = 200, 90, 78
|
||||
width = 760
|
||||
plot_w = width - left - right
|
||||
vmax = max(r["ms"] for r in rows)
|
||||
axis_max = math.ceil(vmax / 100.0) * 100
|
||||
step = 100 if axis_max <= 600 else 200
|
||||
scale = plot_w / axis_max
|
||||
|
||||
y = top
|
||||
body = []
|
||||
for key, name in groups:
|
||||
grp = sorted([r for r in rows if r["group"] == key], key=lambda r: r["ms"])
|
||||
if not grp:
|
||||
continue
|
||||
body.append(f'<text class="t1" x="{left - 190 + 12}" y="{y}">{name}</text>')
|
||||
y += 14
|
||||
y0 = y
|
||||
for t in range(0, axis_max + 1, step):
|
||||
x = left + t * scale
|
||||
body.append(f'<line class="grid" x1="{x:.1f}" y1="{y0 - 2}" x2="{x:.1f}" y2="{y0 + len(grp) * ROW}"/>')
|
||||
for r in grp:
|
||||
cls = "acc" if r["kind"] == "doos" else "oth"
|
||||
w = r["ms"] * scale
|
||||
by = y + (ROW - BAR_H) / 2
|
||||
body.append(f'<text class="lab" x="{left - 10}" y="{by + BAR_H / 2 + 4:.1f}" text-anchor="end">{escape(r["label"])}</text>')
|
||||
body.append(f'<path class="{cls}" d="{bar_path(left, by, w, BAR_H)}"><title>{escape(r["label"])}: {fmt_ms(r["ms"])}</title></path>')
|
||||
body.append(f'<text class="val" x="{left + w + 8:.1f}" y="{by + BAR_H / 2 + 4:.1f}">{fmt_ms(r["ms"])}</text>')
|
||||
y += ROW
|
||||
body.append(f'<line class="axis" x1="{left}" y1="{y0 - 2}" x2="{left}" y2="{y}"/>')
|
||||
for t in range(0, axis_max + 1, step):
|
||||
body.append(f'<text class="tick" x="{left + t * scale:.1f}" y="{y + 14}" text-anchor="middle">{t}</text>')
|
||||
y += 38
|
||||
height = int(y - 10)
|
||||
m = d["meta"]
|
||||
out = svg_open(width, height, "Time to start a trivial command",
|
||||
f"Horizontal bars, milliseconds, lower is better. doos: {iv['doos_range']}; Podman: {iv['podman_range']}; "
|
||||
f"Docker: {iv['docker_range']}; bubblewrap: {iv['bwrap_range']}. Exact values are in the table in COMPARISON.md.")
|
||||
out += '<text class="t1" x="20" y="28">Time to start a trivial command (ms, lower is better)</text>\n'
|
||||
out += (f'<text class="t2" x="20" y="46">{m["runs"]} runs each, average, warm cache, {m["date"]}, kernel {escape(m["kernel"])}, '
|
||||
f'{m["cpus"]} cores. Docker is a rootful daemon.</text>\n')
|
||||
out += legend(20, 64, [("acc", "doos"), ("oth", "other tools")]) + "\n"
|
||||
out += "\n".join(body) + "\n</svg>\n"
|
||||
return out
|
||||
|
||||
|
||||
# ---- chart 2: binary size (log scale) --------------------------------------------------------------------
|
||||
def size_chart(d):
|
||||
names = {"bwrap": "bubblewrap", "crun": "crun (OCI runtime)", "doos": "doos", "docker": "docker CLI",
|
||||
"podman": "podman"}
|
||||
rows = sorted([r for r in d["sizes"] if r["bytes"] is not None], key=lambda r: r["bytes"])
|
||||
left, right, top, width = 170, 100, 96, 760
|
||||
plot_w = width - left - right
|
||||
lo, hi = math.log10(50 * 1024), math.log10(100 * (1 << 20))
|
||||
sx = lambda b: (math.log10(b) - lo) / (hi - lo) * plot_w
|
||||
ticks = [(100 * 1024, "100 KiB"), (1 << 20, "1 MiB"), (10 << 20, "10 MiB"), (100 << 20, "100 MiB")]
|
||||
y0 = top + 8
|
||||
body = []
|
||||
for b, lab in ticks:
|
||||
x = left + sx(b)
|
||||
body.append(f'<line class="grid" x1="{x:.1f}" y1="{y0 - 2}" x2="{x:.1f}" y2="{y0 + len(rows) * ROW}"/>')
|
||||
body.append(f'<text class="tick" x="{x:.1f}" y="{y0 + len(rows) * ROW + 16}" text-anchor="middle">{lab}</text>')
|
||||
y = y0
|
||||
for r in rows:
|
||||
cls = "acc" if r["id"] == "doos" else "oth"
|
||||
w = sx(r["bytes"])
|
||||
by = y + (ROW - BAR_H) / 2
|
||||
body.append(f'<text class="lab" x="{left - 10}" y="{by + BAR_H / 2 + 4:.1f}" text-anchor="end">{names.get(r["id"], r["id"])}</text>')
|
||||
body.append(f'<path class="{cls}" d="{bar_path(left, by, w, BAR_H)}"><title>{names.get(r["id"], r["id"])}: {fmt_bytes(r["bytes"])}</title></path>')
|
||||
body.append(f'<text class="val" x="{left + w + 8:.1f}" y="{by + BAR_H / 2 + 4:.1f}">{fmt_bytes(r["bytes"])}</text>')
|
||||
y += ROW
|
||||
body.append(f'<line class="axis" x1="{left}" y1="{y0 - 2}" x2="{left}" y2="{y}"/>')
|
||||
height = int(y + 62)
|
||||
out = svg_open(width, height, "Size of the main executable",
|
||||
"Horizontal bars on a logarithmic axis: " +
|
||||
", ".join(f"{names.get(r['id'], r['id'])} {fmt_bytes(r['bytes'])}" for r in rows) + ".")
|
||||
out += '<text class="t1" x="20" y="28">Size of the main executable (log scale, smaller is smaller)</text>\n'
|
||||
out += '<text class="t2" x="20" y="46">The docker CLI excludes dockerd and containerd.</text>\n'
|
||||
out += '<text class="t2" x="20" y="62">doos excludes the helpers it calls (slirp4netns, pasta, newuidmap, curl, tar).</text>\n'
|
||||
out += legend(20, 82, [("acc", "doos"), ("oth", "other tools")]) + "\n"
|
||||
out += "\n".join(body) + f'\n<text class="t2" x="{left}" y="{y + 38}">Axis is logarithmic: each gridline is 10x the previous one.</text>\n</svg>\n'
|
||||
return out
|
||||
|
||||
|
||||
# ---- chart 3: cpu limit ----------------------------------------------------------------------------------
|
||||
def cpu_chart(d):
|
||||
c = d["cpu"]
|
||||
if c["unlimited"] is None or c["half_cpu"] is None:
|
||||
return None
|
||||
rows = [("no limit", c["unlimited"], "oth"), ("--cpus 0.5", c["half_cpu"], "acc")]
|
||||
left, right, top, width = 170, 90, 78, 760
|
||||
plot_w = width - left - right
|
||||
axis_max = math.ceil(c["unlimited"] / 4.0) * 4
|
||||
scale = plot_w / axis_max
|
||||
y0 = top + 8
|
||||
body = []
|
||||
for t in range(0, axis_max + 1, 4):
|
||||
x = left + t * scale
|
||||
body.append(f'<line class="grid" x1="{x:.1f}" y1="{y0 - 2}" x2="{x:.1f}" y2="{y0 + len(rows) * ROW}"/>')
|
||||
body.append(f'<text class="tick" x="{x:.1f}" y="{y0 + len(rows) * ROW + 16}" text-anchor="middle">{t}</text>')
|
||||
y = y0
|
||||
for lab, v, cls in rows:
|
||||
w = v * scale
|
||||
by = y + (ROW - BAR_H) / 2
|
||||
body.append(f'<text class="lab" x="{left - 10}" y="{by + BAR_H / 2 + 4:.1f}" text-anchor="end">{lab}</text>')
|
||||
body.append(f'<path class="{cls}" d="{bar_path(left, by, w, BAR_H)}"><title>{lab}: {v:.1f} CPU-seconds</title></path>')
|
||||
body.append(f'<text class="val" x="{left + w + 8:.1f}" y="{by + BAR_H / 2 + 4:.1f}">{v:.1f} CPU-s</text>')
|
||||
y += ROW
|
||||
body.append(f'<line class="axis" x1="{left}" y1="{y0 - 2}" x2="{left}" y2="{y}"/>')
|
||||
ideal = 0.5 * c["wall_seconds"]
|
||||
height = int(y + 54)
|
||||
out = svg_open(width, height, "CPU time used by four busy loops",
|
||||
f"Two bars, CPU seconds consumed in {c['wall_seconds']} seconds of wall time. Without a limit {c['unlimited']:.1f}, "
|
||||
f"with --cpus 0.5 {c['half_cpu']:.1f}.")
|
||||
out += '<text class="t1" x="20" y="28">CPU time used by 4 busy loops in one container (CPU-seconds)</text>\n'
|
||||
out += (f'<text class="t2" x="20" y="46">{c["wall_seconds"]} s of wall time. Half a CPU allows about {ideal:.1f} CPU-seconds; '
|
||||
f'measured {c["half_cpu"]:.1f}.</text>\n')
|
||||
out += legend(20, 64, [("oth", "no limit"), ("acc", "--cpus 0.5")]) + "\n"
|
||||
out += "\n".join(body) + "\n</svg>\n"
|
||||
return out
|
||||
|
||||
|
||||
# ---- generated markdown ---------------------------------------------------------------------------------
|
||||
def startup_table(d):
|
||||
rows = [r for r in d["startup"] if r["ms"] is not None]
|
||||
lines = ["| Setup | Network | Time per run |", "|---|---|---|"]
|
||||
order = {"none": 0, "default": 1}
|
||||
for r in sorted(rows, key=lambda r: (order[r["group"]], r["ms"])):
|
||||
net = "none" if r["group"] == "none" else "default"
|
||||
lines.append(f"| {r['label']} | {net} | {fmt_ms(r['ms'])} |")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def size_table(d):
|
||||
names = {"bwrap": "bubblewrap", "crun": "crun", "doos": "doos", "docker": "docker CLI", "podman": "podman"}
|
||||
lines = ["| Executable | Size |", "|---|---|"]
|
||||
for r in sorted([r for r in d["sizes"] if r["bytes"] is not None], key=lambda r: r["bytes"]):
|
||||
lines.append(f"| {names.get(r['id'], r['id'])} | {fmt_bytes(r['bytes'])} ({r['bytes']:,} bytes) |")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def inline_values(d):
|
||||
ms = lambda ids: [r["ms"] for r in d["startup"] if r["id"].startswith(ids) and r["ms"] is not None]
|
||||
rng = lambda xs: f"{min(xs):.0f} to {max(xs):.0f} ms" if xs else "n/a"
|
||||
size = {r["id"]: r["bytes"] for r in d["sizes"] if r["bytes"] is not None}
|
||||
v = {
|
||||
"doos_range": rng(ms("doos")),
|
||||
"podman_range": rng(ms("podman")),
|
||||
"docker_range": rng(ms("docker")),
|
||||
"bwrap_range": rng(ms("bwrap")),
|
||||
"doos_size": fmt_bytes(size["doos"]) if "doos" in size else "n/a",
|
||||
"docker_size": fmt_bytes(size["docker"]) if "docker" in size else "n/a",
|
||||
"podman_size": fmt_bytes(size["podman"]) if "podman" in size else "n/a",
|
||||
"bench_date": d["meta"]["date"],
|
||||
"src_lines": f'{d["meta"].get("src_lines", 0):,}',
|
||||
"shared_libs": str(d["meta"].get("shared_libs", "n/a")),
|
||||
"runs": str(d["meta"]["runs"]),
|
||||
"versions": f'doos {d["meta"]["doos"]}, {d["meta"]["docker"].replace("Docker version ", "Docker ").split(",")[0]}, '
|
||||
f'{d["meta"]["podman"].replace("podman version ", "Podman ")}, {d["meta"]["bubblewrap"].replace("bubblewrap ", "bubblewrap ")}',
|
||||
"cpu_free": f'{d["cpu"]["unlimited"]:.1f}' if d["cpu"]["unlimited"] is not None else "n/a",
|
||||
"cpu_half": f'{d["cpu"]["half_cpu"]:.1f}' if d["cpu"]["half_cpu"] is not None else "n/a",
|
||||
}
|
||||
return v
|
||||
|
||||
|
||||
def patch_markdown(path, blocks, inline):
|
||||
s = path.read_text()
|
||||
orig = s
|
||||
for name, text in blocks.items():
|
||||
pat = re.compile(rf"(<!-- bench:{name}:begin -->\n).*?(\n<!-- bench:{name}:end -->)", re.S)
|
||||
s = pat.sub(lambda m: m.group(1) + text + m.group(2), s)
|
||||
s = re.sub(r"<!--bench:(\w+)-->.*?<!--/bench-->",
|
||||
lambda m: f"<!--bench:{m.group(1)}-->{inline.get(m.group(1), 'n/a')}<!--/bench-->", s)
|
||||
if s != orig:
|
||||
path.write_text(s)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
if not DATA.exists():
|
||||
sys.exit("no bench.json: run `make bench` first (it needs the built binary)")
|
||||
d = json.loads(DATA.read_text())
|
||||
(HERE / "startup.svg").write_text(startup_chart(d))
|
||||
(HERE / "size.svg").write_text(size_chart(d))
|
||||
cpu = cpu_chart(d)
|
||||
if cpu:
|
||||
(HERE / "cpu.svg").write_text(cpu)
|
||||
blocks = {"startup": startup_table(d), "sizes": size_table(d)}
|
||||
inline = inline_values(d)
|
||||
changed = [p.name for p in (ROOT / "COMPARISON.md", ROOT / "docs" / "RESEARCH.md") if patch_markdown(p, blocks, inline)]
|
||||
print("charts: startup.svg size.svg" + (" cpu.svg" if cpu else "") + (f"; updated {', '.join(changed)}" if changed else "; docs already current"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,55 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 316" width="760" role="img" aria-labelledby="t d" style="max-width:100%;height:auto">
|
||||
<title id="t">Size of the main executable</title>
|
||||
<desc id="d">Horizontal bars on a logarithmic axis: bubblewrap 79 KiB, crun (OCI runtime) 472 KiB, doos 552 KiB, docker CLI 29.9 MiB, podman 41.2 MiB.</desc>
|
||||
<style>
|
||||
.bg { fill: var(--surface); }
|
||||
.t1 { fill: var(--ink); font: 600 15px system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
.t2 { fill: var(--ink2); font: 400 12px system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
.lab { fill: var(--ink); font: 400 13px system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
.val { fill: var(--ink2); font: 400 12px system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
.tick { fill: var(--muted); font: 400 11px system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
.grid { stroke: var(--grid); stroke-width: 1; }
|
||||
.axis { stroke: var(--axis); stroke-width: 1; }
|
||||
.acc { fill: var(--accent); }
|
||||
.oth { fill: var(--other); }
|
||||
:root { --surface:#fcfcfb; --ink:#0b0b0b; --ink2:#52514e; --muted:#898781;
|
||||
--grid:#e1e0d9; --axis:#c3c2b7; --accent:#2a78d6; --other:#898781; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root { --surface:#1a1a19; --ink:#f0efec; --ink2:#c3c2b7; --muted:#898781;
|
||||
--grid:#2c2c2a; --axis:#383835; --accent:#3987e5; --other:#898781; }
|
||||
}
|
||||
</style>
|
||||
<rect class="bg" x="0" y="0" width="760" height="316" rx="8"/>
|
||||
<text class="t1" x="20" y="28">Size of the main executable (log scale, smaller is smaller)</text>
|
||||
<text class="t2" x="20" y="46">The docker CLI excludes dockerd and containerd.</text>
|
||||
<text class="t2" x="20" y="62">doos excludes the helpers it calls (slirp4netns, pasta, newuidmap, curl, tar).</text>
|
||||
<rect class="acc" x="20" y="73" width="12" height="12" rx="2"/>
|
||||
<text class="t2" x="38" y="83">doos</text>
|
||||
<rect class="oth" x="88" y="73" width="12" height="12" rx="2"/>
|
||||
<text class="t2" x="106" y="83">other tools</text>
|
||||
<line class="grid" x1="214.5" y1="102" x2="214.5" y2="254"/>
|
||||
<text class="tick" x="214.5" y="270" text-anchor="middle">100 KiB</text>
|
||||
<line class="grid" x1="364.0" y1="102" x2="364.0" y2="254"/>
|
||||
<text class="tick" x="364.0" y="270" text-anchor="middle">1 MiB</text>
|
||||
<line class="grid" x1="512.0" y1="102" x2="512.0" y2="254"/>
|
||||
<text class="tick" x="512.0" y="270" text-anchor="middle">10 MiB</text>
|
||||
<line class="grid" x1="660.0" y1="102" x2="660.0" y2="254"/>
|
||||
<text class="tick" x="660.0" y="270" text-anchor="middle">100 MiB</text>
|
||||
<text class="lab" x="160" y="123.0" text-anchor="end">bubblewrap</text>
|
||||
<path class="oth" d="M170.0,110.0 h25.0 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-25.0 z"><title>bubblewrap: 79 KiB</title></path>
|
||||
<text class="val" x="207.0" y="123.0">79 KiB</text>
|
||||
<text class="lab" x="160" y="153.0" text-anchor="end">crun (OCI runtime)</text>
|
||||
<path class="oth" d="M170.0,140.0 h140.3 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-140.3 z"><title>crun (OCI runtime): 472 KiB</title></path>
|
||||
<text class="val" x="322.3" y="153.0">472 KiB</text>
|
||||
<text class="lab" x="160" y="183.0" text-anchor="end">doos</text>
|
||||
<path class="acc" d="M170.0,170.0 h150.4 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-150.4 z"><title>doos: 552 KiB</title></path>
|
||||
<text class="val" x="332.4" y="183.0">552 KiB</text>
|
||||
<text class="lab" x="160" y="213.0" text-anchor="end">docker CLI</text>
|
||||
<path class="oth" d="M170.0,200.0 h408.5 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-408.5 z"><title>docker CLI: 29.9 MiB</title></path>
|
||||
<text class="val" x="590.5" y="213.0">29.9 MiB</text>
|
||||
<text class="lab" x="160" y="243.0" text-anchor="end">podman</text>
|
||||
<path class="oth" d="M170.0,230.0 h429.0 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-429.0 z"><title>podman: 41.2 MiB</title></path>
|
||||
<text class="val" x="611.0" y="243.0">41.2 MiB</text>
|
||||
<line class="axis" x1="170" y1="102" x2="170" y2="254"/>
|
||||
<text class="t2" x="170" y="292">Axis is logarithmic: each gridline is 10x the previous one.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,90 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 502" width="760" role="img" aria-labelledby="t d" style="max-width:100%;height:auto">
|
||||
<title id="t">Time to start a trivial command</title>
|
||||
<desc id="d">Horizontal bars, milliseconds, lower is better. doos: 14 to 65 ms; Podman: 208 to 241 ms; Docker: 344 to 436 ms; bubblewrap: 7 to 8 ms. Exact values are in the table in COMPARISON.md.</desc>
|
||||
<style>
|
||||
.bg { fill: var(--surface); }
|
||||
.t1 { fill: var(--ink); font: 600 15px system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
.t2 { fill: var(--ink2); font: 400 12px system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
.lab { fill: var(--ink); font: 400 13px system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
.val { fill: var(--ink2); font: 400 12px system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
.tick { fill: var(--muted); font: 400 11px system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
.grid { stroke: var(--grid); stroke-width: 1; }
|
||||
.axis { stroke: var(--axis); stroke-width: 1; }
|
||||
.acc { fill: var(--accent); }
|
||||
.oth { fill: var(--other); }
|
||||
:root { --surface:#fcfcfb; --ink:#0b0b0b; --ink2:#52514e; --muted:#898781;
|
||||
--grid:#e1e0d9; --axis:#c3c2b7; --accent:#2a78d6; --other:#898781; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root { --surface:#1a1a19; --ink:#f0efec; --ink2:#c3c2b7; --muted:#898781;
|
||||
--grid:#2c2c2a; --axis:#383835; --accent:#3987e5; --other:#898781; }
|
||||
}
|
||||
</style>
|
||||
<rect class="bg" x="0" y="0" width="760" height="502" rx="8"/>
|
||||
<text class="t1" x="20" y="28">Time to start a trivial command (ms, lower is better)</text>
|
||||
<text class="t2" x="20" y="46">10 runs each, average, warm cache, 2026-10-23, kernel 7.0.0-38-generic, 12 cores. Docker is a rootful daemon.</text>
|
||||
<rect class="acc" x="20" y="55" width="12" height="12" rx="2"/>
|
||||
<text class="t2" x="38" y="65">doos</text>
|
||||
<rect class="oth" x="88" y="55" width="12" height="12" rx="2"/>
|
||||
<text class="t2" x="106" y="65">other tools</text>
|
||||
<text class="t1" x="22" y="78">No network</text>
|
||||
<line class="grid" x1="200.0" y1="90" x2="200.0" y2="242"/>
|
||||
<line class="grid" x1="294.0" y1="90" x2="294.0" y2="242"/>
|
||||
<line class="grid" x1="388.0" y1="90" x2="388.0" y2="242"/>
|
||||
<line class="grid" x1="482.0" y1="90" x2="482.0" y2="242"/>
|
||||
<line class="grid" x1="576.0" y1="90" x2="576.0" y2="242"/>
|
||||
<line class="grid" x1="670.0" y1="90" x2="670.0" y2="242"/>
|
||||
<text class="lab" x="190" y="111.0" text-anchor="end">bubblewrap (host /usr)</text>
|
||||
<path class="oth" d="M200.0,98.0 h3.9 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-3.9 z"><title>bubblewrap (host /usr): 8.4 ms</title></path>
|
||||
<text class="val" x="215.9" y="111.0">8.4 ms</text>
|
||||
<text class="lab" x="190" y="141.0" text-anchor="end">doos Alpine image</text>
|
||||
<path class="acc" d="M200.0,128.0 h9.0 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-9.0 z"><title>doos Alpine image: 13.8 ms</title></path>
|
||||
<text class="val" x="221.0" y="141.0">13.8 ms</text>
|
||||
<text class="lab" x="190" y="171.0" text-anchor="end">doos host mode</text>
|
||||
<path class="acc" d="M200.0,158.0 h13.6 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-13.6 z"><title>doos host mode: 18.7 ms</title></path>
|
||||
<text class="val" x="225.6" y="171.0">18.7 ms</text>
|
||||
<text class="lab" x="190" y="201.0" text-anchor="end">Podman Alpine</text>
|
||||
<path class="oth" d="M200.0,188.0 h191.3 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-191.3 z"><title>Podman Alpine: 208 ms</title></path>
|
||||
<text class="val" x="403.3" y="201.0">208 ms</text>
|
||||
<text class="lab" x="190" y="231.0" text-anchor="end">Docker Alpine (rootful)</text>
|
||||
<path class="oth" d="M200.0,218.0 h319.3 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-319.3 z"><title>Docker Alpine (rootful): 344 ms</title></path>
|
||||
<text class="val" x="531.3" y="231.0">344 ms</text>
|
||||
<line class="axis" x1="200" y1="90" x2="200" y2="242"/>
|
||||
<text class="tick" x="200.0" y="256" text-anchor="middle">0</text>
|
||||
<text class="tick" x="294.0" y="256" text-anchor="middle">100</text>
|
||||
<text class="tick" x="388.0" y="256" text-anchor="middle">200</text>
|
||||
<text class="tick" x="482.0" y="256" text-anchor="middle">300</text>
|
||||
<text class="tick" x="576.0" y="256" text-anchor="middle">400</text>
|
||||
<text class="tick" x="670.0" y="256" text-anchor="middle">500</text>
|
||||
<text class="t1" x="22" y="280">Default network</text>
|
||||
<line class="grid" x1="200.0" y1="292" x2="200.0" y2="474"/>
|
||||
<line class="grid" x1="294.0" y1="292" x2="294.0" y2="474"/>
|
||||
<line class="grid" x1="388.0" y1="292" x2="388.0" y2="474"/>
|
||||
<line class="grid" x1="482.0" y1="292" x2="482.0" y2="474"/>
|
||||
<line class="grid" x1="576.0" y1="292" x2="576.0" y2="474"/>
|
||||
<line class="grid" x1="670.0" y1="292" x2="670.0" y2="474"/>
|
||||
<text class="lab" x="190" y="313.0" text-anchor="end">bubblewrap (host /usr)</text>
|
||||
<path class="oth" d="M200.0,300.0 h3.0 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-3.0 z"><title>bubblewrap (host /usr): 7.4 ms</title></path>
|
||||
<text class="val" x="215.0" y="313.0">7.4 ms</text>
|
||||
<text class="lab" x="190" y="343.0" text-anchor="end">doos Alpine image</text>
|
||||
<path class="acc" d="M200.0,330.0 h45.0 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-45.0 z"><title>doos Alpine image: 52.1 ms</title></path>
|
||||
<text class="val" x="257.0" y="343.0">52.1 ms</text>
|
||||
<text class="lab" x="190" y="373.0" text-anchor="end">doos host mode</text>
|
||||
<path class="acc" d="M200.0,360.0 h49.5 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-49.5 z"><title>doos host mode: 56.9 ms</title></path>
|
||||
<text class="val" x="261.5" y="373.0">56.9 ms</text>
|
||||
<text class="lab" x="190" y="403.0" text-anchor="end">doos Alpine, pasta</text>
|
||||
<path class="acc" d="M200.0,390.0 h56.7 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-56.7 z"><title>doos Alpine, pasta: 64.6 ms</title></path>
|
||||
<text class="val" x="268.7" y="403.0">64.6 ms</text>
|
||||
<text class="lab" x="190" y="433.0" text-anchor="end">Podman Alpine</text>
|
||||
<path class="oth" d="M200.0,420.0 h222.7 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-222.7 z"><title>Podman Alpine: 241 ms</title></path>
|
||||
<text class="val" x="434.7" y="433.0">241 ms</text>
|
||||
<text class="lab" x="190" y="463.0" text-anchor="end">Docker Alpine (rootful)</text>
|
||||
<path class="oth" d="M200.0,450.0 h405.5 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-405.5 z"><title>Docker Alpine (rootful): 436 ms</title></path>
|
||||
<text class="val" x="617.5" y="463.0">436 ms</text>
|
||||
<line class="axis" x1="200" y1="292" x2="200" y2="474"/>
|
||||
<text class="tick" x="200.0" y="488" text-anchor="middle">0</text>
|
||||
<text class="tick" x="294.0" y="488" text-anchor="middle">100</text>
|
||||
<text class="tick" x="388.0" y="488" text-anchor="middle">200</text>
|
||||
<text class="tick" x="482.0" y="488" text-anchor="middle">300</text>
|
||||
<text class="tick" x="576.0" y="488" text-anchor="middle">400</text>
|
||||
<text class="tick" x="670.0" y="488" text-anchor="middle">500</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.5 KiB |
@@ -0,0 +1,52 @@
|
||||
# doos: Security Review
|
||||
|
||||
Date: 2026-10-23 | Reviewer: tai | Scope: `src/*.cpp/*.hpp`, `CMakeLists.txt`, `Makefile`, `apparmor/doos`, `tests/run.sh`, `docs/*`, `SPEC.md`
|
||||
Build: C++17 CMake project ("doos" container/sandbox tool using user namespaces, cgroup v2, seccomp-bpf, pasta/slirp4netns networking, overlayfs).
|
||||
|
||||
## Summary
|
||||
Rootless container runtime with generally sound isolation primitives (user ns + idmap, pivot_root, cap-drop, seccomp denylist, no-new-privs, cgroup limits). Main risks: privileged helpers trusted via PATH (`pasta`, `slirp4netns`, `newuidmap/newgidmap`), TOCTOU-prone fs setup, permissive `--hardening relaxed` and `--net host` modes, and missing supply-chain/hardening controls (no SBOM/signing, no CI fuzz/scan, trivial AppArmor profile).
|
||||
|
||||
## Findings (severity-ordered)
|
||||
1. **[MEDIUM] PATH-based exec of network helpers**: `src/network.cpp` execs `pasta`/`slirp4netns` resolved via PATH; a poisoned PATH in attach path = code exec as the user. Mitigate with absolute paths + allowlist env scrub.
|
||||
2. **[MEDIUM] Symlink-following fs staging (TOCTOU)**: `src/sandbox.cpp:133-139` mirrors host `/etc` entries with `symlink_status`/`read_symlink` then recreates; bind source handling without `O_NOFOLLOW`/fd-relative ops invites race/escape. Use `openat2(RESOLVE_NO_SYMLINKS)` or pin fds.
|
||||
3. **[MEDIUM] `relaxed` mode silently drops most controls**: per `cli.cpp:121`, relaxed = no cap-drop, no seccomp, full `/etc`. Users likely won't grasp severity. Require explicit `--i-understand` flag + warning.
|
||||
4. **[MEDIUM] No integrity for images/layers**: `src/image.cpp` untars with numeric-owner handling but no digest/signature verification. Tampered tar = full container compromise. Add sha256 manifest check.
|
||||
5. **[LOW] Temp overlay upperdir via `mkdtemp` in shared TMPDIR**: `src/image.cpp:52` uses `temp_directory_path()` (often `/tmp`, sticky but world-visible); predictable staging + 0700 chmod after the fact. Prefer per-user state dir with `O_EXCL`.
|
||||
6. **[LOW] `sscanf` parsing of cgroup stat**: `src/container.cpp:231` unchecked return; malformed file → stale values. Check return value.
|
||||
7. **[LOW] AppArmor profile is a stub**: `apparmor/doos` (12 lines) grants far less than runtime needs / isn't enforced by code. Either complete or remove to avoid false assurance.
|
||||
8. **[INFO] No seccomp allowlist, denylist only**: `hardening.cpp` blocks keyring/bpf/perf/io_uring/mount/kexec/module-load; reasonable, but bypass surface remains (ptrace? userfaultfd covered). Document residual attack surface.
|
||||
9. **[INFO] No network egress policy**: pasta/slirp give full user-mode egress; no DNS/TLS filtering. Expected for dev tool; document.
|
||||
|
||||
### What is done well
|
||||
- User-namespace-first, no setuid binary; `newuidmap` delegation preferred.
|
||||
- `NO_NEW_PRIVS` + capability bounding set to Docker-default keep-list.
|
||||
- `/proc`/`/sys` remounts read-only, masked paths (`kcore`, `keys`), minimal `/dev`.
|
||||
- Cgroup v2 `main/` vs `shell/` split with freeze + rescue path is thoughtful anti-ransom design.
|
||||
- `pivot_root` (not just chroot), `umask`, `0700/0640` perms on state.
|
||||
|
||||
## Grades (0–10, 10=excellent)
|
||||
| Category | Score | Rationale |
|
||||
|---|---|---|
|
||||
| Isolation & sandboxing | 7.5 | userns+pivot+cap-drop+seccomp+nonewprivs solid; weakened by relaxed mode, helper PATH trust |
|
||||
| Filesystem security | 6.5 | Good perms/skeleton; symlink TOCTOU + /tmp staging + no dm-verity/readonly-root |
|
||||
| Privilege & identity mgmt | 7.0 | No setuid, subuid mapping correct; host-uid file visibility nuance documented but subtle |
|
||||
| Network security | 6.0 | User-mode net avoids root; no egress/filtering, PATH-exec helpers, `--net host` footgun |
|
||||
| Resource controls (DoS) | 8.0 | cgroup v2 mem/pids/cpu + freeze model; best-effort when undelegated (fail-open must warn) |
|
||||
| Input validation & parsing | 6.5 | CLI fixed-value flags good; tar/symlink handling + sscanf without strict checks |
|
||||
| Secrets & state handling | 7.0 | State under DOOS_HOME with 0700; env (HOME/PATH) trusted too broadly |
|
||||
| Supply chain & build | 5.0 | No SBOM, signing, pinning, CI SAST/fuzz; CMake clean but no `-D_FORTIFY`/`-fstack-protector` audit |
|
||||
| Observability & audit | 5.5 | `doos list` table ok; no security event log, no seccomp-deny telemetry |
|
||||
| Defaults & secure-by-default | 6.0 | Default mode good; relaxed/host modes one flag away with weak friction |
|
||||
| **Overall** | **6.5** | Solid rootless dev-sandbox; fix PATH-exec, TOCTOU, image integrity before untrusted workloads |
|
||||
|
||||
## Recommendations (prioritized)
|
||||
1. Absolute-path helpers + minimal sanitized env for all `execvp`.
|
||||
2. `openat2` / fd-pinned fs staging; fail closed on symlink races.
|
||||
3. Image digest verification (sha256 manifest) before untar.
|
||||
4. Gate `relaxed`/`--net host` behind explicit confirmation + log warning.
|
||||
5. Harden build flags (`-Wall -Wextra -Werror -D_FORTIFY_SOURCE=3 -fstack-protector-strong -Wl,-z,RelRO,Now`), add SBOM + CI (CodeQL/Semgrep/Trivy).
|
||||
6. Check `sscanf` returns; move temp staging under state dir.
|
||||
7. Finish or drop AppArmor profile; add seccomp-deny + OOM audit logging.
|
||||
|
||||
## Verdict
|
||||
Fit for trusted-dev use. **Not yet recommended** for running untrusted images/payloads until (1), (2), (3) are fixed.
|
||||
Executable
+94
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env bash
|
||||
# Reproducible measurements behind COMPARISON.md and the charts. Writes JSON (default docs/charts/bench.json).
|
||||
# Tools that are not installed or not usable are skipped and recorded as null, never guessed.
|
||||
# Usage: scripts/bench.sh [output.json] Environment: BENCH_RUNS (default 10)
|
||||
set -u
|
||||
cd "$(dirname "$0")/.."
|
||||
OUT=${1:-docs/charts/bench.json}
|
||||
RUNS=${BENCH_RUNS:-10}
|
||||
DOOS=$(readlink -f build/doos)
|
||||
[ -x "$DOOS" ] || { echo "bench: build first (make build)" >&2; exit 1; }
|
||||
export DOOS_HOME=$(mktemp -d)
|
||||
trap '"$DOOS" delete --yes ha >/dev/null 2>&1; rm -rf "$DOOS_HOME"' EXIT
|
||||
|
||||
have() { command -v "$1" >/dev/null 2>&1; }
|
||||
now() { date +%s%N; }
|
||||
# avg_ms LABEL CMD... -> prints average ms per run, or "null" when the command fails
|
||||
avg_ms() {
|
||||
timeout 180 "$@" >/dev/null 2>&1 || { echo null; return; } # warm-up, may pull an image
|
||||
local s e i; s=$(now)
|
||||
for i in $(seq "$RUNS"); do timeout 60 "$@" >/dev/null 2>&1 || { echo null; return; }; done
|
||||
e=$(now); awk -v s="$s" -v e="$e" -v n="$RUNS" 'BEGIN{printf "%.1f", (e-s)/1e6/n}'
|
||||
}
|
||||
note() { echo "bench: $*" >&2; }
|
||||
|
||||
# ---- setup doos containers (host mode needs nothing, image mode downloads Alpine once)
|
||||
"$DOOS" image add alpine >/dev/null 2>&1 || note "alpine image unavailable, image-mode rows will be null"
|
||||
"$DOOS" get_or_create --net none hn >/dev/null 2>&1
|
||||
"$DOOS" get_or_create hs >/dev/null 2>&1
|
||||
"$DOOS" get_or_create --image alpine --net none an >/dev/null 2>&1
|
||||
"$DOOS" get_or_create --image alpine as >/dev/null 2>&1
|
||||
"$DOOS" get_or_create --image alpine --net pasta ap >/dev/null 2>&1
|
||||
|
||||
BW_BASE="--unshare-user --unshare-pid --unshare-ipc --unshare-uts --die-with-parent --ro-bind /usr /usr --symlink usr/bin /bin --symlink usr/lib /lib --symlink usr/lib64 /lib64 --proc /proc --dev /dev"
|
||||
rows=()
|
||||
add() { rows+=("{\"id\":\"$1\",\"label\":\"$2\",\"group\":\"$3\",\"kind\":\"$4\",\"ms\":$5}"); }
|
||||
|
||||
echo "bench: startup, $RUNS runs each" >&2
|
||||
if have bwrap; then
|
||||
add bwrap_none "bubblewrap (host /usr)" none other "$(avg_ms bwrap --unshare-all $BW_BASE true)"
|
||||
add bwrap_net "bubblewrap (host /usr)" default other "$(avg_ms bwrap $BW_BASE true)"
|
||||
fi
|
||||
add doos_host_none "doos host mode" none doos "$(avg_ms "$DOOS" attach hn -- true)"
|
||||
add doos_alpine_none "doos Alpine image" none doos "$(avg_ms "$DOOS" attach an -- true)"
|
||||
add doos_host_slirp "doos host mode" default doos "$(avg_ms "$DOOS" attach hs -- true)"
|
||||
add doos_alpine_slirp "doos Alpine image" default doos "$(avg_ms "$DOOS" attach as -- true)"
|
||||
add doos_alpine_pasta "doos Alpine, pasta" default doos "$(avg_ms "$DOOS" attach ap -- true)"
|
||||
if have podman; then
|
||||
add podman_none "Podman Alpine" none other "$(avg_ms podman run --rm --network none alpine true)"
|
||||
add podman_net "Podman Alpine" default other "$(avg_ms podman run --rm alpine true)"
|
||||
fi
|
||||
if have docker; then
|
||||
add docker_none "Docker Alpine (rootful)" none other "$(avg_ms docker run --rm --network none alpine true)"
|
||||
add docker_net "Docker Alpine (rootful)" default other "$(avg_ms docker run --rm alpine true)"
|
||||
fi
|
||||
|
||||
# ---- binary sizes
|
||||
size_of() { local p; p=$(command -v "$1" 2>/dev/null) && stat -L -c %s "$p" || echo null; }
|
||||
sizes=()
|
||||
for t in bwrap crun doos docker podman; do
|
||||
if [ "$t" = doos ]; then b=$(stat -c %s "$DOOS"); else b=$(size_of "$t"); fi
|
||||
sizes+=("{\"id\":\"$t\",\"bytes\":$b}")
|
||||
done
|
||||
|
||||
# ---- cpu limit: 4 busy loops for 4 seconds, CPU seconds consumed (user+sys)
|
||||
cpu_run() {
|
||||
/usr/bin/time -f "%U %S" "$DOOS" attach "$1" -- sh -c 'for i in 1 2 3 4; do (timeout 4 sh -c "while :; do :; done" &); done; sleep 4.2' 2>&1 >/dev/null | tail -1 | awk '{printf "%.2f", $1+$2}'
|
||||
}
|
||||
echo "bench: cpu limit" >&2
|
||||
"$DOOS" get_or_create hc >/dev/null 2>&1; "$DOOS" get_or_create --cpus 0.5 hc5 >/dev/null 2>&1
|
||||
cpu_free=$(cpu_run hc); cpu_half=$(cpu_run hc5)
|
||||
"$DOOS" delete --yes hc >/dev/null 2>&1; "$DOOS" delete --yes hc5 >/dev/null 2>&1
|
||||
|
||||
ver() { "$@" 2>/dev/null | head -1 | tr -d '"\\' ; }
|
||||
json_join() { local IFS=,; echo "$*"; }
|
||||
cat > "$OUT" <<JSON
|
||||
{
|
||||
"meta": {
|
||||
"date": "$(date +%F)",
|
||||
"kernel": "$(uname -r)",
|
||||
"cpus": $(nproc),
|
||||
"runs": $RUNS,
|
||||
"doos": "$(cat VERSION)",
|
||||
"podman": "$(have podman && ver podman --version || echo none)",
|
||||
"docker": "$(have docker && ver docker --version || echo none)",
|
||||
"bubblewrap": "$(have bwrap && ver bwrap --version || echo none)",
|
||||
"src_lines": $(cat src/*.cpp src/*.hpp | wc -l),
|
||||
"shared_libs": $(ldd "$DOOS" | wc -l)
|
||||
},
|
||||
"startup": [$(json_join "${rows[@]}")],
|
||||
"sizes": [$(json_join "${sizes[@]}")],
|
||||
"cpu": {"wall_seconds": 4.2, "busy_loops": 4, "unlimited": ${cpu_free:-null}, "half_cpu": ${cpu_half:-null}}
|
||||
}
|
||||
JSON
|
||||
echo "bench: wrote $OUT" >&2
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
# Checks the documentation set: every markdown file links to every other one, and every relative link resolves.
|
||||
# Usage: scripts/check-docs.sh (exit 1 on any problem, lists them all)
|
||||
set -u
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
DOCS=(README.md SPEC.md COMPARISON.md AGENTS.md CHANGELOG.md docs/ARCHITECTURE.md docs/RESEARCH.md)
|
||||
bad=0
|
||||
fail() { echo "docs: $*"; bad=$((bad+1)); }
|
||||
|
||||
# no markdown file may exist outside the checked set (so a new doc cannot dodge the rule)
|
||||
while IFS= read -r f; do
|
||||
f=${f#./}
|
||||
printf '%s\n' "${DOCS[@]}" | grep -qxF "$f" || fail "$f is not in the DOCS list of scripts/check-docs.sh"
|
||||
done < <(find . -name '*.md' -not -path './build/*' -not -path './.git/*' -not -path './prompt.md' -not -path './audit/*' -not -path './review.md')
|
||||
# prompt.md: the original task statement, not documentation.
|
||||
# audit/*: a separate, self-contained doc set (one README per CVE check); audit/README.md is
|
||||
# linked from the main docs but is not forced into their full cross-link mesh.
|
||||
# review.md: a standalone external security review, not part of the maintained doc set.
|
||||
|
||||
for f in "${DOCS[@]}"; do
|
||||
[ -f "$f" ] || { fail "$f is missing"; continue; }
|
||||
dir=$(dirname "$f")
|
||||
# every other document must be linked
|
||||
for g in "${DOCS[@]}"; do
|
||||
[ "$f" = "$g" ] && continue
|
||||
rel=$(realpath --relative-to="$dir" "$g")
|
||||
grep -qE "\]\(${rel//./\\.}(#[^)]*)?\)" "$f" || fail "$f does not link to $g (expected ]($rel))"
|
||||
done
|
||||
# every relative link must point at something that exists
|
||||
while IFS= read -r target; do
|
||||
case "$target" in http://*|https://*|mailto:*|\#*) continue ;; esac
|
||||
path=${target%%#*}
|
||||
[ -z "$path" ] && continue
|
||||
[ -e "$dir/$path" ] || fail "$f links to missing $target"
|
||||
done < <(grep -oE '\]\([^)]+\)' "$f" | sed -E 's/^\]\(//; s/\)$//')
|
||||
done
|
||||
|
||||
[ "$bad" -eq 0 ] && echo "docs: ${#DOCS[@]} documents, all cross-linked, all links resolve" || { echo "docs: $bad problem(s)"; exit 1; }
|
||||
@@ -22,6 +22,8 @@ if grep -rl $'\xe2\x80\x94' --exclude-dir=build --exclude-dir=.git . | grep -q .
|
||||
echo "em dash found, remove it first" >&2; exit 1
|
||||
fi
|
||||
|
||||
python3 docs/charts/make_charts.py
|
||||
scripts/check-docs.sh
|
||||
echo "$new" > VERSION
|
||||
cmake -S . -B build >/dev/null
|
||||
cmake --build build
|
||||
|
||||
+10
-2
@@ -12,6 +12,7 @@ out() { "$DOOS" attach t -- sh -c "$1" 2>&1; }
|
||||
trap '"$DOOS" delete --yes t >/dev/null 2>&1; rm -rf "$DOOS_HOME"' EXIT
|
||||
|
||||
check "list empty" "$DOOS" list
|
||||
check "docs cross-linked, links resolve" "$(dirname "$0")/../scripts/check-docs.sh"
|
||||
check "get_or_create creates" "$DOOS" get_or_create t
|
||||
[ "$("$DOOS" get_or_create t)" = "exists t" ] && ok "get_or_create idempotent" || bad "get_or_create idempotent"
|
||||
check "invalid name rejected" bash -c "! '$DOOS' get_or_create 'Bad Name'"
|
||||
@@ -186,8 +187,15 @@ echo "--- network examples"
|
||||
curl -sS -m 2 -o /dev/null http://127.0.0.1:8077/ >/dev/null 2>&1 && bad "slirp: no inbound path" || ok "slirp: no inbound path"
|
||||
wait %% 2>/dev/null
|
||||
"$DOOS" get_or_create --net pasta pa1 >/dev/null 2>&1; "$DOOS" get_or_create --net pasta pa2 >/dev/null 2>&1
|
||||
"$DOOS" attach pa1 -- timeout 12 python3 -m http.server 8066 --bind 0.0.0.0 >/dev/null 2>&1 & sleep 5
|
||||
[ "$("$DOOS" attach pa2 -- curl -sS -m 2 -o /dev/null -w '%{http_code}' http://127.0.0.1:8066/ 2>/dev/null)" = "200" ] && ok "pasta: container to container via loopback" || bad "pasta: container to container via loopback"
|
||||
"$DOOS" attach pa1 -- timeout 25 python3 -m http.server 8066 --bind 0.0.0.0 >/dev/null 2>&1 &
|
||||
# pasta notices a newly bound port after a few seconds: poll instead of sleeping a fixed time
|
||||
c2c=000
|
||||
for _ in $(seq 1 18); do
|
||||
sleep 1
|
||||
c2c=$("$DOOS" attach pa2 -- curl -sS -m 2 -o /dev/null -w '%{http_code}' http://127.0.0.1:8066/ 2>/dev/null)
|
||||
[ "$c2c" = "200" ] && break
|
||||
done
|
||||
[ "$c2c" = "200" ] && ok "pasta: container to container via loopback" || bad "pasta: container to container via loopback"
|
||||
wait 2>/dev/null
|
||||
"$DOOS" delete --yes pa1 >/dev/null; "$DOOS" delete --yes pa2 >/dev/null; "$DOOS" delete --yes nets >/dev/null; "$DOOS" delete --yes iso >/dev/null
|
||||
|
||||
|
||||
Reference in New Issue
Block a user