magisk-zapret2/uninstall.sh
loop-uh e733873a51 Carry the IPv6 reservation across every boundary that reports it
The rule that an unprovable family is not a clean one held inside the
module and was lost at each point where a result becomes visible.

Status projected a committed stopped receipt through its "nothing is
owned" branch, which asserted a verified ruleset unconditionally — so a
teardown that deliberately withheld that claim had it restored by the
next status read, on the only channel still carrying the reservation.

Direct uninstall told the user the family "is cleared by the reboot".
Only the root-manager branch schedules one; the direct branch leaves the
module installed, requires no reboot, and has just deleted the state
that could identify leftover rules. It now names the reboot as the
action that ends the ambiguity instead of implying it is under way.

A partial purge receipt hardcoded every removal fact to zero. An
unremovable external staging workspace was enough to reach it with both
managed trees already gone, denying a removal that did happen on the
same receipt the app reads. The facts are measured now, and the
diagnostic names what actually survived.

Teardown also discarded the preflight's positive proof: a family read
moments earlier and found empty raised a reservation anyway if the
frontend went busy in between. That proof is now recorded and honoured,
for the mode that produced it only.

Tests: the owned baseline mode had no coverage at all, the boot-recovery
fixture forced autostart off so service.sh's new recovery retry was
never entered, and the purge assertions checked that a string was
present rather than that the two scripts agree. Each new test was proven
by reverting its fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 11:09:07 +03:00

819 lines
32 KiB
Shell

