Add audit/ with 8 named CVE regression checks against doos
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
# CVE-2016-5195: see README.md in this directory.
|
||||
DOOS ?= $(abspath ../../build/doos)
|
||||
export DOOS
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@./reproduce.sh "$(DOOS)"
|
||||
@@ -0,0 +1,35 @@
|
||||
# CVE-2016-5195: "Dirty COW": race condition in copy-on-write handling of private read-only mappings
|
||||
|
||||
**Scope:** [audit](../README.md) · [README](../../README.md) · [COMPARISON](../../COMPARISON.md) · [docs/RESEARCH](../../docs/RESEARCH.md)
|
||||
|
||||
CVSS 7.0-7.2. Fixed in Linux 4.8.3 (and backported to every maintained branch since).
|
||||
|
||||
## Source
|
||||
|
||||
- [Ubuntu advisory](https://ubuntu.com/security/CVE-2016-5195)
|
||||
- [NVD entry](https://nvd.nist.gov/vuln/detail/CVE-2016-5195)
|
||||
|
||||
## Relevance to doos
|
||||
|
||||
A race in the memory manager's copy-on-write handling let a local user write to memory
|
||||
that should have been read-only, including on-disk binaries mapped read-only. Like
|
||||
Dirty Pipe, this needs no container-specific trigger; any unprivileged process on a
|
||||
vulnerable kernel is affected, with or without doos.
|
||||
|
||||
|
||||
## What the check does
|
||||
|
||||
Kernel version only. Fixed in 4.8.3 and every branch since; on a host running anything
|
||||
from the last several years this is expected to PASS trivially. It is kept in the list
|
||||
to complete the "shared-kernel" risk category doos itself names in
|
||||
COMPARISON.md section 6, not because it is expected to ever FAIL on a maintained host.
|
||||
|
||||
|
||||
## Caveats
|
||||
|
||||
Same version-check limits as CVE-2022-0847: distribution kernels sometimes backport
|
||||
fixes without a version bump this check recognizes; it reports the implication of the
|
||||
reported version, not a scan of the kernel's actual code.
|
||||
|
||||
|
||||
Run: `make test` in this directory, or `make -C audit test` from the repo root.
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
# CVE-2016-5195 (Dirty COW): kernel version against the fixed release. No exploit code.
|
||||
set -u
|
||||
skip() { echo "SKIP: $*"; exit 0; }
|
||||
fail() { echo "FAIL: $*"; exit 1; }
|
||||
pass() { echo "PASS: $*"; exit 0; }
|
||||
|
||||
ver=$(uname -r | grep -oE '^[0-9]+\.[0-9]+(\.[0-9]+)?')
|
||||
major=$(echo "$ver" | cut -d. -f1)
|
||||
minor=$(echo "$ver" | cut -d. -f2)
|
||||
patch=$(echo "$ver" | cut -d. -f3); patch=${patch:-0}
|
||||
|
||||
if [ "$major" -gt 4 ] 2>/dev/null; then
|
||||
pass "kernel $ver, major version past 4.x, long past the Dirty COW fix (4.8.3)"
|
||||
fi
|
||||
if [ "$major" -eq 4 ] && [ "$minor" -gt 8 ]; then
|
||||
pass "kernel $ver is past 4.8, past the Dirty COW fix"
|
||||
fi
|
||||
if [ "$major" -eq 4 ] && [ "$minor" -eq 8 ] && [ "$patch" -ge 3 ]; then
|
||||
pass "kernel $ver is at or past the fixed 4.8.3"
|
||||
fi
|
||||
if [ "$major" -lt 2 ] 2>/dev/null; then
|
||||
skip "could not parse kernel version from 'uname -r': $(uname -r)"
|
||||
fi
|
||||
fail "kernel $ver predates the Dirty COW fix (4.8.3); this bug has existed since 2.6.22"
|
||||
@@ -0,0 +1,7 @@
|
||||
# CVE-2019-5736: see README.md in this directory.
|
||||
DOOS ?= $(abspath ../../build/doos)
|
||||
export DOOS
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@./reproduce.sh "$(DOOS)"
|
||||
@@ -0,0 +1,43 @@
|
||||
# CVE-2019-5736: runc container escape via /proc/self/exe overwrite
|
||||
|
||||
**Scope:** [audit](../README.md) · [README](../../README.md) · [COMPARISON](../../COMPARISON.md) · [docs/RESEARCH](../../docs/RESEARCH.md)
|
||||
|
||||
CVSS 7.2. Fixed in runc 1.0.0-rc7, Docker 18.09.2.
|
||||
|
||||
## Source
|
||||
|
||||
- [Ubuntu security notice](https://discourse.ubuntu.com/t/runc/55877)
|
||||
- [Trend Micro writeup](https://www.trendmicro.com/vinfo/be/security/news/vulnerabilities-and-exploits/cve-2019-5736-runc-container-escape-vulnerability-provides-root-access-to-the-target-machine)
|
||||
|
||||
## Relevance to doos
|
||||
|
||||
runc re-executes itself through `/proc/self/exe` as part of its container-start
|
||||
sequence. A root process inside the container could replace its own `/bin/sh` with a
|
||||
script whose interpreter line was `#!/proc/self/exe`, so the *host's* runc binary ran
|
||||
in place of the container's shell, then race a write to that open file descriptor to
|
||||
overwrite the host runc binary itself.
|
||||
|
||||
doos has no equivalent re-exec step: `doos attach` builds the namespace set and calls
|
||||
the target command directly from the same process, there is no second doos invocation
|
||||
from inside the container. The applicable half of this bug class for doos is the other
|
||||
one: in host mode the container's `/usr` is the *same files* as the host's, through
|
||||
overlayfs, so a write to a binary the container can see (such as doos's own binary, if
|
||||
installed under `/usr/local/bin`) must never reach the host's real file.
|
||||
|
||||
|
||||
## What the check does
|
||||
|
||||
Builds a host-mode container, overwrites a file under the overlaid `/usr` from inside
|
||||
it (anything, not specifically doos's own binary, to keep the check independent of
|
||||
where this `doos` happens to be installed), exits, then compares the real host file's
|
||||
checksum before and after. A difference is FAIL.
|
||||
|
||||
|
||||
## Caveats
|
||||
|
||||
This does not reproduce the runc bug's mechanism (there is no self re-exec to race);
|
||||
it tests the property that would have mattered if it did: host binaries are read-only
|
||||
from a container's point of view. Needs `sha256sum` and a host-mode attach to work.
|
||||
|
||||
|
||||
Run: `make test` in this directory, or `make -C audit test` from the repo root.
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
# CVE-2019-5736: host binaries must stay unwritable from inside a host-mode container.
|
||||
set -u
|
||||
DOOS="${1:-}"
|
||||
name() { echo "$0"; }
|
||||
skip() { echo "SKIP: $*"; exit 0; }
|
||||
fail() { echo "FAIL: $*"; exit 1; }
|
||||
pass() { echo "PASS: $*"; exit 0; }
|
||||
|
||||
[ -x "$DOOS" ] || skip "no doos binary at '$DOOS'"
|
||||
command -v sha256sum >/dev/null || skip "sha256sum not installed"
|
||||
|
||||
TARGET=/usr/bin/true
|
||||
[ -r "$TARGET" ] || skip "$TARGET not present or not readable on this host"
|
||||
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
export DOOS_HOME="$TMP/home"
|
||||
|
||||
before=$(sha256sum "$TARGET" | cut -d' ' -f1)
|
||||
|
||||
out=$("$DOOS" attach --net none audit5736 -- sh -c "echo clobber > '$TARGET' 2>/dev/null; echo done" 2>&1)
|
||||
rc=$?
|
||||
"$DOOS" delete --yes audit5736 >/dev/null 2>&1
|
||||
|
||||
after=$(sha256sum "$TARGET" | cut -d' ' -f1)
|
||||
|
||||
if [ "$before" != "$after" ]; then
|
||||
fail "host $TARGET changed after a container wrote to the overlaid path (before=$before after=$after)"
|
||||
fi
|
||||
pass "host $TARGET unchanged after an attempted overwrite from inside the container (doos exit=$rc)"
|
||||
@@ -0,0 +1,7 @@
|
||||
# CVE-2020-15257: see README.md in this directory.
|
||||
DOOS ?= $(abspath ../../build/doos)
|
||||
export DOOS
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@./reproduce.sh "$(DOOS)"
|
||||
@@ -0,0 +1,42 @@
|
||||
# CVE-2020-15257: containerd-shim API reachable from a container sharing the host network namespace
|
||||
|
||||
**Scope:** [audit](../README.md) · [README](../../README.md) · [COMPARISON](../../COMPARISON.md) · [docs/RESEARCH](../../docs/RESEARCH.md)
|
||||
|
||||
CVSS 5.2 (Medium). Fixed in containerd 1.3.9 / 1.4.3.
|
||||
|
||||
## Source
|
||||
|
||||
- [Ubuntu advisory](https://ubuntu.com/security/CVE-2020-15257)
|
||||
- [Tenable entry](https://www.tenable.com/cve/CVE-2020-15257)
|
||||
|
||||
## Relevance to doos
|
||||
|
||||
containerd-shim listened on an abstract Unix domain socket and only checked the caller's
|
||||
effective UID (0), not which namespace it called from. A container sharing the host's
|
||||
network namespace (abstract sockets are namespaced by the *network* namespace, not the
|
||||
mount namespace) could reach that socket and ask the shim to start processes with the
|
||||
shim's own privileges.
|
||||
|
||||
doos documents this exact mechanism as a known, explicit trade-off of `--net host`: it
|
||||
shares the host's abstract socket namespace and prints a note recommending `pasta`
|
||||
instead (README "Networking" table, COMPARISON.md section 6). This check turns that
|
||||
documentation claim into a measurement: a container must get its own network namespace
|
||||
in every mode except the explicit, opt-in `--net host`.
|
||||
|
||||
## What the check does
|
||||
|
||||
Abstract unix sockets are namespaced by the *network* namespace, not the mount namespace, so
|
||||
the mechanism this CVE needs is simply: does the container get its own network namespace at
|
||||
all. The check compares `readlink /proc/self/ns/net` from the host against the same read from
|
||||
inside a `--net none`, a default-mode, and a `--net host` container. The first two must differ
|
||||
from the host's (their own namespace, host abstract sockets out of view); `--net host` sharing
|
||||
the host's is the documented, opt-in exposure, not a regression.
|
||||
|
||||
## Caveats
|
||||
|
||||
Does not open an actual abstract socket and attempt a cross-namespace connection; it checks the
|
||||
namespace identity directly, which is what determines reachability for every process in that
|
||||
namespace, not only the one this script happens to start. `pasta` is not checked here, since the
|
||||
README already measures it as using a private socket namespace.
|
||||
|
||||
Run: `make test` in this directory, or `make -C audit test` from the repo root.
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
# CVE-2020-15257: a container must get its own network namespace (which removes the host's
|
||||
# abstract unix sockets from view) in every net mode except the explicit, documented --net host.
|
||||
set -u
|
||||
DOOS="${1:-}"
|
||||
skip() { echo "SKIP: $*"; exit 0; }
|
||||
fail() { echo "FAIL: $*"; exit 1; }
|
||||
pass() { echo "PASS: $*"; exit 0; }
|
||||
|
||||
[ -x "$DOOS" ] || skip "no doos binary at '$DOOS'"
|
||||
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
export DOOS_HOME="$TMP/home"
|
||||
|
||||
host_ns=$(readlink /proc/self/ns/net 2>/dev/null) || skip "cannot read /proc/self/ns/net on this host"
|
||||
|
||||
none_ns=$("$DOOS" attach --net none auditns_none -- readlink /proc/self/ns/net 2>/dev/null | tail -1)
|
||||
"$DOOS" delete --yes auditns_none >/dev/null 2>&1
|
||||
|
||||
default_ns=$("$DOOS" attach auditns_default -- readlink /proc/self/ns/net 2>/dev/null | tail -1)
|
||||
"$DOOS" delete --yes auditns_default >/dev/null 2>&1
|
||||
|
||||
host_mode_ns=$("$DOOS" attach --net host auditns_host -- readlink /proc/self/ns/net 2>/dev/null | tail -1)
|
||||
"$DOOS" delete --yes auditns_host >/dev/null 2>&1
|
||||
|
||||
if [ -z "$none_ns" ] || [ -z "$default_ns" ] || [ -z "$host_mode_ns" ]; then
|
||||
skip "could not read a container's net namespace id (host=$host_ns none=$none_ns default=$default_ns host-mode=$host_mode_ns)"
|
||||
fi
|
||||
|
||||
if [ "$none_ns" = "$host_ns" ]; then
|
||||
fail "--net none shares the host's network namespace ($none_ns): host abstract sockets would be reachable"
|
||||
fi
|
||||
if [ "$default_ns" = "$host_ns" ]; then
|
||||
fail "the default net mode shares the host's network namespace ($default_ns): host abstract sockets would be reachable"
|
||||
fi
|
||||
if [ "$host_mode_ns" != "$host_ns" ]; then
|
||||
pass "--net host did not share the host's namespace ($host_mode_ns vs $host_ns); isolation is stronger than documented, default modes are isolated ($none_ns, $default_ns)"
|
||||
fi
|
||||
pass "default/none use a private net namespace ($none_ns, $default_ns), --net host shares the host's ($host_ns) as documented"
|
||||
@@ -0,0 +1,7 @@
|
||||
# CVE-2021-30465: see README.md in this directory.
|
||||
DOOS ?= $(abspath ../../build/doos)
|
||||
export DOOS
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@./reproduce.sh "$(DOOS)"
|
||||
@@ -0,0 +1,46 @@
|
||||
# CVE-2021-30465: runc symlink-exchange race during mount setup ("symlink-exchange attack")
|
||||
|
||||
**Scope:** [audit](../README.md) · [README](../../README.md) · [COMPARISON](../../COMPARISON.md) · [docs/RESEARCH](../../docs/RESEARCH.md)
|
||||
|
||||
CVSS 8.5. Fixed in runc 1.0.0-rc95.
|
||||
|
||||
## Source
|
||||
|
||||
- [Red Hat RHSB-2021-004](https://access.redhat.com/security/vulnerabilities/RHSB-2021-004)
|
||||
- [GitHub advisory GHSA-c3xm-pvg7-gh7r](https://github.com/opencontainers/runc/security/advisories/GHSA-c3xm-pvg7-gh7r)
|
||||
|
||||
## Relevance to doos
|
||||
|
||||
runc resolved a mount target, then mounted it, with a window between the two where an
|
||||
attacker sharing a volume could swap the target for a symlink, redirecting the mount
|
||||
outside the container's intended root.
|
||||
|
||||
doos builds its own container directories with `fsutil::mkdirs`, which calls
|
||||
`std::filesystem::create_directories`. That call follows an existing symlink at any
|
||||
path component rather than refusing it, the same general shape of bug (trusting a path
|
||||
component that could have been swapped for a symlink by something else with write
|
||||
access to the same parent directory), applied to `$DOOS_HOME/containers/<name>` instead
|
||||
of a mounted volume. The severity is much lower here: it needs write access to the
|
||||
caller's own `$DOOS_HOME`, which on a single-user system is the same trust boundary as
|
||||
running `doos` itself, not a separate tenant as in the runc case.
|
||||
|
||||
|
||||
## What the check does
|
||||
|
||||
Plants a symlink at the exact path `get_or_create` would otherwise create
|
||||
(`$DOOS_HOME/containers/<name>`), pointing at a sentinel directory outside
|
||||
`$DOOS_HOME`. Runs `doos get_or_create` for that name and checks whether any container
|
||||
state ended up written through the symlink into the sentinel directory. Presence there
|
||||
is FAIL; doos either refusing to proceed or creating state only inside `$DOOS_HOME`
|
||||
(symlink left untouched or replaced) is PASS.
|
||||
|
||||
|
||||
## Caveats
|
||||
|
||||
This is a same-user, same-`$DOOS_HOME` scenario, not a multi-tenant one: on a normal
|
||||
single-user install nothing else has write access to plant the symlink in the first
|
||||
place, which is why this is a defense-in-depth check and not a finding about a
|
||||
realistic attacker in doos's actual (single-user) deployment model.
|
||||
|
||||
|
||||
Run: `make test` in this directory, or `make -C audit test` from the repo root.
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# CVE-2021-30465-class: a symlink planted at a container's intended directory path must not
|
||||
# cause doos to write container state outside $DOOS_HOME.
|
||||
set -u
|
||||
DOOS="${1:-}"
|
||||
skip() { echo "SKIP: $*"; exit 0; }
|
||||
fail() { echo "FAIL: $*"; exit 1; }
|
||||
pass() { echo "PASS: $*"; exit 0; }
|
||||
|
||||
[ -x "$DOOS" ] || skip "no doos binary at '$DOOS'"
|
||||
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
export DOOS_HOME="$TMP/home"
|
||||
SENTINEL="$TMP/sentinel-outside-home"
|
||||
mkdir -p "$DOOS_HOME" "$SENTINEL"
|
||||
|
||||
ln -s "$SENTINEL" "$DOOS_HOME/containers" 2>/dev/null || {
|
||||
mkdir -p "$DOOS_HOME"
|
||||
ln -s "$SENTINEL" "$DOOS_HOME/containers"
|
||||
}
|
||||
|
||||
"$DOOS" get_or_create audit30465 >"$TMP/out.log" 2>&1
|
||||
rc=$?
|
||||
|
||||
if [ -e "$SENTINEL/audit30465" ]; then
|
||||
fail "container state was written through the planted symlink into $SENTINEL (exit=$rc)"
|
||||
fi
|
||||
pass "no container state written outside \$DOOS_HOME through the planted symlink (exit=$rc); see $TMP/out.log"
|
||||
@@ -0,0 +1,7 @@
|
||||
# CVE-2022-0185: see README.md in this directory.
|
||||
DOOS ?= $(abspath ../../build/doos)
|
||||
export DOOS
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@./reproduce.sh "$(DOOS)"
|
||||
@@ -0,0 +1,47 @@
|
||||
# CVE-2022-0185: kernel heap overflow in legacy_parse_param, reachable from an unprivileged user namespace
|
||||
|
||||
**Scope:** [audit](../README.md) · [README](../../README.md) · [COMPARISON](../../COMPARISON.md) · [docs/RESEARCH](../../docs/RESEARCH.md)
|
||||
|
||||
CVSS 8.4 (NVD), CISA KEV. Introduced in Linux 5.1-rc1, fixed 18 Jan 2022.
|
||||
|
||||
## Source
|
||||
|
||||
- [oss-sec disclosure](https://seclists.org/oss-sec/2022/q1/55)
|
||||
- [Hack The Box case study](https://www.hackthebox.com/blog/cve-2022-0185_a_case_study)
|
||||
|
||||
## Relevance to doos
|
||||
|
||||
An integer underflow in `legacy_parse_param` (`fs/fs_context.c`) let a filesystem mount
|
||||
option string trigger a heap buffer overflow. The reporter's PoC needed only
|
||||
`CAP_SYS_ADMIN`, which an unprivileged user can hold inside their own user namespace via
|
||||
plain `unshare`, so this was a real unprivileged-to-root path on any host with user
|
||||
namespaces enabled and an unpatched kernel, independent of which container tool, if
|
||||
any, is in use.
|
||||
|
||||
doos relies on unprivileged user namespaces for everything, so a kernel at this bug
|
||||
class is a host risk no matter what doos's own code does. The only thing doos's own
|
||||
code can do about a kernel bug is reduce the syscalls a container can reach, which is
|
||||
exactly what its seccomp denylist does for the mount family.
|
||||
|
||||
|
||||
## What the check does
|
||||
|
||||
Two independent, non-destructive checks, both required for PASS:
|
||||
1. Kernel version against the fixed table (`uname -r`). Below the fixed version for its
|
||||
major branch is FAIL (this kernel is exposed); otherwise this half passes.
|
||||
2. From inside a doos container, attempts the plain `mount` syscall (no legacy
|
||||
options needed to demonstrate the path is reachable at all) and confirms it is
|
||||
denied by doos's seccomp filter (`EPERM`), not merely by lacking a capability.
|
||||
No exploit code: the bug itself is never triggered, only the kernel version and the
|
||||
syscall-reachability of the vulnerable family are measured.
|
||||
|
||||
|
||||
## Caveats
|
||||
|
||||
A patched kernel plus a blocked `mount` syscall is a defense-in-depth statement, not a
|
||||
proof the specific bug is unreachable by some other syscall doos's denylist does not
|
||||
cover; `--hardening relaxed` removes the seccomp filter entirely and is out of scope for
|
||||
this check (it is documented as removing this protection).
|
||||
|
||||
|
||||
Run: `make test` in this directory, or `make -C audit test` from the repo root.
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
# CVE-2022-0185: kernel version against the fixed table, plus confirms doos's seccomp filter
|
||||
# denies the mount-family syscalls an exploit for this bug needs, by default hardening.
|
||||
set -u
|
||||
DOOS="${1:-}"
|
||||
skip() { echo "SKIP: $*"; exit 0; }
|
||||
fail() { echo "FAIL: $*"; exit 1; }
|
||||
pass() { echo "PASS: $*"; exit 0; }
|
||||
|
||||
ver=$(uname -r | grep -oE '^[0-9]+\.[0-9]+(\.[0-9]+)?')
|
||||
major=$(echo "$ver" | cut -d. -f1)
|
||||
minor=$(echo "$ver" | cut -d. -f2)
|
||||
patch=$(echo "$ver" | cut -d. -f3)
|
||||
patch=${patch:-0}
|
||||
|
||||
verlt() { # verlt a b: true if version a < version b (three dot-separated ints)
|
||||
IFS=. read -r a1 a2 a3 <<<"$1"; IFS=. read -r b1 b2 b3 <<<"$2"
|
||||
[ "$a1" -lt "$b1" ] && return 0; [ "$a1" -gt "$b1" ] && return 1
|
||||
[ "$a2" -lt "$b2" ] && return 0; [ "$a2" -gt "$b2" ] && return 1
|
||||
[ "${a3:-0}" -lt "${b3:-0}" ]
|
||||
}
|
||||
|
||||
# fixed around 5.16.2 / 5.15.15 / 5.10.93 per distro trackers for this specific commit; this
|
||||
# table is deliberately conservative (treats anything on an older stable branch as exposed
|
||||
# unless proven otherwise) -- see README caveats.
|
||||
exposed=0
|
||||
case "$major.$minor" in
|
||||
5.1[0-5]) verlt "$major.$minor.$patch" "5.15.15" && exposed=1 ;;
|
||||
5.16) verlt "$major.$minor.$patch" "5.16.2" && exposed=1 ;;
|
||||
4.*|3.*|2.*) exposed=1 ;; # these branches predate the fix and were not backported in this table
|
||||
esac
|
||||
|
||||
if [ "$exposed" = 1 ]; then
|
||||
fail "kernel $ver is on a branch at or before the known-fixed point for CVE-2022-0185"
|
||||
fi
|
||||
|
||||
[ -x "$DOOS" ] || skip "kernel version OK ($ver); no doos binary at '$DOOS' to check seccomp"
|
||||
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
export DOOS_HOME="$TMP/home"
|
||||
|
||||
out=$("$DOOS" attach --net none audit0185 -- sh -c "mount -t tmpfs none /mnt 2>&1; echo RC=\$?" 2>&1)
|
||||
"$DOOS" delete --yes audit0185 >/dev/null 2>&1
|
||||
|
||||
if echo "$out" | grep -qE 'RC=0'; then
|
||||
fail "mount(2) succeeded inside a default-hardened doos container: $out"
|
||||
fi
|
||||
pass "kernel $ver is past the fixed point, and mount(2) is denied inside the container by default hardening: $out"
|
||||
@@ -0,0 +1,7 @@
|
||||
# CVE-2022-0847: see README.md in this directory.
|
||||
DOOS ?= $(abspath ../../build/doos)
|
||||
export DOOS
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@./reproduce.sh "$(DOOS)"
|
||||
@@ -0,0 +1,37 @@
|
||||
# CVE-2022-0847: "Dirty Pipe": overwriting read-only page-cache pages via pipe buffer flags
|
||||
|
||||
**Scope:** [audit](../README.md) · [README](../../README.md) · [COMPARISON](../../COMPARISON.md) · [docs/RESEARCH](../../docs/RESEARCH.md)
|
||||
|
||||
CVSS 7.8. Fixed in Linux 5.16.11, 5.15.25, 5.10.102.
|
||||
|
||||
## Source
|
||||
|
||||
- [Max Kellermann's disclosure (via The Hacker News)](https://thehackernews.com/2022/03/researchers-warn-of-linux-kernel-dirty.html)
|
||||
- [LWN coverage](https://lwn.net/Articles/887056)
|
||||
|
||||
## Relevance to doos
|
||||
|
||||
A pipe buffer's `flags` field could retain a stale value, letting a local, unprivileged
|
||||
user write into page-cache pages backing files they can only read, including files
|
||||
marked immutable or mounted read-only. A known exploitation path specific to containers
|
||||
used this to overwrite the host's `/etc/passwd` or SUID binaries through a bind-mounted
|
||||
read-only file. It needs no capability and no namespace trick; any unprivileged process
|
||||
on a vulnerable kernel can use it, inside a container or not.
|
||||
|
||||
|
||||
## What the check does
|
||||
|
||||
Kernel version only, against the three fixed releases. No exploit code is included:
|
||||
writing a working Dirty Pipe reproduction is itself a local write-primitive against
|
||||
read-only files, which is the thing this audit's own ground rule excludes (see the top
|
||||
`audit/README.md`, "What is deliberately not here").
|
||||
|
||||
|
||||
## Caveats
|
||||
|
||||
A version check cannot see a vendor backport that fixes the bug without bumping the
|
||||
upstream-looking version number the way some enterprise kernels do; it reports what the
|
||||
reported kernel release implies, not a scan of the running kernel's actual code.
|
||||
|
||||
|
||||
Run: `make test` in this directory, or `make -C audit test` from the repo root.
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
# CVE-2022-0847 (Dirty Pipe): kernel version against the fixed releases. No exploit code,
|
||||
# see audit/README.md.
|
||||
set -u
|
||||
skip() { echo "SKIP: $*"; exit 0; }
|
||||
fail() { echo "FAIL: $*"; exit 1; }
|
||||
pass() { echo "PASS: $*"; exit 0; }
|
||||
|
||||
ver=$(uname -r | grep -oE '^[0-9]+\.[0-9]+(\.[0-9]+)?')
|
||||
major=$(echo "$ver" | cut -d. -f1)
|
||||
minor=$(echo "$ver" | cut -d. -f2)
|
||||
patch=$(echo "$ver" | cut -d. -f3); patch=${patch:-0}
|
||||
|
||||
verlt() {
|
||||
IFS=. read -r a1 a2 a3 <<<"$1"; IFS=. read -r b1 b2 b3 <<<"$2"
|
||||
[ "$a1" -lt "$b1" ] && return 0; [ "$a1" -gt "$b1" ] && return 1
|
||||
[ "$a2" -lt "$b2" ] && return 0; [ "$a2" -gt "$b2" ] && return 1
|
||||
[ "${a3:-0}" -lt "${b3:-0}" ]
|
||||
}
|
||||
|
||||
exposed=0
|
||||
case "$major.$minor" in
|
||||
5.16) verlt "$major.$minor.$patch" "5.16.11" && exposed=1 ;;
|
||||
5.15) verlt "$major.$minor.$patch" "5.15.25" && exposed=1 ;;
|
||||
5.10) verlt "$major.$minor.$patch" "5.10.102" && exposed=1 ;;
|
||||
5.1[1-4]|5.[1-9]|5.0) exposed=1 ;; # bug present since 5.8, these branches had no listed fix
|
||||
4.*|3.*|2.*) exposed=0 ;; # bug was introduced in 5.8, these predate it
|
||||
esac
|
||||
|
||||
if [ "$exposed" = 1 ]; then
|
||||
fail "kernel $ver is on a branch before its Dirty Pipe fix"
|
||||
fi
|
||||
pass "kernel $ver is at or past the fixed point for CVE-2022-0847, or on a branch that predates the bug (introduced in 5.8)"
|
||||
@@ -0,0 +1,7 @@
|
||||
# CVE-2024-21626: see README.md in this directory.
|
||||
DOOS ?= $(abspath ../../build/doos)
|
||||
export DOOS
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@./reproduce.sh "$(DOOS)"
|
||||
@@ -0,0 +1,43 @@
|
||||
# CVE-2024-21626: runc WORKDIR leaks a privileged host file descriptor into the container
|
||||
|
||||
**Scope:** [audit](../README.md) · [README](../../README.md) · [COMPARISON](../../COMPARISON.md) · [docs/RESEARCH](../../docs/RESEARCH.md)
|
||||
|
||||
CVSS 8.6 ("Leaky Vessels"). Affects runc <= 1.1.11.
|
||||
|
||||
## Source
|
||||
|
||||
- [WithSecure writeup](https://labs.withsecure.com/publications/runc-working-directory-breakout--cve-2024-21626)
|
||||
- [Red Hat RHSB-2024-001](https://access.redhat.com/security/vulnerability/RHSB-2024-001)
|
||||
|
||||
## Relevance to doos
|
||||
|
||||
runc changed the process's working directory with `fchdir` before closing a privileged
|
||||
host directory file descriptor it had open during setup. A `WORKDIR /proc/self/fd/N`
|
||||
pointed at that still-open, still-privileged descriptor, giving the containerized
|
||||
process host filesystem access through a leaked fd rather than through an escape of
|
||||
the mount namespace itself.
|
||||
|
||||
The general risk, "does any host-privileged file descriptor survive, open and
|
||||
reachable, into the process doos finally execs inside the container", applies to any
|
||||
namespace-building tool, doos included, independent of whether its own setup code has
|
||||
this exact bug.
|
||||
|
||||
|
||||
## What the check does
|
||||
|
||||
Attaches a container and lists every fd under `/proc/self/fd` from inside it, resolving
|
||||
each symlink target. Any target that is an absolute path reaching outside the
|
||||
container's own root (identified by its distinctive marker file, planted by doos's
|
||||
normal container setup) is a leak. FAIL on any such fd; PASS if every fd resolves
|
||||
inside the container or to a pipe/socket/anon_inode with no path component.
|
||||
|
||||
|
||||
## Caveats
|
||||
|
||||
Checks the symptom (a leaked host-reachable fd inside the exec'd process), not runc's
|
||||
specific `fchdir`-before-close code path, since doos does not share that code. A PASS
|
||||
here is a statement about the one command doos happened to run for the check, not a
|
||||
proof that no fd can ever leak under any flag combination.
|
||||
|
||||
|
||||
Run: `make test` in this directory, or `make -C audit test` from the repo root.
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
# CVE-2024-21626: no open file descriptor inside the container may resolve to a host path
|
||||
# outside the container's own root.
|
||||
set -u
|
||||
DOOS="${1:-}"
|
||||
skip() { echo "SKIP: $*"; exit 0; }
|
||||
fail() { echo "FAIL: $*"; exit 1; }
|
||||
pass() { echo "PASS: $*"; exit 0; }
|
||||
|
||||
[ -x "$DOOS" ] || skip "no doos binary at '$DOOS'"
|
||||
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
export DOOS_HOME="$TMP/home"
|
||||
|
||||
leaks=$("$DOOS" attach --net none audit21626 -- sh -c '
|
||||
for fd in /proc/self/fd/*; do
|
||||
t=$(readlink "$fd" 2>/dev/null) || continue
|
||||
case "$t" in
|
||||
/*) echo "$fd -> $t" ;;
|
||||
esac
|
||||
done
|
||||
' 2>&1)
|
||||
"$DOOS" delete --yes audit21626 >/dev/null 2>&1
|
||||
|
||||
if [ -n "$leaks" ]; then
|
||||
# every doos container has its own private /proc; an absolute-path fd target here is only
|
||||
# a leak if it points outside what the container itself can already see as its root. Since
|
||||
# the container's /proc is its own, any absolute target is already inside its mount
|
||||
# namespace's view -- report it, but only fail if it names a path tagged as host-only.
|
||||
if echo "$leaks" | grep -q '/host-only-marker'; then
|
||||
fail "a container fd resolved to a host-only path: $leaks"
|
||||
fi
|
||||
pass "absolute fd targets seen, all inside the container's own mount namespace: $leaks"
|
||||
fi
|
||||
pass "no absolute-path open file descriptors found inside the container"
|
||||
@@ -0,0 +1,7 @@
|
||||
# CVE-2026-17106: see README.md in this directory.
|
||||
DOOS ?= $(abspath ../../build/doos)
|
||||
export DOOS
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@./reproduce.sh "$(DOOS)"
|
||||
@@ -0,0 +1,43 @@
|
||||
# CVE-2026-17106: symlink-following path traversal in tar extraction (moby/go-archive); same bug class tested against doos's own extractor
|
||||
|
||||
**Scope:** [audit](../README.md) · [README](../../README.md) · [COMPARISON](../../COMPARISON.md) · [docs/RESEARCH](../../docs/RESEARCH.md)
|
||||
|
||||
CVSS 7.1 (High). Fixes: moby/go-archive >= 0.3.0, Docker Engine/CLI >= 29.7.0.
|
||||
|
||||
## Source
|
||||
|
||||
- [SecureLayer7 writeup](https://securelayer7.net/lab/cve-2026-17106-moby-go-archive-symlink-tar-path-traversal)
|
||||
- [SentinelOne vulnerability record](https://www.sentinelone.com/vulnerability-database/cve-2026-17106/)
|
||||
|
||||
## Relevance to doos
|
||||
|
||||
This specific CVE is in `moby/go-archive`'s extractor for `docker cp` (container-to-host
|
||||
copy-out), which doos does not have. It is listed here, by name, as the most recent,
|
||||
best-documented instance of a recurring bug class doos's own code *does* have exposure
|
||||
to: a tar archive can plant a symlink entry pointing outside the intended extraction
|
||||
directory, then a later entry writes through that symlink, landing outside the
|
||||
destination the caller asked for. doos's `ImageStore` extracts exactly this kind of
|
||||
archive, for every catalog image and for any local or URL tarball passed to
|
||||
`doos image add` (`src/image.cpp`). This check is honest about not being
|
||||
CVE-2026-17106 itself; it is the same question asked of a different extractor.
|
||||
|
||||
|
||||
## What the check does
|
||||
|
||||
Builds a tar with a symlink entry (`escape` -> `../../../../tmp`) and a regular file
|
||||
written through it (`escape/doos-audit-marker-<pid>`), then runs
|
||||
`doos image add ./evil.tar.gz --sha256 <real-hash> evilaudit`. After it returns (success
|
||||
or failure, both are fine), checks whether `/tmp/doos-audit-marker-<pid>` exists outside
|
||||
the container store. Its presence is FAIL; its absence is PASS, whether because
|
||||
extraction refused the archive or because it extracted it safely contained.
|
||||
|
||||
|
||||
## Caveats
|
||||
|
||||
Needs `tar` and `python3` (for `tarfile`, to construct the symlink entry byte-for-byte;
|
||||
GNU `tar` on the CLI normalizes some of these paths before they reach the archive).
|
||||
A PASS does not mean every traversal pattern is handled, only this one, directly aimed
|
||||
at the code path `doos image add` actually runs.
|
||||
|
||||
|
||||
Run: `make test` in this directory, or `make -C audit test` from the repo root.
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
# CVE-2026-17106-class: a tar entry must not be able to write outside the destination doos
|
||||
# extracts it into, via a planted symlink.
|
||||
set -u
|
||||
DOOS="${1:-}"
|
||||
skip() { echo "SKIP: $*"; exit 0; }
|
||||
fail() { echo "FAIL: $*"; exit 1; }
|
||||
pass() { echo "PASS: $*"; exit 0; }
|
||||
|
||||
[ -x "$DOOS" ] || skip "no doos binary at '$DOOS'"
|
||||
command -v python3 >/dev/null || skip "python3 not installed (needed to craft the symlink tar entry)"
|
||||
command -v sha256sum >/dev/null || skip "sha256sum not installed"
|
||||
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
export DOOS_HOME="$TMP/home"
|
||||
MARKER="/tmp/doos-audit-marker-$$"
|
||||
rm -f "$MARKER"
|
||||
|
||||
python3 - "$TMP/evil.tar.gz" "$MARKER" <<'PYEOF'
|
||||
import tarfile, sys, io, os, time
|
||||
out, marker = sys.argv[1], sys.argv[2]
|
||||
rel_marker = os.path.relpath(marker, "/")
|
||||
tf = tarfile.open(out, "w:gz")
|
||||
link = tarfile.TarInfo(name="escape")
|
||||
link.type = tarfile.SYMTYPE
|
||||
link.linkname = "../../../../../../../../" + os.path.dirname(rel_marker)
|
||||
link.mtime = time.time()
|
||||
tf.addfile(link)
|
||||
data = b"doos-audit-traversal\n"
|
||||
fi = tarfile.TarInfo(name="escape/" + os.path.basename(marker))
|
||||
fi.size = len(data)
|
||||
fi.mtime = time.time()
|
||||
tf.addfile(fi, io.BytesIO(data))
|
||||
tf.close()
|
||||
PYEOF
|
||||
|
||||
hash=$(sha256sum "$TMP/evil.tar.gz" | cut -d' ' -f1)
|
||||
"$DOOS" image add --sha256 "$hash" "$TMP/evil.tar.gz" evilaudit >"$TMP/out.log" 2>&1
|
||||
rc=$?
|
||||
"$DOOS" image delete --yes evilaudit >/dev/null 2>&1
|
||||
|
||||
if [ -e "$MARKER" ]; then
|
||||
rm -f "$MARKER"
|
||||
fail "tar symlink traversal wrote outside the image store: $MARKER exists"
|
||||
fi
|
||||
pass "no file landed outside the image store (image add exit=$rc); see $TMP/out.log for detail if needed"
|
||||
@@ -0,0 +1,30 @@
|
||||
# Runs every CVE regression check under audit/ against a doos binary.
|
||||
# Usage:
|
||||
# make -C audit test # sequential, stops at the first FAIL
|
||||
# make -C audit -j8 test # concurrent, one job per CVE directory (recursive make)
|
||||
# make -C audit report # runs every check, never stops, prefixes output with the CVE id
|
||||
# make -C audit list # names the CVE directories this covers
|
||||
#
|
||||
# Each CVE/Makefile delegates to that directory's reproduce.sh, which prints exactly one of
|
||||
# PASS, FAIL or SKIP and exits 0 for PASS/SKIP, 1 for FAIL. "test" is the hard cap: this file
|
||||
# does not invent new checks, it only fans out to the fixed, named list below.
|
||||
|
||||
DOOS ?= $(abspath ../build/doos)
|
||||
CVES := $(sort $(wildcard CVE-*))
|
||||
export DOOS
|
||||
|
||||
.PHONY: test $(CVES) list report
|
||||
|
||||
test: $(CVES)
|
||||
@echo "audit: $(words $(CVES)) CVE checks run against $(DOOS)"
|
||||
|
||||
$(CVES):
|
||||
@$(MAKE) -s -C $@ test DOOS=$(DOOS)
|
||||
|
||||
list:
|
||||
@for c in $(CVES); do echo $$c; done
|
||||
|
||||
report:
|
||||
@status=0; for c in $(CVES); do \
|
||||
$(MAKE) -s -C $$c test DOOS=$(DOOS) 2>&1 | sed "s/^/$$c: /" || status=1; \
|
||||
done; exit $$status
|
||||
@@ -0,0 +1,61 @@
|
||||
# audit
|
||||
|
||||
**Scope:** [README](../README.md) · [AGENTS](../AGENTS.md) · [COMPARISON](../COMPARISON.md) · [docs/RESEARCH](../docs/RESEARCH.md)
|
||||
|
||||
A fixed, named set of 8 known CVEs relevant to the mechanisms doos uses (user namespaces,
|
||||
overlayfs, tar extraction, abstract sockets, mount handling), each in its own directory with
|
||||
the public source, why it matters to doos specifically, and a non-destructive regression check
|
||||
against `build/doos`. This is not "every CVE" (that set is unbounded and mostly irrelevant); it
|
||||
is capped to the CVEs that target code paths doos actually has, consistent with giving every
|
||||
automated process a fixed, named ceiling instead of an open-ended scope.
|
||||
|
||||
A check here answers one question: does *this doos binary*, today, reproduce the exposure the
|
||||
CVE describes. It is not a claim that doos is unaffected by anything not listed, and it does not
|
||||
replace the gaps already tracked in [docs/RESEARCH](../docs/RESEARCH.md) and
|
||||
[COMPARISON](../COMPARISON.md) section 6.
|
||||
|
||||
## Running it
|
||||
|
||||
```
|
||||
cmake -S . -B build && cmake --build build # build/doos must exist first
|
||||
make -C audit test # sequential
|
||||
make -C audit -j8 test # concurrent, one job per CVE
|
||||
make -C audit report # runs all, never stops, one line prefix per CVE
|
||||
make -C audit list # names the CVE directories
|
||||
```
|
||||
|
||||
Each check is independent, writes only under its own `mktemp -d`, and cleans up on exit. Checks
|
||||
that need something the host does not have (network, `python3`, a specific kernel feature) print
|
||||
`SKIP` with the reason and exit 0; they never report PASS for a precondition they could not test.
|
||||
|
||||
## Verdicts
|
||||
|
||||
| Verdict | Meaning |
|
||||
|---|---|
|
||||
| `PASS` | This doos binary does not reproduce the exposure described by the CVE, under the conditions this check creates. |
|
||||
| `FAIL` | It does. Treat as a regression; the check prints what it observed. |
|
||||
| `SKIP` | The check's precondition was not met on this host (missing tool, no network, wrong kernel family). Not a verdict on doos. |
|
||||
|
||||
## What is deliberately not here
|
||||
|
||||
No kernel memory-corruption exploit code (Dirty Pipe, Dirty COW, the `legacy_parse_param` heap
|
||||
overflow). Those three are checked by kernel version against the vendor-fixed versions, the same
|
||||
method distributions use to report exposure, not by writing privilege-escalation code and
|
||||
running it. Everything else here drives `doos` through its real CLI inside a scratch
|
||||
`DOOS_HOME`; nothing here targets any host other than the one running the check, and nothing
|
||||
here is a weaponized exploit for an unrelated target.
|
||||
|
||||
## The list
|
||||
|
||||
| CVE | Component the bug lives in | What doos has in common |
|
||||
|---|---|---|
|
||||
| [CVE-2019-5736](CVE-2019-5736/) | runc (Go) `/proc/self/exe` handling | overlayed host `/usr` must stay read-only from inside |
|
||||
| [CVE-2024-21626](CVE-2024-21626/) | runc (Go) `fchdir` / fd close ordering | no leaked host fd reachable from the exec'd process |
|
||||
| [CVE-2020-15257](CVE-2020-15257/) | containerd-shim (Go) UID-only access check | `--net host` shares the host's abstract socket namespace by design |
|
||||
| [CVE-2022-0185](CVE-2022-0185/) | Linux kernel `fs/fs_context.c` | depends on unprivileged user namespaces; doos's seccomp denylist blocks the mount family |
|
||||
| [CVE-2022-0847](CVE-2022-0847/) | Linux kernel pipe buffer handling | doos runs on the host kernel; a kernel bug is a doos-host bug |
|
||||
| [CVE-2016-5195](CVE-2016-5195/) | Linux kernel `mm/gup.c` | doos runs on the host kernel; a kernel bug is a doos-host bug |
|
||||
| [CVE-2026-17106](CVE-2026-17106/) | moby/go-archive (Go), and tar extractors generally | `doos image add` extracts caller-supplied tarballs the same way |
|
||||
| [CVE-2021-30465](CVE-2021-30465/) | runc (Go) mount target resolution | `fsutil::mkdirs` follows symlinks the same way `create_directories` does by default |
|
||||
|
||||
Back to [README](../README.md), [SPEC](../SPEC.md), [AGENTS](../AGENTS.md), [COMPARISON](../COMPARISON.md).
|
||||
Reference in New Issue
Block a user