commit 36feeb5b099d9e835e67636a5ec87911283aead4
parent c717fb5b4469200bea55ac296bb38372c7d33218
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 9 Jun 2026 20:56:58 -0700
test: add test-dist target with read-side adversarial CAS coverage
`make test-dist` is now the one-command verification of the dist subsystem
(content-addressed store + signed packaging). It aggregates the CAS and pkg
driver corpora (test-driver-cas + test-driver-pkg), which already cover
round-trip, integrity/tamper detection, the trust store, TOFU, and
format-mismatch rejection.
Adds read-side adversarial cases to test/cas/run.sh. The existing suite only
rejected unsafe paths at *creation* (`cas add-tree --map`); these craft a
malicious CAS tree object directly — bypassing add-tree — and assert it is
rejected at materialize/verify time, the real attack surface for an untrusted
tree fetched from a remote or unpacked from a package:
- parent-dir traversal (`../escape.txt`): rejected, and nothing is written
outside the -C target (containment asserted explicitly).
- absolute path (`/...`): rejected.
- embedded NUL in a path: rejected, never silently truncated.
These exercise the read-path validator (dist_tree_path_valid) and the tree KV
NUL check, and guard the dist hardening landed in the previous commit.
cas: 48 pass / 0 fail / 0 skip; pkg: 182 pass / 0 fail / 0 skip.
Diffstat:
2 files changed, 73 insertions(+), 0 deletions(-)
diff --git a/mk/test.mk b/mk/test.mk
@@ -72,6 +72,7 @@ TEST_TARGETS = \
test-driver-objcopy \
test-driver-objdump \
test-driver-pkg \
+ test-dist \
test-driver-strings \
test-driver-tools \
test-driver-wasm \
@@ -300,6 +301,13 @@ test-driver-objdump: bin
test-driver-pkg: bin
@KIT=$(abspath $(BIN)) sh test/pkg/run.sh
+# test-dist: one-command verification of the dist subsystem (content-addressed
+# store + signed packaging). Runs the CAS and pkg driver corpora, which cover
+# round-trip, integrity/tamper detection, trust store, and the read-side
+# adversarial cases (untrusted-tree path-traversal and NUL-byte rejection) in
+# test/cas/run.sh.
+test-dist: test-driver-cas test-driver-pkg
+
test-driver-strings: bin
@KIT=$(abspath $(BIN)) sh test/strings/run.sh
diff --git a/test/cas/run.sh b/test/cas/run.sh
@@ -147,5 +147,70 @@ else
skip_test "cas-corruption-tests"
fi
+# ---------------------------------------------------------------------------
+# Read-side adversarial trees. A CAS tree object is untrusted input (it may be
+# fetched from a remote or unpacked from a package). Crafting a malicious tree
+# object directly — bypassing `cas add-tree`, which validates paths at creation
+# — must still be rejected at materialize/verify time. This exercises the
+# read-path validator (dist_tree_path_valid) and the tree KV NUL-byte check,
+# the real attack surface for an untrusted tree.
+# ---------------------------------------------------------------------------
+blake2b_id() { "$KIT" hash -a blake2b "$1" | awk '{print $1}'; }
+
+# Store a hand-crafted tree object under its own content id; echoes the id.
+place_tree_object() {
+ src=$1
+ id=$(blake2b_id "$src")
+ dest=$(cas_object_path "$work/cas" tree "$id")
+ mkdir -p "$(dirname "$dest")"
+ cp "$src" "$dest"
+ printf '%s\n' "$id"
+}
+
+printf 'good/ok.txt - %s/src/share/a.txt\n' "$work" > "$work/adv.map"
+run_ok "cas-adv-base-tree" "$KIT" cas add-tree --cas "$work/cas" --map "$work/adv.map"
+adv_tree_id=$(first_hex_id "$work/cas-adv-base-tree.out")
+if [ -n "$adv_tree_id" ]; then
+ adv_base=$(cas_object_path "$work/cas" tree "$adv_tree_id")
+
+ # 1) parent-dir traversal: reject, and write nothing outside the -C target.
+ sed 's#path = good/ok.txt#path = ../escape.txt#' "$adv_base" > "$work/adv-traverse.tree"
+ tid=$(place_tree_object "$work/adv-traverse.tree")
+ run_fail "cas-materialize-traversal-rejected" \
+ "$KIT" cas materialize --cas "$work/cas" "$tid" -C "$work/out/adv-traverse"
+ if [ -e "$work/out/escape.txt" ]; then
+ echo "materialize escaped the target directory via ../" \
+ > "$work/cas-materialize-traversal-contained.diag"
+ not_ok "cas-materialize-traversal-contained" \
+ "$work/cas-materialize-traversal-contained.diag"
+ else
+ ok "cas-materialize-traversal-contained"
+ fi
+ run_fail "cas-verify-traversal-rejected" \
+ "$KIT" cas verify-tree --cas "$work/cas" "$tid"
+
+ # 2) absolute path.
+ sed 's#path = good/ok.txt#path = /tmp/kit-cas-escape.txt#' "$adv_base" > "$work/adv-abs.tree"
+ tid=$(place_tree_object "$work/adv-abs.tree")
+ run_fail "cas-materialize-absolute-rejected" \
+ "$KIT" cas materialize --cas "$work/cas" "$tid" -C "$work/out/adv-abs"
+
+ # 3) embedded NUL in a path: must be rejected, never silently truncated.
+ if command -v perl >/dev/null 2>&1; then
+ perl -pe 's{path = good/ok\.txt}{path = good/ok\0evil.txt}' "$adv_base" \
+ > "$work/adv-nul.tree"
+ tid=$(place_tree_object "$work/adv-nul.tree")
+ run_fail "cas-materialize-nul-rejected" \
+ "$KIT" cas materialize --cas "$work/cas" "$tid" -C "$work/out/adv-nul"
+ run_fail "cas-verify-nul-rejected" \
+ "$KIT" cas verify-tree --cas "$work/cas" "$tid"
+ else
+ skip_test "cas-materialize-nul-rejected"
+ skip_test "cas-verify-nul-rejected"
+ fi
+else
+ skip_test "cas-read-side-adversarial"
+fi
+
kit_summary cas
kit_exit