#!/system/bin/sh
##########################################################################################
# Zapret2 root module - Uninstall Script
##########################################################################################
SCRIPT_MODPATH="${0%/*}"
if [ -n "${MODPATH:-}" ] && [ "$MODPATH" != "$SCRIPT_MODPATH" ]; then
printf '%s\n' "ERROR: uninstall path disagrees with the executing module" >&2
exit 1
fi
MODPATH="$SCRIPT_MODPATH"
MODULE_PROP="$MODPATH/module.prop"
[ -d "$MODPATH" ] && [ ! -L "$MODPATH" ] &&
[ -f "$MODULE_PROP" ] && [ ! -L "$MODULE_PROP" ] &&
[ "$(grep -c '^id=' "$MODULE_PROP" 2>/dev/null)" = 1 ] &&
grep -qx 'id=zapret2' "$MODULE_PROP" || {
printf '%s\n' "ERROR: refusing uninstall without the exact Zapret2 module identity" >&2
exit 1
}
MODULES_DIR="${MODPATH%/*}"
MODULE_STORAGE="${MODULES_DIR%/*}"
MODULE_ID="${MODPATH##*/}"
[ "$MODULES_DIR" = "$MODULE_STORAGE/modules" ] &&
[ "$MODULE_STORAGE" = /data/adb ] && [ "$MODULE_ID" = zapret2 ] || {
printf '%s\n' "ERROR: refusing uninstall outside the supported root-manager storage" >&2
exit 1
}
PENDING_MODPATH="$MODULE_STORAGE/modules_update/$MODULE_ID"
ZAPRET_DIR="$MODPATH/zapret2"
SCRIPT_DIR="$ZAPRET_DIR/scripts"
STOP_SCRIPT="$SCRIPT_DIR/zapret-stop.sh"
COMMON_SCRIPT="$SCRIPT_DIR/common.sh"
PURGE_CONTRACT="$SCRIPT_DIR/lifecycle/purge-contract.sh"
EXPECTED_STATE_DIR="/data/adb/zapret2-state"
# Uninstall owns the same fixed privileged namespace as the installer. The
# shared helpers permit STATE_DIR overrides for isolated callers, but an
# environment inherited from a root manager must not redirect live cleanup.
STATE_DIR="$EXPECTED_STATE_DIR"
STOP_STATUS=127
STOP_MAX_ATTEMPTS=3
STOP_RETRY_DELAY_SECONDS=1
LOCK_ACQUIRED=0
LEGACY_RUNTIME_OWNED=0
COMPLETED_ROLLBACK_PENDING=0
ROLLBACK_GENERATION=""
ROLLBACK_ARCHIVE_SHA256=""
# Keep the outer uninstall retry bounded even when another lifecycle caller is
# present. A busy lock is never deleted by this script.
LIFECYCLE_LOCK_WAIT_SECONDS=3
export LIFECYCLE_LOCK_WAIT_SECONDS
LEGACY_PIDFILE="/data/local/tmp/nfqws2.pid"
LEGACY_OWNER_STATE="/data/local/tmp/zapret2-owner.state"
LEGACY_CMDLINE_FILE="/data/local/tmp/nfqws2-cmdline.txt"
LEGACY_STARTUP_LOG="/data/local/tmp/nfqws2-startup.log"
LEGACY_ERROR_LOG="/data/local/tmp/nfqws2-error.log"
LEGACY_DEBUG_LOG="/data/local/tmp/nfqws2-debug.log"
LEGACY_LOGFILE="/data/local/tmp/zapret2.log"
LEGACY_LOGFILE_PREVIOUS="/data/local/tmp/zapret2.log.1"
LEGACY_OWNER_MARKER="/data/local/tmp/zapret2-runtime.owner"
report_warning() {
printf '%s\n' "WARNING: $1" >&2
/system/bin/log -p w -t "Zapret2" "$1" 2>/dev/null
}
report_error() {
printf '%s\n' "ERROR: $1" >&2
/system/bin/log -p e -t "Zapret2" "$1" 2>/dev/null
}
report_notice() {
printf '%s\n' "$1"
/system/bin/log -t "Zapret2" "$1" 2>/dev/null
}
if [ ! -f "$COMMON_SCRIPT" ] || [ -L "$COMMON_SCRIPT" ]; then
report_error "zapret2 common lifecycle helpers are unavailable; state was preserved"
exit 1
fi
MODDIR="$MODPATH"
. "$COMMON_SCRIPT"
command -v audit_recovery_artifacts >/dev/null 2>&1 || {
report_error "common lifecycle helpers lack the recovery-artifact audit"
exit 1
}
INSTALL_GENERATION_META="$MODPATH/zapret2/install-generation.meta"
valid_archive_sha256() {
[ "${#1}" -eq 64 ] 2>/dev/null || return 1
case "$1" in *[!0-9a-f]*) return 1 ;; *) return 0 ;; esac
}
# The generation record is parsed by common.sh, which authenticates it against
# MODDIR — set to MODPATH above, so this script checks the same identity with
# the stricter implementation (it also proves the link count).
read_completed_rollback_meta() {
local key value version="" module="" token="" generation="" archive=""
local completed="" complete="" diagnostic="" seen="" size
state_file_is_secure "$FULL_ROLLBACK_META" && [ -r "$FULL_ROLLBACK_META" ] || return 1
size="$(wc -c < "$FULL_ROLLBACK_META" 2>/dev/null)" || return 1
is_decimal "$size" && [ "$size" -gt 0 ] 2>/dev/null && [ "$size" -le 4096 ] 2>/dev/null || return 1
while IFS='=' read -r key value; do
case "$key" in
version) case "$seen" in *v*) return 1 ;; esac; version="$value"; seen="${seen}v" ;;
module_dir) case "$seen" in *m*) return 1 ;; esac; module="$value"; seen="${seen}m" ;;
token) case "$seen" in *t*) return 1 ;; esac; token="$value"; seen="${seen}t" ;;
generation) case "$seen" in *g*) return 1 ;; esac; generation="$value"; seen="${seen}g" ;;
archive_sha256) case "$seen" in *a*) return 1 ;; esac; archive="$value"; seen="${seen}a" ;;
completed_epoch) case "$seen" in *e*) return 1 ;; esac; completed="$value"; seen="${seen}e" ;;
complete) case "$seen" in *c*) return 1 ;; esac; complete="$value"; seen="${seen}c" ;;
diagnostic) case "$seen" in *d*) return 1 ;; esac; diagnostic="$value"; seen="${seen}d" ;;
*) return 1 ;;
esac
done < "$FULL_ROLLBACK_META"
[ "${#seen}" -eq 8 ] 2>/dev/null &&
[ "$version" = "$FULL_ROLLBACK_VERSION" ] && [ "$module" = "$MODPATH" ] &&
is_safe_token "$token" && is_safe_token "$generation" && [ "${#generation}" -le 128 ] 2>/dev/null &&
valid_archive_sha256 "$archive" && is_decimal "$completed" && [ "$complete" = 1 ] &&
[ -n "$diagnostic" ] || return 1
ROLLBACK_META_GENERATION="$generation"
ROLLBACK_META_ARCHIVE_SHA256="$archive"
}
authenticate_completed_rollback_generation() {
read_completed_rollback_meta && read_install_generation_meta || return 1
[ "$ROLLBACK_META_GENERATION" = "$INSTALL_META_GENERATION" ] &&
[ "$ROLLBACK_META_ARCHIVE_SHA256" = "$INSTALL_META_ARCHIVE_SHA256" ] || return 1
ROLLBACK_GENERATION="$ROLLBACK_META_GENERATION"
ROLLBACK_ARCHIVE_SHA256="$ROLLBACK_META_ARCHIVE_SHA256"
COMPLETED_ROLLBACK_PENDING=1
}
state_dir_resolves_exactly() {
local resolved
[ "$STATE_DIR" = "$EXPECTED_STATE_DIR" ] || return 1
[ -d "$STATE_DIR" ] && [ ! -L "$STATE_DIR" ] || return 1
resolved="$(CDPATH=; cd "$STATE_DIR" 2>/dev/null && pwd -P)" || return 1
[ "$resolved" = "$EXPECTED_STATE_DIR" ]
}
legacy_lock_present() {
local path
for path in \
/data/local/tmp/zapret2-lifecycle.lock \
/data/local/tmp/zapret2-lifecycle.lock.*; do
if [ -e "$path" ] || [ -L "$path" ]; then
LEGACY_LOCK_PATH="$path"
return 0
fi
done
LEGACY_LOCK_PATH=""
return 1
}
legacy_runtime_file_present() {
local path
for path in \
"$LEGACY_PIDFILE" \
"$LEGACY_OWNER_STATE" \
"$LEGACY_CMDLINE_FILE" \
"$LEGACY_STARTUP_LOG" \
"$LEGACY_ERROR_LOG" \
"$LEGACY_DEBUG_LOG" \
"$LEGACY_LOGFILE" \
"$LEGACY_LOGFILE_PREVIOUS"; do
if [ -e "$path" ] || [ -L "$path" ]; then
return 0
fi
done
return 1
}
read_legacy_owner_marker() {
local key value version="" module="" nfqws=""
local seen_version=0 seen_module=0 seen_nfqws=0
[ -f "$LEGACY_OWNER_MARKER" ] && [ ! -L "$LEGACY_OWNER_MARKER" ] || return 1
while IFS='=' read -r key value; do
case "$key" in
version)
[ "$seen_version" -eq 0 ] || return 1
version="$value"; seen_version=1
;;
module_dir)
[ "$seen_module" -eq 0 ] || return 1
module="$value"; seen_module=1
;;
nfqws)
[ "$seen_nfqws" -eq 0 ] || return 1
nfqws="$value"; seen_nfqws=1
;;
*)
return 1
;;
esac
done < "$LEGACY_OWNER_MARKER"
[ "$version" = 1 ] &&
[ "$module" = "$MODPATH" ] &&
[ "$nfqws" = "$ZAPRET_DIR/nfqws2" ]
}
validate_legacy_cleanup_set() {
local path
if [ -e "$LEGACY_OWNER_MARKER" ] || [ -L "$LEGACY_OWNER_MARKER" ]; then
if ! read_legacy_owner_marker; then
LEGACY_AUDIT_ERROR="legacy ownership marker is malformed, unsafe, or belongs to another installation"
return 1
fi
LEGACY_RUNTIME_OWNED=1
elif legacy_runtime_file_present; then
LEGACY_AUDIT_ERROR="legacy runtime files exist without a strong Zapret2 ownership marker"
return 1
else
LEGACY_RUNTIME_OWNED=0
return 0
fi
for path in \
"$LEGACY_PIDFILE" \
"$LEGACY_OWNER_STATE" \
"$LEGACY_CMDLINE_FILE" \
"$LEGACY_STARTUP_LOG" \
"$LEGACY_ERROR_LOG" \
"$LEGACY_DEBUG_LOG" \
"$LEGACY_LOGFILE" \
"$LEGACY_LOGFILE_PREVIOUS"; do
if [ -e "$path" ] || [ -L "$path" ]; then
if [ -L "$path" ] || [ ! -f "$path" ]; then
LEGACY_AUDIT_ERROR="legacy runtime path is a symlink or special file: $path"
return 1
fi
fi
done
return 0
}
firewall_is_clean() {
local family_state
if ! command -v iptables >/dev/null 2>&1 ||
! iptables -t mangle -S OUTPUT >/dev/null 2>&1; then
AUDIT_ERROR="unable to verify IPv4 firewall state"
return 1
fi
owned_family_present iptables; family_state=$?
case "$family_state" in
1) ;;
0) AUDIT_ERROR="owned IPv4 firewall artifacts remain"; return 1 ;;
*) AUDIT_ERROR="unable to query complete IPv4 firewall state"; return 1 ;;
esac
if command -v ip6tables >/dev/null 2>&1; then
if ! ip6tables -t mangle -S OUTPUT >/dev/null 2>&1; then
# A single failed probe is usually a busy xtables lock. Only a
# condition that persists may be accepted: refusing forever would
# fence uninstall on every device whose kernel lacks the IPv6
# mangle backend. What clears the family is the next reboot, which
# this path does not schedule — so the caller is told, not assured.
if ! firewall_family_persistently_unavailable ip6tables; then
AUDIT_ERROR="unable to verify IPv6 firewall state"
return 1
fi
UNINSTALL_IPV6_UNVERIFIED=1
report_warning "Z2_FIREWALL_IPV6_UNVERIFIED: IPv6 mangle backend stayed unavailable; IPv6 ownership could not be re-verified"
return 0
fi
owned_family_present ip6tables; family_state=$?
case "$family_state" in
1) ;;
0) AUDIT_ERROR="owned IPv6 firewall artifacts remain"; return 1 ;;
*) AUDIT_ERROR="unable to query complete IPv6 firewall state"; return 1 ;;
esac
fi
return 0
}
process_is_stopped() {
local candidate
if [ -e "$OWNER_STATE" ] || [ -L "$OWNER_STATE" ]; then
if [ -L "$OWNER_STATE" ] || [ ! -f "$OWNER_STATE" ]; then
AUDIT_ERROR="ownership metadata is a symlink or special file"
return 1
fi
if ! read_owner_state; then
AUDIT_ERROR="ownership metadata is malformed or unsafe"
return 1
fi
if kill -0 "$OWNER_STATE_PID" 2>/dev/null; then
AUDIT_ERROR="owner-state PID $OWNER_STATE_PID is still live"
return 1
fi
fi
if [ -e "$PIDFILE" ] || [ -L "$PIDFILE" ]; then
if [ -L "$PIDFILE" ] || [ ! -f "$PIDFILE" ]; then
AUDIT_ERROR="PID metadata is a symlink or special file"
return 1
fi
candidate="$(cat "$PIDFILE" 2>/dev/null)"
if ! is_decimal "$candidate" || [ "$candidate" -le 0 ] 2>/dev/null; then
AUDIT_ERROR="PID metadata is malformed"
return 1
fi
if kill -0 "$candidate" 2>/dev/null; then
if read_verified_pidfile; then
AUDIT_ERROR="verified nfqws2 PID $VERIFIED_PID is still live"
else
AUDIT_ERROR="unverified live PID $candidate remains in process metadata"
fi
return 1
fi
fi
# Catch an exact module-binary process even if all metadata was lost.
scan_exact_owned_nfqws >/dev/null 2>&1 || {
AUDIT_ERROR="unable to complete exact module-process scan"
return 1
}
if [ -n "$OWNED_SCAN_PIDS" ]; then
AUDIT_ERROR="module nfqws2 process(es) remain without usable metadata: $OWNED_SCAN_PIDS"
return 1
fi
return 0
}
remove_state_regular_file() {
local path="$1"
[ -n "$path" ] || return 1
case "$path" in
"$STATE_DIR"/*) ;;
*) return 1 ;;
esac
if [ -e "$path" ] || [ -L "$path" ]; then
[ ! -L "$path" ] && [ -f "$path" ] || return 1
rm -f "$path" 2>/dev/null || return 1
[ ! -e "$path" ] && [ ! -L "$path" ] || return 1
fi
return 0
}
state_directory_has_preserved_children() {
local entry restore_noglob=0
case "$-" in *f*) restore_noglob=1; set +f ;; esac
set -- "$STATE_DIR"/* "$STATE_DIR"/.[!.]* "$STATE_DIR"/..?*
[ "$restore_noglob" = 1 ] && set -f
for entry in "$@"; do
[ -e "$entry" ] || [ -L "$entry" ] || continue
[ "$entry" = "$UNINSTALL_TOMBSTONE" ] && continue
return 0
done
return 1
}
retire_authenticated_completed_rollback() {
[ "$COMPLETED_ROLLBACK_PENDING" -eq 1 ] || return 0
audit_recovery_artifacts uninstall || return 1
[ "$RECOVERY_ARTIFACT_CLASS" = rollback-complete ] || return 1
read_completed_rollback_meta || return 1
[ "$ROLLBACK_META_GENERATION" = "$ROLLBACK_GENERATION" ] &&
[ "$ROLLBACK_META_ARCHIVE_SHA256" = "$ROLLBACK_ARCHIVE_SHA256" ] || return 1
# The authenticated completion record is the commit marker and is removed
# last. A retry can therefore finish retirement if backup removal succeeds
# but a later durability/removal step fails.
if [ -e "$FULL_ROLLBACK_HOSTS_BACKUP" ] || [ -L "$FULL_ROLLBACK_HOSTS_BACKUP" ]; then
state_file_is_secure "$FULL_ROLLBACK_HOSTS_BACKUP" || return 1
remove_state_regular_file "$FULL_ROLLBACK_HOSTS_BACKUP" || return 1
command -v sync >/dev/null 2>&1 && sync >/dev/null 2>&1 || return 1
fi
read_completed_rollback_meta || return 1
[ "$ROLLBACK_META_GENERATION" = "$ROLLBACK_GENERATION" ] &&
[ "$ROLLBACK_META_ARCHIVE_SHA256" = "$ROLLBACK_ARCHIVE_SHA256" ] || return 1
remove_state_regular_file "$FULL_ROLLBACK_META" || return 1
command -v sync >/dev/null 2>&1 && sync >/dev/null 2>&1 || return 1
if ! audit_recovery_artifacts uninstall || [ "$RECOVERY_ARTIFACT_CLASS" != clean ]; then
return 1
fi
COMPLETED_ROLLBACK_PENDING=0
}
remove_legacy_regular_file() {
local path="$1"
case "$path" in
"$LEGACY_PIDFILE"|"$LEGACY_OWNER_STATE"|"$LEGACY_CMDLINE_FILE"|\
"$LEGACY_STARTUP_LOG"|"$LEGACY_ERROR_LOG"|"$LEGACY_DEBUG_LOG"|\
"$LEGACY_LOGFILE"|"$LEGACY_LOGFILE_PREVIOUS"|"$LEGACY_OWNER_MARKER") ;;
*) return 1 ;;
esac
if [ -e "$path" ] || [ -L "$path" ]; then
[ ! -L "$path" ] && [ -f "$path" ] || return 1
rm -f "$path" 2>/dev/null || return 1
[ ! -e "$path" ] && [ ! -L "$path" ] || return 1
fi
return 0
}
unlock_after_signal() {
trap - HUP INT TERM
if [ "$LOCK_ACQUIRED" -eq 1 ]; then
release_lifecycle_lock >/dev/null 2>&1 || true
LOCK_ACQUIRED=0
fi
report_error "Zapret2 uninstall was interrupted; remaining state was preserved"
exit 1
}
fail_while_locked() {
local message="$1"
local unlock_error=""
trap - HUP INT TERM
if [ "$LOCK_ACQUIRED" -eq 1 ]; then
if ! release_lifecycle_lock; then
unlock_error="; lifecycle lock release could not be verified"
fi
LOCK_ACQUIRED=0
fi
report_error "$message$unlock_error"
exit 1
}
publish_uninstall_tombstone() {
local self_start token tmp
self_start="$(proc_starttime "$$")" || return 1
token="$(new_lifecycle_token)" || return 1
is_safe_token "$token" || return 1
if [ -e "$UNINSTALL_TOMBSTONE" ] || [ -L "$UNINSTALL_TOMBSTONE" ]; then
[ ! -L "$UNINSTALL_TOMBSTONE" ] && read_uninstall_tombstone || return 1
uninstall_tombstone_owner_alive && return 1
fi
lock_owner_alive || return 1
[ "$LOCK_FILE_PID" = "$$" ] && [ "$LOCK_FILE_START" = "$self_start" ] || return 1
state_file_target_is_safe "$UNINSTALL_TOMBSTONE" || return 1
tmp="$UNINSTALL_TOMBSTONE.tmp.$$.$token"
state_path_is_managed_file "$tmp" || return 1
[ ! -e "$tmp" ] && [ ! -L "$tmp" ] || return 1
umask 077
{
printf 'version=%s\n' "$UNINSTALL_TOMBSTONE_VERSION"
printf 'pid=%s\nstarttime=%s\ntoken=%s\n' "$$" "$self_start" "$token"
printf 'module_dir=%s\n' "$MODPATH"
} > "$tmp" || { rm -f "$tmp"; return 1; }
chmod 0600 "$tmp" 2>/dev/null || { rm -f "$tmp"; return 1; }
mv -f "$tmp" "$UNINSTALL_TOMBSTONE" 2>/dev/null || { rm -f "$tmp"; return 1; }
state_file_is_secure "$UNINSTALL_TOMBSTONE" && read_uninstall_tombstone || return 1
[ "$UNINSTALL_FILE_PID" = "$$" ] && [ "$UNINSTALL_FILE_START" = "$self_start" ] &&
[ "$UNINSTALL_FILE_TOKEN" = "$token" ] &&
[ "$UNINSTALL_FILE_MODULE" = "$MODPATH" ] || return 1
ZAPRET2_UNINSTALL_TOKEN="$token"
ZAPRET2_UNINSTALL_OWNER_PID="$$"
ZAPRET2_UNINSTALL_OWNER_START="$self_start"
export ZAPRET2_UNINSTALL_TOKEN ZAPRET2_UNINSTALL_OWNER_PID ZAPRET2_UNINSTALL_OWNER_START
uninstall_tombstone_allows_stop
}
manager_removal_marker_is_exact() {
local marker="$MODPATH/remove" size
[ -f "$marker" ] && [ ! -L "$marker" ] && path_uid_is_root "$marker" &&
path_nlink_is_one "$marker" || return 1
size="$(wc -c < "$marker" 2>/dev/null)" || return 1
[ "$size" = 0 ]
}
manager_remove_all_owned_state() {
local rc
# This branch also runs from the Action purge while the device is up, so a
# lifecycle transaction may be mid-flight. Removing its state directory —
# including the lock it holds — would let that transaction republish a
# daemon and rules that nothing owns any more. At boot the lock is free;
# a lock left behind by a dead previous-boot owner is not a live holder
# and must not fence the root manager's own removal.
if acquire_lifecycle_lock; then
:
else
classify_lifecycle_lock
if [ "$LIFECYCLE_OBSERVED_STATE" = active ]; then
report_error "A live Zapret2 lifecycle transaction owns the module; removal was refused"
return 1
fi
# "ambiguous" means the owner record could not be parsed, not that
# someone holds it — and such a record is a permanent hard-fail for
# every other entry point, so refusing here would leave the private
# state tree with nothing able to remove it. The steps below stop
# every exact owned process and purge the namespace regardless.
report_warning "No live lifecycle owner holds the module ($LIFECYCLE_OBSERVED_STATE); continuing root-manager removal"
fi
manager_remove_locked_state
rc=$?
# The success path removed the state directory together with the lock it
# published; on failure the lock must not outlive this process, or every
# later observation reports a recovery barrier against a dead owner.
if [ "$rc" -ne 0 ] && [ "${LOCK_HELD:-0}" = 1 ] &&
{ [ -e "$LIFECYCLE_LOCK" ] || [ -L "$LIFECYCLE_LOCK" ]; }; then
release_lifecycle_lock >/dev/null 2>&1 || true
fi
LOCK_HELD=0
return "$rc"
}
manager_remove_locked_state() {
local tool pending_nfqws="$PENDING_MODPATH/zapret2/nfqws2"
[ -f "$PURGE_CONTRACT" ] && [ ! -L "$PURGE_CONTRACT" ] || {
report_error "Root-manager removal cleanup contract is unavailable"
return 1
}
. "$PURGE_CONTRACT" || return 1
# The root-owned empty module remove marker is the durable global fence.
# zapret-start and every mutation entry refuse work while it exists, so no
# lifecycle tombstone is needed after the whole private state tree is gone.
stop_all_exact_owned_nfqws_for_path "$NFQWS2" || {
report_error "Unable to stop every exact live-module nfqws2 process"
return 1
}
if [ -f "$pending_nfqws" ] && [ ! -L "$pending_nfqws" ]; then
stop_all_exact_owned_nfqws_for_path "$pending_nfqws" || {
report_error "Unable to stop every exact staged-module nfqws2 process"
return 1
}
fi
for tool in iptables ip6tables; do
if ! command -v "$tool" >/dev/null 2>&1; then
[ "$tool" = ip6tables ] && continue
report_error "Unable to access $tool during root-manager removal"
return 1
fi
purge_zapret2_namespace "$tool" && continue
# A device whose IPv6 mangle table does not exist at all would
# otherwise strand the private state tree forever, and removal is
# already committed with a reboot to follow. But a busy xtables lock
# looks identical to a missing table in a single probe, so require the
# condition to persist before accepting it — otherwise a moment of
# contention with netd would silently leave real rules behind.
if [ "$tool" = ip6tables ] && firewall_family_persistently_unavailable ip6tables; then
# Marker consumed by zapret-purge.sh: the caller must not
# report a verified-clean firewall after this.
MANAGER_REMOVE_IPV6_UNVERIFIED=1
report_warning "Z2_FIREWALL_IPV6_UNVERIFIED: IPv6 mangle backend stayed unavailable; any IPv6 rules are left to the pending reboot"
continue
fi
report_error "Unable to remove the strict Zapret2 namespace from $tool"
return 1
done
if [ -e "$STATE_DIR" ] || [ -L "$STATE_DIR" ]; then
state_dir_is_secure && state_dir_resolves_exactly || {
report_error "Zapret2 state path is not the exact secure owned directory"
return 1
}
fi
z2_purge_remove_managed_tree "$Z2_PURGE_CANONICAL_PENDING_DIR" || return 1
z2_purge_remove_external_workspaces || return 1
z2_purge_remove_legacy_files || return 1
z2_purge_remove_managed_tree "$Z2_PURGE_CANONICAL_STATE_DIR" || return 1
sync >/dev/null 2>&1 || return 1
[ ! -e "$STATE_DIR" ] && [ ! -L "$STATE_DIR" ] || return 1
if [ "${MANAGER_REMOVE_IPV6_UNVERIFIED:-0}" = 1 ]; then
report_notice "Root-manager removal marker verified; all Zapret2 service and private state was removed, and the unverifiable IPv6 ruleset is cleared by the pending reboot"
else
report_notice "Root-manager removal marker verified; all Zapret2 service, firewall, and private state was removed"
fi
return 0
}
if module_removal_pending; then
if ! manager_removal_marker_is_exact; then
report_error "Root-manager removal marker is unsafe; refusing uninstall"
exit 1
fi
manager_remove_all_owned_state
exit $?
else
report_warning "Root-manager removal marker is absent (direct invocation); relying on the persistent uninstall tombstone"
fi
if [ -e "$STATE_DIR" ] || [ -L "$STATE_DIR" ]; then
if ! state_dir_is_secure || ! state_dir_resolves_exactly; then
report_error "Zapret2 state directory is unsafe or does not resolve to the exact owned path"
exit 1
fi
elif ! ensure_state_dir || ! state_dir_resolves_exactly; then
report_error "Unable to create the secure state directory required for serialized uninstall"
exit 1
fi
if ! audit_recovery_artifacts uninstall; then
report_error "Recovery artifacts block uninstall before mutation: ${RECOVERY_ARTIFACT_DIAGNOSTIC:-unsafe recovery state}"
exit 1
fi
if legacy_lock_present; then
report_error "Legacy lifecycle lock is present and was preserved: $LEGACY_LOCK_PATH"
exit 1
fi
LEGACY_AUDIT_ERROR=""
if ! validate_legacy_cleanup_set; then
report_error "$LEGACY_AUDIT_ERROR; ambiguous legacy files were preserved"
exit 1
fi
# Hold one lifecycle lock continuously across stop, final audit, and metadata
# cleanup. zapret-stop.sh inherits the lock and therefore cannot race another
# start/stop caller; only this parent may release it.
if ! acquire_lifecycle_lock; then
report_error "Zapret2 lifecycle is busy; state and locks were preserved"
exit 1
fi
LOCK_ACQUIRED=1
trap unlock_after_signal HUP INT TERM
if ! audit_recovery_artifacts uninstall; then
fail_while_locked "Recovery artifacts changed while uninstall was serializing: ${RECOVERY_ARTIFACT_DIAGNOSTIC:-unsafe recovery state}"
fi
case "$RECOVERY_ARTIFACT_CLASS" in
clean) ;;
rollback-complete)
authenticate_completed_rollback_generation ||
fail_while_locked "Completed rollback does not authenticate to this installed generation/archive"
;;
*) fail_while_locked "Unexpected recovery-artifact class blocks uninstall: $RECOVERY_ARTIFACT_CLASS" ;;
esac
# Publish the persistent start gate while the uninstall parent still owns the
# lifecycle lock. zapret-stop.sh receives the exact live owner credentials;
# every other start/stop/restart caller is refused. The tombstone deliberately
# survives successful uninstall until a fully staged reinstall clears it.
if ! publish_uninstall_tombstone; then
fail_while_locked "Unable to publish and verify the uninstall tombstone; state was preserved"
fi
attempt=1
while [ "$attempt" -le "$STOP_MAX_ATTEMPTS" ]; do
if [ ! -f "$STOP_SCRIPT" ] || [ -L "$STOP_SCRIPT" ]; then
STOP_STATUS=127
report_warning "zapret-stop.sh is unavailable during uninstall"
break
fi
/system/bin/sh "$STOP_SCRIPT"
STOP_STATUS=$?
[ "$STOP_STATUS" -eq 0 ] && break
report_warning "zapret-stop.sh attempt $attempt/$STOP_MAX_ATTEMPTS failed during uninstall (exit $STOP_STATUS)"
if [ "$attempt" -lt "$STOP_MAX_ATTEMPTS" ]; then
sleep "$STOP_RETRY_DELAY_SECONDS"
fi
attempt=$((attempt + 1))
done
if [ "$STOP_STATUS" -ne 0 ]; then
fail_while_locked "Zapret2 stop did not complete after bounded retries; process/firewall state was preserved"
fi
AUDIT_ERROR=""
if ! process_is_stopped || ! firewall_is_clean; then
fail_while_locked "Zapret2 final shutdown verification failed: ${AUDIT_ERROR:-unknown state}; metadata was preserved"
fi
if ! audit_recovery_artifacts uninstall; then
fail_while_locked "Recovery-artifact commit audit failed: ${RECOVERY_ARTIFACT_DIAGNOSTIC:-unsafe recovery state}"
fi
case "$RECOVERY_ARTIFACT_CLASS:$COMPLETED_ROLLBACK_PENDING" in
clean:0) ;;
rollback-complete:1)
read_completed_rollback_meta && read_install_generation_meta &&
[ "$ROLLBACK_META_GENERATION" = "$ROLLBACK_GENERATION" ] &&
[ "$ROLLBACK_META_ARCHIVE_SHA256" = "$ROLLBACK_ARCHIVE_SHA256" ] &&
[ "$INSTALL_META_GENERATION" = "$ROLLBACK_GENERATION" ] &&
[ "$INSTALL_META_ARCHIVE_SHA256" = "$ROLLBACK_ARCHIVE_SHA256" ] ||
fail_while_locked "Completed rollback identity changed before uninstall commit"
;;
*) fail_while_locked "Recovery-artifact class changed before uninstall commit" ;;
esac
if ! retire_authenticated_completed_rollback; then
fail_while_locked "Authenticated completed rollback generation/hosts backup could not be retired safely"
fi
# Remove only named regular metadata while the lifecycle is serialized. The
# active lifecycle lock and any unknown child are deliberately not removed.
STATE_CLEANUP_ERROR=""
for state_file in \
"$PIDFILE" \
"$OWNER_STATE" \
"$CMDLINE_FILE" \
"$STARTUP_LOG" \
"$ERROR_LOG" \
"$DEBUG_LOG" \
"$LOGFILE" \
"$LOGFILE_PREVIOUS" \
"$STATUS_SNAPSHOT" \
"$COMPILED_ARGV_FILE" \
"$COMPILED_VALIDATION_RECEIPT" \
"$RUNTIME_OWNER_MARKER" \
"$LEGACY_MIGRATION_MARKER" \
"$PURGE_REQUEST"; do
if ! remove_state_regular_file "$state_file"; then
STATE_CLEANUP_ERROR="unsafe or unremovable state entry: $state_file"
break
fi
done
# The scratch directory holds only PID-suffixed ephemeral files; crash
# residue there is disposable by construction and must never block uninstall.
if [ -z "$STATE_CLEANUP_ERROR" ] && { [ -e "$Z2_STATE_TMP" ] || [ -L "$Z2_STATE_TMP" ]; }; then
if [ -d "$Z2_STATE_TMP" ] && [ ! -L "$Z2_STATE_TMP" ]; then
rm -rf "$Z2_STATE_TMP" 2>/dev/null || STATE_CLEANUP_ERROR="scratch directory could not be removed"
else
rm -f "$Z2_STATE_TMP" 2>/dev/null || STATE_CLEANUP_ERROR="unsafe scratch entry could not be removed"
fi
fi
# Staging files are never authoritative: each one is a partial write whose
# atomic rename never happened. Creator-liveness sweeping can miss them when a
# dead creator's PID gets reused, and an uninstall that leaves them behind
# fails as "unknown state entries were preserved" on every retry, forever.
if [ -z "$STATE_CLEANUP_ERROR" ]; then
for staging_file in "$STATE_DIR"/*.tmp.*; do
{ [ -e "$staging_file" ] || [ -L "$staging_file" ]; } || continue
rm -rf "$staging_file" 2>/dev/null ||
STATE_CLEANUP_ERROR="staging residue could not be removed: $staging_file"
done
fi
if [ -n "$STATE_CLEANUP_ERROR" ]; then
fail_while_locked "$STATE_CLEANUP_ERROR; state directory was preserved"
fi
trap - HUP INT TERM
if ! release_lifecycle_lock; then
LOCK_ACQUIRED=0
report_error "Lifecycle lock release could not be verified; state directory was preserved"
exit 1
fi
LOCK_ACQUIRED=0
# The state directory intentionally remains as a tiny root-only tombstone
# carrier. Removing it after unlocking would reopen the final-start race.
if ! state_dir_is_secure || ! state_dir_resolves_exactly; then
report_error "State directory changed during uninstall; it was preserved"
exit 1
fi
for lock_artifact in "$STATE_DIR"/lifecycle.lock*; do
if [ -e "$lock_artifact" ] || [ -L "$lock_artifact" ]; then
report_error "Lifecycle lock artifact appeared during uninstall; the state directory was preserved"
exit 1
fi
done
if ! state_file_is_secure "$UNINSTALL_TOMBSTONE" || ! read_uninstall_tombstone ||
[ "$UNINSTALL_FILE_PID" != "$$" ] ||
[ "$UNINSTALL_FILE_TOKEN" != "$ZAPRET2_UNINSTALL_TOKEN" ] ||
[ "$UNINSTALL_FILE_MODULE" != "$MODPATH" ]; then
report_error "Persistent uninstall tombstone changed after lifecycle unlock; manual repair is required"
exit 1
fi
if legacy_lock_present; then
report_error "Legacy lock appeared during uninstall and was preserved: $LEGACY_LOCK_PATH"
exit 1
fi
if [ "$LEGACY_RUNTIME_OWNED" -eq 1 ]; then
# Revalidate the marker and every target immediately before touching the
# public legacy parent. The exact ownership marker is removed last.
LEGACY_AUDIT_ERROR=""
if ! validate_legacy_cleanup_set; then
report_error "$LEGACY_AUDIT_ERROR; legacy state was preserved"
exit 1
fi
for legacy_file in \
"$LEGACY_PIDFILE" \
"$LEGACY_OWNER_STATE" \
"$LEGACY_CMDLINE_FILE" \
"$LEGACY_STARTUP_LOG" \
"$LEGACY_ERROR_LOG" \
"$LEGACY_DEBUG_LOG" \
"$LEGACY_LOGFILE" \
"$LEGACY_LOGFILE_PREVIOUS"; do
if ! remove_legacy_regular_file "$legacy_file"; then
report_error "Verified legacy runtime file could not be removed safely: $legacy_file"
exit 1
fi
done
if ! read_legacy_owner_marker || ! remove_legacy_regular_file "$LEGACY_OWNER_MARKER"; then
report_error "Verified legacy ownership marker changed or could not be removed safely"
exit 1
fi
if legacy_runtime_file_present || [ -e "$LEGACY_OWNER_MARKER" ] || [ -L "$LEGACY_OWNER_MARKER" ]; then
report_error "Legacy runtime cleanup could not be verified"
exit 1
fi
fi
# Unknown root-state children are deliberately never deleted. Their presence
# means the owned service/firewall cleanup completed only partially from the
# installer's perspective, so keep the tombstone gate and return a non-success
# result instead of claiming the whole state directory is clean.
if state_directory_has_preserved_children; then
report_error "Zapret2 service/firewall cleanup completed, but unknown state entries were preserved; uninstall cleanup is partial"
exit 1
fi
if [ "${UNINSTALL_IPV6_UNVERIFIED:-0}" = 1 ]; then
# The uninstall did everything it owns, but one family could not be
# re-read. Saying "verified clean" would claim a proof this run does not
# have. The manager path exits above, so this is a direct invocation: no
# removal marker exists, nothing schedules the reboot that would clear the
# family, and the state that could identify leftover rules is now gone.
# Name the one action that ends the ambiguity instead of implying it is
# already under way.
echo "Zapret2 stopped and uninstalled; the IPv6 mangle backend stayed unavailable, so owned IPv6 rules could not be re-verified. Reboot to clear any that remain"
else
echo "Zapret2 stopped, verified clean, and uninstalled"
fi
exit 0