magisk-zapret2/tests/device/smoke.sh
loop-uh 05b9e0af8c Let one preset change be one transaction, not six conversations
Applying a preset was a conversation the app held with the module: snapshot
runtime.ini, ask whether the service was running, read runtime.ini again, stage
a candidate, commit it, verify it, then restart. Seven privileged round trips,
each paying a libsu round trip and a full common.sh sourcing, and — worse — the
rollback decisions lived in Kotlin. The app was deciding what the module's state
was, which is exactly what the typed machine payload exists to prevent.

zapret2/scripts/zapret-apply-preset.sh is now the single app-facing entry point
for that mutation. It sources the lifecycle helpers once, inherits the Android
lease through ZAPRET2_LIFECYCLE_TOKEN, and under that one lock it validates the
request with the packaged name policy, qualifies the preset through the same
compile-and-dry-run the preview machinery already owned, publishes the selection
through runtime-config.sh --commit-candidate, and hands the replacement to
zapret-start.sh --replace exactly as zapret-restart.sh does. Nothing here
reimplements a transaction that already had an owner.

Because the qualification happens before the commit, an incompatible preset is
now refused with the live selection untouched instead of being written, failed
and rolled back. A failed replacement restores the previous generation and says
so, and a rollback that cannot restore it says that instead — the payload
distinguishes the two, carrying the exact Z2_ERROR identity of whichever nested
stage refused. A service the user had stopped is still only saved to, measured
from the committed lifecycle receipt the way zapret-status.sh derives Z2_PROCESS.

The app projects that payload and nothing more. It keeps the stepwise flow for
one reason: a module generation installed before this entry point existed cannot
grow it, so the same round trip answers with an unsupported sentinel and the old
path runs. A payload that is truncated, inconsistent or written by a newer module
is never guessed at — the repository reads the published selection, and accepts
an application only when the live owner generation is the one this lease stamped,
which is the same proof the restart path already required.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 16:58:07 +03:00

1233 lines
59 KiB
Shell

