magisk-zapret2/service.sh
loop-uh 1c79e0cb0a Give the IPv6 ownership rule one home, and fix the sixth round
The invariant that broke in rounds 4 and 5 was implemented in four
places by three different rules, and the fourth site was found wrong
again. resolve_ipv6_ownership_expectation now answers both questions
once — may an unqueryable family be skipped, and is there evidence we
published there — with a single priority order: what this transaction
did, then the owner record, then a snapshot committed with a verified
ruleset, then the conservative default. Stop, full rollback, start's
rollback and the failure snapshot all call it.

Other findings:

- A purge receipt claiming "complete" alongside an unverified firewall
  is rejected wholesale by the app as a protocol violation, so the user
  saw a failed erase after a successful one, and the APK data was never
  cleared. Such a run reports partial, which is what it is. A contract
  test now rejects any complete receipt with a non-affirmative field.
- The boot wait could hang forever if the property reached 1 between our
  read and resetprop's own; it is handed the value we observed.
- A completed stop no longer writes an errors= line to carry a note.
- service.sh logged a variable that has not existed for a long time.
- The duplicate install-generation parser in uninstall.sh is gone: it
  authenticated the same identity as the shared one, with weaker checks.
- The installer distinguishes "cannot chmod" from "archive contains a
  link" instead of reporting the second for both.
- Recovery refusals now say a reboot clears them, and the user guide has
  the section that says so.

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

203 lines
8.1 KiB
Shell

