magisk-zapret2/zapret2/scripts/lifecycle-lease.sh
2026-07-26 15:10:58 +03:00

372 lines
17 KiB
Shell

#!/system/bin/sh
# Cross-process ownership protocol for ordinary Android-side module writes.
#
# The app used to inline this whole protocol into its persistent root shell,
# where the interpreter shim cannot apply and every fork pays the full mksh
# image tax. As a packaged entry script it runs under the standalone-ash
# interpreter and the lease ceremony costs a fraction of a second.
#
# The record intentionally remains readable by common.sh (pid/starttime/token),
# while the additional exact fields let Android distinguish its own stale
# records from foreign lifecycle owners. Unknown or malformed lifecycle locks
# are never removed by this protocol.
#
# Usage:
# lifecycle-lease.sh acquire APP_PID TOKEN
# lifecycle-lease.sh probe APP_PID TOKEN
# lifecycle-lease.sh release PID STARTTIME BOOT_ID TOKEN RELEASE_TOKEN
#
# stdout contracts (parsed by the app; byte-stable):
# acquire/probe success: 5 Z2_MUTATION_LOCK_* lines ending COMPLETE=1
# probe with no lock: Z2_MUTATION_LOCK_ABSENT=1
# release success: Z2_MUTATION_LOCK_RELEASED=1
case "$0" in
/*//*|/*/./*|/*/../*|*/..|*/.) SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" ;;
/*/*) SCRIPT_DIR="${0%/*}" ;;
*) SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" ;;
esac
ZAPRET_DIR="${SCRIPT_DIR%/*}"
MODDIR="${ZAPRET_DIR%/*}"
ZAPRET2_LAZY_FIREWALL_RECONCILER=1
LIFECYCLE_LOCK_WAIT_SECONDS=1
. "$SCRIPT_DIR/common.sh"
Z2_LEASE_VERSION=1
Z2_LEASE_KIND=android-mutation
module_dir="$MODDIR"
lease_usage_error() {
echo "ERROR: usage: lifecycle-lease.sh acquire|probe|release ..." >&2
exit 2
}
is_safe_lease_token() {
is_safe_token "$1" && [ "${#1}" -le 128 ] 2>/dev/null
}
# Exact-schema reader for the android-mutation owner record. Field order and
# multiplicity are pinned; anything else is a foreign owner and is preserved.
z2_read_android_mutation_owner() {
local key value size links
z2_owner_version=""; z2_owner_kind=""; z2_owner_pid=""; z2_owner_start=""
z2_owner_boot=""; z2_owner_token=""; z2_owner_module=""
z2_owner_order=""
z2_seen_version=0; z2_seen_kind=0; z2_seen_pid=0; z2_seen_start=0
z2_seen_boot=0; z2_seen_token=0; z2_seen_module=0
state_dir_is_secure || return 1
[ -d "$LIFECYCLE_LOCK" ] && [ ! -L "$LIFECYCLE_LOCK" ] || return 1
path_uid_is_root "$LIFECYCLE_LOCK" || return 1
[ "$(stat -c %a "$LIFECYCLE_LOCK" 2>/dev/null)" = 700 ] || return 1
[ -f "$LIFECYCLE_LOCK_OWNER" ] && [ ! -L "$LIFECYCLE_LOCK_OWNER" ] || return 1
path_uid_is_root "$LIFECYCLE_LOCK_OWNER" || return 1
[ "$(stat -c %a "$LIFECYCLE_LOCK_OWNER" 2>/dev/null)" = 600 ] || return 1
links=$(stat -c %h "$LIFECYCLE_LOCK_OWNER" 2>/dev/null) || return 1
[ "$links" = 1 ] || return 1
size=$(stat -c %s "$LIFECYCLE_LOCK_OWNER" 2>/dev/null) || return 1
[ "$size" -gt 0 ] && [ "$size" -le 1024 ] || return 1
while IFS='=' read -r key value; do
case "$key" in
version) [ "$z2_seen_version" = 0 ] || return 1; z2_owner_version="$value"; z2_seen_version=1; z2_owner_order="${z2_owner_order}v" ;;
kind) [ "$z2_seen_kind" = 0 ] || return 1; z2_owner_kind="$value"; z2_seen_kind=1; z2_owner_order="${z2_owner_order}k" ;;
pid) [ "$z2_seen_pid" = 0 ] || return 1; z2_owner_pid="$value"; z2_seen_pid=1; z2_owner_order="${z2_owner_order}p" ;;
starttime) [ "$z2_seen_start" = 0 ] || return 1; z2_owner_start="$value"; z2_seen_start=1; z2_owner_order="${z2_owner_order}s" ;;
boot_id) [ "$z2_seen_boot" = 0 ] || return 1; z2_owner_boot="$value"; z2_seen_boot=1; z2_owner_order="${z2_owner_order}b" ;;
token) [ "$z2_seen_token" = 0 ] || return 1; z2_owner_token="$value"; z2_seen_token=1; z2_owner_order="${z2_owner_order}t" ;;
module_dir) [ "$z2_seen_module" = 0 ] || return 1; z2_owner_module="$value"; z2_seen_module=1; z2_owner_order="${z2_owner_order}m" ;;
*) return 1 ;;
esac
done < "$LIFECYCLE_LOCK_OWNER"
[ "$z2_seen_version:$z2_seen_kind:$z2_seen_pid:$z2_seen_start:$z2_seen_boot:$z2_seen_token:$z2_seen_module" = 1:1:1:1:1:1:1 ] || return 1
[ "$z2_owner_order" = vkpsbtm ] || return 1
[ "$z2_owner_version" = "$Z2_LEASE_VERSION" ] && [ "$z2_owner_kind" = "$Z2_LEASE_KIND" ] || return 1
is_decimal "$z2_owner_pid" && [ "$z2_owner_pid" -gt 0 ] 2>/dev/null || return 1
is_canonical_nonnegative_i64 "$z2_owner_start" || return 1
is_valid_boot_id "$z2_owner_boot" || return 1
is_safe_lease_token "$z2_owner_token" || return 1
[ "$z2_owner_module" = "$module_dir" ]
}
z2_android_mutation_owner_state() {
z2_owner_state=ambiguous
if [ "$z2_owner_boot" != "$current_boot" ]; then
z2_owner_state=stale
return 1
fi
if [ ! -e "/proc/$z2_owner_pid" ]; then
z2_owner_state=stale
return 1
fi
proc_starttime_read "$z2_owner_pid" || return 2
if [ "$PROC_STARTTIME" = "$z2_owner_start" ]; then
z2_owner_state=active
return 0
fi
z2_owner_state=stale
return 1
}
lease_emit() {
echo "Z2_MUTATION_LOCK_PID=$1"
echo "Z2_MUTATION_LOCK_START=$2"
echo "Z2_MUTATION_LOCK_BOOT=$3"
echo "Z2_MUTATION_LOCK_TOKEN=$4"
echo Z2_MUTATION_LOCK_COMPLETE=1
}
lease_acquire() {
local app_pid="$1" app_token="$2"
local self_start app_start app_start_after current_boot boot_after
local stale_pid stale_start stale_boot stale_token stale_module
local entries quarantine candidate_size candidate_entries
is_decimal "$app_pid" && [ "$app_pid" -gt 0 ] 2>/dev/null &&
is_safe_lease_token "$app_token" || lease_usage_error
z2_candidate=""
z2_gate_held=0
z2_gate_token="android.$app_token"
z2_cleanup_acquire() {
local rc=$?
trap - EXIT HUP INT TERM
if [ -n "$z2_candidate" ] && [ -d "$z2_candidate" ] && [ ! -L "$z2_candidate" ]; then
if [ -f "$z2_candidate/owner" ] && [ ! -L "$z2_candidate/owner" ]; then
rm -f "$z2_candidate/owner" 2>/dev/null || rc=1
fi
rmdir "$z2_candidate" 2>/dev/null || rc=1
fi
if [ "$z2_gate_held" = 1 ]; then
release_lifecycle_gate "$z2_gate_token" >/dev/null 2>&1 || rc=1
fi
exit "$rc"
}
trap z2_cleanup_acquire EXIT
trap 'exit 1' HUP INT TERM
ensure_state_dir || exit 1
proc_starttime_read "$$" || exit 1
self_start="$PROC_STARTTIME"
proc_starttime_read "$app_pid" || exit 1
app_start="$PROC_STARTTIME"
IFS= read -r current_boot < /proc/sys/kernel/random/boot_id || exit 1
is_valid_boot_id "$current_boot" || exit 1
claim_lifecycle_gate "$self_start" "$z2_gate_token" || {
echo "ERROR: Zapret2 lifecycle is busy; changes were not started" >&2
exit 1
}
z2_gate_held=1
if [ -e "$LIFECYCLE_LOCK" ] || [ -L "$LIFECYCLE_LOCK" ]; then
z2_read_android_mutation_owner || {
echo "ERROR: foreign, malformed, or unknown lifecycle owner was preserved" >&2
exit 1
}
stale_pid="$z2_owner_pid"; stale_start="$z2_owner_start"; stale_boot="$z2_owner_boot"
stale_token="$z2_owner_token"; stale_module="$z2_owner_module"
z2_android_mutation_owner_state >/dev/null 2>&1 || true
[ "$z2_owner_state" = stale ] || {
echo "ERROR: active or ambiguous lifecycle owner blocks mutation" >&2
exit 1
}
sleep 1
z2_read_android_mutation_owner || exit 1
[ "$z2_owner_pid" = "$stale_pid" ] &&
[ "$z2_owner_start" = "$stale_start" ] &&
[ "$z2_owner_boot" = "$stale_boot" ] &&
[ "$z2_owner_token" = "$stale_token" ] &&
[ "$z2_owner_module" = "$stale_module" ] || exit 1
z2_android_mutation_owner_state >/dev/null 2>&1 || true
[ "$z2_owner_state" = stale ] || exit 1
entries=$(find "$LIFECYCLE_LOCK" -mindepth 1 -maxdepth 1 -print 2>/dev/null) || exit 1
[ "$entries" = "$LIFECYCLE_LOCK_OWNER" ] || {
echo "ERROR: lifecycle lock contains unknown entries; it was preserved" >&2
exit 1
}
quarantine="$LIFECYCLE_LOCK_QUARANTINE.android.$$.$app_token"
[ ! -e "$quarantine" ] && [ ! -L "$quarantine" ] || exit 1
mv "$LIFECYCLE_LOCK" "$quarantine" || exit 1
rm -f "$quarantine/owner" || exit 1
rmdir "$quarantine" || exit 1
fi
z2_candidate="$LIFECYCLE_LOCK.candidate.android.$$.$app_token"
[ ! -e "$z2_candidate" ] && [ ! -L "$z2_candidate" ] || exit 1
mkdir "$z2_candidate" || exit 1
chmod 0700 "$z2_candidate" || exit 1
printf 'version=%s\nkind=%s\npid=%s\nstarttime=%s\nboot_id=%s\ntoken=%s\nmodule_dir=%s\n' \
"$Z2_LEASE_VERSION" "$Z2_LEASE_KIND" \
"$app_pid" "$app_start" "$current_boot" "$app_token" "$module_dir" > "$z2_candidate/owner" || exit 1
chmod 0600 "$z2_candidate/owner" || exit 1
[ "$(stat -c %u "$z2_candidate" 2>/dev/null)" = 0 ] || exit 1
[ "$(stat -c %u "$z2_candidate/owner" 2>/dev/null)" = 0 ] || exit 1
[ "$(stat -c %h "$z2_candidate/owner" 2>/dev/null)" = 1 ] || exit 1
candidate_size=$(stat -c %s "$z2_candidate/owner" 2>/dev/null) || exit 1
[ "$candidate_size" -gt 0 ] && [ "$candidate_size" -le 1024 ] || exit 1
candidate_entries=$(find "$z2_candidate" -mindepth 1 -maxdepth 1 -print 2>/dev/null) || exit 1
[ "$candidate_entries" = "$z2_candidate/owner" ] || exit 1
proc_starttime_read "$app_pid" || exit 1
app_start_after="$PROC_STARTTIME"
IFS= read -r boot_after < /proc/sys/kernel/random/boot_id || exit 1
[ "$app_start_after" = "$app_start" ] && [ "$boot_after" = "$current_boot" ] || exit 1
[ ! -e "$LIFECYCLE_LOCK" ] && [ ! -L "$LIFECYCLE_LOCK" ] || exit 1
mv "$z2_candidate" "$LIFECYCLE_LOCK" || exit 1
z2_candidate=""
release_lifecycle_gate "$z2_gate_token" >/dev/null 2>&1 || true
z2_gate_held=0
trap - EXIT HUP INT TERM
lease_emit "$app_pid" "$app_start" "$current_boot" "$app_token"
}
# Probes only the exact record this app may have published after an ambiguous
# command result. Read-only: the before/after content identity proves nothing
# was mutated while the record was being read.
lease_probe() {
local expected_pid="$1" expected_token="$2"
local version_line kind_line pid_line start_line boot_line token_line module_line
local start boot before after current_boot entries
is_decimal "$expected_pid" && [ "$expected_pid" -gt 0 ] 2>/dev/null &&
is_safe_lease_token "$expected_token" || lease_usage_error
state_dir_is_secure || exit 1
if [ ! -e "$LIFECYCLE_LOCK" ] && [ ! -L "$LIFECYCLE_LOCK" ]; then
echo Z2_MUTATION_LOCK_ABSENT=1
exit 0
fi
[ -d "$LIFECYCLE_LOCK" ] && [ ! -L "$LIFECYCLE_LOCK" ] || exit 1
path_uid_is_root "$LIFECYCLE_LOCK" || exit 1
[ "$(stat -c %a "$LIFECYCLE_LOCK" 2>/dev/null)" = 700 ] || exit 1
[ -f "$LIFECYCLE_LOCK_OWNER" ] && [ ! -L "$LIFECYCLE_LOCK_OWNER" ] || exit 1
path_uid_is_root "$LIFECYCLE_LOCK_OWNER" || exit 1
[ "$(stat -c %a "$LIFECYCLE_LOCK_OWNER" 2>/dev/null)" = 600 ] || exit 1
[ "$(stat -c %h "$LIFECYCLE_LOCK_OWNER" 2>/dev/null)" = 1 ] || exit 1
[ "$(wc -l < "$LIFECYCLE_LOCK_OWNER" 2>/dev/null)" = 7 ] || exit 1
before=$(sha256sum "$LIFECYCLE_LOCK_OWNER" 2>/dev/null) || exit 1
before="${before%% *}"
{
IFS= read -r version_line || exit 1
IFS= read -r kind_line || exit 1
IFS= read -r pid_line || exit 1
IFS= read -r start_line || exit 1
IFS= read -r boot_line || exit 1
IFS= read -r token_line || exit 1
IFS= read -r module_line || exit 1
} < "$LIFECYCLE_LOCK_OWNER"
[ "$version_line" = "version=$Z2_LEASE_VERSION" ] && [ "$kind_line" = "kind=$Z2_LEASE_KIND" ] || exit 1
[ "$pid_line" = "pid=$expected_pid" ] && [ "$token_line" = "token=$expected_token" ] || exit 1
[ "$module_line" = "module_dir=$module_dir" ] || exit 1
start="${start_line#starttime=}"; boot="${boot_line#boot_id=}"
[ "$start_line" = "starttime=$start" ] && [ "$boot_line" = "boot_id=$boot" ] || exit 1
is_canonical_nonnegative_i64 "$start" && is_valid_boot_id "$boot" || exit 1
IFS= read -r current_boot < /proc/sys/kernel/random/boot_id || exit 1
[ "$current_boot" = "$boot" ] || exit 1
proc_starttime_read "$expected_pid" || exit 1
[ "$PROC_STARTTIME" = "$start" ] || exit 1
entries=$(find "$LIFECYCLE_LOCK" -mindepth 1 -maxdepth 1 -print 2>/dev/null) || exit 1
[ "$entries" = "$LIFECYCLE_LOCK_OWNER" ] || exit 1
after=$(sha256sum "$LIFECYCLE_LOCK_OWNER" 2>/dev/null) || exit 1
after="${after%% *}"
[ "$after" = "$before" ] || exit 1
lease_emit "$expected_pid" "$start" "$boot" "$expected_token"
}
lease_release() {
local expected_pid="$1" expected_start="$2" expected_boot="$3"
local expected_token="$4" release_token="$5"
local gate_token self_start attempts quarantine
is_decimal "$expected_pid" && [ "$expected_pid" -gt 0 ] 2>/dev/null &&
is_canonical_nonnegative_i64 "$expected_start" &&
is_valid_boot_id "$expected_boot" &&
is_safe_lease_token "$expected_token" &&
is_safe_lease_token "$release_token" || lease_usage_error
gate_token="android-release.$release_token"
z2_read_exact_owner() {
local key value size entries version kind pid start boot token module owner_order
local seen_version seen_kind seen_pid seen_start seen_boot seen_token seen_module
version=""; kind=""; pid=""; start=""; boot=""; token=""; module=""
owner_order=""
seen_version=0; seen_kind=0; seen_pid=0; seen_start=0; seen_boot=0; seen_token=0; seen_module=0
state_dir_is_secure || return 1
[ -d "$LIFECYCLE_LOCK" ] && [ ! -L "$LIFECYCLE_LOCK" ] || return 1
path_uid_is_root "$LIFECYCLE_LOCK" || return 1
[ "$(stat -c %a "$LIFECYCLE_LOCK" 2>/dev/null)" = 700 ] || return 1
[ -f "$LIFECYCLE_LOCK_OWNER" ] && [ ! -L "$LIFECYCLE_LOCK_OWNER" ] || return 1
path_uid_is_root "$LIFECYCLE_LOCK_OWNER" || return 1
[ "$(stat -c %a "$LIFECYCLE_LOCK_OWNER" 2>/dev/null)" = 600 ] || return 1
[ "$(stat -c %h "$LIFECYCLE_LOCK_OWNER" 2>/dev/null)" = 1 ] || return 1
size=$(stat -c %s "$LIFECYCLE_LOCK_OWNER" 2>/dev/null) || return 1
[ "$size" -gt 0 ] && [ "$size" -le 1024 ] || return 1
while IFS='=' read -r key value; do
case "$key" in
version) [ "$seen_version" = 0 ] || return 1; version="$value"; seen_version=1; owner_order="${owner_order}v" ;;
kind) [ "$seen_kind" = 0 ] || return 1; kind="$value"; seen_kind=1; owner_order="${owner_order}k" ;;
pid) [ "$seen_pid" = 0 ] || return 1; pid="$value"; seen_pid=1; owner_order="${owner_order}p" ;;
starttime) [ "$seen_start" = 0 ] || return 1; start="$value"; seen_start=1; owner_order="${owner_order}s" ;;
boot_id) [ "$seen_boot" = 0 ] || return 1; boot="$value"; seen_boot=1; owner_order="${owner_order}b" ;;
token) [ "$seen_token" = 0 ] || return 1; token="$value"; seen_token=1; owner_order="${owner_order}t" ;;
module_dir) [ "$seen_module" = 0 ] || return 1; module="$value"; seen_module=1; owner_order="${owner_order}m" ;;
*) return 1 ;;
esac
done < "$LIFECYCLE_LOCK_OWNER"
[ "$seen_version:$seen_kind:$seen_pid:$seen_start:$seen_boot:$seen_token:$seen_module" = 1:1:1:1:1:1:1 ] || return 1
[ "$owner_order" = vkpsbtm ] || return 1
[ "$version" = "$Z2_LEASE_VERSION" ] && [ "$kind" = "$Z2_LEASE_KIND" ] && [ "$module" = "$module_dir" ] || return 1
[ "$pid" = "$expected_pid" ] && [ "$start" = "$expected_start" ] &&
[ "$boot" = "$expected_boot" ] && [ "$token" = "$expected_token" ] || return 1
entries=$(find "$LIFECYCLE_LOCK" -mindepth 1 -maxdepth 1 -print 2>/dev/null) || return 1
[ "$entries" = "$LIFECYCLE_LOCK_OWNER" ]
}
gate_held=0
z2_cleanup_release() {
local rc=$?
trap - EXIT HUP INT TERM
if [ "$gate_held" = 1 ]; then
release_lifecycle_gate "$gate_token" >/dev/null 2>&1 || rc=1
fi
exit "$rc"
}
trap z2_cleanup_release EXIT
trap 'exit 1' HUP INT TERM
proc_starttime_read "$$" || exit 1
self_start="$PROC_STARTTIME"
attempts=0
while [ "$attempts" -lt 3 ]; do
claim_lifecycle_gate "$self_start" "$gate_token" && break
attempts=$((attempts + 1))
sleep 1
done
[ "$attempts" -lt 3 ] || exit 1
gate_held=1
z2_read_exact_owner || {
release_lifecycle_gate "$gate_token" >/dev/null 2>&1 || true
gate_held=0
echo "ERROR: lifecycle ownership changed; foreign evidence was preserved" >&2
exit 1
}
quarantine="$LIFECYCLE_LOCK_QUARANTINE.release.android.$$.$gate_token"
[ ! -e "$quarantine" ] && [ ! -L "$quarantine" ] || exit 1
mv "$LIFECYCLE_LOCK" "$quarantine" || exit 1
rm -f "$quarantine/owner" || exit 1
rmdir "$quarantine" || exit 1
release_lifecycle_gate "$gate_token" >/dev/null 2>&1 || true
gate_held=0
trap - EXIT HUP INT TERM
echo Z2_MUTATION_LOCK_RELEASED=1
}
case "${1:-}" in
acquire)
[ "$#" -eq 3 ] || lease_usage_error
lease_acquire "$2" "$3"
;;
probe)
[ "$#" -eq 3 ] || lease_usage_error
lease_probe "$2" "$3"
;;
release)
[ "$#" -eq 6 ] || lease_usage_error
lease_release "$2" "$3" "$4" "$5" "$6"
;;
*) lease_usage_error ;;
esac
exit 0