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>
203 lines
8.1 KiB
Shell
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"
|