#!/system/bin/sh
# Module package generations are activated only by the root manager at boot.
##########################################################################################
# Zapret2 root module - Service Script (runs at boot)
##########################################################################################
MODDIR="${0%/*}"
ZAPRET_DIR="$MODDIR/zapret2"
SCRIPT_DIR="$ZAPRET_DIR/scripts"
COMMON_SCRIPT="$SCRIPT_DIR/common.sh"
START_SCRIPT="$SCRIPT_DIR/zapret-start.sh"
LOG_READY=0
MODULE_DISABLED=0
# This is the root-manager boot entry point. The lifecycle lock in zapret-start.sh
# serializes this invocation with other lifecycle callers.
log() {
if [ "$LOG_READY" = "1" ]; then
append_lifecycle_log "$(date '+%Y-%m-%d %H:%M:%S') [SERVICE] $1" || LOG_READY=0
fi
/system/bin/log -t "Zapret2" "$1" 2>/dev/null
}
# Root-manager disable markers are authoritative at boot. A disabled module
# still retires authenticated previous-boot runtime metadata when such state is
# already present; with no state at all it remains a mutation-free no-op.
# Unsafe marker types fail closed before state creation or lifecycle mutation.
DISABLE_MARKER="$MODDIR/disable"
if [ -e "$DISABLE_MARKER" ] || [ -L "$DISABLE_MARKER" ]; then
if [ -f "$DISABLE_MARKER" ] && [ ! -L "$DISABLE_MARKER" ]; then
MODULE_DISABLED=1
if [ ! -e "${STATE_DIR:-/data/adb/zapret2-state}" ] &&
[ ! -L "${STATE_DIR:-/data/adb/zapret2-state}" ]; then
log "Module disable marker is present; no runtime state requires boot recovery"
exit 0
fi
else
/system/bin/log -p e -t "Zapret2" "Unsafe module disable marker; boot startup was refused" 2>/dev/null
exit 1
fi
fi
if [ ! -f "$COMMON_SCRIPT" ] || [ -L "$COMMON_SCRIPT" ] ||
[ ! -f "$START_SCRIPT" ] || [ -L "$START_SCRIPT" ]; then
/system/bin/log -p e -t "Zapret2" "Secure lifecycle helpers are unavailable; boot startup was refused" 2>/dev/null
exit 1
fi
. "$COMMON_SCRIPT"
# The boot service owns only the dedicated root state directory. Refuse an
# unsafe existing path instead of repairing it implicitly. Create it only for
# an enabled module; disabled/no-state boot was handled above as a clean no-op.
if [ -e "$STATE_DIR" ] || [ -L "$STATE_DIR" ]; then
state_dir_is_secure || {
/system/bin/log -p e -t "Zapret2" "Secure state directory is unavailable; boot startup was refused" 2>/dev/null
exit 1
}
elif [ "$MODULE_DISABLED" = 1 ]; then
log "Module disable marker is present; boot startup skipped"
exit 0
elif ! ensure_state_dir; then
/system/bin/log -p e -t "Zapret2" "Secure state directory is unavailable; boot startup was refused" 2>/dev/null
exit 1
fi
# Wait for boot to complete. resetprop -w blocks on the property instead of
# forking getprop once a second; fall back to the poll loop without it.
BOOT_STATE="$(getprop sys.boot_completed)"
while [ "$BOOT_STATE" != "1" ]; do
# Wait on the property rather than polling it, but hand resetprop the
# value we just observed: waiting on "whatever it is now" would block
# forever if the property reached 1 between our read and its own.
if command -v resetprop >/dev/null 2>&1; then
resetprop -w sys.boot_completed "$BOOT_STATE" >/dev/null 2>&1 || sleep 1
else
sleep 1
fi
BOOT_STATE="$(getprop sys.boot_completed)"
done
# When autostart runs, zapret-start.sh performs the identical recovery audit
# under its own lifecycle lock, so a healthy boot needs one cycle instead of
# two. The standalone pass runs on the paths that never reach zapret-start.sh,
# and as the retry path when a start is blocked by recovery state.
if [ "$MODULE_DISABLED" = 1 ]; then
if ! command -v recover_boot_stale_runtime_state >/dev/null 2>&1 ||
! recover_boot_stale_runtime_state; then
log "ERROR: Previous-boot runtime recovery failed: ${BOOT_RECOVERY_DIAGNOSTIC:-unsafe recovery state}"
exit 1
fi
log "Module disable marker is present; previous-boot recovery completed and startup was skipped"
exit 0
fi
if ! prepare_lifecycle_log; then
LOG_READY=0
/system/bin/log -p w -t "Zapret2" "Lifecycle file logging is unavailable; continuing in logcat only" 2>/dev/null
fi
log "=== Zapret2 service starting ==="
log "Boot completed; starting the network-independent firewall lifecycle"
# Every lifecycle entry point audits recovery state before doing its work, and
# that audit can refuse. Only a refusal *by* recovery state may run the
# boot-only recovery pass: it happens before the entry point mutates anything,
# so the pass still sees previous-boot state only. Any other failure may have
# published a process or rules during this boot, and discarding state wholesale
# would then destroy live evidence rather than stale generations.
run_start_script() {
START_OUTPUT="$(/system/bin/sh "$START_SCRIPT" "$@" 2>&1)"
START_RC=$?
[ -z "$START_OUTPUT" ] || log "$START_OUTPUT"
case "$START_RC:$START_OUTPUT" in
0:*) return 0 ;;
*Z2_ERROR_CODE=RECOVERY_BLOCKED*) ;;
*) return "$START_RC" ;;
esac
log "Lifecycle entry was refused by recovery state; running previous-boot recovery"
if ! recover_boot_stale_runtime_state; then
log "ERROR: Previous-boot runtime recovery failed: ${BOOT_RECOVERY_DIAGNOSTIC:-unsafe recovery state}"
return "$START_RC"
fi
if [ "$BOOT_INCOMPATIBLE_STATE_RETIRED" = 1 ]; then
log "Incompatible boot-local state was discarded"
fi
START_OUTPUT="$(/system/bin/sh "$START_SCRIPT" "$@" 2>&1)"
START_RC=$?
[ -z "$START_OUTPUT" ] || log "$START_OUTPUT"
return "$START_RC"
}
# Check if autostart is enabled. This preflight is read-only; any migration is
# performed by zapret-start.sh only after update/lifecycle serialization.
load_effective_core_config_readonly
CONFIG_RC=$?
if [ "$CONFIG_RC" -ne 0 ]; then
log "runtime.ini requires serialized repair: ${RUNTIME_CONFIG_ERROR:-unknown error}"
# The repair runs after the start script's own recovery audit, so a
# previous-boot artifact would otherwise fence config repair on every boot
# forever — the boot pass is the only authority that can clear it.
run_start_script --repair-runtime-only
REPAIR_RC=$?
if [ "$REPAIR_RC" -ne 0 ]; then
log "ERROR: Serialized runtime.ini repair failed (exit $REPAIR_RC)"
log "=== Zapret2 service script failed ==="
exit 1
fi
if ! load_effective_core_config_readonly; then
log "ERROR: Repaired runtime.ini still failed strict read-only validation"
log "=== Zapret2 service script failed ==="
exit 1
fi
fi
case "$RUNTIME_CONFIG_STATUS" in
loaded|regenerated)
log "$(runtime_config_status_message)"
;;
unavailable)
log "$(runtime_config_status_message)"
;;
esac
log "$(core_config_source_message)"
if [ "$AUTOSTART" = "1" ]; then
log "Autostart enabled, launching zapret2..."
# Package updates are activated by the root manager only at boot.
# zapret-start.sh gates runtime state, module removal, and uninstall
# tombstones, and its own audit retires proven cross-boot publications
# under the lifecycle lock — so a healthy boot needs no separate pass.
run_start_script
START_RC=$?
if [ "$START_RC" -eq 0 ]; then
log "Autostart command completed successfully (exit $START_RC)"
else
log "ERROR: Autostart command failed (exit $START_RC)"
fi
else
# No start transaction will run, so retire previous-boot runtime state in
# a standalone recovery pass here.
if ! recover_boot_stale_runtime_state; then
log "ERROR: Previous-boot runtime recovery failed: ${BOOT_RECOVERY_DIAGNOSTIC:-unsafe recovery state}"
log "=== Zapret2 service script failed ==="
exit 1
fi
if [ "$BOOT_INCOMPATIBLE_STATE_RETIRED" = 1 ]; then
log "Incompatible boot-local state was discarded"
fi
START_RC=0
log "Autostart disabled in effective core config ($CORE_CONFIG_SOURCE)"
fi
if [ "$START_RC" -eq 0 ]; then
log "=== Zapret2 service script finished successfully (exit $START_RC) ==="
else
log "=== Zapret2 service script finished with errors (exit $START_RC) ==="
fi
exit "$START_RC"