Add --mount for host-dir bind mounts (e.g. a WebDAV share via rclone)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 07:35:30 +02:00
co-authored by Claude Sonnet 5
parent 4c754b20da
commit 7adf43dd03
17 changed files with 232 additions and 199 deletions
+5 -1
View File
@@ -2,7 +2,11 @@
**Docs:** [README](README.md) · [SPEC](SPEC.md) · [ARCHITECTURE](docs/ARCHITECTURE.md) · [RESEARCH](docs/RESEARCH.md) · [COMPARISON](COMPARISON.md) · [AGENTS](AGENTS.md) · [CHANGELOG](CHANGELOG.md) · [CI](docs/CI.md)
## 1.3.2 - 2026-10-24
## 1.5.0 - 2026-10-24
- `--mount SRC:DST[:ro]` (repeatable, max 8): bind-mounts a host directory into the container, outside the `--size` cap. Lets a container reach storage that is not local, for example a WebDAV share mounted on the host first with `rclone mount` or `davfs2`; doos never speaks a network filesystem protocol itself, it only bind-mounts what the host already mounted. Changeable like `--memory`/`--pids`/`--cpus`: replaces the whole mount list, applies at the next start. Tracked as gap G12 in [RESEARCH](docs/RESEARCH.md); 5 new end-to-end checks (read, write, read-only, missing source rejected, too many rejected).
- Docs: [COMPARISON](COMPARISON.md) section 1 and the gaps list updated (doos now has host-dir bind mounts); charts and generated numbers re-measured (`make bench && make charts`).
## 1.4.0 - 2026-10-24
- CI: `.gitea/workflows/ci.yml` (Gitea Actions) runs `build`, `valgrind`, `test` and `audit` on every push. `build`/`valgrind` run on any Docker-mode runner; `test`/`audit` need a self-hosted host-mode runner (real namespaces, cgroup v2 delegation, the AppArmor profile), documented with its one-time setup in the new [docs/CI.md](docs/CI.md). Every step wraps its own `timeout`, since Gitea Actions parses but does not enforce `timeout-minutes`/`concurrency` (checked against the 1.25 docs).
- Fixed, from [review2.md](review2.md): `parseSize` (`src/util.cpp`) overflowed silently on an oversized input (`--size 16777217t` was accepted as exactly `1.0T` instead of rejected); now checked before the shift and rejected. Tracked as gap G9 in [RESEARCH](docs/RESEARCH.md); regression case added to `tests/run.sh`.
- Fixed, from [review2.md](review2.md): `newuidmap`, `newgidmap`, `slirp4netns` and `pasta` were each pinned to one hardcoded `/usr/bin/...` path; they now go through `resolveBinary()` (state-dir override, then a fixed list of system directories), the same resolution `curl`/`tar`/`sha256sum`/`gpg` already used. Tracked as gap G10.
+19 -19
View File
@@ -22,7 +22,7 @@ Versions compared: doos 1.1.1, Docker 29.1.3 (rootful daemon on this machine), P
| 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 |
| Host dirs into the container (`-v`) | yes, `--mount SRC:DST[:ro]`, not subject to the size cap [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)] |
@@ -30,7 +30,7 @@ Versions compared: doos 1.1.1, Docker 29.1.3 (rootful daemon on this machine), P
| 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] |
| Binary size | <!--bench:doos_size-->532 KiB<!--/bench-->, <!--bench:shared_libs-->6<!--/bench--> shared libraries, <!--bench:src_lines-->3,712<!--/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 |
@@ -48,24 +48,24 @@ Versions compared: doos 1.1.1, Docker 29.1.3 (rootful daemon on this machine), P
## 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.
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-24<!--/bench-->. Versions: <!--bench:versions-->doos 1.4.0, 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.
![Horizontal bar chart of the time to start a trivial command for doos, bubblewrap, Podman and Docker, with and without network. doos is highlighted. Exact values are in the table below.](docs/charts/startup.svg)
<!-- 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 |
| bubblewrap (host /usr) | none | 8.8 ms |
| doos Alpine image | none | 14.2 ms |
| doos host mode | none | 20.2 ms |
| Podman Alpine | none | 283 ms |
| Docker Alpine (rootful) | none | 463 ms |
| bubblewrap (host /usr) | default | 7.8 ms |
| doos Alpine image | default | 58 ms |
| doos host mode | default | 61.4 ms |
| doos Alpine, pasta | default | 82.5 ms |
| Podman Alpine | default | 313 ms |
| Docker Alpine (rootful) | default | 645 ms |
<!-- bench:startup:end -->
How to read this honestly:
@@ -82,7 +82,7 @@ How to read this honestly:
|---|---|
| bubblewrap | 79 KiB (80,424 bytes) |
| crun | 472 KiB (483,392 bytes) |
| doos | 552 KiB (565,680 bytes) |
| doos | 532 KiB (544,640 bytes) |
| docker CLI | 29.9 MiB (31,374,336 bytes) |
| podman | 41.2 MiB (43,180,896 bytes) |
<!-- bench:sizes:end -->
@@ -105,8 +105,8 @@ The Podman figure is this machine's accumulated state, not a like-for-like compa
**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].
- Tiny and auditable: <!--bench:doos_size-->532 KiB<!--/bench-->, <!--bench:src_lines-->3,712<!--/bench--> lines of C++ [M], one process tree, no background service, nothing listening.
- Instant: <!--bench:doos_range-->14 to 82 ms<!--/bench--> against <!--bench:docker_range-->463 to 645 ms<!--/bench--> for a rootful Docker run and <!--bench:podman_range-->283 to 313 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/)].
@@ -175,7 +175,7 @@ These are the gaps that matter when comparing, each next to where it lives. They
| 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 devices, no GPU passthrough (host directory bind mounts exist: `--mount`) | `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 |
@@ -186,7 +186,7 @@ These are the gaps that matter when comparing, each next to where it lives. They
## 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.
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-->532 KiB<!--/bench--> single binary, it starts in <!--bench:doos_range-->14 to 82 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:
+3
View File
@@ -114,9 +114,12 @@ Notes, all measured:
| `--memory S` | memory cap | 8g | changeable |
| `--pids N` | process/thread cap | 4096 | changeable |
| `--cpus N` | cpu cap, e.g. `0.5`, `2` | all cores | changeable |
| `--mount SRC:DST[:ro]` | bind-mount host dir `SRC` at `DST` inside, repeatable (max 8) | none | changeable |
Changeable options apply the next time the container starts. Examples: `doos attach --memory 2g --cpus 1 build`, `doos attach --net pasta --image debian-bookworm web`.
`--mount` gives a container extra storage without counting against `--size`: mount a remote filesystem on the host first (for example a WebDAV share with `rclone mount webdav-remote: /mnt/webdav`), then `doos attach --mount /mnt/webdav:/data NAME` to reach it inside. `SRC` must exist and be a directory when the container starts; it fails loudly otherwise. `--mount SRC:DST:ro` mounts it read-only.
## Limits, size cap and hardening
- **Limits** (`--memory`, `--pids`, `--cpus`): cgroup v2, measured here: a 256m container touching 400 MB is killed whole and doos says so; 80 forks against `--pids 32` fail and are reported; `--cpus 0.5` held 4 busy loops to 2.3 CPU-seconds in 4.3 s of wall time.
+4 -2
View File
@@ -32,8 +32,9 @@ Container options (accepted by `get_or_create` and `attach`):
| `--memory SIZE` | Memory cap (cgroup `memory.max`, swap 0, whole-container OOM kill). | 8g | yes |
| `--pids N` | Process and thread cap (`pids.max`). | 4096 | yes |
| `--cpus N` | CPU cap, decimals allowed (`0.5`, `2`); `0` = all cores. | 0 | yes |
| `--mount SRC:DST[:ro]` | Bind-mount host directory `SRC` at `DST` inside the container, repeatable (max 8). Not subject to `--size`: it is host storage, not the container's writable layer. Replaces the whole mount list when given. | none | yes |
Changeable options apply at the next start of the container. Naming a fixed option on an existing container with a different value is an error, never a silent change.
Changeable options apply at the next start of the container. Naming a fixed option on an existing container with a different value is an error, never a silent change. `--mount`'s `SRC` must exist and be a directory at the time the container starts (host responsibility, e.g. `rclone mount` a WebDAV share there first); a missing `SRC` fails the start loudly.
## Limits (hard caps)
@@ -59,6 +60,7 @@ Changeable options apply at the next start of the container. Naming a fixed opti
| slirp4netns ready wait | 5 s |
| Lock wait | 5 s |
| Tree recursion depth | 256 |
| `--mount` entries | 8 |
## State layout
@@ -69,7 +71,7 @@ containers/NAME/{meta,pid,lock,seeded.*,rootfs/,layers/<dir>/{upper,work},privat
images/NAME/{meta,rootfs/}
```
`meta` keys: `net created version image ids size`.
`meta` keys: `net created version image ids size memory pids cpus hardening mounts`.
## Container filesystem, host mode (no `--image`)
+1 -1
View File
@@ -1 +1 @@
1.4.0
1.5.0
+3 -1
View File
@@ -71,7 +71,9 @@ Image mode:
2. Mountpoints (`/proc /dev /tmp /run ...`) created inside the merged tree.
Both modes then: tmpfs `/tmp` and `/run`; `/dev` (tmpfs, bound host nodes, new devpts, `/dev/shm`, symlinks);
`/proc` with read-only remounts and masks from `spec.hpp`; `chdir(rootfs)`, `pivot_root(".", ".")`, lazy
`/proc` with read-only remounts and masks from `spec.hpp`; `mountExtra` binds each `Container::mounts()`
entry (`--mount SRC:DST[:ro]`) at `DST`, creating the mountpoint first (`fsutil::mkdirs`), read-only ones
remounted the same way `/proc`'s read-only paths are; `chdir(rootfs)`, `pivot_root(".", ".")`, lazy
unmount of the old root.
## Seeding (host mode, once per container)
+2 -1
View File
@@ -66,6 +66,7 @@ removed afterwards. Rows marked "unmeasured" have no run behind them.
| G9 | `parseSize` left-shifted without an overflow check: `--size 16777217t` wrapped to exactly `1.0T` and was accepted instead of rejected | check `v > (UINT64_MAX >> shift)` before shifting, reject instead of wrapping | **done in 1.3.2**, reproduced before the fix, re-run after: rejected | `parseSize` in `src/util.cpp` |
| G10 | `newuidmap`/`newgidmap`/`slirp4netns`/`pasta` were pinned to a single hardcoded `/usr/bin/...` path, unlike `curl`/`tar`/`sha256sum`/`gpg` which go through `resolveBinary()`'s state-dir-override-then-fixed-dirs search; a distro installing them elsewhere would fail even though PATH-poisoning was never possible either way | route all four through `resolveBinary()`, same as the other helpers | **done in 1.3.2** | `idmap.cpp`, `network.cpp` |
| G11 | Alpine's `latest-releases.yaml` and Debian's `index.json` (embedded base64 manifest) are read with fixed string/byte-offset search, not a YAML/JSON library; a publisher reformatting either file breaks extraction with a thrown error (fails closed) rather than silently misreading it | none: a dependency to parse two small, publisher-controlled files is a worse trade than failing loudly when their format changes, given the project's own "fewer dependencies" rule | **accepted, not fixed**; this row exists so the trade-off is written down, not rediscovered | `Catalog::resolve` in `src/image.cpp` |
| G12 | No way to give a container storage that does not count against `--size` (remote/network storage, e.g. a WebDAV share); every writable path was either the host overlay (capped) or a private dir (capped) | `--mount SRC:DST[:ro]`: bind-mount an already-mounted host path into the container, outside the size watchdog's scan set (`Container::usedBytes` only scans `layers/`/`private/`, never `--mount` targets). doos itself never speaks a network filesystem protocol: mount WebDAV/NFS/etc. on the host first (e.g. `rclone mount`), doos only bind-mounts what is already there | **done in 1.5.0** (`--mount`, max 8 per container, changeable, applies at next start); missing `SRC` fails the start loudly | `Container::mounts`, `mountExtra` in `src/sandbox.cpp` |
### G1: memory, cpu and pids limits (cgroup v2)
@@ -271,7 +272,7 @@ delegated with `cpuset cpu io memory pids`). The end-to-end suite passed 83 chec
![Horizontal bar chart of CPU-seconds used by four busy loops in one container, without a limit and with --cpus 0.5. The limited container is highlighted. Values are in the sentence below.](charts/cpu.svg)
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).
The chart above is generated from `docs/charts/bench.json` (`make bench`, then `make charts`); it shows <!--bench:cpu_free-->15.6<!--/bench--> CPU-seconds without a limit and <!--bench:cpu_half-->2.4<!--/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)
+6 -110
View File
@@ -1,121 +1,17 @@
{
"meta": {
"date": "2026-10-23",
"date": "2026-10-24",
"kernel": "7.0.0-38-generic",
"cpus": 12,
"runs": 10,
"doos": "1.1.2",
"doos": "1.4.0",
"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,
"src_lines": 3712,
"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
}
"startup": [{"id":"bwrap_none","label":"bubblewrap (host /usr)","group":"none","kind":"other","ms":8.8},{"id":"bwrap_net","label":"bubblewrap (host /usr)","group":"default","kind":"other","ms":7.8},{"id":"doos_host_none","label":"doos host mode","group":"none","kind":"doos","ms":20.2},{"id":"doos_alpine_none","label":"doos Alpine image","group":"none","kind":"doos","ms":14.2},{"id":"doos_host_slirp","label":"doos host mode","group":"default","kind":"doos","ms":61.4},{"id":"doos_alpine_slirp","label":"doos Alpine image","group":"default","kind":"doos","ms":58.0},{"id":"doos_alpine_pasta","label":"doos Alpine, pasta","group":"default","kind":"doos","ms":82.5},{"id":"podman_none","label":"Podman Alpine","group":"none","kind":"other","ms":282.9},{"id":"podman_net","label":"Podman Alpine","group":"default","kind":"other","ms":313.2},{"id":"docker_none","label":"Docker Alpine (rootful)","group":"none","kind":"other","ms":462.6},{"id":"docker_net","label":"Docker Alpine (rootful)","group":"default","kind":"other","ms":645.1}],
"sizes": [{"id":"bwrap","bytes":80424},{"id":"crun","bytes":483392},{"id":"doos","bytes":544640},{"id":"docker","bytes":31374336},{"id":"podman","bytes":43180896}],
"cpu": {"wall_seconds": 4.2, "busy_loops": 4, "unlimited": 15.56, "half_cpu": 2.38}
}
+13 -15
View File
@@ -1,6 +1,6 @@
<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>
<desc id="d">Two bars, CPU seconds consumed in 4.2 seconds of wall time. Without a limit 15.6, with --cpus 0.5 2.4.</desc>
<style>
.bg { fill: var(--surface); }
.t1 { fill: var(--ink); font: 600 15px system-ui, -apple-system, "Segoe UI", sans-serif; }
@@ -21,28 +21,26 @@
</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>
<text class="t2" x="20" y="46">4.2 s of wall time. Half a CPU allows about 2.1 CPU-seconds; measured 2.4.</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="295.0" y1="84" x2="295.0" y2="146"/>
<text class="tick" x="295.0" y="162" text-anchor="middle">4</text>
<line class="grid" x1="420.0" y1="84" x2="420.0" y2="146"/>
<text class="tick" x="420.0" y="162" text-anchor="middle">8</text>
<line class="grid" x1="545.0" y1="84" x2="545.0" y2="146"/>
<text class="tick" x="545.0" y="162" text-anchor="middle">12</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="tick" x="670.0" y="162" text-anchor="middle">16</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>
<path class="oth" d="M170.0,92.0 h482.2 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-482.2 z"><title>no limit: 15.6 CPU-seconds</title></path>
<text class="val" x="664.2" y="105.0">15.6 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>
<path class="acc" d="M170.0,122.0 h70.4 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-70.4 z"><title>--cpus 0.5: 2.4 CPU-seconds</title></path>
<text class="val" x="252.4" y="135.0">2.4 CPU-s</text>
<line class="axis" x1="170" y1="84" x2="170" y2="146"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

