magisk-zapret2/tests/shell/status-snapshot-fast-path.sh
loop-uh e733873a51 Carry the IPv6 reservation across every boundary that reports it
The rule that an unprovable family is not a clean one held inside the
module and was lost at each point where a result becomes visible.

Status projected a committed stopped receipt through its "nothing is
owned" branch, which asserted a verified ruleset unconditionally — so a
teardown that deliberately withheld that claim had it restored by the
next status read, on the only channel still carrying the reservation.

Direct uninstall told the user the family "is cleared by the reboot".
Only the root-manager branch schedules one; the direct branch leaves the
module installed, requires no reboot, and has just deleted the state
that could identify leftover rules. It now names the reboot as the
action that ends the ambiguity instead of implying it is under way.

A partial purge receipt hardcoded every removal fact to zero. An
unremovable external staging workspace was enough to reach it with both
managed trees already gone, denying a removal that did happen on the
same receipt the app reads. The facts are measured now, and the
diagnostic names what actually survived.

Teardown also discarded the preflight's positive proof: a family read
moments earlier and found empty raised a reservation anyway if the
frontend went busy in between. That proof is now recorded and honoured,
for the mode that produced it only.

Tests: the owned baseline mode had no coverage at all, the boot-recovery
fixture forced autostart off so service.sh's new recovery retry was
never entered, and the purge assertions checked that a string was
present rather than that the two scripts agree. Each new test was proven
by reverting its fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 11:09:07 +03:00

184 lines
6.1 KiB
Shell

