Android prices every fork at ~25ms and every exec at ~90ms, and the preset apply was paying that price for questions it had already answered: 95 separate stat calls re-proving file metadata one field at a time, command substitutions around proc readers whose parsing was already builtin-only, expected firewall rules rebuilt through captures on every verify, and printf — an external binary on Android's mksh — invoked once per line of every publication. One stat capture now serves every metadata question a proof asks (path_meta_capture, consumed by the uid/mode/nlink/size predicates and retired on proof exit); the state-dir proof and a fully-pinned process identity become lock-scoped facts on the same single-writer argument as the owner read cache; proc_starttime/proc_argv0 gain fork-free global-return forms with the printf wrappers kept as seams; the qnum argument check reuses the cmdline snapshot argv0 already paid for; expected firewall rules are built as data instead of captured; and multi-line writers emit once through z2_emit_line, which resolves to mksh's raw print builtin on the device and printf elsewhere. Load-time probes replace the per-call command -v PATH walks, and log stamps reuse one date exec per second via EPOCHREALTIME. Measured on the Pixel 9 Pro XL: preset apply 56s before this series, 40s after the owner read cache, 14.6s now. tests/shell/run.sh passes; the three test seams that named replaced internals (proc_starttime, z2_fw_restore_command, the owner printf format) follow the new forms. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
273 lines
13 KiB
Shell
273 lines
13 KiB
Shell
#!/system/bin/sh
|
|
# Transactional stop: detach owned firewall first, then stop only a verified PID.
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
. "$SCRIPT_DIR/common.sh"
|
|
|
|
log_msg() {
|
|
z2_log_stamp_read
|
|
append_lifecycle_log "$Z2_LOG_STAMP [STOP] $1"
|
|
if command -v log >/dev/null 2>&1; then log -t Zapret2 "$1" 2>/dev/null; fi
|
|
}
|
|
|
|
stop_error_exit() {
|
|
local domain="$1" code="$2" stage="$3" retryable="$4" message="$5"
|
|
z2_error_set "$domain" "$code" "$stage" "$message" ||
|
|
z2_error_set LIFECYCLE LIFECYCLE_FAILED STOP "$message"
|
|
z2_error_emit_machine
|
|
echo "ERROR: $message"
|
|
exit 1
|
|
}
|
|
|
|
write_stop_status() {
|
|
local state="$1" message="$2"
|
|
# main() restores the status facts once for the whole transaction; only a
|
|
# signal arriving before that point needs a fallback restore here.
|
|
[ "${STOP_FACTS_RESTORED:-0}" = 1 ] || restore_status_facts
|
|
STATUS_RULES_OK=0
|
|
if [ "$state" = stopped ]; then STATUS_RULES_FAIL=0; STATUS_RULES_TOTAL=0
|
|
else STATUS_RULES_FAIL=1; STATUS_RULES_TOTAL=1; fi
|
|
STATUS_ERRORS="$message"; STATUS_OWN_PID=""; STATUS_OWN_PID_STARTTIME=""
|
|
STATUS_OWN_ARGV_SHA256=""; STATUS_OWNER_GENERATION=""
|
|
STATUS_PID_VERIFIED=0; STATUS_OWNER_METADATA_VERIFIED=0
|
|
# Only a completed stop that proved every family proves the owned ruleset
|
|
# is gone. A failed stop records what it could not prove, and a stop that
|
|
# had to skip an unqueryable family proved one family less — claiming
|
|
# verification in either case would let the next teardown read this
|
|
# snapshot back as proof there is nothing left, a claim that then sustains
|
|
# itself because this stop also retires the owner record.
|
|
if [ "$state" = stopped ] && [ "${FIREWALL_IPV6_SKIPPED_UNPROVEN:-0}" != 1 ]; then
|
|
STATUS_RULESET_VERIFIED=1
|
|
else
|
|
STATUS_RULESET_VERIFIED=0
|
|
fi
|
|
# ipv6_active answers "did this module publish there", and a teardown that
|
|
# skipped an unqueryable family has no evidence that it did. Recording 1
|
|
# would invent one, and every later teardown would read it back as proof.
|
|
# The withheld verification above is what carries the uncertainty.
|
|
STATUS_RULES_EXPECTED=0; STATUS_QNUM="${STOP_QNUM:-${STATUS_QNUM:-${QNUM:-}}}"
|
|
STATUS_IPV4_ACTIVE=0; STATUS_IPV6_ACTIVE=0; STATUS_CHAINS=0; STATUS_ANCHORS=0
|
|
STATUS_IPV4_RULES=0; STATUS_IPV6_RULES=0
|
|
STATUS_NFQUEUE_SUPPORTED="${STATUS_NFQUEUE_SUPPORTED:-0}"
|
|
STATUS_QUEUE_BYPASS_SUPPORTED="${STATUS_QUEUE_BYPASS_SUPPORTED:-0}"
|
|
STATUS_CONNBYTES_SUPPORTED="${STATUS_CONNBYTES_SUPPORTED:-0}"
|
|
STATUS_MULTIPORT_SUPPORTED="${STATUS_MULTIPORT_SUPPORTED:-0}"
|
|
STATUS_MARK_SUPPORTED="${STATUS_MARK_SUPPORTED:-0}"
|
|
# A note is diagnostic only: a completed stop must not publish an errors=
|
|
# line, and an error must not lose its message to a note.
|
|
STATUS_FALLBACK_MODE=0
|
|
if [ "$state" = stopped ]; then
|
|
STATUS_ERRORS=""; STATUS_DIAGNOSTICS="${STOP_NOTE:-}"
|
|
else
|
|
STATUS_DIAGNOSTICS="$message"
|
|
fi
|
|
if [ "$state" = stopped ]; then
|
|
STATUS_ERROR_STATUS=OK; STATUS_ERROR_DOMAIN=NONE; STATUS_ERROR_CODE=NONE
|
|
STATUS_ERROR_STAGE=NONE; STATUS_ERROR_DETAIL=""
|
|
else
|
|
STATUS_ERROR_STATUS=ERROR
|
|
STATUS_ERROR_DOMAIN="${STOP_ERROR_DOMAIN:-LIFECYCLE}"
|
|
STATUS_ERROR_CODE="${STOP_ERROR_CODE:-LIFECYCLE_FAILED}"
|
|
STATUS_ERROR_STAGE="${STOP_ERROR_STAGE:-STOP}"
|
|
STATUS_ERROR_DETAIL="$(z2_error_detail_normalize "$message")"
|
|
fi
|
|
write_iptables_status "$state"
|
|
}
|
|
|
|
remove_transient_diagnostics() {
|
|
local path rc=0
|
|
[ "${STOP_RUNTIME_OWNED:-0}" = 1 ] || return 0
|
|
for path in "$CMDLINE_FILE" "$STARTUP_LOG" "$ERROR_LOG" "$DEBUG_LOG"; do
|
|
if [ -e "$path" ] || [ -L "$path" ]; then rm -f "$path" 2>/dev/null || rc=1; fi
|
|
done
|
|
return "$rc"
|
|
}
|
|
|
|
stop_interrupted() {
|
|
local message="stop interrupted by signal"
|
|
trap '' HUP INT TERM
|
|
STOP_ERROR_DOMAIN=LIFECYCLE
|
|
STOP_ERROR_CODE=LIFECYCLE_FAILED
|
|
STOP_ERROR_STAGE=STOP_SIGNAL
|
|
write_stop_status error "$message" >/dev/null 2>&1 || true
|
|
release_lifecycle_lock
|
|
trap - HUP INT TERM
|
|
exit 1
|
|
}
|
|
|
|
main() {
|
|
ensure_state_dir ||
|
|
stop_error_exit STATE STATE_UNAVAILABLE STOP_STATE 0 \
|
|
"insecure or unavailable zapret2 state directory: $STATE_DIR"
|
|
acquire_lifecycle_lock ||
|
|
stop_error_exit LIFECYCLE LIFECYCLE_BUSY STOP_LOCK 1 "zapret2 lifecycle is busy"
|
|
if ! audit_recovery_artifacts lifecycle; then
|
|
message="stop blocked by recovery state: ${RECOVERY_ARTIFACT_DIAGNOSTIC:-unsafe recovery artifact}$(recovery_block_remedy)"
|
|
release_lifecycle_lock
|
|
stop_error_exit LIFECYCLE RECOVERY_BLOCKED STOP_RECOVERY 0 "$message"
|
|
fi
|
|
if ! uninstall_tombstone_allows_stop; then
|
|
message="stop blocked by uninstall serialization: $UNINSTALL_TOMBSTONE_ERROR"
|
|
release_lifecycle_lock
|
|
stop_error_exit LIFECYCLE UNINSTALL_BLOCKED STOP_UNINSTALL 1 "$message"
|
|
fi
|
|
trap stop_interrupted HUP INT TERM
|
|
if ! prepare_lifecycle_log; then
|
|
LOG_READY=0
|
|
if command -v log >/dev/null 2>&1; then log -p w -t Zapret2 "Lifecycle file logging disabled: unsafe or unavailable path" 2>/dev/null; fi
|
|
fi
|
|
restore_status_facts
|
|
STOP_FACTS_RESTORED=1
|
|
STOP_RUNTIME_OWNED=0
|
|
OWNER_STATE_AVAILABLE=0
|
|
if read_owner_state >/dev/null 2>&1; then OWNER_STATE_AVAILABLE=1; fi
|
|
if [ "$OWNER_STATE_AVAILABLE" = 1 ] || read_runtime_owner_marker >/dev/null 2>&1 ||
|
|
read_verified_pidfile >/dev/null 2>&1; then
|
|
STOP_RUNTIME_OWNED=1
|
|
fi
|
|
STOP_QNUM="${STATUS_FILE_QNUM:-}"
|
|
[ "$OWNER_STATE_AVAILABLE" != 1 ] || STOP_QNUM="$OWNER_STATE_QNUM"
|
|
resolve_ipv6_ownership_expectation "$OWNER_STATE_AVAILABLE"
|
|
if [ -z "$STOP_QNUM" ]; then
|
|
# Neither the committed snapshot nor an owner record carries the queue
|
|
# number (first stop on a fresh boot); fall back to the configured one.
|
|
load_effective_core_config_readonly >/dev/null 2>&1 || true
|
|
STOP_QNUM="${QNUM:-}"
|
|
fi
|
|
log_msg "Stopping Zapret2"
|
|
[ -z "$UNINSTALL_TOMBSTONE_DIAGNOSTIC" ] || log_msg "$UNINSTALL_TOMBSTONE_DIAGNOSTIC"
|
|
|
|
rc=0
|
|
STOP_STATUS_COMMITTED=0
|
|
errors=""
|
|
STOP_ERROR_DOMAIN=NONE; STOP_ERROR_CODE=NONE
|
|
STOP_ERROR_STAGE=NONE; STOP_ERROR_RETRYABLE=0
|
|
# Prove the complete process identity/publication before any chain
|
|
# mutation. The later stop consumes this same snapshot and generation, so
|
|
# a replacement process cannot be killed after teardown.
|
|
if ! preflight_owned_process_cleanup; then
|
|
errors="process cleanup preflight blocked: $PROCESS_CLEANUP_PREFLIGHT_ERROR; firewall and daemon teardown were not attempted"
|
|
STOP_ERROR_DOMAIN=PROCESS; STOP_ERROR_CODE=PROCESS_STOP_FAILED; STOP_ERROR_STAGE=STOP_PREFLIGHT
|
|
elif ! audit_owned_firewall_for_cleanup; then
|
|
errors="firewall generation preflight blocked: $FIREWALL_CLEANUP_PREFLIGHT_ERROR; firewall and daemon teardown were not attempted"
|
|
STOP_ERROR_DOMAIN=FIREWALL; STOP_ERROR_CODE=FIREWALL_CLEANUP_FAILED; STOP_ERROR_STAGE=STOP_PREFLIGHT
|
|
fi
|
|
if [ -n "$errors" ]; then
|
|
trap '' HUP INT TERM
|
|
write_stop_status error "$errors" >/dev/null 2>&1 || true
|
|
release_lifecycle_lock
|
|
trap - HUP INT TERM
|
|
log_msg "ERROR: $errors"
|
|
z2_error_set "$STOP_ERROR_DOMAIN" "$STOP_ERROR_CODE" "$STOP_ERROR_STAGE" "$errors" ||
|
|
z2_error_set LIFECYCLE LIFECYCLE_FAILED STOP "$errors"
|
|
z2_error_emit_machine
|
|
echo "ERROR: Zapret2 stop incomplete: $errors"
|
|
exit 1
|
|
fi
|
|
firewall_detached=0
|
|
if ! cleanup_owned_firewall audited; then
|
|
rc=1
|
|
STOP_ERROR_DOMAIN=FIREWALL; STOP_ERROR_CODE=FIREWALL_CLEANUP_FAILED; STOP_ERROR_STAGE=STOP_FIREWALL
|
|
if [ -n "$errors" ]; then errors="$errors; owned firewall cleanup failed"
|
|
else errors="owned firewall cleanup failed: ${FIREWALL_CLEANUP_PREFLIGHT_ERROR:-ambiguous ownership}; daemon teardown was not attempted"; fi
|
|
elif ! command -v ip6tables >/dev/null 2>&1 && [ "$IPV6_PUBLICATION_RECORDED" = 1 ]; then
|
|
# cleanup_owned_firewall proves per-family absence as the postcondition
|
|
# of each family it touches, and fails when a present frontend cannot
|
|
# be queried while IPv6 ownership is expected. The one fact left is a
|
|
# device that lost its ip6tables frontend entirely while this
|
|
# generation did publish IPv6 rules.
|
|
rc=1
|
|
STOP_ERROR_DOMAIN=FIREWALL; STOP_ERROR_CODE=POSTCONDITION_FAILED; STOP_ERROR_STAGE=STOP_FIREWALL
|
|
if [ -n "$errors" ]; then errors="$errors; owned firewall artifacts remain"
|
|
else errors="IPv6 owned rules cannot be disproved because ip6tables is unavailable; daemon teardown was not attempted"; fi
|
|
else
|
|
firewall_detached=1
|
|
[ "${FIREWALL_IPV6_SKIPPED_UNPROVEN:-0}" != 1 ] ||
|
|
log_msg "WARNING: the IPv6 mangle table could not be queried; rules from an earlier generation, if any, were not removed"
|
|
fi
|
|
|
|
if [ "$firewall_detached" = 1 ] && ! stop_pidfile_process; then
|
|
rc=1
|
|
STOP_ERROR_DOMAIN=PROCESS; STOP_ERROR_CODE=PROCESS_STOP_FAILED; STOP_ERROR_STAGE=STOP_PROCESS
|
|
if [ -n "$errors" ]; then errors="$errors; verified process stop failed"
|
|
else errors="verified process stop failed"; fi
|
|
fi
|
|
|
|
if [ "$firewall_detached" = 1 ] && read_verified_pidfile; then
|
|
rc=1
|
|
STOP_ERROR_DOMAIN=PROCESS; STOP_ERROR_CODE=POSTCONDITION_FAILED; STOP_ERROR_STAGE=STOP_VERIFY
|
|
if [ -n "$errors" ]; then errors="$errors; verified nfqws2 process remains"
|
|
else errors="verified nfqws2 process remains"; fi
|
|
elif [ -e "$PIDFILE" ]; then
|
|
if read_live_pidfile; then
|
|
rc=1
|
|
STOP_ERROR_DOMAIN=PROCESS; STOP_ERROR_CODE=POSTCONDITION_FAILED; STOP_ERROR_STAGE=STOP_VERIFY
|
|
if [ -n "$errors" ]; then errors="$errors; unverified live PID remains"
|
|
else errors="unverified live PID remains"; fi
|
|
fi
|
|
fi
|
|
|
|
if [ "$rc" -eq 0 ]; then
|
|
# stop_pidfile_process just proved no exact process remains and
|
|
# removed the publication files itself; asserting their absence is
|
|
# the whole retirement postcondition without another /proc walk.
|
|
if [ -e "$PIDFILE" ] || [ -L "$PIDFILE" ] || [ -e "$OWNER_STATE" ] || [ -L "$OWNER_STATE" ]; then
|
|
rc=1
|
|
if [ -n "$errors" ]; then errors="$errors; ownership publication survived a verified teardown"
|
|
else errors="ownership publication survived a verified teardown"; fi
|
|
fi
|
|
fi
|
|
|
|
if [ "$rc" -eq 0 ]; then
|
|
if ! remove_transient_diagnostics; then
|
|
log_msg "WARNING: one or more transient diagnostics could not be removed safely"
|
|
fi
|
|
STOP_NOTE=""
|
|
[ "${FIREWALL_IPV6_SKIPPED_UNPROVEN:-0}" != 1 ] ||
|
|
STOP_NOTE="IPv6 mangle table unavailable; rules from an earlier generation, if any, were left for the next reboot"
|
|
if write_stop_status stopped ""; then
|
|
STOP_STATUS_COMMITTED=1
|
|
else
|
|
# Process and firewall cleanup is already verified. A diagnostic
|
|
# status-write failure must not turn a completed stop into an
|
|
# unverifiable process-cleanup failure after metadata retirement.
|
|
log_msg "WARNING: stopped state is clean but status file update failed"
|
|
fi
|
|
else
|
|
write_stop_status error "$errors" >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
receipt_lifecycle_state=idle; receipt_owner_kind=none
|
|
if [ "$LOCK_HELD" = inherited ]; then
|
|
receipt_lifecycle_state=owned; receipt_owner_kind=android-mutation
|
|
fi
|
|
if ! release_lifecycle_lock; then
|
|
rc=1
|
|
STOP_ERROR_DOMAIN=LIFECYCLE; STOP_ERROR_CODE=LIFECYCLE_FAILED
|
|
STOP_ERROR_STAGE=STOP_LOCK_RELEASE
|
|
errors="${errors:+$errors; }lifecycle ownership release failed"
|
|
fi
|
|
trap - HUP INT TERM
|
|
if [ "$rc" -eq 0 ]; then
|
|
log_msg "Zapret2 stopped and owned state is clean"
|
|
# The receipt is this operation's own report, reservation included. It
|
|
# used to be withheld when a family had to be skipped, because the
|
|
# caller rejected a stopped receipt that declined to certify the
|
|
# ruleset — which only moved the problem to the observation the caller
|
|
# made instead, where the same reservation had to survive a second
|
|
# trip through the snapshot. It travels on the receipt now.
|
|
if [ "$STOP_STATUS_COMMITTED" = 1 ]; then
|
|
emit_committed_status_v6 stopped "$receipt_lifecycle_state" "$receipt_owner_kind" || true
|
|
fi
|
|
echo "Zapret2 stopped"
|
|
else
|
|
log_msg "ERROR: $errors"
|
|
z2_error_set "$STOP_ERROR_DOMAIN" "$STOP_ERROR_CODE" "$STOP_ERROR_STAGE" "$errors" ||
|
|
z2_error_set LIFECYCLE LIFECYCLE_FAILED STOP "$errors"
|
|
z2_error_emit_machine
|
|
echo "ERROR: Zapret2 stop incomplete: $errors"
|
|
fi
|
|
exit "$rc"
|
|
}
|
|
|
|
main "$@"
|