+3 -3
View File
@@ -1,6 +1,6 @@
<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>
<desc id="d">Horizontal bars on a logarithmic axis: bubblewrap 79 KiB, crun (OCI runtime) 472 KiB, doos 532 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; }
@@ -42,8 +42,8 @@
<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>
<path class="acc" d="M170.0,170.0 h147.9 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-147.9 z"><title>doos: 532 KiB</title></path>
<text class="val" x="329.9" y="183.0">532 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>

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

+36 -44
View File
@@ -1,6 +1,6 @@
<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>
<desc id="d">Horizontal bars, milliseconds, lower is better. doos: 14 to 82 ms; Podman: 283 to 313 ms; Docker: 463 to 645 ms; bubblewrap: 8 to 9 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; }
@@ -21,70 +21,62 @@
</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>
<text class="t2" x="20" y="46">10 runs each, average, warm cache, 2026-10-24, 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"/>
<line class="grid" x1="334.3" y1="90" x2="334.3" y2="242"/>
<line class="grid" x1="468.6" y1="90" x2="468.6" y2="242"/>
<line class="grid" x1="602.9" y1="90" x2="602.9" 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>
<path class="oth" d="M200.0,98.0 h1.9 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-1.9 z"><title>bubblewrap (host /usr): 8.8 ms</title></path>
<text class="val" x="213.9" y="111.0">8.8 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>
<path class="acc" d="M200.0,128.0 h5.5 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-5.5 z"><title>doos Alpine image: 14.2 ms</title></path>
<text class="val" x="217.5" y="141.0">14.2 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>
<path class="acc" d="M200.0,158.0 h9.6 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-9.6 z"><title>doos host mode: 20.2 ms</title></path>
<text class="val" x="221.6" y="171.0">20.2 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>
<path class="oth" d="M200.0,188.0 h185.9 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-185.9 z"><title>Podman Alpine: 283 ms</title></path>
<text class="val" x="397.9" y="201.0">283 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>
<path class="oth" d="M200.0,218.0 h306.6 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-306.6 z"><title>Docker Alpine (rootful): 463 ms</title></path>
<text class="val" x="518.6" y="231.0">463 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="tick" x="334.3" y="256" text-anchor="middle">200</text>
<text class="tick" x="468.6" y="256" text-anchor="middle">400</text>
<text class="tick" x="602.9" y="256" text-anchor="middle">600</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"/>
<line class="grid" x1="334.3" y1="292" x2="334.3" y2="474"/>
<line class="grid" x1="468.6" y1="292" x2="468.6" y2="474"/>
<line class="grid" x1="602.9" y1="292" x2="602.9" 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>
<path class="oth" d="M200.0,300.0 h1.2 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-1.2 z"><title>bubblewrap (host /usr): 7.8 ms</title></path>
<text class="val" x="213.2" y="313.0">7.8 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>
<path class="acc" d="M200.0,330.0 h34.9 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-34.9 z"><title>doos Alpine image: 58 ms</title></path>
<text class="val" x="246.9" y="343.0">58 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>
<path class="acc" d="M200.0,360.0 h37.2 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-37.2 z"><title>doos host mode: 61.4 ms</title></path>
<text class="val" x="249.2" y="373.0">61.4 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>
<path class="acc" d="M200.0,390.0 h51.4 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-51.4 z"><title>doos Alpine, pasta: 82.5 ms</title></path>
<text class="val" x="263.4" y="403.0">82.5 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>
<path class="oth" d="M200.0,420.0 h206.3 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-206.3 z"><title>Podman Alpine: 313 ms</title></path>
<text class="val" x="418.3" y="433.0">313 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>
<path class="oth" d="M200.0,450.0 h429.1 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-429.1 z"><title>Docker Alpine (rootful): 645 ms</title></path>
<text class="val" x="641.1" y="463.0">645 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>
<text class="tick" x="334.3" y="488" text-anchor="middle">200</text>
<text class="tick" x="468.6" y="488" text-anchor="middle">400</text>
<text class="tick" x="602.9" y="488" text-anchor="middle">600</text>
</svg>

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

