Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b5e28e06e |
@@ -0,0 +1,77 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
Rules for any AI agent (or human) changing this repository. The user's global rules
|
||||||
|
also apply; this file adds the project-specific ones. Keep this file current: it is
|
||||||
|
part of every release.
|
||||||
|
|
||||||
|
## Project in one paragraph
|
||||||
|
|
||||||
|
`doos` is a user-level container tool in C++20. `attach` builds a user + mount + pid
|
||||||
|
+ uts + ipc (+ net) namespace set, overlays the host's `/usr`, `/etc`, `/opt` with a
|
||||||
|
per-container upper dir, pivots into it and runs a command. Networking is slirp4netns.
|
||||||
|
No root at runtime. Authoritative behavior: `SPEC.md`. Design notes: `docs/ARCHITECTURE.md`.
|
||||||
|
Measurements and gaps: `docs/RESEARCH.md`.
|
||||||
|
|
||||||
|
## Mandatory release workflow (every change, same session)
|
||||||
|
|
||||||
|
1. Make the change. Smallest diff that does what was asked.
|
||||||
|
2. Update docs in the same change (see the table below).
|
||||||
|
3. Pick the bump:
|
||||||
|
- `patch`: bug fix, docs, tests, tooling, no behavior change for users.
|
||||||
|
- `minor`: new or changed user-visible behavior (flag, command, mount, limit).
|
||||||
|
- `major`: breaking change to CLI, state layout, or container compatibility.
|
||||||
|
4. Run `scripts/release.sh <patch|minor|major> "imperative commit subject"`.
|
||||||
|
It bumps `VERSION`, builds, runs `tests/run.sh`, commits all changes, and creates
|
||||||
|
the annotated tag `vX.Y.Z`. It aborts on any build or test failure.
|
||||||
|
5. Verify live: run `build/doos` once against the real thing (the e2e suite does this).
|
||||||
|
6. Report honestly what passed and what did not. Do not push unless the user asks.
|
||||||
|
|
||||||
|
Never hand-edit `VERSION` or create tags manually. Never skip the tests to release.
|
||||||
|
|
||||||
|
## Docs that must be updated with the code
|
||||||
|
|
||||||
|
| If you change | Update |
|
||||||
|
|---|---|
|
||||||
|
| CLI commands, flags, output | `README.md`, `SPEC.md` (CLI), `src/cli.cpp` usage text |
|
||||||
|
| Limits or constants in `src/spec.hpp` | `SPEC.md` limits/layout tables (both must match) |
|
||||||
|
| Mounts, namespaces, isolation | `SPEC.md`, `docs/ARCHITECTURE.md`, `docs/RESEARCH.md` host safety section |
|
||||||
|
| Networking | `SPEC.md`, `docs/RESEARCH.md` networking table |
|
||||||
|
| Source files added/removed/renamed | `docs/ARCHITECTURE.md` file map, `CMakeLists.txt`, this file |
|
||||||
|
| A new measurement | `docs/RESEARCH.md` with machine, kernel, and what was run |
|
||||||
|
| A new known gap or a closed gap | `docs/RESEARCH.md` gaps list and `README.md` |
|
||||||
|
| These workflow rules | this file |
|
||||||
|
| Anything above, in every release | `README.md` version line and `CHANGELOG.md` entry |
|
||||||
|
|
||||||
|
Docs describe what the system does today. No roadmap text. A gap is listed next to
|
||||||
|
what would close it. A number in the docs needs a run behind it, else say it is unmeasured.
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
Every release adds a `CHANGELOG.md` entry (newest first): version, date, bullets of
|
||||||
|
what changed for users. The release script does not write it; write it before running
|
||||||
|
the script.
|
||||||
|
|
||||||
|
## Code rules
|
||||||
|
|
||||||
|
- C++20, extremely object oriented: one class per responsibility (see file map).
|
||||||
|
- Every limit is a named constant in `src/spec.hpp` and listed in `SPEC.md`.
|
||||||
|
- No unbounded loops, sizes or recursion. Add a cap when you add a loop.
|
||||||
|
- Destructive operations: dry-run by default, `--yes` to act, scoped to the store.
|
||||||
|
- Fail loudly with a clear message. Never swallow an error that leaves state half-built.
|
||||||
|
- Match surrounding style. No new dependencies without a stated reason.
|
||||||
|
- Never use em dashes (U+2014) anywhere. The release script rejects them.
|
||||||
|
- Output must stay readable on a narrow terminal.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
`tests/run.sh [path/to/doos]` runs the real path: real overlayfs, namespaces,
|
||||||
|
slirp4netns, filesystem. Add a case for every behavior change. A green run is required
|
||||||
|
before release. On Ubuntu 24.04+ the AppArmor profile in `apparmor/doos` must be
|
||||||
|
installed once (needs sudo, ask the user, do not do it yourself).
|
||||||
|
|
||||||
|
## Environment notes
|
||||||
|
|
||||||
|
- State dir: `$DOOS_HOME`, else `$XDG_DATA_HOME/doos`, else `~/.local/share/doos`.
|
||||||
|
Tests use a temp `DOOS_HOME`; never point tests at the user's real store.
|
||||||
|
- Build: `cmake -S . -B build && cmake --build build`.
|
||||||
|
- Requirements: Linux >= 5.11, `slirp4netns` for the default network mode.
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 0.1.1 - 2026-10-23
|
||||||
|
- Docs: AGENTS.md release workflow, ARCHITECTURE.md, expanded README, this changelog.
|
||||||
|
- Tooling: `scripts/release.sh` (bump, build, test, commit, tag).
|
||||||
|
|
||||||
|
## 0.1.0 - 2026-10-23
|
||||||
|
- First release: `list`, `get_or_create`, `attach`, `delete`.
|
||||||
|
- overlayfs over host `/usr /etc /opt`, user/mount/pid/uts/ipc/net namespaces.
|
||||||
|
- Networking: slirp4netns (default), `host`, `none`.
|
||||||
|
- End-to-end test suite and AppArmor profile for Ubuntu 24.04+.
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
# doos
|
# doos
|
||||||
|
|
||||||
|
Version: see `VERSION` (currently 0.1.1). Release history: `CHANGELOG.md`.
|
||||||
|
|
||||||
User-level containers in C++20: overlayfs over the host's `/usr`, `/etc`, `/opt`, user/mount/pid namespaces, networking through slirp4netns. No root at runtime.
|
User-level containers in C++20: overlayfs over the host's `/usr`, `/etc`, `/opt`, user/mount/pid namespaces, networking through slirp4netns. No root at runtime.
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -18,3 +20,54 @@ sudo cp apparmor/doos /etc/apparmor.d/doos && sudo apparmor_parser -r /etc/appar
|
|||||||
```
|
```
|
||||||
|
|
||||||
See `SPEC.md` (behavior, limits) and `docs/RESEARCH.md` (what works, measurements, known gaps).
|
See `SPEC.md` (behavior, limits) and `docs/RESEARCH.md` (what works, measurements, known gaps).
|
||||||
|
|
||||||
|
## Usage examples
|
||||||
|
|
||||||
|
```
|
||||||
|
doos get_or_create dev # created dev
|
||||||
|
doos attach dev # interactive shell as root inside
|
||||||
|
doos attach dev -- apt --version # run one command
|
||||||
|
doos attach --net none sandbox -- sh # no network at all
|
||||||
|
doos list
|
||||||
|
doos delete dev # dry run: shows what would go
|
||||||
|
doos delete --yes dev # really deletes
|
||||||
|
```
|
||||||
|
|
||||||
|
Container writes to `/usr`, `/etc`, `/opt` stay in the container (copy-on-write);
|
||||||
|
`/root` and `/var` are private and persistent. `/tmp` is memory-backed and lost on exit.
|
||||||
|
A container is `running` while its first `attach` command is alive; more `attach` calls
|
||||||
|
join it. Data persists until `delete --yes`.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
| Variable | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `DOOS_HOME` | State directory (default `~/.local/share/doos`). No `,` or `:` allowed. |
|
||||||
|
| `XDG_DATA_HOME` | Used as `$XDG_DATA_HOME/doos` when `DOOS_HOME` is unset. |
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Message | Cause and fix |
|
||||||
|
|---|---|
|
||||||
|
| `write /proc/<pid>/uid_map ... blocked` or `mount ...: Permission denied` | AppArmor restricts user namespaces. Install `apparmor/doos` (command above). |
|
||||||
|
| `slirp4netns failed to start` | Install `slirp4netns`, or use `--net none` / `--net host`. |
|
||||||
|
| `state path must not contain ',' or ':'` | Set `DOOS_HOME` to a plain path. |
|
||||||
|
| `container 'x' is running` on delete | Exit the attached command first. |
|
||||||
|
| `container limit reached (64)` | Delete one first. |
|
||||||
|
|
||||||
|
## Safety summary
|
||||||
|
|
||||||
|
Host files are never written: system dirs are reached only through overlayfs. Host
|
||||||
|
`/home`, `/sys` and host loopback are not reachable in the default mode. `--net host`
|
||||||
|
removes network isolation and is opt-in. Known gaps (no cgroups, readable host `/etc`)
|
||||||
|
are listed in `docs/RESEARCH.md`.
|
||||||
|
|
||||||
|
## Project docs
|
||||||
|
|
||||||
|
| File | Content |
|
||||||
|
|---|---|
|
||||||
|
| `SPEC.md` | Behavior contract and limits |
|
||||||
|
| `docs/ARCHITECTURE.md` | Code map and flows |
|
||||||
|
| `docs/RESEARCH.md` | What works unprivileged, measurements, gaps |
|
||||||
|
| `AGENTS.md` | Rules for AI agents, release workflow |
|
||||||
|
| `CHANGELOG.md` | Release history |
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
How doos works internally. Behavior contract is `SPEC.md`; this explains the code.
|
||||||
|
|
||||||
|
## File map
|
||||||
|
|
||||||
|
| File | Class / role |
|
||||||
|
|---|---|
|
||||||
|
| `src/spec.hpp` | `spec::` constants: caps, mount lists, device list, paths. Single source of truth with `SPEC.md`. |
|
||||||
|
| `src/util.{hpp,cpp}` | `SysError` (errno-carrying exception), `Fd` (RAII fd), `FileLock` (bounded flock), `fsutil` (mkdirs, read/write, bounded `removeTree`). |
|
||||||
|
| `src/network.{hpp,cpp}` | `NetMode` + `Network` (parse, resolv.conf per mode), `SlirpSession` (owns the slirp4netns process). |
|
||||||
|
| `src/container.{hpp,cpp}` | `Container` (one container's paths, meta, pid file, layout), `Store` (list, get_or_create, remove). |
|
||||||
|
| `src/sandbox.{hpp,cpp}` | `Sandbox`: start a container (clone + mounts + pivot_root + init) or join a running one (setns). |
|
||||||
|
| `src/cli.{hpp,cpp}` | `Cli`: argument parsing and the four commands. |
|
||||||
|
| `src/main.cpp` | Catches exceptions, prints `doos: <message>`, exit 1. |
|
||||||
|
| `tests/run.sh` | End-to-end suite. |
|
||||||
|
| `scripts/release.sh` | Bump, build, test, commit, tag. |
|
||||||
|
| `apparmor/doos` | Profile granting `userns` on Ubuntu 24.04+. |
|
||||||
|
|
||||||
|
## attach flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Cli::cmdAttach
|
||||||
|
Store::getOrCreate lock, create layout + meta if missing
|
||||||
|
Sandbox::run
|
||||||
|
FileLock (start-or-join decision)
|
||||||
|
runningPid()? yes -> join() no -> start()
|
||||||
|
|
||||||
|
start():
|
||||||
|
clone(NEWUSER|NEWNS|NEWPID|NEWUTS|NEWIPC[|NEWNET]) child = container pid 1
|
||||||
|
parent writes /proc/<child>/{setgroups,uid_map,gid_map} 0 -> invoking uid, size 1
|
||||||
|
slirp mode: SlirpSession.start(child) waits for ready-fd
|
||||||
|
recordPid(child) (pid + starttime, defeats pid reuse)
|
||||||
|
lock.unlock() joiners must not wait for container lifetime
|
||||||
|
write 'G' on sync pipe child continues
|
||||||
|
parent: ignore SIGINT/SIGQUIT, forward SIGTERM/SIGHUP, waitpid(child)
|
||||||
|
on exit: pid file cleared, slirp stopped (RAII)
|
||||||
|
|
||||||
|
child (containerInit, pid 1):
|
||||||
|
PDEATHSIG=SIGKILL, close inherited lock fd, wait for 'G'
|
||||||
|
setupRoot -> sethostname -> loopback up (isolated netns) -> rlimits/env/no_new_privs
|
||||||
|
fork worker -> exec command
|
||||||
|
pid 1 loop: forward signals, reap all, on worker exit kill(-1, SIGKILL), exit with its code
|
||||||
|
```
|
||||||
|
|
||||||
|
## setupRoot order
|
||||||
|
|
||||||
|
1. `MS_REC|MS_PRIVATE` on `/` so nothing propagates to the host.
|
||||||
|
2. Bind the container `rootfs/` onto itself (pivot_root needs a mountpoint).
|
||||||
|
3. Write `resolv.conf` into the `/etc` upper dir (matches network mode every start).
|
||||||
|
4. Overlay mounts (`lowerdir=<host dir>,upperdir,workdir,userxattr`, nosuid, nodev).
|
||||||
|
5. Compat dirs: recreate host symlinks (usr-merge) or overlay real dirs.
|
||||||
|
6. Bind private dirs (`/root`, `/var`), mount tmpfs (`/tmp`, `/run`).
|
||||||
|
7. `/dev`: tmpfs, bind host device nodes, new devpts, `/dev/shm`, symlinks.
|
||||||
|
8. `/proc`, then read-only remount and masks from `spec.hpp`.
|
||||||
|
9. `chdir(rootfs)`, `pivot_root(".", ".")`, lazy unmount of old root.
|
||||||
|
|
||||||
|
## join flow
|
||||||
|
|
||||||
|
Open `/proc/<pid>/ns/{user,mnt,uts,ipc,net,pid}` and `/proc/<pid>/root` first, fork,
|
||||||
|
`setns` user first (grants capability for the rest), `fchdir`+`chroot` to the container
|
||||||
|
root, fork again (pid ns applies to children), apply env and limits, exec.
|
||||||
|
In `host` network mode the net namespace is not joined.
|
||||||
|
|
||||||
|
## Lifetime model
|
||||||
|
|
||||||
|
The namespaces live as long as the first `attach` command. When it exits, pid 1 kills
|
||||||
|
the rest and the container is `stopped`. State (upper dirs, `/root`, `/var`) persists on
|
||||||
|
disk until `delete --yes`. There is no daemon.
|
||||||
|
|
||||||
|
## Error handling
|
||||||
|
|
||||||
|
All failures throw (`SysError` or `std::runtime_error`); `main` prints one line.
|
||||||
|
Inside the child, errors print `doos: <message>` and exit 125. A parent-side failure
|
||||||
|
after `clone` kills the child (`Reaper`), so no half-built container survives.
|
||||||
Executable
+35
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Bump VERSION, build, run the e2e suite, commit everything, tag.
|
||||||
|
# Usage: scripts/release.sh patch|minor|major "commit subject"
|
||||||
|
# Refuses to run when the build or tests fail. Does not push.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
kind=${1:?usage: release.sh patch|minor|major "subject"}
|
||||||
|
subject=${2:?usage: release.sh patch|minor|major "subject"}
|
||||||
|
|
||||||
|
IFS=. read -r maj min pat < VERSION
|
||||||
|
case "$kind" in
|
||||||
|
patch) pat=$((pat+1)) ;;
|
||||||
|
minor) min=$((min+1)); pat=0 ;;
|
||||||
|
major) maj=$((maj+1)); min=0; pat=0 ;;
|
||||||
|
*) echo "kind must be patch, minor or major" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
new="$maj.$min.$pat"
|
||||||
|
|
||||||
|
if git rev-parse "v$new" >/dev/null 2>&1; then echo "tag v$new already exists" >&2; exit 1; fi
|
||||||
|
if grep -rl $'\xe2\x80\x94' --exclude-dir=build --exclude-dir=.git . | grep -q .; then
|
||||||
|
echo "em dash found, remove it first" >&2; exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$new" > VERSION
|
||||||
|
cmake -S . -B build >/dev/null
|
||||||
|
cmake --build build
|
||||||
|
tests/run.sh
|
||||||
|
|
||||||
|
git add -A
|
||||||
|
git commit -m "$subject
|
||||||
|
|
||||||
|
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>"
|
||||||
|
git tag -a "v$new" -m "doos $new"
|
||||||
|
echo "released v$new"
|
||||||
Reference in New Issue
Block a user