#!/bin/sh
set -eu
ROOT=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd)
CASE="${Z2_TEST_TMP:?}/status-snapshot-fast-path"
STATE="$CASE/state"
MODULE="$CASE/module"
OUTPUT="$CASE/status.out"
fail() { echo "FAIL: status-snapshot-fast-path: $*" >&2; exit 1; }
mkdir -p "$STATE" "$MODULE"
chmod 0700 "$STATE"
cp "$ROOT/zapret2/runtime.ini" "$MODULE/runtime.ini"
cp "$(command -v sleep)" "$MODULE/nfqws2"
chmod 0755 "$MODULE/nfqws2"
"$MODULE/nfqws2" 30 &
NFQWS_PID=$!
trap 'kill "$NFQWS_PID" 2>/dev/null || true; wait "$NFQWS_PID" 2>/dev/null || true' EXIT
NFQWS_START=$(awk '{print $22}' "/proc/$NFQWS_PID/stat")
NFQWS_ARGV_SHA=$(sha256sum "/proc/$NFQWS_PID/cmdline")
NFQWS_ARGV_SHA=${NFQWS_ARGV_SHA%% *}
printf '%s\n' "$NFQWS_PID" > "$STATE/nfqws2.pid"
chmod 0600 "$STATE/nfqws2.pid"
write_snapshot() {
argv_sha="$1"
cat > "$STATE/status.snapshot" <<EOF
status=ok
boot_id=$(cat /proc/sys/kernel/random/boot_id)
rules_total=2
own_pid=$NFQWS_PID
own_pid_starttime=$NFQWS_START
own_argv_sha256=$argv_sha
owner_generation=status-fast-path
owner_metadata_verified=1
ruleset_verified=1
rules_expected=2
qnum=200
ipv4_active=1
ipv6_active=0
ipv4_rules=2
ipv6_rules=0
chains=1
anchors=1
nfqueue_supported=1
queue_bypass_supported=1
connbytes_supported=0
multiport_supported=1
mark_supported=1
error_schema=1
error_status=OK
error_domain=NONE
error_code=NONE
error_stage=NONE
error_detail=
EOF
chmod 0600 "$STATE/status.snapshot"
}
run_status() {
rc=0
STATE_DIR="$STATE" MODDIR="$CASE" ZAPRET_DIR="$MODULE" \
sh "$ROOT/zapret2/scripts/zapret-status.sh" --machine-v6 > "$OUTPUT" 2>&1 || rc=$?
}
write_snapshot "$NFQWS_ARGV_SHA"
run_status
[ "$rc" -eq 0 ] || fail "valid snapshot returned $rc"
grep -Fxq 'Z2_STATUS=ok' "$OUTPUT" || fail "valid snapshot was not accepted"
grep -Fxq 'Z2_PID_VERIFIED=1' "$OUTPUT" || fail "live process identity was not verified"
grep -Fxq 'Z2_CHAINS=1' "$OUTPUT" || fail "published chain count was not projected"
write_snapshot 0000000000000000000000000000000000000000000000000000000000000000
run_status
[ "$rc" -eq 2 ] || fail "mismatched command digest returned $rc instead of degraded"
grep -Fxq 'Z2_STATUS=degraded' "$OUTPUT" ||
fail "mismatched command digest did not fail closed"
grep -Fxq 'Z2_PID_VERIFIED=0' "$OUTPUT" ||
fail "mismatched command digest retained verified process state"
# A snapshot describes processes and netfilter objects that a reboot destroys,
# so one stamped with another boot must be rejected whole: no facts from it may
# reach the projection, not even the capability flags or the queue number.
write_snapshot "$NFQWS_ARGV_SHA"
# Give the foreign snapshot a queue number that appears nowhere else, so a
# leak is distinguishable from the value runtime.ini would supply anyway.
sed -e 's/^boot_id=.*/boot_id=11111111-1111-1111-1111-111111111111/' \
-e 's/^qnum=.*/qnum=777/' \
"$STATE/status.snapshot" > "$STATE/status.snapshot.foreign"
mv "$STATE/status.snapshot.foreign" "$STATE/status.snapshot"
chmod 0600 "$STATE/status.snapshot"
run_status
grep -Fxq 'Z2_STATUS=ok' "$OUTPUT" && fail "a snapshot from another boot was accepted"
grep -Fxq 'Z2_QNUM=777' "$OUTPUT" && fail "queue number leaked from the rejected snapshot"
# and the projection still carries the configured queue number instead of none
grep -Fxq 'Z2_QNUM=200' "$OUTPUT" || fail "rejected snapshot left the queue number unresolved"
grep -Fxq 'Z2_EXPECTED_RULES=0' "$OUTPUT" ||
fail "expected rule count leaked from the rejected snapshot"
grep -Fxq 'Z2_NFQUEUE=0' "$OUTPUT" ||
fail "capability facts leaked from the rejected snapshot"
# The pre-2.2.0 format carries no boot binding at all and is equally stale.
grep -v '^boot_id=' "$STATE/status.snapshot" > "$STATE/status.snapshot.legacy"
mv "$STATE/status.snapshot.legacy" "$STATE/status.snapshot"
chmod 0600 "$STATE/status.snapshot"
run_status
grep -Fxq 'Z2_STATUS=ok' "$OUTPUT" && fail "an unbound legacy snapshot was accepted"
# A teardown that could not read one firewall family commits a stopped receipt
# that deliberately withholds its verification claim. By then nothing of ours
# is owned, so the projection takes its "nothing is owned" branch — and that
# branch asserted a proof this device cannot produce, laundering the single
# reservation the module kept on the only channel that still carries it.
MOCK="$CASE/bin"
mkdir -p "$MOCK"
cat > "$MOCK/iptables" <<'EOF'
#!/bin/sh
case " $* " in
*' -t mangle -L OUTPUT -n '*) exit 0 ;;
*' -t mangle -S ZAPRET2_OUT '*|*' -t mangle -S ZAPRET2_IN '*) exit 1 ;;
' -t mangle -S ') exit 0 ;;
esac
exit 1
EOF
cp "$MOCK/iptables" "$MOCK/ip6tables"
chmod 0755 "$MOCK/iptables" "$MOCK/ip6tables"
write_stopped_snapshot() {
cat > "$STATE/status.snapshot" <<EOF
status=stopped
boot_id=$(cat /proc/sys/kernel/random/boot_id)
rules_total=0
rules_expected=0
qnum=200
ipv4_active=0
ipv6_active=$2
ipv4_rules=0
ipv6_rules=0
chains=0
anchors=0
nfqueue_supported=1
queue_bypass_supported=1
connbytes_supported=0
multiport_supported=1
mark_supported=1
ruleset_verified=$1
owner_metadata_verified=0
error_schema=1
error_status=OK
error_domain=NONE
error_code=NONE
error_stage=NONE
error_detail=
EOF
chmod 0600 "$STATE/status.snapshot"
}
run_unowned_status() {
rc=0
PATH="$MOCK:$PATH" STATE_DIR="$STATE" MODDIR="$CASE" ZAPRET_DIR="$MODULE" \
sh "$ROOT/zapret2/scripts/zapret-status.sh" --machine-v6 > "$OUTPUT" 2>&1 || rc=$?
grep -Fxq 'Z2_OWNED=0' "$OUTPUT" ||
fail "the reservation case never reached the unowned projection"
}
# This is the exact shape a reserved teardown commits: it never claims an
# active IPv6 family, it only declines to certify the ruleset.
rm -f "$STATE/nfqws2.pid"
write_stopped_snapshot 0 0
run_unowned_status
grep -Fxq 'Z2_RULESET_VERIFIED=0' "$OUTPUT" ||
fail "a stopped receipt that withheld its verification claim was reported as verified"
# The ordinary stopped receipt still projects a verified ruleset, so the branch
# above narrows the claim rather than abandoning it.
write_stopped_snapshot 1 0
run_unowned_status
grep -Fxq 'Z2_RULESET_VERIFIED=1' "$OUTPUT" ||
fail "a fully verified stopped receipt lost its verification claim"
echo "Status snapshot fast-path tests passed"