+34 -1
View File
@@ -18,6 +18,30 @@ namespace {
std::uint32_t parseCount(const std::string& text);
std::uint32_t parseCpus(const std::string& text);
// "SRC:DST" or "SRC:DST:ro"/"SRC:DST:rw" (paths themselves may not contain ':').
MountSpec parseMountFlag(const std::string& text) {
std::vector<std::string> parts;
std::string cur;
for (char ch : text) {
if (ch == ':') { parts.push_back(cur); cur.clear(); }
else cur += ch;
}
parts.push_back(cur);
if (parts.size() < 2 || parts.size() > 3) {
throw std::runtime_error("--mount needs SRC:DST or SRC:DST:ro (got '" + text + "')");
}
MountSpec m;
m.src = parts[0];
m.dst = parts[1];
if (parts.size() == 3) {
if (parts[2] != "ro" && parts[2] != "rw") {
throw std::runtime_error("--mount's third field must be 'ro' or 'rw' (got '" + parts[2] + "')");
}
m.readOnly = parts[2] == "ro";
}
return m;
}
// Leading --options, then the first positional, then everything else untouched.
struct Parsed {
Options opts;
@@ -59,6 +83,13 @@ Parsed parse(const std::vector<std::string>& rest, Allowed allow, std::size_t wa
if (v != "default" && v != "relaxed") throw std::runtime_error("--hardening takes default or relaxed");
p.opts.relaxed = (v == "relaxed");
}
else if (allow.container && is(a, "--mount")) {
if (!p.opts.mounts) p.opts.mounts = std::vector<MountSpec>{};
if (p.opts.mounts->size() >= spec::kMaxMounts) {
throw std::runtime_error("too many --mount flags (max " + std::to_string(spec::kMaxMounts) + ")");
}
p.opts.mounts->push_back(parseMountFlag(value(i, "--mount")));
}
else if (allow.yes && a == "--yes") p.yes = true;
else if (a == "--i-understand") p.iUnderstand = true;
else if (allow.sha256 && is(a, "--sha256")) p.sha256 = value(i, "--sha256");
@@ -141,7 +172,9 @@ void Cli::usage(std::FILE* out) {
" --memory SIZE memory cap, 64m..1t, default 8g\n"
" --pids N process cap, 16..4194304, default 4096\n"
" --cpus N cpu cap, 0.1..1024, default 0 (all cores)\n"
" size, memory, pids and cpus can be changed on any later call; they apply at the next start.\n"
" --mount SRC:DST[:ro] bind-mount host path SRC at DST inside the container, repeatable (max 8)\n"
" size, memory, pids, cpus and mount can be changed on any later call; they apply at the next start.\n"
" mount replaces the whole mount list; SRC must exist and be a directory when the container starts.\n"
"\n"
"SOURCE for image add: alpine, ubuntu, ubuntu-24.04, debian, debian-bookworm, an https URL (needs --sha256), or a tar file.\n"
"attach creates NAME when missing. delete only reports unless --yes is given.\n"
+58 -1
View File
@@ -26,6 +26,44 @@ std::string slug(std::string_view hostDir) {
return s;
}
void validateMount(const MountSpec& m) {
auto bad = [](const std::string& s) {
return s.find(':') != std::string::npos || s.find(';') != std::string::npos || s.find('\n') != std::string::npos;
};
if (m.src.empty() || m.src[0] != '/' || bad(m.src)) {
throw std::runtime_error("--mount source must be an absolute path without ':' or ';': '" + m.src + "'");
}
if (m.dst.empty() || m.dst[0] != '/' || m.dst == "/" || bad(m.dst)) {
throw std::runtime_error("--mount destination must be an absolute path, not '/', without ':' or ';': '" + m.dst + "'");
}
}
std::string serializeMounts(const std::vector<MountSpec>& mounts) {
std::string out;
for (const auto& m : mounts) {
if (!out.empty()) out += ';';
out += m.src + ":" + m.dst + ":" + (m.readOnly ? "ro" : "rw");
}
return out;
}
std::vector<MountSpec> parseMounts(const std::string& text) {
std::vector<MountSpec> out;
std::istringstream in(text);
for (std::string entry; std::getline(in, entry, ';');) {
if (entry.empty()) continue;
std::size_t c1 = entry.find(':');
std::size_t c2 = entry.rfind(':');
if (c1 == std::string::npos || c2 == c1) throw std::runtime_error("corrupt mount entry in meta: '" + entry + "'");
MountSpec m;
m.src = entry.substr(0, c1);
m.dst = entry.substr(c1 + 1, c2 - c1 - 1);
m.readOnly = entry.substr(c2 + 1) == "ro";
out.push_back(std::move(m));
}
return out;
}
std::map<std::string, std::string> parseKv(const std::string& text) {
std::map<std::string, std::string> kv;
std::istringstream in(text);
@@ -240,7 +278,8 @@ void Container::writeMeta() const {
"\nversion=" DOOS_VERSION "\nimage=" + image_ + "\nids=" + IdMap::name(ids_) +
"\nsize=" + std::to_string(size_) + "\nmemory=" + std::to_string(memory_) +
"\npids=" + std::to_string(pids_) + "\ncpus=" + std::to_string(milliCpus_) +
"\nhardening=" + (relaxed_ ? "relaxed" : "default") + "\n");
"\nhardening=" + (relaxed_ ? "relaxed" : "default") +
"\nmounts=" + serializeMounts(mounts_) + "\n");
}
void Container::setSizeCap(std::uint64_t bytes) {
@@ -252,6 +291,15 @@ void Container::setSizeCap(std::uint64_t bytes) {
writeMeta();
}
void Container::setMounts(const std::vector<MountSpec>& mounts) {
if (mounts.size() > spec::kMaxMounts) {
throw std::runtime_error("too many mounts (max " + std::to_string(spec::kMaxMounts) + ")");
}
for (const auto& m : mounts) validateMount(m);
mounts_ = mounts;
writeMeta();
}
void Container::setLimits(std::optional<std::uint64_t> memory, std::optional<std::uint32_t> pids,
std::optional<std::uint32_t> milliCpus) {
if (memory) {
@@ -287,6 +335,13 @@ void Container::create(const NetMode& net, const std::string& image, IdMode ids,
pids_ = spec::kDefaultPids;
milliCpus_ = 0;
if (o.memory || o.pids || o.milliCpus) setLimits(o.memory, o.pids, o.milliCpus);
if (o.mounts) {
if (o.mounts->size() > spec::kMaxMounts) {
throw std::runtime_error("too many mounts (max " + std::to_string(spec::kMaxMounts) + ")");
}
for (const auto& m : *o.mounts) validateMount(m);
mounts_ = *o.mounts;
}
created_ = std::time(nullptr);
fsutil::mkdirs(dir_);
prepareLayout();
@@ -309,6 +364,7 @@ void Container::readMeta() {
pids_ = kv.count("pids") ? static_cast<std::uint32_t>(std::stoul(kv["pids"])) : spec::kDefaultPids;
milliCpus_ = kv.count("cpus") ? static_cast<std::uint32_t>(std::stoul(kv["cpus"])) : 0;
relaxed_ = kv.count("hardening") && kv["hardening"] == "relaxed";
mounts_ = kv.count("mounts") ? parseMounts(kv["mounts"]) : std::vector<MountSpec>{};
}
std::optional<pid_t> Container::runningPid() const {
@@ -399,6 +455,7 @@ std::pair<Container, bool> Store::getOrCreate(const std::string& name, const Opt
if (opts.relaxed) fixed("hardening", c.relaxed() ? "relaxed" : "default", *opts.relaxed ? "relaxed" : "default");
if (opts.size && *opts.size != c.sizeCap()) c.setSizeCap(*opts.size);
if (opts.memory || opts.pids || opts.milliCpus) c.setLimits(opts.memory, opts.pids, opts.milliCpus);
if (opts.mounts) c.setMounts(*opts.mounts);
};
if (fs::exists(dir / "meta", ec)) {
+14
View File
@@ -15,6 +15,15 @@
namespace doos {
// One --mount SRC:DST[:ro]: SRC is bind-mounted at DST inside the container, in addition to the
// usual overlay/image root. Not subject to the --size cap: it is host storage (or whatever the
// host mounted at SRC, e.g. a WebDAV share via rclone), not the container's own writable layer.
struct MountSpec {
std::string src; // host path, must exist as a directory when the container starts
std::string dst; // absolute path inside the container
bool readOnly = false;
};
// What the user asked for. Unset fields mean "default on create, no constraint on existing".
struct Options {
std::optional<NetMode> net;
@@ -25,6 +34,7 @@ struct Options {
std::optional<std::uint32_t> pids;
std::optional<std::uint32_t> milliCpus; // 0 = all cores
std::optional<bool> relaxed; // --hardening relaxed
std::optional<std::vector<MountSpec>> mounts; // --mount, replaces the whole list when given
};
@@ -56,8 +66,11 @@ public:
std::uint32_t pidsCap() const { return pids_; }
std::uint32_t milliCpus() const { return milliCpus_; }
bool relaxed() const { return relaxed_; }
const std::vector<MountSpec>& mounts() const { return mounts_; }
// Limits take effect the next time the container starts.
void setLimits(std::optional<std::uint64_t> memory, std::optional<std::uint32_t> pids, std::optional<std::uint32_t> milliCpus);
// Replaces the whole mount list; takes effect the next time the container starts.
void setMounts(const std::vector<MountSpec>& mounts);
void create(const NetMode& net, const std::string& image, IdMode ids, const Options& resolved);
void readMeta(); // meta only, no side effects
@@ -89,6 +102,7 @@ private:
std::uint32_t pids_ = 0;
std::uint32_t milliCpus_ = 0;
bool relaxed_ = false;
std::vector<MountSpec> mounts_;
};
class Store {
+15
View File
@@ -166,6 +166,19 @@ void mountImageRoot(const Container& c, const fs::path& root, const fs::path& im
for (auto d : spec::kEmptyDirs) fsutil::mkdirs(sub(root, d));
}
// --mount: extra bind mounts beyond the overlay/image root, outside the --size cap.
void mountExtra(const Container& c, const fs::path& root) {
for (const auto& m : c.mounts()) {
if (!fs::is_directory(m.src)) {
throw std::runtime_error("mount source '" + m.src + "' is not a directory (mount it on the host first)");
}
std::string target = sub(root, m.dst);
fsutil::mkdirs(target);
doMount(m.src.c_str(), target, nullptr, MS_BIND | MS_REC);
if (m.readOnly) remountReadOnly(target);
}
}
void setupRoot(const Container& c, const fs::path& imageRoot) {
fs::path root = c.rootfs();
doMount(nullptr, "/", nullptr, MS_REC | MS_PRIVATE);
@@ -202,6 +215,8 @@ void setupRoot(const Container& c, const fs::path& imageRoot) {
for (auto p : spec::kProcReadOnly) remountReadOnly(sub(root, p));
for (auto p : spec::kProcMasked) doMount((root / "dev/null").c_str(), sub(root, p), nullptr, MS_BIND);
mountExtra(c, root);
// Switch root, then drop the old one.
if (::chdir(root.c_str()) != 0) throw SysError("chdir newroot");
if (::syscall(SYS_pivot_root, ".", ".") != 0) throw SysError("pivot_root");
+1
View File
@@ -12,6 +12,7 @@ constexpr std::size_t kMaxContainers = 64;
constexpr std::size_t kMaxNameLen = 32;
constexpr std::size_t kMaxCommandArgs = 256;
constexpr std::size_t kMaxTreeDepth = 256; // removal recursion cap
constexpr std::size_t kMaxMounts = 8; // extra --mount bind mounts per container
constexpr unsigned long kMaxOpenFiles = 4096; // RLIMIT_NOFILE ceiling inside
constexpr int kNetReadyTimeoutMs = 5000; // slirp4netns must be ready by then
constexpr int kLockTimeoutMs = 5000; // per-container lock wait
+15
View File
@@ -72,6 +72,21 @@ check "ids fixed at creation" bash -c "! '$DOOS' get_or_create --ids subui
check "bad size rejected" bash -c "! '$DOOS' get_or_create --size 1x z1"
check "size below minimum rejected" bash -c "! '$DOOS' get_or_create --size 1m z2"
check "size overflow rejected, not silently wrapped" bash -c "! '$DOOS' get_or_create --size 16777217t z3"
# --mount: extra bind mounts
mntd=$(mktemp -d); echo host-data > "$mntd/f"
"$DOOS" get_or_create --mount "$mntd:/data" m >/dev/null
[ "$("$DOOS" attach m -- cat /data/f)" = "host-data" ] && ok "mount: readable inside" || bad "mount: readable inside"
"$DOOS" attach m -- sh -c 'echo from-container > /data/g'
[ "$(cat "$mntd/g" 2>/dev/null)" = "from-container" ] && ok "mount: writes land on host" || bad "mount: writes land on host"
"$DOOS" delete --yes m >/dev/null
"$DOOS" get_or_create --mount "$mntd:/data:ro" mro >/dev/null
"$DOOS" attach mro -- sh -c 'echo x > /data/should-fail' 2>/dev/null && bad "mount: ro rejects writes" || ok "mount: ro rejects writes"
"$DOOS" delete --yes mro >/dev/null
check "mount: missing source rejected" bash -c "'$DOOS' get_or_create --mount /no/such/dir:/data mbad >/dev/null && ! '$DOOS' attach mbad -- true"
"$DOOS" delete --yes mbad >/dev/null 2>&1
check "mount: too many rejected" bash -c "! '$DOOS' get_or_create $(for i in 1 2 3 4 5 6 7 8 9; do printf -- '--mount %s:/m%s ' "$mntd" "$i"; done) mtoomany"
rm -rf "$mntd"
"$DOOS" attach s -- sh -c 'apt-get update >/dev/null 2>&1; apt-get install -y --no-install-recommends cowsay >/dev/null 2>&1; dpkg --audit && /usr/games/cowsay ok >/dev/null' && ok "host mode: apt install completes" || bad "host mode: apt install completes"
[ ! -e /usr/games/cowsay ] && ok "host untouched by apt install" || bad "host untouched by apt install"
"$DOOS" delete --yes s >/dev/null; "$DOOS" delete --yes s1 >/dev/null