The DNS manager used to apply a selection by writing system/etc/hosts into the live module directory and asking for a reboot, so the root manager would pick it up while assembling /system. That put the module's own tree in the path of whatever the manager does between boots: KernelSU v3 moved that assembly into a separate metamodule, a second hosts module publishing the same path raced it silently, and an upgrade replaced the tree and discarded the user's selection with it. The published file now lives in /data/adb/zapret2-hosts and the module bind-mounts it over /system/etc/hosts from post-fs-data.sh, which is the same mechanism on Magisk, KernelSU and APatch. hosts-overlay.sh owns that mount: it snapshots the untouched system file before covering it, carries a pre-2.3.0 in-tree overlay over and takes it out of the tree, relabels the source so netd can still read it, and yields to another enabled hosts module instead of fighting it for the path. Because the module publishes the mount itself, an edit now takes effect immediately: publication is still an atomic rename, and --apply republishes the mount rather than waiting for a reboot. A rename leaves the live mount on the replaced inode, so the mount identity is recorded when it is published and consulted only against a live mount — a foreign mount is never released. Rollback, uninstall and purge follow the file to its new home, and the installer carries an existing selection across the upgrade that would otherwise drop it. Also releases the tethering runtime key and owner state v9, and lifts the packaged lifecycle contract to 9.
857 lines
34 KiB
Shell
857 lines
34 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_hosts_overlay || 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 durable 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. A failed or interrupted
|
|
# uninstall preserves the tombstone; the successful direct path retires it in
|
|
# its final serialized commit below.
|
|
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 remains a tiny root-only tombstone carrier until every
|
|
# late cleanup step has succeeded. Removing it before the final serialized
|
|
# commit 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
|
|
|
|
# A direct invocation leaves the module package installed. The tombstone is a
|
|
# transaction fence, not permanent state: reacquire the lifecycle lock after
|
|
# every late cleanup, authenticate the same live uninstall owner once more,
|
|
# retire the fence, then release. A crash before this commit preserves the
|
|
# tombstone and continues to block start; a completed uninstall never strands
|
|
# the installed module behind a dead owner.
|
|
trap unlock_after_signal HUP INT TERM
|
|
if ! acquire_lifecycle_lock; then
|
|
report_error "Completed uninstall could not reacquire lifecycle ownership; the tombstone was preserved"
|
|
exit 1
|
|
fi
|
|
LOCK_ACQUIRED=1
|
|
if ! state_file_is_secure "$UNINSTALL_TOMBSTONE" || ! read_uninstall_tombstone ||
|
|
[ "$UNINSTALL_FILE_PID" != "$$" ] ||
|
|
[ "$UNINSTALL_FILE_TOKEN" != "$ZAPRET2_UNINSTALL_TOKEN" ] ||
|
|
[ "$UNINSTALL_FILE_MODULE" != "$MODPATH" ]; then
|
|
fail_while_locked "Completed uninstall tombstone changed before retirement; it was preserved"
|
|
fi
|
|
rm -f "$UNINSTALL_TOMBSTONE" 2>/dev/null ||
|
|
fail_while_locked "Completed uninstall tombstone could not be retired"
|
|
[ ! -e "$UNINSTALL_TOMBSTONE" ] && [ ! -L "$UNINSTALL_TOMBSTONE" ] ||
|
|
fail_while_locked "Completed uninstall tombstone retirement could not be verified"
|
|
if ! release_lifecycle_lock; then
|
|
LOCK_ACQUIRED=0
|
|
report_error "Uninstall committed, but final lifecycle lock release could not be verified"
|
|
exit 1
|
|
fi
|
|
LOCK_ACQUIRED=0
|
|
trap - HUP INT TERM
|
|
|
|
# No authoritative boot-local state remains. An already-racing new start may
|
|
# recreate or occupy this directory after the lock release; in that case rmdir
|
|
# simply leaves the new transaction's state intact.
|
|
rmdir "$STATE_DIR" 2>/dev/null || :
|
|
|
|
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
|