mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-30 07:37:36 +00:00
Case (e) of the firewall suite piped `docker exec ... nft list table inet fips` into `grep -q` under pipefail. grep exits at its match; when the exec client was still writing the rest of the listing it took SIGPIPE (141), and pipefail turned a rule that was present into "the sed did not change the scoping rule". It false-failed intermittently in local and GitHub CI. The check moves into table_has in testing/firewall/table-check.sh, which captures the whole listing and then matches it with grep -qF on a here-string, so no pipe is left. It returns 0 when the rule is present, 1 when it is absent and 2 when the table could not be listed; case (e) gives each failure its own message. The assertion is unchanged: same container, same table, same rule text. testing/firewall/table-check-test.sh replaces docker with a shell function that prints the listing in two chunks, the rule in the first and a 0.2 s pause before the second, which is the race's shape. It runs as a guard in every local run (run_fwtable) and as a matching GitHub CI step. With table_has written as the old pipeline, moved verbatim, the test failed (the pipeline's status was 141, the producer's SIGPIPE): FAIL rule present, writer still writing after the match (run 1): returns 0 (rc=1) ... runs 2 to 5 the same PASS rule absent: returns 1 (rc=1) FAIL listing could not be read: returns 2 (rc=1) 6 table_has check(s) failed With the capture form all seven checks pass.
96 lines
2.9 KiB
Bash
Executable File
96 lines
2.9 KiB
Bash
Executable File
#!/bin/bash
|
|
# Tests for table_has() in table-check.sh, the firewall suite's check that a
|
|
# loaded nftables table contains a given rule.
|
|
#
|
|
# `docker` is replaced by a shell function that prints a canned
|
|
# `nft list table inet fips` listing in two chunks, with the matching rule in
|
|
# the first and a pause before the second. That is the shape of the defect this
|
|
# guards against: the reader has its match and exits while the producer is still
|
|
# writing. A check that pipes the producer into `grep -q` under pipefail sees
|
|
# the producer's SIGPIPE (141) and reports a rule that is present as absent.
|
|
# No containers are involved, so the suite is hermetic; it takes about two
|
|
# seconds.
|
|
#
|
|
# Run:
|
|
# ./table-check-test.sh
|
|
# Exits 0 only if every case passes; non-zero if any case fails.
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
# shellcheck source=SCRIPTDIR/table-check.sh
|
|
source "$SCRIPT_DIR/table-check.sh"
|
|
|
|
FAILURES=0
|
|
RULE='iifname != "notfips0" return'
|
|
STUB_MODE=present
|
|
|
|
# Stand-in for `docker exec <container> nft list table inet fips`. STUB_MODE
|
|
# picks a listing with the rule (present), one without it (absent), or an exec
|
|
# that fails (fail).
|
|
docker() {
|
|
case "$STUB_MODE" in
|
|
fail)
|
|
echo "Error response from daemon: container is not running" >&2
|
|
return 1
|
|
;;
|
|
present)
|
|
printf '%s\n' 'table inet fips {' ' chain input {' \
|
|
' type filter hook input priority filter; policy accept;' \
|
|
" $RULE"
|
|
;;
|
|
absent)
|
|
printf '%s\n' 'table inet fips {' ' chain input {' \
|
|
' type filter hook input priority filter; policy accept;' \
|
|
' iifname != "fips0" return'
|
|
;;
|
|
esac
|
|
sleep 0.2
|
|
printf '%s\n' ' ct state established,related accept' \
|
|
' icmpv6 type echo-request accept' ' counter drop' ' }' '}'
|
|
return 0
|
|
}
|
|
|
|
# Record a single assertion result.
|
|
check() {
|
|
local name="$1"
|
|
local ok="$2" # 0 = pass, anything else = fail
|
|
local detail="${3:-}"
|
|
if [ "$ok" -eq 0 ]; then
|
|
echo "PASS $name${detail:+ ($detail)}"
|
|
else
|
|
echo "FAIL $name${detail:+ ($detail)}"
|
|
FAILURES=$((FAILURES + 1))
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# Run table_has against the stand-in in the given mode; sets RC.
|
|
probe() {
|
|
STUB_MODE="$1"
|
|
RC=0
|
|
table_has fips-fw-container-b "$RULE" 2>/dev/null || RC=$?
|
|
return 0
|
|
}
|
|
|
|
for i in 1 2 3 4 5; do
|
|
probe present
|
|
check "rule present, writer still writing after the match (run $i): returns 0" \
|
|
"$([ "$RC" -eq 0 ] && echo 0 || echo 1)" "rc=$RC"
|
|
done
|
|
|
|
probe absent
|
|
check "rule absent: returns 1" "$([ "$RC" -eq 1 ] && echo 0 || echo 1)" "rc=$RC"
|
|
|
|
probe fail
|
|
check "listing could not be read: returns 2" \
|
|
"$([ "$RC" -eq 2 ] && echo 0 || echo 1)" "rc=$RC"
|
|
|
|
echo ""
|
|
if [ "$FAILURES" -eq 0 ]; then
|
|
echo "All table_has cases passed"
|
|
exit 0
|
|
fi
|
|
echo "$FAILURES table_has check(s) failed"
|
|
exit 1
|