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:
+5
-1
@@ -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
@@ -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.
|
||||
|
||||

|
||||
|
||||
<!-- 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:
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`)
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||

|
||||
|
||||
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
@@ -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
@@ -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 |
@@ -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
@@ -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
@@ -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
@@ -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)) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user