#!/bin/sh
set -eu
# Rooted-device release smoke harness. The default operation is a read-only
# preflight. Device mutations are possible only after all opt-in gates below
# have been checked and a successful preflight has completed.
umask 077
PROGRAM=${0##*/}
ADB_BIN=${ADB_BIN:-adb}
SERIAL=""
EVIDENCE_DIR=""
EVIDENCE_EXPLICIT=0
STAGE=preflight
DRY_RUN=0
REQUIRE_IPV6=0
ALLOW_MUTATIONS=0
ALLOW_UPDATE=0
ALLOW_FULL_ROLLBACK=0
ALLOW_UNINSTALL=0
ACK_DISPOSABLE=""
ACK_RECOVERY=""
RECOVERY_ACK_TEXT=I_HAVE_A_TESTED_RECOVERY_PLAN
MODULE_ZIP=""
MODULE_SHA256=""
APK_FILE=""
APK_SHA256=""
MUTATION_ARMED=0
IPV6_READY=0
EVIDENCE_IPV6_CAPABILITY=""
MODULE_DIR=/data/adb/modules/zapret2
STATE_DIR=/data/adb/zapret2-state
STATUS_ENTRY=/system/bin/zapret2-status
START_ENTRY=/system/bin/zapret2-start
STOP_ENTRY=/system/bin/zapret2-stop
RESTART_ENTRY=/system/bin/zapret2-restart
ROLLBACK_ENTRY=/system/bin/zapret2-full-rollback
SHELL_STAGE_MODULE=""
SHELL_STAGE_APK=""
ROOT_STAGE_DIR=""
ROOT_STAGE_MODULE=""
ROOT_STAGE_APK=""
say() { printf '%s\n' "$*"; }
warn() { printf 'WARNING: %s\n' "$*" >&2; }
fail() { printf 'ERROR: %s\n' "$*" >&2; exit 2; }
usage() {
cat <<EOF
Usage:
$PROGRAM --serial SERIAL [--evidence-dir DIR] [artifact options]
$PROGRAM --serial SERIAL --evidence-dir DIR --stage STAGE [mutation gates]
The default stage is read-only preflight. STAGE is one of:
preflight, stop, start, restart, update, full-rollback,
uninstall, uninstall-verify
Every mutating stage requires all of:
--allow-mutations
--ack-disposable-device SERIAL
--ack-recovery $RECOVERY_ACK_TEXT
Additional one-stage gates:
update: --allow-update plus both exact artifact/hash pairs
full-rollback: --allow-full-rollback
uninstall: --allow-uninstall (arms a manual root-manager uninstall only)
Artifact/hash pairs:
--module-zip FILE --module-sha256 64_HEX_DIGITS
--apk FILE --apk-sha256 64_HEX_DIGITS
Other options:
--dry-run perform the complete read-only preflight and print the plan
--require-ipv6 fail preflight unless ip6tables has every required extension
-h, --help
Stages use one evidence directory and are enforced in this exact order:
preflight -> stop -> start -> restart -> update -> full-rollback
-> uninstall -> uninstall-verify
The uninstall stage never removes a module itself. It records the checkpoint;
the operator must remove Zapret2 in the root-manager UI, reboot, and then run
uninstall-verify. The harness never invokes an internal update guard.
EOF
}
while [ "$#" -gt 0 ]; do
case "$1" in
--serial) [ "$#" -ge 2 ] || fail "--serial needs a value"; SERIAL=$2; shift 2 ;;
--evidence-dir) [ "$#" -ge 2 ] || fail "--evidence-dir needs a value"; EVIDENCE_DIR=$2; EVIDENCE_EXPLICIT=1; shift 2 ;;
--stage) [ "$#" -ge 2 ] || fail "--stage needs a value"; STAGE=$2; shift 2 ;;
--module-zip) [ "$#" -ge 2 ] || fail "--module-zip needs a value"; MODULE_ZIP=$2; shift 2 ;;
--module-sha256) [ "$#" -ge 2 ] || fail "--module-sha256 needs a value"; MODULE_SHA256=$2; shift 2 ;;
--apk) [ "$#" -ge 2 ] || fail "--apk needs a value"; APK_FILE=$2; shift 2 ;;
--apk-sha256) [ "$#" -ge 2 ] || fail "--apk-sha256 needs a value"; APK_SHA256=$2; shift 2 ;;
--allow-mutations) ALLOW_MUTATIONS=1; shift ;;
--allow-update) ALLOW_UPDATE=1; shift ;;
--allow-full-rollback) ALLOW_FULL_ROLLBACK=1; shift ;;
--allow-uninstall) ALLOW_UNINSTALL=1; shift ;;
--ack-disposable-device) [ "$#" -ge 2 ] || fail "--ack-disposable-device needs the exact serial"; ACK_DISPOSABLE=$2; shift 2 ;;
--ack-recovery) [ "$#" -ge 2 ] || fail "--ack-recovery needs the exact acknowledgement"; ACK_RECOVERY=$2; shift 2 ;;
--dry-run) DRY_RUN=1; shift ;;
--require-ipv6) REQUIRE_IPV6=1; shift ;;
-h|--help) usage; exit 0 ;;
*) fail "unknown argument: $1" ;;
esac
done
case "$STAGE" in
preflight|stop|start|restart|update|full-rollback|uninstall|uninstall-verify) ;;
*) fail "unknown stage: $STAGE" ;;
esac
[ -n "$SERIAL" ] || fail "an explicit --serial is required before any ADB access"
case "$SERIAL" in
-*|*[!A-Za-z0-9._:-]*) fail "unsafe device serial: $SERIAL" ;;
esac
MUTATION_REQUESTED=0
case "$STAGE" in
stop|start|restart|update|full-rollback|uninstall) MUTATION_REQUESTED=1 ;;
esac
# All consent checks intentionally happen before require_adb/adb devices. A
# typo or omitted acknowledgement therefore cannot even reach the device.
if [ "$MUTATION_REQUESTED" = 1 ]; then
[ "$EVIDENCE_EXPLICIT" = 1 ] || fail "mutating stages require an explicit existing --evidence-dir"
[ "$ALLOW_MUTATIONS" = 1 ] || fail "refusing mutation without --allow-mutations"
[ "$ACK_DISPOSABLE" = "$SERIAL" ] || fail "--ack-disposable-device must equal the exact --serial"
[ "$ACK_RECOVERY" = "$RECOVERY_ACK_TEXT" ] || fail "recovery acknowledgement must be exactly $RECOVERY_ACK_TEXT"
fi
case "$STAGE" in
update)
[ "$ALLOW_UPDATE" = 1 ] || fail "update requires --allow-update"
;;
full-rollback)
[ "$ALLOW_FULL_ROLLBACK" = 1 ] || fail "full-rollback requires --allow-full-rollback"
;;
uninstall)
[ "$ALLOW_UNINSTALL" = 1 ] || fail "uninstall requires --allow-uninstall"
;;
esac
normalize_hash() {
nh_value=$(printf '%s' "$1" | tr 'A-F' 'a-f')
[ "${#nh_value}" -eq 64 ] || fail "SHA-256 must contain exactly 64 hexadecimal digits"
case "$nh_value" in *[!0-9a-f]*) fail "SHA-256 contains a non-hexadecimal character" ;; esac
printf '%s\n' "$nh_value"
}
sha256_file() {
sf_path=$1
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$sf_path" | awk '{print tolower($1)}'
elif command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$sf_path" | awk '{print tolower($1)}'
elif command -v openssl >/dev/null 2>&1; then
openssl dgst -sha256 "$sf_path" | awk '{print tolower($NF)}'
else
fail "sha256sum, shasum, or openssl is required"
fi
}
verify_artifact_pair() {
va_kind=$1 va_path=$2 va_expected=$3
[ -f "$va_path" ] && [ ! -L "$va_path" ] && [ -s "$va_path" ] || fail "$va_kind artifact must be a non-empty regular, non-symlink file: $va_path"
va_actual=$(sha256_file "$va_path")
[ "$va_actual" = "$va_expected" ] || fail "$va_kind SHA-256 mismatch: expected $va_expected, got $va_actual"
}
if [ -n "$MODULE_ZIP" ] || [ -n "$MODULE_SHA256" ] || [ -n "$APK_FILE" ] || [ -n "$APK_SHA256" ]; then
[ -n "$MODULE_ZIP" ] && [ -n "$MODULE_SHA256" ] && [ -n "$APK_FILE" ] && [ -n "$APK_SHA256" ] ||
fail "artifact verification requires both module and APK paths and both exact SHA-256 values"
MODULE_SHA256=$(normalize_hash "$MODULE_SHA256")
APK_SHA256=$(normalize_hash "$APK_SHA256")
verify_artifact_pair module "$MODULE_ZIP" "$MODULE_SHA256"
verify_artifact_pair APK "$APK_FILE" "$APK_SHA256"
elif [ "$STAGE" = update ]; then
fail "update requires exact module and APK artifacts with SHA-256 values"
fi
require_adb() {
case "$ADB_BIN" in
*/*|*\\*) [ -x "$ADB_BIN" ] || fail "ADB_BIN is not executable: $ADB_BIN" ;;
*) command -v "$ADB_BIN" >/dev/null 2>&1 || fail "adb is not available" ;;
esac
}
adb_host() { "$ADB_BIN" "$@"; }
adb_device() { "$ADB_BIN" -s "$SERIAL" "$@"; }
adb_root() {
# adb shell concatenates argv on some client/server combinations. Stream
# the fixed command over stdin so metacharacters remain inside the root
# shell without creating a script or temporary file on the device.
printf '%s\n' "$1" | adb_device shell su -c /system/bin/sh
}
authorize_device() {
ad_listing=$(adb_host devices 2>&1) || fail "adb devices failed: $ad_listing"
ad_state=$(printf '%s\n' "$ad_listing" | tr -d '\r' | awk -v s="$SERIAL" '$1 == s { print $2 }')
[ "$ad_state" = device ] || fail "device $SERIAL is not uniquely present and authorized (state: ${ad_state:-missing})"
ad_get_state=$(adb_device get-state 2>&1 | tr -d '\r') || fail "cannot query device state: $ad_get_state"
[ "$ad_get_state" = device ] || fail "device $SERIAL is not ready: $ad_get_state"
ad_reported=$(adb_device get-serialno 2>&1 | tr -d '\r') || fail "cannot confirm device serial: $ad_reported"
[ "$ad_reported" = "$SERIAL" ] || fail "ADB selected $ad_reported instead of exact serial $SERIAL"
}
init_new_evidence() {
if [ -z "$EVIDENCE_DIR" ]; then
ine_stamp=$(date -u '+%Y%m%dT%H%M%SZ')
EVIDENCE_DIR=$PWD/zapret2-device-evidence-$SERIAL-$ine_stamp
fi
[ ! -e "$EVIDENCE_DIR" ] && [ ! -L "$EVIDENCE_DIR" ] || fail "new preflight refuses to overwrite evidence path: $EVIDENCE_DIR"
mkdir -p "$EVIDENCE_DIR" || fail "cannot create evidence directory: $EVIDENCE_DIR"
chmod 0700 "$EVIDENCE_DIR" || fail "cannot secure evidence directory"
EVIDENCE_DIR=$(CDPATH= cd "$EVIDENCE_DIR" && pwd -P) || fail "cannot resolve evidence directory"
mkdir "$EVIDENCE_DIR/stages" || fail "cannot create evidence stages directory"
chmod 0700 "$EVIDENCE_DIR/stages"
}
manifest_value() {
mv_key=$1 mv_file=$2
[ -f "$mv_file" ] && [ ! -L "$mv_file" ] || fail "unsafe or missing evidence manifest: $mv_file"
mv_value=$(sed -n "s/^$mv_key=//p" "$mv_file")
[ "$(grep -c "^$mv_key=" "$mv_file")" -eq 1 ] || fail "malformed evidence manifest key: $mv_key"
printf '%s\n' "$mv_value"
}
validate_capability_manifest() {
vcm_file=$1
[ -f "$vcm_file" ] && [ ! -L "$vcm_file" ] || fail "unsafe or missing capability manifest: $vcm_file"
vcm_expected='schema
ipv4
ipv6'
vcm_actual=$(sed 's/=.*//' "$vcm_file")
[ "$vcm_actual" = "$vcm_expected" ] ||
fail "capability manifest is truncated, reordered, duplicated, or unknown: $vcm_file"
[ "$(sed -n 's/^schema=//p' "$vcm_file")" = 1 ] || fail "unsupported capability manifest schema"
[ "$(sed -n 's/^ipv4=//p' "$vcm_file")" = ready ] || fail "recorded IPv4 capability is not ready"
vcm_ipv6=$(sed -n 's/^ipv6=//p' "$vcm_file")
case "$vcm_ipv6" in ready|not_available) ;; *) fail "invalid recorded IPv6 capability: $vcm_ipv6";; esac
}
open_existing_evidence() {
[ -d "$EVIDENCE_DIR" ] && [ ! -L "$EVIDENCE_DIR" ] || fail "evidence directory is missing or a symlink: $EVIDENCE_DIR"
EVIDENCE_DIR=$(CDPATH= cd "$EVIDENCE_DIR" && pwd -P) || fail "cannot resolve evidence directory"
[ -f "$EVIDENCE_DIR/preflight.complete" ] && [ ! -L "$EVIDENCE_DIR/preflight.complete" ] || fail "evidence has no completed preflight"
oe_serial=$(manifest_value serial "$EVIDENCE_DIR/run.meta")
[ "$oe_serial" = "$SERIAL" ] || fail "evidence belongs to serial $oe_serial, not $SERIAL"
validate_capability_manifest "$EVIDENCE_DIR/capabilities.meta"
EVIDENCE_IPV6_CAPABILITY=$(manifest_value ipv6 "$EVIDENCE_DIR/capabilities.meta")
}
sequence_state() { manifest_value stage "$EVIDENCE_DIR/sequence.state"; }
require_previous_stage() {
rps_expected=$1
rps_actual=$(sequence_state)
[ "$rps_actual" = "$rps_expected" ] || fail "stage $STAGE requires sequence state $rps_expected; found $rps_actual"
}
advance_sequence() {
as_stage=$1 as_tmp=$EVIDENCE_DIR/.sequence.state.$$
[ ! -e "$as_tmp" ] && [ ! -L "$as_tmp" ] || fail "unsafe evidence sequence temporary path"
printf 'stage=%s\n' "$as_stage" > "$as_tmp"
chmod 0600 "$as_tmp"
mv -f "$as_tmp" "$EVIDENCE_DIR/sequence.state"
}
record_initial_manifests() {
rim_utc=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
{
printf 'schema=1\n'
printf 'serial=%s\n' "$SERIAL"
printf 'created_utc=%s\n' "$rim_utc"
printf 'require_ipv6=%s\n' "$REQUIRE_IPV6"
} > "$EVIDENCE_DIR/run.meta"
{
printf 'module_sha256=%s\n' "$MODULE_SHA256"
printf 'apk_sha256=%s\n' "$APK_SHA256"
printf 'module_name=%s\n' "${MODULE_ZIP##*/}"
printf 'apk_name=%s\n' "${APK_FILE##*/}"
} > "$EVIDENCE_DIR/artifacts.meta"
chmod 0600 "$EVIDENCE_DIR/run.meta" "$EVIDENCE_DIR/artifacts.meta"
}
record_capability_manifest() {
rcm_ipv6=not_available
[ "$IPV6_READY" = 0 ] || rcm_ipv6=ready
{
printf 'schema=1\n'
printf 'ipv4=ready\n'
printf 'ipv6=%s\n' "$rcm_ipv6"
} > "$EVIDENCE_DIR/capabilities.meta"
chmod 0600 "$EVIDENCE_DIR/capabilities.meta"
validate_capability_manifest "$EVIDENCE_DIR/capabilities.meta"
EVIDENCE_IPV6_CAPABILITY=$rcm_ipv6
}
require_capability_consistency() {
validate_capability_manifest "$EVIDENCE_DIR/capabilities.meta"
rcc_recorded=$(manifest_value ipv6 "$EVIDENCE_DIR/capabilities.meta")
[ "$rcc_recorded" = "$EVIDENCE_IPV6_CAPABILITY" ] ||
fail "IPv6 capability evidence changed during stage execution"
rcc_current=not_available
[ "$IPV6_READY" = 0 ] || rcc_current=ready
[ "$rcc_current" = "$rcc_recorded" ] ||
fail "IPv6 capability drift: evidence=$rcc_recorded current=$rcc_current"
}
verify_recorded_artifacts() {
vra_module=$(manifest_value module_sha256 "$EVIDENCE_DIR/artifacts.meta")
vra_apk=$(manifest_value apk_sha256 "$EVIDENCE_DIR/artifacts.meta")
[ -n "$vra_module" ] && [ -n "$vra_apk" ] || fail "initial preflight did not bind this evidence bundle to exact release artifacts"
[ "$MODULE_SHA256" = "$vra_module" ] || fail "module hash differs from the preflight evidence bundle"
[ "$APK_SHA256" = "$vra_apk" ] || fail "APK hash differs from the preflight evidence bundle"
}
remote_path_state() {
rps_path=$1 rps_kind=$2
case "$rps_kind" in d) rps_test=-d ;; f) rps_test=-f ;; *) fail "internal path-kind error" ;; esac
adb_root "if [ -L $rps_path ]; then echo LINK; elif [ $rps_test $rps_path ]; then stat -c %a:%u:%g $rps_path 2>/dev/null || echo STAT_FAILED; else echo MISSING; fi" 2>&1 | tr -d '\r'
}
check_remote_path() {
crp_report=$1 crp_path=$2 crp_kind=$3 crp_modes=$4
crp_state=$(remote_path_state "$crp_path" "$crp_kind") || fail "cannot inspect $crp_path: $crp_state"
printf 'path\t%s\t%s\n' "$crp_path" "$crp_state" >> "$crp_report"
crp_mode=${crp_state%%:*}
crp_rest=${crp_state#*:}
crp_uid=${crp_rest%%:*}
crp_gid=${crp_rest#*:}
[ "$crp_uid" = 0 ] && [ "$crp_gid" = 0 ] || fail "path is not root-owned: $crp_path ($crp_state)"
case ",$crp_modes," in *",$crp_mode,"*) ;; *) fail "unexpected mode for $crp_path: $crp_state (expected $crp_modes)" ;; esac
}
check_optional_marker() {
com_report=$1 com_path=$2
com_state=$(adb_root "if [ -L $com_path ]; then echo LINK; elif [ -e $com_path ]; then stat -c %a:%u:%g $com_path 2>/dev/null || echo STAT_FAILED; else echo ABSENT; fi" 2>&1 | tr -d '\r') ||
fail "cannot inspect optional marker $com_path"
printf 'marker\t%s\t%s\n' "$com_path" "$com_state" >> "$com_report"
case "$com_state" in ABSENT|600:0:0) ;; *) fail "unsafe optional marker $com_path: $com_state" ;; esac
}
probe_help() {
ph_tool=$1 ph_args=$2 ph_needle=$3 ph_file=$4
set +e
ph_output=$(adb_root "$ph_tool $ph_args -h 2>&1")
ph_rc=$?
set -e
printf '%s\n' "$ph_output" >> "$ph_file"
case "$ph_output" in *"$ph_needle"*) return 0 ;; esac
printf 'probe failed: %s %s (rc=%s, missing %s)\n' "$ph_tool" "$ph_args" "$ph_rc" "$ph_needle" >> "$ph_file"
return 1
}
probe_firewall_family() {
pff_tool=$1 pff_dir=$2 pff_ready=1
PFF_CONNBYTES=0
mkdir "$pff_dir" || fail "cannot create capability evidence directory"
set +e
adb_root "command -v $pff_tool && $pff_tool --version" > "$pff_dir/version.txt" 2>&1
pff_rc=$?
set -e
[ "$pff_rc" -eq 0 ] || pff_ready=0
probe_help "$pff_tool" '-j NFQUEUE' 'NFQUEUE' "$pff_dir/nfqueue.txt" || pff_ready=0
probe_help "$pff_tool" '-j NFQUEUE' '--queue-bypass' "$pff_dir/queue-bypass.txt" || pff_ready=0
probe_help "$pff_tool" '-m connbytes' '--connbytes' "$pff_dir/connbytes.txt" && PFF_CONNBYTES=1 || :
probe_help "$pff_tool" '-m multiport' '--dports' "$pff_dir/multiport.txt" || pff_ready=0
probe_help "$pff_tool" '-m mark' '--mark' "$pff_dir/mark.txt" || pff_ready=0
[ "$pff_ready" -eq 1 ]
}
validate_status_schema() {
vss_file=$1
vss_expected='Z2_STATUS
Z2_OWNED
Z2_PROCESS
Z2_ACTIVE
Z2_PID
Z2_PID_VERIFIED
Z2_PID_STARTTIME
Z2_OWNER_GENERATION
Z2_OWNER_METADATA_VERIFIED
Z2_QNUM
Z2_IPV4
Z2_IPV6
Z2_RULES
Z2_EXPECTED_RULES
Z2_IPV4_RULES
Z2_IPV6_RULES
Z2_RULESET_VERIFIED
Z2_NFQUEUE
Z2_QUEUE_BYPASS
Z2_UPDATE_BLOCKED
Z2_UNINSTALL_TOMBSTONE
Z2_COMPLETE'
vss_actual=$(sed 's/=.*//' "$vss_file")
[ "$vss_actual" = "$vss_expected" ] || fail "status machine schema is truncated, reordered, duplicated, or unknown: $vss_file"
[ "$(sed -n 's/^Z2_COMPLETE=//p' "$vss_file")" = 1 ] || fail "status machine sentinel is incomplete"
vss_status=$(sed -n 's/^Z2_STATUS=//p' "$vss_file")
vss_rc=$(cat "$vss_file.rc")
case "$vss_status:$vss_rc" in ok:0|stopped:1|degraded:2) ;; *) fail "status/exit mismatch: $vss_status/$vss_rc" ;; esac
for vss_key in Z2_OWNED Z2_PROCESS Z2_ACTIVE Z2_PID_VERIFIED Z2_OWNER_METADATA_VERIFIED Z2_IPV4 Z2_IPV6 Z2_RULESET_VERIFIED Z2_NFQUEUE Z2_QUEUE_BYPASS Z2_UPDATE_BLOCKED Z2_UNINSTALL_TOMBSTONE; do
vss_value=$(sed -n "s/^$vss_key=//p" "$vss_file")
case "$vss_value" in 0|1) ;; *) fail "invalid boolean $vss_key=$vss_value" ;; esac
done
}
capture_status() {
cs_file=$1
set +e
cs_output=$(adb_root "$STATUS_ENTRY --machine" 2>&1)
cs_rc=$?
set -e
printf '%s\n' "$cs_output" | tr -d '\r' > "$cs_file"
printf '%s\n' "$cs_rc" > "$cs_file.rc"
validate_status_schema "$cs_file"
}
status_value() { sed -n "s/^$2=//p" "$1"; }
require_running_status() {
rrs_file=$1
[ "$(status_value "$rrs_file" Z2_STATUS)" = ok ] || fail "service is not healthy after $STAGE"
for rrs_key in Z2_PROCESS Z2_ACTIVE Z2_PID_VERIFIED Z2_OWNER_METADATA_VERIFIED Z2_RULESET_VERIFIED Z2_NFQUEUE Z2_QUEUE_BYPASS; do
[ "$(status_value "$rrs_file" "$rrs_key")" = 1 ] || fail "$rrs_key is not verified after $STAGE"
done
[ "$(status_value "$rrs_file" Z2_UPDATE_BLOCKED)" = 0 ] || fail "update serialization remains blocked after $STAGE"
[ "$(status_value "$rrs_file" Z2_UNINSTALL_TOMBSTONE)" = 0 ] || fail "uninstall tombstone is active after $STAGE"
}
require_stopped_status() {
rss_file=$1
[ "$(status_value "$rss_file" Z2_STATUS)" = stopped ] || fail "service did not reach exact stopped status"
for rss_key in Z2_OWNED Z2_PROCESS Z2_ACTIVE Z2_RULES Z2_EXPECTED_RULES; do
[ "$(status_value "$rss_file" "$rss_key")" = 0 ] || fail "$rss_key is not clean after stop"
done
[ "$(status_value "$rss_file" Z2_RULESET_VERIFIED)" = 1 ] || fail "stopped ruleset is not verified"
}
capture_query() {
cq_file=$1; shift
set +e
"$@" > "$cq_file" 2>&1
cq_rc=$?
set -e
printf '%s\n' "$cq_rc" > "$cq_file.rc"
}
capture_root_query() {
crq_file=$1 crq_command=$2
set +e
adb_root "set +e
{ $crq_command; }
z2_query_rc=\$?
printf '\nZ2_QUERY_RC=%s\nZ2_QUERY_COMPLETE=1\n' \"\$z2_query_rc\"
exit 0" > "$crq_file" 2>&1
crq_transport_rc=$?
set -e
printf '%s\n' "$crq_transport_rc" > "$crq_file.transport.rc"
if [ "$crq_transport_rc" -ne 0 ]; then
crq_diagnostic=$(sed -n '1,8p' "$crq_file" | tr '\r\n' ' ')
fail "root query transport failed (rc=$crq_transport_rc): $crq_file: $crq_diagnostic"
fi
[ "$(tail -n 1 "$crq_file")" = Z2_QUERY_COMPLETE=1 ] || fail "root query output is truncated: $crq_file"
[ "$(tail -n 2 "$crq_file" | sed -n '1p')" = Z2_QUERY_RC=0 ] || fail "root query failed or has an invalid footer: $crq_file"
}
validate_query_schema() {
vqs_file=$1 vqs_expected=$2
vqs_actual=$(sed -n '/^Z2_QUERY_RC=/q; s/=.*//p' "$vqs_file")
[ "$vqs_actual" = "$vqs_expected" ] || fail "query schema is truncated, reordered, duplicated, or unknown: $vqs_file"
}
valid_boot_id() {
vbi_value=$1
[ "${#vbi_value}" -eq 36 ] || return 1
case "$vbi_value" in ????????-????-????-????-????????????) ;; *) return 1;; esac
vbi_hex=$(printf '%s\n' "$vbi_value" | tr -d '-')
case "$vbi_hex" in *[!0-9a-f]*) return 1;; esac
}
validate_uninstall_reboot_checkpoint() {
vurc_file=$1
[ -f "$vurc_file" ] && [ ! -L "$vurc_file" ] || fail "unsafe or missing uninstall reboot checkpoint: $vurc_file"
vurc_expected='schema
serial
run_created_utc
boot_before_file
boot_before_size
boot_before_sha256
pre_boot_id
complete'
vurc_actual=$(sed 's/=.*//' "$vurc_file")
[ "$vurc_actual" = "$vurc_expected" ] || fail "uninstall reboot checkpoint is truncated, reordered, duplicated, or unknown"
[ "$(manifest_value schema "$vurc_file")" = 1 ] || fail "unsupported uninstall reboot checkpoint schema"
[ "$(manifest_value serial "$vurc_file")" = "$SERIAL" ] || fail "uninstall reboot checkpoint belongs to a different serial"
vurc_run=$(manifest_value run_created_utc "$vurc_file")
[ "$vurc_run" = "$(manifest_value created_utc "$EVIDENCE_DIR/run.meta")" ] || fail "uninstall reboot checkpoint belongs to a different evidence run"
vurc_name=$(manifest_value boot_before_file "$vurc_file")
[ "$vurc_name" = boot-before.txt ] || fail "uninstall reboot checkpoint names an unexpected raw boot evidence file"
vurc_raw=${vurc_file%/*}/$vurc_name
[ -f "$vurc_raw" ] && [ ! -L "$vurc_raw" ] || fail "raw uninstall boot evidence is missing or unsafe"
vurc_size=$(manifest_value boot_before_size "$vurc_file")
case "$vurc_size" in ''|*[!0-9]*) fail "uninstall reboot checkpoint has a malformed raw evidence size";; esac
[ "$vurc_size" -gt 0 ] 2>/dev/null || fail "raw uninstall boot evidence is empty"
[ "$(wc -c < "$vurc_raw" | tr -d ' ')" = "$vurc_size" ] || fail "raw uninstall boot evidence size changed after checkpoint"
vurc_hash=$(manifest_value boot_before_sha256 "$vurc_file")
[ "${#vurc_hash}" -eq 64 ] || fail "uninstall reboot checkpoint has a malformed raw evidence hash"
case "$vurc_hash" in *[!0-9a-f]*) fail "uninstall reboot checkpoint has a malformed raw evidence hash";; esac
[ "$(sha256_file "$vurc_raw")" = "$vurc_hash" ] || fail "raw uninstall boot evidence hash changed after checkpoint"
[ "$(tail -n 1 "$vurc_raw")" = Z2_QUERY_COMPLETE=1 ] || fail "raw uninstall boot evidence is truncated"
[ "$(tail -n 2 "$vurc_raw" | sed -n '1p')" = Z2_QUERY_RC=0 ] || fail "raw uninstall boot evidence query did not succeed"
validate_query_schema "$vurc_raw" 'Z2_UNINSTALL_BOOT_ID
Z2_UNINSTALL_BOOT_COMPLETE'
vurc_raw_boot=$(sed -n 's/^Z2_UNINSTALL_BOOT_ID=//p' "$vurc_raw")
valid_boot_id "$vurc_raw_boot" || fail "raw uninstall boot evidence has a malformed boot identity"
grep -Fxq Z2_UNINSTALL_BOOT_COMPLETE=1 "$vurc_raw" || fail "raw uninstall boot evidence is incomplete"
vurc_boot=$(manifest_value pre_boot_id "$vurc_file")
valid_boot_id "$vurc_boot" || fail "uninstall reboot checkpoint has a malformed boot identity"
[ "$vurc_boot" = "$vurc_raw_boot" ] || fail "uninstall reboot checkpoint boot identity is not bound to its raw evidence"
[ "$(manifest_value complete "$vurc_file")" = 1 ] || fail "uninstall reboot checkpoint is incomplete"
}
capture_no_owned_process_audit() {
cnopa_file=$1
capture_root_query "$cnopa_file" "
owner_meta=absent
pidfile=absent
owner_pid=
pidfile_pid=
if [ -L $STATE_DIR/owner.meta ]; then exit 31
elif [ -e $STATE_DIR/owner.meta ]; then
[ -f $STATE_DIR/owner.meta ] || exit 32
owner_stat=\$(stat -c %h:%a:%u:%g $STATE_DIR/owner.meta 2>/dev/null) || exit 33
[ \"\$owner_stat\" = 1:600:0:0 ] || exit 34
owner_pid=\$(awk -F= '\$1 == \"pid\" { print \$2; count++ } END { if (count != 1) exit 1 }' $STATE_DIR/owner.meta) || exit 35
case \"\$owner_pid\" in ''|*[!0-9]*) exit 36;; esac
owner_meta=present
fi
if [ -L $STATE_DIR/nfqws2.pid ]; then exit 41
elif [ -e $STATE_DIR/nfqws2.pid ]; then
[ -f $STATE_DIR/nfqws2.pid ] || exit 42
pid_stat=\$(stat -c %h:%a:%u:%g $STATE_DIR/nfqws2.pid 2>/dev/null) || exit 43
[ \"\$pid_stat\" = 1:600:0:0 ] || exit 44
IFS= read -r pidfile_pid < $STATE_DIR/nfqws2.pid || exit 45
case \"\$pidfile_pid\" in ''|*[!0-9]*) exit 46;; esac
pidfile=present
fi
matches=0
for proc in /proc/[0-9]*; do
[ -d \"\$proc\" ] || continue
exe=\$(readlink \"\$proc/exe\" 2>/dev/null) || exe=
cmdline=\$(tr '\000' ' ' < \"\$proc/cmdline\" 2>/dev/null) || cmdline=
case \"\$exe\" in $MODULE_DIR/zapret2/bin/*/nfqws2*) matches=\$((matches + 1)); continue;; esac
case \"\$cmdline\" in *$MODULE_DIR/zapret2/bin/*/nfqws2*) matches=\$((matches + 1));; esac
done
printf 'Z2_PROC_OWNER_META=%s\n' \"\$owner_meta\"
printf 'Z2_PROC_PIDFILE=%s\n' \"\$pidfile\"
printf 'Z2_PROC_MATCHES=%s\n' \"\$matches\"
printf 'Z2_PROC_AUDIT_COMPLETE=1\n'
[ \"\$owner_meta\" = absent ] && [ \"\$pidfile\" = absent ] && [ \"\$matches\" = 0 ]"
validate_query_schema "$cnopa_file" 'Z2_PROC_OWNER_META
Z2_PROC_PIDFILE
Z2_PROC_MATCHES
Z2_PROC_AUDIT_COMPLETE'
grep -Fxq Z2_PROC_OWNER_META=absent "$cnopa_file" || fail "owner metadata remains: $cnopa_file"
grep -Fxq Z2_PROC_PIDFILE=absent "$cnopa_file" || fail "PID metadata remains: $cnopa_file"
grep -Fxq Z2_PROC_MATCHES=0 "$cnopa_file" || fail "module-owned /proc executable/cmdline remains: $cnopa_file"
grep -Fxq Z2_PROC_AUDIT_COMPLETE=1 "$cnopa_file" || fail "process audit sentinel is missing: $cnopa_file"
}
capture_clean_firewall_audit() {
ccfa_dir=$1
capture_root_query "$ccfa_dir/iptables-save.txt" 'command -v iptables-save >/dev/null 2>&1 && iptables-save'
case "$EVIDENCE_IPV6_CAPABILITY" in
ready)
capture_root_query "$ccfa_dir/ip6tables-save.txt" 'command -v ip6tables-save >/dev/null 2>&1 && ip6tables-save'
;;
not_available)
capture_root_query "$ccfa_dir/ip6tables-save.txt" "printf 'Z2_IP6TABLES_SAVE=not_available\\n'"
[ "$(sed -n '1p' "$ccfa_dir/ip6tables-save.txt")" = Z2_IP6TABLES_SAVE=not_available ] ||
fail "canonical IPv6 non-availability evidence is missing"
;;
*) fail "firewall audit has no exact recorded IPv6 capability" ;;
esac
for ccfa_file in "$ccfa_dir/iptables-save.txt"; do
if grep -E '(^|[[:space:]:-])ZAPRET2_(OUT|IN|PROBE)($|[[:space:]])' "$ccfa_file" >/dev/null 2>&1; then
fail "successful firewall dump still contains an owned chain, anchor, or rule: $ccfa_file"
fi
done
if [ "$EVIDENCE_IPV6_CAPABILITY" = ready ] &&
grep -E '(^|[[:space:]:-])ZAPRET2_(OUT|IN|PROBE)($|[[:space:]])' "$ccfa_dir/ip6tables-save.txt" >/dev/null 2>&1; then
fail "successful IPv6 firewall dump still contains an owned chain, anchor, or rule"
fi
}
capture_snapshot() {
csp_dir=$1
mkdir "$csp_dir" || fail "cannot create snapshot directory: $csp_dir"
chmod 0700 "$csp_dir"
capture_query "$csp_dir/adb-version.txt" adb_host version
capture_query "$csp_dir/build.txt" adb_device shell getprop ro.build.fingerprint
capture_query "$csp_dir/api.txt" adb_device shell getprop ro.build.version.sdk
capture_query "$csp_dir/abi.txt" adb_device shell getprop ro.product.cpu.abi
capture_root_query "$csp_dir/root-id.txt" 'id'
capture_root_query "$csp_dir/kernel.txt" 'uname -a'
capture_root_query "$csp_dir/selinux.txt" 'getenforce 2>/dev/null || echo unknown'
capture_root_query "$csp_dir/magisk.txt" 'magisk -v; magisk -V'
capture_root_query "$csp_dir/module-prop.txt" "cat $MODULE_DIR/module.prop"
capture_root_query "$csp_dir/install-generation.meta" "cat $MODULE_DIR/zapret2/install-generation.meta"
capture_root_query "$csp_dir/module-hashes.txt" "sha256sum $MODULE_DIR/module.prop $MODULE_DIR/zapret2/runtime.ini $MODULE_DIR/zapret2/strategy-catalogs/*.txt $MODULE_DIR/zapret2/install-generation.meta 2>/dev/null"
capture_root_query "$csp_dir/state-listing.txt" "ls -lan $STATE_DIR 2>/dev/null"
capture_root_query "$csp_dir/module-listing.txt" "ls -lan $MODULE_DIR $MODULE_DIR/zapret2 $MODULE_DIR/system/bin 2>/dev/null"
capture_root_query "$csp_dir/processes.txt" "ps -A 2>/dev/null | grep nfqws2 || true"
capture_root_query "$csp_dir/iptables-save.txt" 'command -v iptables-save >/dev/null 2>&1 && iptables-save'
case "$IPV6_READY" in
1)
capture_root_query "$csp_dir/ip6tables-save.txt" 'command -v ip6tables-save >/dev/null 2>&1 && ip6tables-save'
;;
0)
capture_root_query "$csp_dir/ip6tables-save.txt" "printf 'Z2_IP6TABLES_SAVE=not_available\\n'"
[ "$(sed -n '1p' "$csp_dir/ip6tables-save.txt")" = Z2_IP6TABLES_SAVE=not_available ] ||
fail "snapshot lacks canonical IPv6 non-availability evidence"
;;
*) fail "snapshot has no exact current IPv6 capability" ;;
esac
capture_root_query "$csp_dir/netfilter-proc.txt" 'for f in /proc/net/ip_tables_targets /proc/net/ip_tables_matches /proc/net/ip6_tables_targets /proc/net/ip6_tables_matches /proc/net/netfilter/nfnetlink_queue /proc/net/netfilter/nf_queue; do echo ===$f; cat $f 2>/dev/null || true; done'
capture_root_query "$csp_dir/hosts.sha256.txt" 'sha256sum /system/etc/hosts 2>/dev/null || true'
capture_root_query "$csp_dir/mounts.txt" 'mount 2>/dev/null | grep -E "zapret2|/system" || true'
capture_status "$csp_dir/status.machine"
chmod 0600 "$csp_dir"/* 2>/dev/null || true
}
preflight_device() {
pd_dir=$1 pd_report=$pd_dir/preflight.tsv
mkdir "$pd_dir" || fail "cannot create preflight evidence directory: $pd_dir"
chmod 0700 "$pd_dir"
: > "$pd_report"
pd_api=$(adb_device shell getprop ro.build.version.sdk 2>&1 | tr -d '\r') || fail "cannot read Android API: $pd_api"
case "$pd_api" in ''|*[!0-9]*) fail "invalid Android API: $pd_api" ;; esac
[ "$pd_api" -ge 24 ] || fail "Android API $pd_api is below the supported minimum 24"
printf 'api\t%s\n' "$pd_api" >> "$pd_report"
pd_abi=$(adb_device shell getprop ro.product.cpu.abi 2>&1 | tr -d '\r') || fail "cannot read primary ABI: $pd_abi"
case "$pd_abi" in
arm64-v8a) pd_binary=$MODULE_DIR/zapret2/bin/arm64-v8a/nfqws2 ;;
armeabi-v7a|armeabi) pd_binary=$MODULE_DIR/zapret2/bin/armeabi-v7a/nfqws2 ;;
*) fail "unsupported primary ABI: $pd_abi (release contains ARM/ARM64 binaries only)" ;;
esac
printf 'abi\t%s\n' "$pd_abi" >> "$pd_report"
pd_uid=$(adb_root 'id -u' 2>&1 | tr -d '\r') || fail "root command failed or authorization was denied: $pd_uid"
[ "$pd_uid" = 0 ] || fail "su did not return uid 0: $pd_uid"
pd_magisk_code=$(adb_root 'magisk -V' 2>&1 | tr -d '\r') || fail "Magisk CLI is unavailable: $pd_magisk_code"
case "$pd_magisk_code" in ''|*[!0-9]*) fail "invalid Magisk version code: $pd_magisk_code" ;; esac
[ "$pd_magisk_code" -ge 20400 ] || fail "Magisk 20.4+ is required; version code is $pd_magisk_code"
printf 'root_uid\t0\nmagisk_version_code\t%s\n' "$pd_magisk_code" >> "$pd_report"
check_remote_path "$pd_report" "$MODULE_DIR" d 755
check_remote_path "$pd_report" "$STATE_DIR" d 700
check_remote_path "$pd_report" "$MODULE_DIR/module.prop" f 644
check_remote_path "$pd_report" "$MODULE_DIR/zapret2/runtime.ini" f 644
check_remote_path "$pd_report" "$MODULE_DIR/zapret2/strategy-catalogs/tcp.txt" f 644
check_remote_path "$pd_report" "$MODULE_DIR/zapret2/strategy-catalogs/udp.txt" f 644
check_remote_path "$pd_report" "$MODULE_DIR/zapret2/strategy-catalogs/voice.txt" f 644
check_remote_path "$pd_report" "$MODULE_DIR/zapret2/strategy-catalogs/http80.txt" f 644
check_remote_path "$pd_report" "$MODULE_DIR/zapret2/install-generation.meta" f 600
check_remote_path "$pd_report" "$MODULE_DIR/system/bin/zapret2-start" f 755
check_remote_path "$pd_report" "$MODULE_DIR/system/bin/zapret2-stop" f 755
check_remote_path "$pd_report" "$MODULE_DIR/system/bin/zapret2-restart" f 755
check_remote_path "$pd_report" "$MODULE_DIR/system/bin/zapret2-status" f 755
check_remote_path "$pd_report" "$MODULE_DIR/system/bin/zapret2-full-rollback" f 755
check_remote_path "$pd_report" "$MODULE_DIR/zapret2/scripts/zapret-start.sh" f 755
check_remote_path "$pd_report" "$MODULE_DIR/zapret2/scripts/zapret-stop.sh" f 755
check_remote_path "$pd_report" "$MODULE_DIR/zapret2/scripts/zapret-restart.sh" f 755
check_remote_path "$pd_report" "$MODULE_DIR/zapret2/scripts/zapret-apply-preset.sh" f 755
check_remote_path "$pd_report" "$MODULE_DIR/zapret2/scripts/zapret-status.sh" f 755
check_remote_path "$pd_report" "$MODULE_DIR/zapret2/scripts/zapret-full-rollback.sh" f 755
check_remote_path "$pd_report" "$pd_binary" f 755
check_optional_marker "$pd_report" "$MODULE_DIR/disable"
pd_remove=$(adb_root "if [ -e $MODULE_DIR/remove ] || [ -L $MODULE_DIR/remove ]; then echo PRESENT; else echo ABSENT; fi" 2>&1 | tr -d '\r') || fail "cannot inspect removal marker"
[ "$pd_remove" = ABSENT ] || fail "root manager removal marker is already present"
pd_entry=$(adb_root "command -v $STATUS_ENTRY" 2>&1 | tr -d '\r') || fail "packaged status entry is unavailable: $pd_entry"
[ "$pd_entry" = "$STATUS_ENTRY" ] || fail "unexpected status entry: $pd_entry"
if ! probe_firewall_family iptables "$pd_dir/iptables-capabilities"; then
fail "IPv4 iptables lacks NFQUEUE/queue-bypass, multiport, or mark support"
fi
IPV4_CONNBYTES_READY="$PFF_CONNBYTES"
printf 'ipv4_capabilities\tready\nipv4_connbytes\t%s\n' \
"$([ "$IPV4_CONNBYTES_READY" = 1 ] && printf ready || printf optional-fallback)" >> "$pd_report"
if probe_firewall_family ip6tables "$pd_dir/ip6tables-capabilities"; then
IPV6_READY=1
IPV6_CONNBYTES_READY="$PFF_CONNBYTES"
printf 'ipv6_capabilities\tready\nipv6_connbytes\t%s\n' \
"$([ "$IPV6_CONNBYTES_READY" = 1 ] && printf ready || printf optional-fallback)" >> "$pd_report"
else
IPV6_READY=0
printf 'ipv6_capabilities\tunavailable\n' >> "$pd_report"
[ "$REQUIRE_IPV6" = 0 ] || fail "--require-ipv6 was set but ip6tables capability checks failed"
warn "IPv6 capability check failed; IPv4 is ready and the module will skip IPv6"
fi
capture_status "$pd_dir/status.machine"
capture_snapshot "$pd_dir/before-state"
chmod 0600 "$pd_report"
}
validate_rollback_schema() {
vrs_file=$1
vrs_expected='Z2_RB_STATUS
Z2_RB_PROCESS_CLEAN
Z2_RB_FIREWALL_CLEAN
Z2_RB_ROLLBACK_ARMED
Z2_RB_HOSTS_PRESERVED
Z2_RB_REBOOT_REQUIRED
Z2_RB_USER_DATA_PRESERVED
Z2_RB_LEGACY_AMBIGUOUS
Z2_RB_DIAGNOSTIC
Z2_RB_COMPLETE'
vrs_actual=$(sed 's/=.*//' "$vrs_file")
[ "$vrs_actual" = "$vrs_expected" ] || fail "full-rollback machine schema is truncated, reordered, duplicated, or unknown"
for vrs_pair in \
Z2_RB_STATUS=complete Z2_RB_PROCESS_CLEAN=1 Z2_RB_FIREWALL_CLEAN=1 \
Z2_RB_ROLLBACK_ARMED=1 Z2_RB_HOSTS_PRESERVED=1 Z2_RB_REBOOT_REQUIRED=1 \
Z2_RB_USER_DATA_PRESERVED=1 Z2_RB_LEGACY_AMBIGUOUS=0 Z2_RB_COMPLETE=1; do
grep -Fxq "$vrs_pair" "$vrs_file" || fail "full rollback did not satisfy $vrs_pair"
done
}
mutate_root_to_file() {
mrf_file=$1 mrf_command=$2
[ "$MUTATION_ARMED" = 1 ] || fail "internal mutation gate is not armed"
[ "$DRY_RUN" = 0 ] || fail "internal error: mutation reached during dry-run"
require_capability_consistency
set +e
adb_root "$mrf_command" > "$mrf_file" 2>&1
mrf_rc=$?
set -e
printf '%s\n' "$mrf_rc" > "$mrf_file.rc"
[ "$mrf_rc" -eq 0 ] || fail "device command failed (rc=$mrf_rc); see $mrf_file"
}
mutate_adb_to_file() {
maf_file=$1; shift
[ "$MUTATION_ARMED" = 1 ] || fail "internal mutation gate is not armed"
[ "$DRY_RUN" = 0 ] || fail "internal error: mutation reached during dry-run"
require_capability_consistency
set +e
adb_device "$@" > "$maf_file" 2>&1
maf_rc=$?
set -e
printf '%s\n' "$maf_rc" > "$maf_file.rc"
[ "$maf_rc" -eq 0 ] || fail "ADB mutation failed (rc=$maf_rc); see $maf_file"
}
stage_stop() {
ss_dir=$1
mutate_root_to_file "$ss_dir/stop-1.txt" "$STOP_ENTRY"
capture_status "$ss_dir/status-1.machine"; require_stopped_status "$ss_dir/status-1.machine"
mutate_root_to_file "$ss_dir/stop-2.txt" "$STOP_ENTRY"
capture_status "$ss_dir/status-2.machine"; require_stopped_status "$ss_dir/status-2.machine"
}
stage_start() {
ss_dir=$1
mutate_root_to_file "$ss_dir/start-1.txt" "$START_ENTRY"
capture_status "$ss_dir/status-1.machine"; require_running_status "$ss_dir/status-1.machine"
mutate_root_to_file "$ss_dir/start-2.txt" "$START_ENTRY"
capture_status "$ss_dir/status-2.machine"; require_running_status "$ss_dir/status-2.machine"
}
stage_restart() {
sr_dir=$1
mutate_root_to_file "$sr_dir/restart-1.txt" "$RESTART_ENTRY"
capture_status "$sr_dir/status-1.machine"; require_running_status "$sr_dir/status-1.machine"
mutate_root_to_file "$sr_dir/restart-2.txt" "$RESTART_ENTRY"
capture_status "$sr_dir/status-2.machine"; require_running_status "$sr_dir/status-2.machine"
}
new_stage_nonce() {
if command -v od >/dev/null 2>&1 && [ -r /dev/urandom ]; then
nsn_value=$(od -An -N16 -tx1 /dev/urandom | tr -d ' \n')
elif command -v openssl >/dev/null 2>&1; then
nsn_value=$(openssl rand -hex 16)
else
fail "secure randomness is required for the root-private update staging directory"
fi
case "$nsn_value" in
[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]) ;;
*) fail "secure random staging nonce is malformed" ;;
esac
printf '%s\n' "$nsn_value"
}
cleanup_shell_stage() {
[ "$MUTATION_ARMED" = 1 ] || return 0
[ -n "$SHELL_STAGE_MODULE" ] && [ -n "$SHELL_STAGE_APK" ] || return 0
case "$SHELL_STAGE_MODULE:$SHELL_STAGE_APK" in
/data/local/tmp/zapret2-device-smoke-[0-9a-f]*.module.zip:/data/local/tmp/zapret2-device-smoke-[0-9a-f]*.control.apk) ;;
*) warn "refusing cleanup of malformed shell staging paths"; return 1 ;;
esac
require_capability_consistency
adb_root "for z2_shell_file in $SHELL_STAGE_MODULE $SHELL_STAGE_APK; do
if [ -L \"\$z2_shell_file\" ]; then exit 71
elif [ -e \"\$z2_shell_file\" ]; then
[ -f \"\$z2_shell_file\" ] || exit 72
z2_shell_links=\$(stat -c %h \"\$z2_shell_file\" 2>/dev/null) || exit 73
[ \"\$z2_shell_links\" = 1 ] || exit 74
rm -f \"\$z2_shell_file\" || exit 75
fi
done" >/dev/null 2>&1 || { warn "shell staging cleanup was refused or failed"; return 1; }
SHELL_STAGE_MODULE=""; SHELL_STAGE_APK=""
}
cleanup_root_stage() {
[ "$MUTATION_ARMED" = 1 ] || return 0
[ -n "$ROOT_STAGE_DIR" ] || return 0
case "$ROOT_STAGE_DIR" in /data/adb/zapret2-device-smoke-[0-9a-f]*) ;; *) warn "refusing cleanup of malformed root staging path"; return 1;; esac
require_capability_consistency
adb_root "if [ -L $ROOT_STAGE_DIR ]; then exit 81
elif [ -e $ROOT_STAGE_DIR ]; then
[ -d $ROOT_STAGE_DIR ] || exit 82
z2_dir_stat=\$(stat -c %a:%u:%g $ROOT_STAGE_DIR 2>/dev/null) || exit 83
[ \"\$z2_dir_stat\" = 700:0:0 ] || exit 84
z2_cleanup_seen=0
for z2_root_file in $ROOT_STAGE_DIR/* $ROOT_STAGE_DIR/.[!.]* $ROOT_STAGE_DIR/..?*; do
[ -e \"\$z2_root_file\" ] || [ -L \"\$z2_root_file\" ] || continue
case \"\$z2_root_file\" in $ROOT_STAGE_APK|$ROOT_STAGE_MODULE) ;; *) exit 86;; esac
[ -f \"\$z2_root_file\" ] && [ ! -L \"\$z2_root_file\" ] || exit 87
z2_file_stat=\$(stat -c %h:%a:%u:%g \"\$z2_root_file\" 2>/dev/null) || exit 88
[ \"\$z2_file_stat\" = 1:600:0:0 ] || exit 89
z2_cleanup_seen=\$((z2_cleanup_seen + 1))
done
[ \"\$z2_cleanup_seen\" -le 2 ] || exit 90
rm -f $ROOT_STAGE_APK $ROOT_STAGE_MODULE || exit 90
rmdir $ROOT_STAGE_DIR || exit 91
fi" >/dev/null 2>&1 || { warn "root-private staging cleanup was refused or failed; inspect it manually"; return 1; }
ROOT_STAGE_DIR=""; ROOT_STAGE_MODULE=""; ROOT_STAGE_APK=""
}
cleanup_update_stages() {
cleanup_shell_stage || true
cleanup_root_stage || true
}
validate_stage_copy_schema() {
vscs_file=$1 vscs_kind=$2 vscs_hash=$3
vscs_expected=$(printf 'Z2_STAGE_KIND=%s\nZ2_STAGE_SHA256=%s\nZ2_STAGE_SOURCE_SECURE=1\nZ2_STAGE_PRIVATE_SECURE=1\nZ2_STAGE_COPY_COMPLETE=1' "$vscs_kind" "$vscs_hash")
vscs_actual=$(cat "$vscs_file")
[ "$vscs_actual" = "$vscs_expected" ] ||
fail "root-private copy schema is truncated, reordered, duplicated, unknown, or unbound: $vscs_file"
}
validate_root_stage_schema() {
vrss_file=$1 vrss_path=$2
vrss_expected=$(printf 'Z2_ROOT_STAGE_PATH=%s\nZ2_ROOT_STAGE_SECURE=1\nZ2_ROOT_STAGE_COMPLETE=1' "$vrss_path")
vrss_actual=$(cat "$vrss_file")
[ "$vrss_actual" = "$vrss_expected" ] ||
fail "root stage schema is truncated, reordered, duplicated, unknown, insecure, or unbound: $vrss_file"
}
copy_to_root_stage() {
ctrs_log=$1 ctrs_kind=$2 ctrs_source=$3 ctrs_dest=$4 ctrs_expected=$5
mutate_root_to_file "$ctrs_log" "
[ -d $ROOT_STAGE_DIR ] && [ ! -L $ROOT_STAGE_DIR ] || exit 101
[ \"\$(stat -c %a:%u:%g $ROOT_STAGE_DIR 2>/dev/null)\" = 700:0:0 ] || exit 102
[ -f $ctrs_source ] && [ ! -L $ctrs_source ] || exit 103
z2_src_stat=\$(stat -c %h:%a:%u:%g:%s $ctrs_source 2>/dev/null) || exit 104
z2_src_links=\${z2_src_stat%%:*}; z2_src_rest=\${z2_src_stat#*:}
z2_src_mode=\${z2_src_rest%%:*}; z2_src_rest=\${z2_src_rest#*:}
z2_src_uid=\${z2_src_rest%%:*}; z2_src_rest=\${z2_src_rest#*:}
z2_src_gid=\${z2_src_rest%%:*}; z2_src_size=\${z2_src_rest#*:}
[ \"\$z2_src_links\" = 1 ] || exit 105
case \"\$z2_src_mode\" in 600|640|644) ;; *) exit 106;; esac
case \"\$z2_src_uid\" in 0|2000) ;; *) exit 107;; esac
case \"\$z2_src_gid\" in 0|2000) ;; *) exit 108;; esac
case \"\$z2_src_size\" in ''|0|*[!0-9]*) exit 109;; esac
z2_src_hash=\$(sha256sum $ctrs_source 2>/dev/null) || exit 110
z2_src_hash=\${z2_src_hash%% *}
[ \"\$z2_src_hash\" = $ctrs_expected ] || exit 111
[ ! -e $ctrs_dest ] && [ ! -L $ctrs_dest ] || exit 112
cp $ctrs_source $ctrs_dest || exit 113
chown 0:0 $ctrs_dest || exit 114
chmod 0600 $ctrs_dest || exit 115
[ -f $ctrs_dest ] && [ ! -L $ctrs_dest ] || exit 116
z2_dst_stat=\$(stat -c %h:%a:%u:%g:%s $ctrs_dest 2>/dev/null) || exit 117
[ \"\$z2_dst_stat\" = \"1:600:0:0:\$z2_src_size\" ] || exit 118
z2_dst_hash=\$(sha256sum $ctrs_dest 2>/dev/null) || exit 119
z2_dst_hash=\${z2_dst_hash%% *}
[ \"\$z2_dst_hash\" = $ctrs_expected ] || exit 120
printf 'Z2_STAGE_KIND=$ctrs_kind\n'
printf 'Z2_STAGE_SHA256=%s\n' \"\$z2_dst_hash\"
printf 'Z2_STAGE_SOURCE_SECURE=1\n'
printf 'Z2_STAGE_PRIVATE_SECURE=1\n'
printf 'Z2_STAGE_COPY_COMPLETE=1\n'"
validate_stage_copy_schema "$ctrs_log" "$ctrs_kind" "$ctrs_expected"
}
remote_sha256() {
rs_path=$1
rs_line=$(adb_root "sha256sum $rs_path" 2>&1 | tr -d '\r') || fail "cannot hash remote artifact $rs_path: $rs_line"
rs_hash=${rs_line%% *}
normalize_hash "$rs_hash"
}
installed_module_hash() {
imh_value=$(adb_root "sed -n 's/^archive_sha256=//p' $MODULE_DIR/zapret2/install-generation.meta" 2>&1 | tr -d '\r') ||
fail "cannot read installed module archive hash: $imh_value"
normalize_hash "$imh_value"
}
installed_apk_hash() {
iah_path=$(adb_device shell pm path com.zapret2.app 2>&1 | tr -d '\r' | sed -n 's/^package://p' | sed -n '1p') ||
fail "cannot locate installed control APK"
case "$iah_path" in /data/app/*/base.apk) ;; *) fail "unexpected installed APK path: $iah_path" ;; esac
case "$iah_path" in *[!A-Za-z0-9._/+=~-]*) fail "unsafe installed APK path: $iah_path" ;; esac
remote_sha256 "$iah_path"
}
stage_update() {
su_dir=$1
verify_recorded_artifacts
su_nonce=$(new_stage_nonce)
SHELL_STAGE_MODULE=/data/local/tmp/zapret2-device-smoke-$su_nonce.module.zip
SHELL_STAGE_APK=/data/local/tmp/zapret2-device-smoke-$su_nonce.control.apk
ROOT_STAGE_DIR=/data/adb/zapret2-device-smoke-$su_nonce
ROOT_STAGE_MODULE=$ROOT_STAGE_DIR/module.zip
ROOT_STAGE_APK=$ROOT_STAGE_DIR/control.apk
trap 'cleanup_update_stages' EXIT
mutate_adb_to_file "$su_dir/push-module.txt" push "$MODULE_ZIP" "$SHELL_STAGE_MODULE"
mutate_adb_to_file "$su_dir/push-apk.txt" push "$APK_FILE" "$SHELL_STAGE_APK"
mutate_root_to_file "$su_dir/create-root-stage.txt" "
[ ! -e $ROOT_STAGE_DIR ] && [ ! -L $ROOT_STAGE_DIR ] || exit 121
mkdir $ROOT_STAGE_DIR || exit 122
chown 0:0 $ROOT_STAGE_DIR || exit 123
chmod 0700 $ROOT_STAGE_DIR || exit 124
[ -d $ROOT_STAGE_DIR ] && [ ! -L $ROOT_STAGE_DIR ] || exit 125
[ \"\$(stat -c %a:%u:%g $ROOT_STAGE_DIR 2>/dev/null)\" = 700:0:0 ] || exit 126
printf 'Z2_ROOT_STAGE_PATH=$ROOT_STAGE_DIR\nZ2_ROOT_STAGE_SECURE=1\nZ2_ROOT_STAGE_COMPLETE=1\n'"
validate_root_stage_schema "$su_dir/create-root-stage.txt" "$ROOT_STAGE_DIR"
copy_to_root_stage "$su_dir/copy-module.txt" module "$SHELL_STAGE_MODULE" "$ROOT_STAGE_MODULE" "$MODULE_SHA256"
copy_to_root_stage "$su_dir/copy-apk.txt" apk "$SHELL_STAGE_APK" "$ROOT_STAGE_APK" "$APK_SHA256"
cleanup_shell_stage || fail "could not retire shell-accessible staging files after private copy"
mutate_root_to_file "$su_dir/install-module.txt" "
[ -d $ROOT_STAGE_DIR ] && [ ! -L $ROOT_STAGE_DIR ] || exit 131
[ \"\$(stat -c %a:%u:%g $ROOT_STAGE_DIR 2>/dev/null)\" = 700:0:0 ] || exit 132
[ -f $ROOT_STAGE_MODULE ] && [ ! -L $ROOT_STAGE_MODULE ] || exit 133
[ \"\$(stat -c %h:%a:%u:%g $ROOT_STAGE_MODULE 2>/dev/null)\" = 1:600:0:0 ] || exit 134
z2_install_hash=\$(sha256sum $ROOT_STAGE_MODULE 2>/dev/null) || exit 135
z2_install_hash=\${z2_install_hash%% *}
[ \"\$z2_install_hash\" = $MODULE_SHA256 ] || exit 136
printf 'Z2_INSTALL_PATH=$ROOT_STAGE_MODULE\nZ2_INSTALL_SHA256=%s\n' \"\$z2_install_hash\"
magisk --install-module $ROOT_STAGE_MODULE || exit 137
printf 'Z2_INSTALL_COMPLETE=1\n'"
grep -Fxq "Z2_INSTALL_PATH=$ROOT_STAGE_MODULE" "$su_dir/install-module.txt" || fail "module installer path was not bound"
grep -Fxq "Z2_INSTALL_SHA256=$MODULE_SHA256" "$su_dir/install-module.txt" || fail "module installer hash was not bound"
[ "$(tail -n 1 "$su_dir/install-module.txt")" = Z2_INSTALL_COMPLETE=1 ] || fail "module install output is truncated"
mutate_root_to_file "$su_dir/install-apk.txt" "
[ -d $ROOT_STAGE_DIR ] && [ ! -L $ROOT_STAGE_DIR ] || exit 141
[ \"\$(stat -c %a:%u:%g $ROOT_STAGE_DIR 2>/dev/null)\" = 700:0:0 ] || exit 142
[ -f $ROOT_STAGE_APK ] && [ ! -L $ROOT_STAGE_APK ] || exit 143
z2_apk_stat=\$(stat -c %h:%a:%u:%g:%s $ROOT_STAGE_APK 2>/dev/null) || exit 144
z2_apk_size=\${z2_apk_stat##*:}
[ \"\$z2_apk_stat\" = \"1:600:0:0:\$z2_apk_size\" ] || exit 145
case \"\$z2_apk_size\" in ''|0|*[!0-9]*) exit 146;; esac
z2_apk_hash=\$(sha256sum $ROOT_STAGE_APK 2>/dev/null) || exit 147
z2_apk_hash=\${z2_apk_hash%% *}
[ \"\$z2_apk_hash\" = $APK_SHA256 ] || exit 148
printf 'Z2_APK_INSTALL_PATH=$ROOT_STAGE_APK\nZ2_APK_INSTALL_SHA256=%s\n' \"\$z2_apk_hash\"
cat $ROOT_STAGE_APK | pm install -r -S \"\$z2_apk_size\" || exit 149
printf 'Z2_APK_INSTALL_COMPLETE=1\n'"
grep -Fxq "Z2_APK_INSTALL_PATH=$ROOT_STAGE_APK" "$su_dir/install-apk.txt" || fail "APK installer path was not bound"
grep -Fxq "Z2_APK_INSTALL_SHA256=$APK_SHA256" "$su_dir/install-apk.txt" || fail "APK installer hash was not bound"
[ "$(tail -n 1 "$su_dir/install-apk.txt")" = Z2_APK_INSTALL_COMPLETE=1 ] || fail "APK install output is truncated"
cleanup_root_stage || fail "root-private staging cleanup failed"
trap - EXIT
[ "$(installed_module_hash)" = "$MODULE_SHA256" ] || fail "installed module generation does not match the exact release ZIP"
[ "$(installed_apk_hash)" = "$APK_SHA256" ] || fail "installed base APK does not match the exact release APK"
capture_status "$su_dir/status.machine"
require_running_status "$su_dir/status.machine"
}
stage_full_rollback() {
sfr_dir=$1
mutate_root_to_file "$sfr_dir/full-rollback.machine" "$ROLLBACK_ENTRY --machine"
validate_rollback_schema "$sfr_dir/full-rollback.machine"
capture_clean_firewall_audit "$sfr_dir"
capture_no_owned_process_audit "$sfr_dir/process-audit.txt"
}
stage_uninstall_checkpoint() {
suc_dir=$1
suc_boot_query=$suc_dir/boot-before.txt
capture_root_query "$suc_boot_query" '
z2_boot_id=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null) || exit 151
printf "Z2_UNINSTALL_BOOT_ID=%s\n" "$z2_boot_id"
printf "Z2_UNINSTALL_BOOT_COMPLETE=1\n"'
validate_query_schema "$suc_boot_query" 'Z2_UNINSTALL_BOOT_ID
Z2_UNINSTALL_BOOT_COMPLETE'
suc_boot_id=$(sed -n 's/^Z2_UNINSTALL_BOOT_ID=//p' "$suc_boot_query")
valid_boot_id "$suc_boot_id" || fail "uninstall checkpoint received a malformed boot identity"
grep -Fxq Z2_UNINSTALL_BOOT_COMPLETE=1 "$suc_boot_query" || fail "uninstall checkpoint boot query is truncated"
chmod 0600 "$suc_boot_query"
suc_boot_size=$(wc -c < "$suc_boot_query" | tr -d ' ')
suc_boot_hash=$(sha256_file "$suc_boot_query")
suc_run=$(manifest_value created_utc "$EVIDENCE_DIR/run.meta")
suc_meta_tmp=$suc_dir/.uninstall-reboot.meta.$$
[ ! -e "$suc_meta_tmp" ] && [ ! -L "$suc_meta_tmp" ] || fail "unsafe uninstall reboot checkpoint temporary path"
{
printf 'schema=1\n'
printf 'serial=%s\n' "$SERIAL"
printf 'run_created_utc=%s\n' "$suc_run"
printf 'boot_before_file=boot-before.txt\n'
printf 'boot_before_size=%s\n' "$suc_boot_size"
printf 'boot_before_sha256=%s\n' "$suc_boot_hash"
printf 'pre_boot_id=%s\n' "$suc_boot_id"
printf 'complete=1\n'
} > "$suc_meta_tmp"
chmod 0600 "$suc_meta_tmp"
mv -f "$suc_meta_tmp" "$suc_dir/uninstall-reboot.meta"
validate_uninstall_reboot_checkpoint "$suc_dir/uninstall-reboot.meta"
cat > "$suc_dir/ROOT-MANAGER-UNINSTALL-REQUIRED.txt" <<EOF
Exact device: $SERIAL
1. Open the trusted root-manager UI on this disposable test device.
2. Remove only the Zapret2 module (id: zapret2).
3. Confirm the root manager ran the packaged uninstall hook successfully.
4. Reboot the device.
5. Run this harness with the same serial/evidence directory and:
--stage uninstall-verify
Do not reinstall in this evidence sequence. Reinstall is a separate case and
must start with a new preflight evidence directory after uninstall verification.
EOF
chmod 0600 "$suc_dir/ROOT-MANAGER-UNINSTALL-REQUIRED.txt"
say "Uninstall checkpoint recorded. Remove only Zapret2 in the root-manager UI, reboot, then run uninstall-verify."
}
verify_uninstall() {
vu_dir=$1
mkdir "$vu_dir" || fail "cannot create uninstall verification evidence"
chmod 0700 "$vu_dir"
vu_checkpoint=$EVIDENCE_DIR/stages/uninstall/uninstall-reboot.meta
validate_uninstall_reboot_checkpoint "$vu_checkpoint"
vu_pre_boot=$(manifest_value pre_boot_id "$vu_checkpoint")
vu_run=$(manifest_value run_created_utc "$vu_checkpoint")
capture_root_query "$vu_dir/boot-after.txt" '
z2_boot_id=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null) || exit 151
printf "Z2_UNINSTALL_BOOT_ID=%s\n" "$z2_boot_id"
printf "Z2_UNINSTALL_BOOT_COMPLETE=1\n"'
validate_query_schema "$vu_dir/boot-after.txt" 'Z2_UNINSTALL_BOOT_ID
Z2_UNINSTALL_BOOT_COMPLETE'
vu_post_boot=$(sed -n 's/^Z2_UNINSTALL_BOOT_ID=//p' "$vu_dir/boot-after.txt")
valid_boot_id "$vu_post_boot" || fail "uninstall verification received a malformed boot identity"
grep -Fxq Z2_UNINSTALL_BOOT_COMPLETE=1 "$vu_dir/boot-after.txt" || fail "uninstall verification boot query is truncated"
[ "$vu_post_boot" != "$vu_pre_boot" ] || fail "uninstall verification requires a proven reboot after the uninstall checkpoint"
{
printf 'schema=1\nserial=%s\nrun_created_utc=%s\n' "$SERIAL" "$vu_run"
printf 'pre_boot_id=%s\npost_boot_id=%s\ncomplete=1\n' "$vu_pre_boot" "$vu_post_boot"
} > "$vu_dir/boot-transition.meta"
chmod 0600 "$vu_dir/boot-transition.meta"
capture_root_query "$vu_dir/root-audit.txt" '
z2_uid=$(id -u) || exit 151
printf "Z2_UNINSTALL_ROOT_UID=%s\n" "$z2_uid"
printf "Z2_UNINSTALL_ROOT_COMPLETE=1\n"
[ "$z2_uid" = 0 ]'
validate_query_schema "$vu_dir/root-audit.txt" 'Z2_UNINSTALL_ROOT_UID
Z2_UNINSTALL_ROOT_COMPLETE'
grep -Fxq Z2_UNINSTALL_ROOT_UID=0 "$vu_dir/root-audit.txt" || fail "uninstall verification does not have uid 0"
grep -Fxq Z2_UNINSTALL_ROOT_COMPLETE=1 "$vu_dir/root-audit.txt" || fail "root audit is truncated"
capture_root_query "$vu_dir/path-audit.txt" "
z2_live=absent; z2_update=absent; z2_wrappers=absent
{ [ -e $MODULE_DIR ] || [ -L $MODULE_DIR ]; } && z2_live=present
{ [ -e /data/adb/modules_update/zapret2 ] || [ -L /data/adb/modules_update/zapret2 ]; } && z2_update=present
for z2_wrapper in $START_ENTRY $STOP_ENTRY $RESTART_ENTRY $STATUS_ENTRY $ROLLBACK_ENTRY; do
if [ -e \"\$z2_wrapper\" ] || [ -L \"\$z2_wrapper\" ]; then z2_wrappers=present; fi
done
printf 'Z2_UNINSTALL_LIVE_MODULE=%s\n' \"\$z2_live\"
printf 'Z2_UNINSTALL_UPDATE_MODULE=%s\n' \"\$z2_update\"
printf 'Z2_UNINSTALL_WRAPPERS=%s\n' \"\$z2_wrappers\"
printf 'Z2_UNINSTALL_PATHS_COMPLETE=1\n'
[ \"\$z2_live\" = absent ] && [ \"\$z2_update\" = absent ] && [ \"\$z2_wrappers\" = absent ]"
validate_query_schema "$vu_dir/path-audit.txt" 'Z2_UNINSTALL_LIVE_MODULE
Z2_UNINSTALL_UPDATE_MODULE
Z2_UNINSTALL_WRAPPERS
Z2_UNINSTALL_PATHS_COMPLETE'
for vu_pair in Z2_UNINSTALL_LIVE_MODULE=absent Z2_UNINSTALL_UPDATE_MODULE=absent Z2_UNINSTALL_WRAPPERS=absent Z2_UNINSTALL_PATHS_COMPLETE=1; do
grep -Fxq "$vu_pair" "$vu_dir/path-audit.txt" || fail "post-uninstall path audit failed: $vu_pair"
done
capture_clean_firewall_audit "$vu_dir"
capture_no_owned_process_audit "$vu_dir/process-audit.txt"
capture_root_query "$vu_dir/state-audit.txt" "
[ -d $STATE_DIR ] && [ ! -L $STATE_DIR ] || exit 161
z2_state_stat=\$(stat -c %a:%u:%g $STATE_DIR 2>/dev/null) || exit 162
[ \"\$z2_state_stat\" = 700:0:0 ] || exit 163
z2_state_entries=\$(ls -A $STATE_DIR 2>/dev/null) || exit 164
[ \"\$z2_state_entries\" = uninstall.tombstone ] || exit 165
[ -f $STATE_DIR/uninstall.tombstone ] && [ ! -L $STATE_DIR/uninstall.tombstone ] || exit 166
z2_tombstone_stat=\$(stat -c %h:%a:%u:%g $STATE_DIR/uninstall.tombstone 2>/dev/null) || exit 167
[ \"\$z2_tombstone_stat\" = 1:600:0:0 ] || exit 168
z2_tombstone_keys=\$(sed 's/=.*//' $STATE_DIR/uninstall.tombstone 2>/dev/null) || exit 169
[ \"\$z2_tombstone_keys\" = 'version
pid
starttime
token
module_dir' ] || exit 170
z2_tombstone_version=\$(sed -n '1s/^version=//p' $STATE_DIR/uninstall.tombstone) || exit 171
z2_tombstone_pid=\$(sed -n '2s/^pid=//p' $STATE_DIR/uninstall.tombstone) || exit 172
z2_tombstone_start=\$(sed -n '3s/^starttime=//p' $STATE_DIR/uninstall.tombstone) || exit 173
z2_tombstone_token=\$(sed -n '4s/^token=//p' $STATE_DIR/uninstall.tombstone) || exit 174
z2_tombstone_module=\$(sed -n '5s/^module_dir=//p' $STATE_DIR/uninstall.tombstone) || exit 175
[ \"\$z2_tombstone_version\" = 1 ] || exit 176
case \"\$z2_tombstone_pid\" in ''|*[!0-9]*) exit 177;; esac
[ \"\$z2_tombstone_pid\" -gt 0 ] 2>/dev/null || exit 178
case \"\$z2_tombstone_start\" in ''|*[!0-9]*) exit 179;; esac
case \"\$z2_tombstone_token\" in ''|*[!A-Za-z0-9._-]*) exit 180;; esac
[ \"\$z2_tombstone_module\" = /data/adb/modules/zapret2 ] || exit 181
if [ -e /proc/\$z2_tombstone_pid ] || [ -L /proc/\$z2_tombstone_pid ]; then
[ -r /proc/\$z2_tombstone_pid/stat ] || exit 182
z2_tombstone_live_start=\$(awk '{ print \$22 }' /proc/\$z2_tombstone_pid/stat 2>/dev/null) || exit 183
case \"\$z2_tombstone_live_start\" in ''|*[!0-9]*) exit 184;; esac
[ \"\$z2_tombstone_live_start\" != \"\$z2_tombstone_start\" ] || exit 185
else
[ ! -e /proc/\$z2_tombstone_pid/stat ] && [ ! -L /proc/\$z2_tombstone_pid/stat ] || exit 186
fi
printf 'Z2_UNINSTALL_STATE_DIR=700:0:0\n'
printf 'Z2_UNINSTALL_STATE_ENTRIES=uninstall.tombstone\n'
printf 'Z2_UNINSTALL_TOMBSTONE=1:600:0:0\n'
printf 'Z2_UNINSTALL_TOMBSTONE_VERSION=%s\n' \"\$z2_tombstone_version\"
printf 'Z2_UNINSTALL_TOMBSTONE_PID=%s\n' \"\$z2_tombstone_pid\"
printf 'Z2_UNINSTALL_TOMBSTONE_STARTTIME=%s\n' \"\$z2_tombstone_start\"
printf 'Z2_UNINSTALL_TOMBSTONE_TOKEN=%s\n' \"\$z2_tombstone_token\"
printf 'Z2_UNINSTALL_TOMBSTONE_MODULE_DIR=%s\n' \"\$z2_tombstone_module\"
printf 'Z2_UNINSTALL_TOMBSTONE_OWNER=dead\n'
printf 'Z2_UNINSTALL_STATE_COMPLETE=1\n'"
validate_query_schema "$vu_dir/state-audit.txt" 'Z2_UNINSTALL_STATE_DIR
Z2_UNINSTALL_STATE_ENTRIES
Z2_UNINSTALL_TOMBSTONE
Z2_UNINSTALL_TOMBSTONE_VERSION
Z2_UNINSTALL_TOMBSTONE_PID
Z2_UNINSTALL_TOMBSTONE_STARTTIME
Z2_UNINSTALL_TOMBSTONE_TOKEN
Z2_UNINSTALL_TOMBSTONE_MODULE_DIR
Z2_UNINSTALL_TOMBSTONE_OWNER
Z2_UNINSTALL_STATE_COMPLETE'
for vu_pair in Z2_UNINSTALL_STATE_DIR=700:0:0 Z2_UNINSTALL_STATE_ENTRIES=uninstall.tombstone Z2_UNINSTALL_TOMBSTONE=1:600:0:0 Z2_UNINSTALL_TOMBSTONE_VERSION=1 Z2_UNINSTALL_TOMBSTONE_MODULE_DIR=/data/adb/modules/zapret2 Z2_UNINSTALL_TOMBSTONE_OWNER=dead Z2_UNINSTALL_STATE_COMPLETE=1; do
grep -Fxq "$vu_pair" "$vu_dir/state-audit.txt" || fail "post-uninstall state audit failed: $vu_pair"
done
vu_tombstone_pid=$(sed -n 's/^Z2_UNINSTALL_TOMBSTONE_PID=//p' "$vu_dir/state-audit.txt")
case "$vu_tombstone_pid" in ''|*[!0-9]*) fail "post-uninstall tombstone pid is malformed";; esac
[ "$vu_tombstone_pid" -gt 0 ] 2>/dev/null || fail "post-uninstall tombstone pid is not positive"
vu_tombstone_start=$(sed -n 's/^Z2_UNINSTALL_TOMBSTONE_STARTTIME=//p' "$vu_dir/state-audit.txt")
case "$vu_tombstone_start" in ''|*[!0-9]*) fail "post-uninstall tombstone starttime is malformed";; esac
vu_tombstone_token=$(sed -n 's/^Z2_UNINSTALL_TOMBSTONE_TOKEN=//p' "$vu_dir/state-audit.txt")
case "$vu_tombstone_token" in ''|*[!A-Za-z0-9._-]*) fail "post-uninstall tombstone token is malformed";; esac
capture_root_query "$vu_dir/state-listing.txt" "ls -lan $STATE_DIR"
}
require_adb
authorize_device
if [ "$STAGE" = preflight ]; then
init_new_evidence
record_initial_manifests
preflight_device "$EVIDENCE_DIR/preflight"
record_capability_manifest
printf 'complete=1\n' > "$EVIDENCE_DIR/preflight.complete"
chmod 0600 "$EVIDENCE_DIR/preflight.complete"
advance_sequence preflight
say "Read-only preflight complete. Private before-state evidence: $EVIDENCE_DIR"
exit 0
fi
open_existing_evidence
case "$STAGE" in
stop) require_previous_stage preflight ;;
start) require_previous_stage stop ;;
restart) require_previous_stage start ;;
update) require_previous_stage restart; verify_recorded_artifacts ;;
full-rollback) require_previous_stage update ;;
uninstall) require_previous_stage full-rollback ;;
uninstall-verify) require_previous_stage uninstall ;;
esac
if [ "$STAGE" = uninstall-verify ]; then
authorize_device
verify_uninstall "$EVIDENCE_DIR/stages/uninstall-verify"
advance_sequence uninstall-verify
say "Uninstall verification complete. Reinstall must use a new evidence directory."
exit 0
fi
if [ "$DRY_RUN" = 1 ]; then
pd_dry_stamp=$(date -u '+%Y%m%dT%H%M%SZ')
pd_stage_dir=$EVIDENCE_DIR/stages/dry-run-$STAGE-$pd_dry_stamp-$$
else
pd_stage_dir=$EVIDENCE_DIR/stages/$STAGE
fi
[ ! -e "$pd_stage_dir" ] && [ ! -L "$pd_stage_dir" ] || fail "stage evidence already exists: $pd_stage_dir"
mkdir "$pd_stage_dir"; chmod 0700 "$pd_stage_dir"
preflight_device "$pd_stage_dir/read-only-preflight"
require_capability_consistency
if [ "$DRY_RUN" = 1 ]; then
say "DRY RUN: preflight passed; stage '$STAGE' would be next. No device mutation was executed and sequence state was not advanced."
exit 0
fi
# This is the only point at which the mutation wrappers can become active.
MUTATION_ARMED=1
case "$STAGE" in
stop) stage_stop "$pd_stage_dir" ;;
start) stage_start "$pd_stage_dir" ;;
restart) stage_restart "$pd_stage_dir" ;;
update) stage_update "$pd_stage_dir" ;;
full-rollback) stage_full_rollback "$pd_stage_dir" ;;
uninstall) stage_uninstall_checkpoint "$pd_stage_dir" ;;
esac
capture_snapshot "$pd_stage_dir/after-state"
advance_sequence "$STAGE"
say "Stage '$STAGE' complete. Evidence: $pd_stage_dir"