valgrind
Scope: README · AGENTS · docs/RESEARCH
Memcheck run against a fixed, named set of doos invocations, to catch real memory bugs
(invalid reads/writes, use-after-free, definite leaks) in doos's own code: namespace setup,
mount handling, the CLI, the image store. Separate from audit/ (named CVEs against other
software) and from tests/run.sh (behavior, not memory safety).
Running it
make valgrind # builds build/doos first, then runs scripts/valgrind.sh against it
scripts/valgrind.sh PATH # run directly against a specific binary, e.g. a copy with an
# installed AppArmor profile (see README's apparmor section)
Reports land in build/valgrind-reports/<step>.log, one per invocation, kept (not deleted)
after the run so a failure can be inspected. Each step prints ok or FAIL; the run exits
non-zero only if valgrind itself flagged something, never because doos exited non-zero for
its own reasons (an expected delete dry-run notice, an AppArmor block on a binary with no
profile installed, and so on, all exit non-zero as designed and are not memory bugs).
What it checks
A fixed, capped list (scripts/valgrind.sh): list, get_or_create, attach (twice, to
exercise both the create path and joining an already-running container), image list,
delete --yes. All run with --net none and no image, so the check needs no network and no
download; it is about doos's own allocation code, not about what happens after it execs the
contained command.
Suppressions (doos.supp)
Six specific, named, commented allocation sites, each captured with
valgrind --gen-suppressions=all against a real run and explained in the file itself: the
clone(2) child's 1 MiB stack buffer (lifetime tied to the child process, not a scope), and five
"an error path constructed an exception or opened a stream, then the namespace child exited
right after" patterns (doMount's thrown SysError's message string and the exception object
itself, once for a mount failure inside setupRoot and once for the mount that runs directly in
containerInit before it, such as an AppArmor-blocked mount bind /; plus an ifstream opened
in containerInit's cold path). None of these are "definitely lost"; valgrind calls all of them
"possibly lost" because the process ends before the normal C++ unwind would run their
destructors, which is expected for a namespace child that is about to disappear regardless. A
genuinely new leak anywhere else is not covered by these exact stacks and still fails the check.
To add a suppression for a new, understood, one-time allocation: reproduce it, add
--gen-suppressions=all to the valgrind invocation in scripts/valgrind.sh temporarily, copy
the exact block doos.supp already prints into the file with a name and a comment explaining
why it is not a real leak, then remove the temporary flag. Never add a suppression you cannot
explain in one sentence.
What this does not cover
- Anything that happens after doos execs the user's command: valgrind traces doos's own process tree, not an arbitrary program run inside the container.
- Image download and extraction (
--image): not in the fixed list, since it needs network and addscurl/tar/gpgsubprocess noise that is not doos's own memory. Add a step toscripts/valgrind.shfollowing the same pattern if that code path needs a dedicated run. slirp4netns/pasta: separate processes, not doos; seeaudit/CVE-2020-7039for how their memory safety is tracked instead (upstream version, not valgrind).
Back to README, AGENTS, docs/RESEARCH.