The DNS manager used to apply a selection by writing system/etc/hosts into the live module directory and asking for a reboot, so the root manager would pick it up while assembling /system. That put the module's own tree in the path of whatever the manager does between boots: KernelSU v3 moved that assembly into a separate metamodule, a second hosts module publishing the same path raced it silently, and an upgrade replaced the tree and discarded the user's selection with it. The published file now lives in /data/adb/zapret2-hosts and the module bind-mounts it over /system/etc/hosts from post-fs-data.sh, which is the same mechanism on Magisk, KernelSU and APatch. hosts-overlay.sh owns that mount: it snapshots the untouched system file before covering it, carries a pre-2.3.0 in-tree overlay over and takes it out of the tree, relabels the source so netd can still read it, and yields to another enabled hosts module instead of fighting it for the path. Because the module publishes the mount itself, an edit now takes effect immediately: publication is still an atomic rename, and --apply republishes the mount rather than waiting for a reboot. A rename leaves the live mount on the replaced inode, so the mount identity is recorded when it is published and consulted only against a live mount — a foreign mount is never released. Rollback, uninstall and purge follow the file to its new home, and the installer carries an existing selection across the upgrade that would otherwise drop it. Also releases the tethering runtime key and owner state v9, and lifts the packaged lifecycle contract to 9.
225 lines
9.2 KiB
Shell
Executable file
225 lines
9.2 KiB
Shell
Executable file
#!/bin/sh
|
|
set -eu
|
|
|
|
ROOT=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd)
|
|
FIXTURE_SOURCE="$ROOT/tests/fixtures/runtime-config"
|
|
TMP_ROOT="${Z2_TEST_TMP:-$(mktemp -d "${TMPDIR:-/tmp}/zapret2-runtime-contract.XXXXXX")}"
|
|
if [ -z "${Z2_TEST_TMP:-}" ]; then
|
|
trap 'rm -rf "$TMP_ROOT"' EXIT HUP INT TERM
|
|
fi
|
|
FIXTURES="$TMP_ROOT/runtime-config-fixtures"
|
|
mkdir -p "$FIXTURES"
|
|
cp "$FIXTURE_SOURCE"/* "$FIXTURES/"
|
|
# adb push commonly materializes files as 0666. These are content fixtures,
|
|
# so give their private copies a deterministic safe mode before exercising the
|
|
# runtime parser's real ownership/mode checks.
|
|
chmod 0600 "$FIXTURES"/*
|
|
|
|
fail() { echo "FAIL: $*" >&2; exit 1; }
|
|
|
|
SCRIPT_DIR="$ROOT/zapret2/scripts"
|
|
ZAPRET_DIR="$ROOT/zapret2"
|
|
. "$SCRIPT_DIR/common.sh"
|
|
|
|
# Content fixtures are intentionally portable across unprivileged JVM and
|
|
# root-owned device tests. File ownership itself is covered by lifecycle tests.
|
|
path_uid_is_root() { return 0; }
|
|
|
|
classify_runtime_file() {
|
|
RUNTIME_CONFIG="$1"
|
|
set_core_config_defaults
|
|
RUNTIME_CONFIG_ERROR=""
|
|
if apply_runtime_core_overrides; then
|
|
printf '%s\n' VALID
|
|
return 0
|
|
fi
|
|
case "$RUNTIME_CONFIG_ERROR" in
|
|
"unsupported runtime.ini schema_version"|"unsupported runtime.ini config_format")
|
|
printf '%s\n' UNSUPPORTED_SCHEMA
|
|
;;
|
|
*) printf '%s\n' MALFORMED ;;
|
|
esac
|
|
}
|
|
|
|
if [ "${1:-}" = --file ]; then
|
|
[ "$#" -eq 2 ] || fail "usage: runtime-config-contract.sh --file PATH"
|
|
classify_runtime_file "$2"
|
|
exit 0
|
|
fi
|
|
if [ "${1:-}" = --values ]; then
|
|
[ "$#" -eq 2 ] || fail "usage: runtime-config-contract.sh --values PATH"
|
|
RUNTIME_CONFIG="$2"
|
|
set_core_config_defaults
|
|
RUNTIME_CONFIG_ERROR=""
|
|
apply_runtime_core_overrides || fail "cannot emit values for an invalid runtime fixture"
|
|
printf 'schema_version\t1\n'
|
|
printf 'config_format\truntime-v1\n'
|
|
printf 'runtime_source\t%s\n' "$RUNTIME_SOURCE"
|
|
printf 'autostart\t%s\n' "$AUTOSTART"
|
|
printf 'wifi_only\t%s\n' "$WIFI_ONLY"
|
|
printf 'tethering\t%s\n' "$TETHERING"
|
|
printf 'debug\t%s\n' "$DEBUG"
|
|
printf 'qnum\t%s\n' "$QNUM"
|
|
printf 'desync_mark\t%s\n' "$DESYNC_MARK"
|
|
printf 'active_preset\t%s\n' "$ACTIVE_PRESET"
|
|
printf 'nfqws_uid\t%s\n' "$NFQWS_UID"
|
|
printf 'log_mode\t%s\n' "$LOG_MODE"
|
|
exit 0
|
|
fi
|
|
[ "$#" -eq 0 ] || fail "unexpected arguments"
|
|
|
|
tab="$(printf '\t')"
|
|
while IFS="$tab" read -r fixture expected extra || [ -n "${fixture:-}" ]; do
|
|
case "$fixture" in ""|\#*) continue ;; esac
|
|
[ -z "${extra:-}" ] || fail "invalid fixture manifest row: $fixture"
|
|
path="$FIXTURES/$fixture"
|
|
[ -f "$path" ] || fail "fixture is missing: $fixture"
|
|
actual="$(classify_runtime_file "$path")"
|
|
[ "$actual" = "$expected" ] ||
|
|
fail "$fixture: shell classified $actual, expected $expected"
|
|
done < "$FIXTURES/manifest.tsv"
|
|
|
|
runtime_tool_path=$PATH
|
|
if [ "$(id -u)" != 0 ]; then
|
|
mock_bin="$TMP_ROOT/runtime-config-mock-bin"
|
|
mkdir -p "$mock_bin"
|
|
real_stat="$(command -v stat)" || fail "host stat is unavailable"
|
|
cat > "$mock_bin/stat" <<'EOF'
|
|
#!/bin/sh
|
|
case "$1:$2" in
|
|
-c:%u) printf '%s\n' 0; exit 0 ;;
|
|
-c:%u:%a:%h:%s)
|
|
shift 2
|
|
meta="$("$Z2_REAL_STAT" -c '%a:%h:%s' "$1")" || exit 1
|
|
printf '0:%s\n' "$meta"
|
|
exit 0
|
|
;;
|
|
-c:%d:%i:%u:%a:%h:%s)
|
|
shift 2
|
|
meta="$("$Z2_REAL_STAT" -c '%d:%i:%a:%h:%s' "$1")" || exit 1
|
|
device="${meta%%:*}"
|
|
rest="${meta#*:}"
|
|
inode="${rest%%:*}"
|
|
rest="${rest#*:}"
|
|
printf '%s:%s:0:%s\n' "$device" "$inode" "$rest"
|
|
exit 0
|
|
;;
|
|
esac
|
|
exec "$Z2_REAL_STAT" "$@"
|
|
EOF
|
|
chmod 0755 "$mock_bin/stat"
|
|
runtime_tool_path="$mock_bin:$PATH"
|
|
export Z2_REAL_STAT="$real_stat"
|
|
fi
|
|
|
|
repair_target="$TMP_ROOT/runtime-repair/runtime.ini"
|
|
mkdir -p "$(dirname "$repair_target")"
|
|
cp "$FIXTURES/invalid-missing-key.ini" "$repair_target"
|
|
cat >> "$repair_target" <<'EOF'
|
|
|
|
[dns_manager]
|
|
selected_dns=cloudflare|google
|
|
EOF
|
|
chmod 0644 "$repair_target"
|
|
PATH="$runtime_tool_path" sh "$ROOT/zapret2/scripts/runtime-config.sh" \
|
|
--repair "$repair_target" >/dev/null || fail "dedicated runtime repair failed"
|
|
[ "$(classify_runtime_file "$repair_target")" = VALID ] ||
|
|
fail "dedicated runtime repair did not produce a valid core"
|
|
grep -Fxq '[dns_manager]' "$repair_target" &&
|
|
grep -Fxq 'selected_dns=cloudflare|google' "$repair_target" ||
|
|
fail "dedicated runtime repair discarded a non-core section"
|
|
grep -Fxq 'runtime_source=self-healed-runtime' "$repair_target" ||
|
|
fail "repair provenance is not explicit"
|
|
inspect_output="$TMP_ROOT/runtime-repair.inspect"
|
|
PATH="$runtime_tool_path" sh "$ROOT/zapret2/scripts/runtime-config.sh" \
|
|
--inspect-machine "$repair_target" > "$inspect_output" ||
|
|
fail "runtime inspect machine protocol failed"
|
|
grep -Fxq "Z2_ERROR_STATUS=OK" "$inspect_output" ||
|
|
fail "runtime inspect did not emit a successful diagnostic envelope"
|
|
grep -Eq '^Z2_RUNTIME_SHA256 [0-9a-f]{64}$' "$inspect_output" ||
|
|
fail "runtime inspect omitted the validated content identity"
|
|
grep -Fxq "Z2_RUNTIME_CORE${tab}active_preset${tab}Default v1 (game filter).txt" \
|
|
"$inspect_output" || fail "runtime inspect omitted the active preset"
|
|
|
|
invalid_target="$TMP_ROOT/runtime-repair/invalid/runtime.ini"
|
|
mkdir -p "$(dirname "$invalid_target")"
|
|
cp "$FIXTURES/invalid-qnum.ini" "$invalid_target"
|
|
chmod 0644 "$invalid_target"
|
|
invalid_output="$TMP_ROOT/runtime-repair.invalid.inspect"
|
|
PATH="$runtime_tool_path" sh "$ROOT/zapret2/scripts/runtime-config.sh" \
|
|
--inspect-machine "$invalid_target" > "$invalid_output" ||
|
|
fail "invalid runtime inspection command failed"
|
|
grep -Fxq "Z2_ERROR_STATUS=ERROR" "$invalid_output" &&
|
|
grep -Fxq "Z2_ERROR_DOMAIN=CONFIG" "$invalid_output" &&
|
|
grep -Fxq "Z2_ERROR_STAGE=RUNTIME_PARSE" "$invalid_output" &&
|
|
grep -Fxq "Z2_ERROR_CODE=INVALID_QNUM" "$invalid_output" ||
|
|
fail "invalid qnum did not produce the generic diagnostic envelope"
|
|
detail="$(sed -n 's/^Z2_ERROR_DETAIL=//p' "$invalid_output")"
|
|
[ "$detail" = "qnum=70000, expected 1..65535" ] &&
|
|
[ "${#detail}" -le 512 ] ||
|
|
fail "runtime diagnostic detail is empty or unbounded"
|
|
case "$detail" in *"
|
|
"*|*"
|
|
"*) fail "runtime diagnostic detail is not single-line" ;; esac
|
|
|
|
commit_target="$TMP_ROOT/runtime-commit/runtime.ini"
|
|
mkdir -p "$(dirname "$commit_target")"
|
|
cp "$FIXTURES/valid-default.ini" "$commit_target"
|
|
chmod 0644 "$commit_target"
|
|
valid_candidate="$commit_target.candidate.123.456"
|
|
cp "$FIXTURES/valid-quoted-preset.ini" "$valid_candidate"
|
|
chmod 0644 "$valid_candidate"
|
|
stale_recovery_candidate="$commit_target.candidate.111.222"
|
|
cp "$FIXTURES/valid-default.ini" "$stale_recovery_candidate"
|
|
chmod 0644 "$stale_recovery_candidate"
|
|
commit_digest="$(sha256sum "$commit_target")"
|
|
commit_digest="${commit_digest%% *}"
|
|
PATH="$runtime_tool_path" sh "$ROOT/zapret2/scripts/runtime-config.sh" \
|
|
--commit-candidate "$valid_candidate" "$commit_digest" "$commit_target" \
|
|
> "$TMP_ROOT/runtime-commit.valid" ||
|
|
fail "valid runtime candidate commit failed"
|
|
[ ! -e "$valid_candidate" ] && [ ! -e "$stale_recovery_candidate" ] &&
|
|
[ "$(classify_runtime_file "$commit_target")" = VALID ] ||
|
|
fail "valid runtime candidate was not atomically published"
|
|
grep -Fxq "Z2_ERROR_STATUS=OK" "$TMP_ROOT/runtime-commit.valid" ||
|
|
fail "valid runtime commit omitted the successful envelope"
|
|
|
|
before_invalid_commit="$(sha256sum "$commit_target")"
|
|
before_invalid_commit="${before_invalid_commit%% *}"
|
|
invalid_candidate="$commit_target.candidate.789.012"
|
|
cp "$FIXTURES/invalid-qnum.ini" "$invalid_candidate"
|
|
chmod 0644 "$invalid_candidate"
|
|
if PATH="$runtime_tool_path" sh "$ROOT/zapret2/scripts/runtime-config.sh" \
|
|
--commit-candidate "$invalid_candidate" "$before_invalid_commit" "$commit_target" \
|
|
> "$TMP_ROOT/runtime-commit.invalid"; then
|
|
fail "invalid runtime candidate commit succeeded"
|
|
fi
|
|
after_invalid_commit="$(sha256sum "$commit_target")"
|
|
after_invalid_commit="${after_invalid_commit%% *}"
|
|
[ "$after_invalid_commit" = "$before_invalid_commit" ] ||
|
|
fail "invalid runtime candidate replaced the authoritative runtime"
|
|
grep -Fxq "Z2_ERROR_STATUS=ERROR" "$TMP_ROOT/runtime-commit.invalid" &&
|
|
grep -Fxq "Z2_ERROR_STAGE=RUNTIME_COMMIT" "$TMP_ROOT/runtime-commit.invalid" ||
|
|
fail "invalid runtime candidate omitted the failure envelope"
|
|
rm -f "$invalid_candidate"
|
|
|
|
stale_candidate="$commit_target.candidate.345.678"
|
|
cp "$FIXTURES/valid-default.ini" "$stale_candidate"
|
|
chmod 0644 "$stale_candidate"
|
|
if PATH="$runtime_tool_path" sh "$ROOT/zapret2/scripts/runtime-config.sh" \
|
|
--commit-candidate "$stale_candidate" \
|
|
0000000000000000000000000000000000000000000000000000000000000000 \
|
|
"$commit_target" > "$TMP_ROOT/runtime-commit.stale"; then
|
|
fail "stale runtime candidate commit succeeded"
|
|
fi
|
|
grep -Fxq "Z2_ERROR_CODE=RUNTIME_SOURCE_CHANGED" "$TMP_ROOT/runtime-commit.stale" ||
|
|
fail "stale runtime candidate did not report compare-and-swap failure"
|
|
rm -f "$stale_candidate"
|
|
|
|
init_target="$TMP_ROOT/runtime-init/runtime.ini"
|
|
mkdir -p "$(dirname "$init_target")"
|
|
sh "$ROOT/zapret2/scripts/runtime-init.sh" "$init_target" >/dev/null ||
|
|
fail "runtime initialization wrapper failed"
|
|
[ "$(classify_runtime_file "$init_target")" = VALID ] ||
|
|
fail "runtime initialization wrapper did not produce a valid core"
|
|
|
|
echo "Runtime config contract tests passed"
|