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.
3898 lines
164 KiB
Shell
3898 lines
164 KiB
Shell
#!/system/bin/sh
|
|
# Shared lifecycle, configuration and ownership helpers for zapret2.
|
|
#
|
|
# Durability boundary:
|
|
# Files under STATE_DIR describe boot-local processes, kernel firewall state,
|
|
# lifecycle ownership, and bounded recovery evidence. Atomic rename makes those
|
|
# publications indivisible to same-boot readers and preserves evidence when a
|
|
# shell process exits or is killed. They must not call the global sync command:
|
|
# a reboot removes the processes and netfilter state they describe, while
|
|
# sync() flushes unrelated dirty data from every mounted filesystem.
|
|
#
|
|
# Persistent package, runtime-configuration, rollback, and purge transactions
|
|
# own their durability barriers in their dedicated mutation scripts.
|
|
|
|
# Interpreter shim: Android's mksh pays 100-200 ms for every fork because the
|
|
# loaded lifecycle library makes the process image large, and a transaction
|
|
# forks dozens of times. The root manager's busybox in standalone-ash mode
|
|
# runs most of those helpers as in-process applets, which cuts a preset
|
|
# switch from ~8 s to ~2 s on the same code. Re-exec once, before anything
|
|
# heavy is defined; without a usable busybox the script simply continues
|
|
# under the invoking shell.
|
|
# The re-exec is only valid when $0 is one of the packaged entry scripts
|
|
# invoked by path: the app also sources this library from inline root-shell
|
|
# commands, where $0 is the interactive shell's own name and an exec here
|
|
# would kill the app's persistent root shell instead of re-running a script.
|
|
if [ -z "${Z2_RESHELLED:-}" ]; then
|
|
case "$0" in
|
|
*/zapret2/scripts/*.sh)
|
|
if [ -f "$0" ] && [ ! -L "$0" ]; then
|
|
for Z2_BB in /data/adb/magisk/busybox /data/adb/ksu/bin/busybox \
|
|
/data/adb/ap/bin/busybox; do
|
|
if [ -x "$Z2_BB" ] && [ ! -L "$Z2_BB" ]; then
|
|
Z2_RESHELLED=1 ASH_STANDALONE=1 exec "$Z2_BB" sh "$0" "$@"
|
|
fi
|
|
done
|
|
fi
|
|
;;
|
|
esac
|
|
Z2_RESHELLED=fallback
|
|
fi
|
|
|
|
ZAPRET_DIR="${ZAPRET_DIR:-$(dirname "$SCRIPT_DIR")}"
|
|
MODDIR="${MODDIR:-$(dirname "$ZAPRET_DIR")}"
|
|
FIREWALL_RECONCILER="$SCRIPT_DIR/firewall-reconciler.sh"
|
|
Z2_FIREWALL_RECONCILER_LOADED=0
|
|
|
|
# Most configuration mutations never inspect or publish netfilter state. They
|
|
# used to parse the complete reconciler anyway, making every later fork from
|
|
# Android's mksh copy another large function tree. Callers on those paths set
|
|
# ZAPRET2_LAZY_FIREWALL_RECONCILER=1 and pay for this layer only if an
|
|
# exceptional rollback actually has to dismantle retained rules.
|
|
z2_load_firewall_reconciler() {
|
|
[ "$Z2_FIREWALL_RECONCILER_LOADED" = 1 ] && return 0
|
|
[ -r "$FIREWALL_RECONCILER" ] && [ ! -L "$FIREWALL_RECONCILER" ] || return 1
|
|
. "$FIREWALL_RECONCILER" || return 1
|
|
command -v z2_fw_reconcile_family >/dev/null 2>&1 || return 1
|
|
Z2_FIREWALL_RECONCILER_LOADED=1
|
|
}
|
|
|
|
if [ "${ZAPRET2_LAZY_FIREWALL_RECONCILER:-0}" != 1 ]; then
|
|
z2_load_firewall_reconciler || :
|
|
fi
|
|
|
|
umask 077
|
|
|
|
# Private state files are published by creating a fresh sibling under this
|
|
# umask and renaming it over the target. POSIX file creation applies the
|
|
# umask, so such a file is 0600 the moment it exists: the chmod that used to
|
|
# follow each creation set the mode it already had, and every one of those
|
|
# was a fork of /system/bin/chmod. The publishers that truncate an existing
|
|
# file, rotate the log, or hand a directory to another owner still chmod,
|
|
# because there the mode is not a property of creation.
|
|
|
|
# Android mksh ships printf as an external binary, so each printf call is a
|
|
# fork+exec (~100ms on device). mksh's raw print builtin covers the single
|
|
# '%s\n' shape exactly, including embedded newlines; every other shell this
|
|
# module meets (dash, bash) has printf as a builtin, so the fallback costs
|
|
# nothing there. Detect mksh by its version marker rather than probing a
|
|
# print command that other systems may resolve to an unrelated binary.
|
|
case "${KSH_VERSION:-}" in
|
|
*KSH*) z2_emit_line() { print -r -- "$1"; } ;;
|
|
*) z2_emit_line() { printf '%s\n' "$1"; } ;;
|
|
esac
|
|
|
|
Z2_NL='
|
|
'
|
|
Z2_PRINTF_TAB=' '
|
|
|
|
# Line parsers strip CR from CRLF input, and each one used to mint the byte
|
|
# with its own "$(printf '\r')" — a full subshell fork for one character, on
|
|
# the hottest parsing paths. mksh spells the byte as a literal; every other
|
|
# shell this module meets pays the fork once at load instead of per call.
|
|
case "${KSH_VERSION:-}" in
|
|
*KSH*) Z2_CR=$'\r' ;;
|
|
*) Z2_CR="$(printf '\r')" ;;
|
|
esac
|
|
|
|
# Generalizes the z2_emit_line decision to every printf in the module: on mksh
|
|
# each printf is a fork+exec of /system/bin/printf (~45ms on a modern Pixel),
|
|
# and the machine protocol emits dozens per operation. The adapter renders the
|
|
# narrow format dialect this codebase writes — literal text with \t and \n
|
|
# plus at most one %s cycled over the arguments — through the raw print
|
|
# builtin, and forks the real printf for anything outside it. Other shells
|
|
# never see the override: their printf is a builtin already, and print is not
|
|
# portable.
|
|
z2_printf_expand_read() {
|
|
local rest="$1" out=""
|
|
Z2_PRINTF_EXPANDED=""
|
|
while :; do
|
|
case "$rest" in
|
|
*'\t'*)
|
|
out="$out${rest%%'\t'*}$Z2_PRINTF_TAB"
|
|
rest="${rest#*'\t'}"
|
|
;;
|
|
*) break ;;
|
|
esac
|
|
done
|
|
out="$out$rest"
|
|
case "$out" in *\\*|*%*) return 1 ;; esac
|
|
Z2_PRINTF_EXPANDED="$out"
|
|
return 0
|
|
}
|
|
|
|
case "${KSH_VERSION:-}" in
|
|
*KSH*)
|
|
printf() {
|
|
local fmt="${1-}" before after a
|
|
[ "$#" -eq 0 ] || shift
|
|
case "$fmt" in
|
|
'%s')
|
|
if [ "$#" -eq 1 ]; then
|
|
print -nr -- "$1"
|
|
return 0
|
|
fi
|
|
;;
|
|
*%s*)
|
|
before="${fmt%%'%s'*}"
|
|
after="${fmt#*'%s'}"
|
|
case "$before$after" in
|
|
*%*) ;;
|
|
*)
|
|
case "$after" in
|
|
*'\n')
|
|
if z2_printf_expand_read "$before"; then
|
|
before="$Z2_PRINTF_EXPANDED"
|
|
if z2_printf_expand_read "${after%'\n'}"; then
|
|
after="$Z2_PRINTF_EXPANDED"
|
|
if [ "$#" -le 1 ]; then
|
|
print -r -- "$before${1-}$after"
|
|
else
|
|
for a in "$@"; do
|
|
print -r -- "$before$a$after"
|
|
done
|
|
fi
|
|
return 0
|
|
fi
|
|
fi
|
|
;;
|
|
esac
|
|
;;
|
|
esac
|
|
;;
|
|
*%*) ;;
|
|
*'\n')
|
|
if [ "$#" -eq 0 ] && z2_printf_expand_read "${fmt%'\n'}"; then
|
|
print -r -- "$Z2_PRINTF_EXPANDED"
|
|
return 0
|
|
fi
|
|
;;
|
|
esac
|
|
command printf "$fmt" "$@"
|
|
}
|
|
;;
|
|
esac
|
|
|
|
# One PATH probe at load answers every later "is stat available" question:
|
|
# command -v walks the whole PATH on Android (~30ms) and the metadata
|
|
# predicates used to re-ask it on every call.
|
|
Z2_HAVE_STAT=0
|
|
command -v stat >/dev/null 2>&1 && Z2_HAVE_STAT=1
|
|
Z2_HAVE_SHA256SUM=0
|
|
command -v sha256sum >/dev/null 2>&1 && Z2_HAVE_SHA256SUM=1
|
|
|
|
|
|
# Stable adapter-owned error protocol shared by module scripts and the Android
|
|
# app. The app validates only these bounds and displays all identity fields
|
|
# opaquely, so adding a future domain, stage or code does not require an APK.
|
|
Z2_ERROR_SCHEMA_VERSION=1
|
|
Z2_ERROR_DETAIL_MAX_BYTES=512
|
|
|
|
z2_error_token_is_valid() {
|
|
[ -n "$1" ] && [ "${#1}" -le 64 ] || return 1
|
|
case "$1" in *[!A-Z0-9_]*) return 1 ;; esac
|
|
}
|
|
|
|
# Detail values on the mutation path are already clean single-line text, so
|
|
# the common case must not pay the three-exec pipeline: pass a control-free,
|
|
# in-bounds value through untouched and reserve tr|cut for the rest.
|
|
z2_error_detail_normalize_read() {
|
|
local LC_ALL=C
|
|
Z2_ERROR_DETAIL_NORMALIZED=""
|
|
case "$1" in
|
|
*[[:cntrl:]]*) ;;
|
|
*)
|
|
if [ "${#1}" -le "$Z2_ERROR_DETAIL_MAX_BYTES" ] 2>/dev/null; then
|
|
Z2_ERROR_DETAIL_NORMALIZED="$1"
|
|
return 0
|
|
fi
|
|
;;
|
|
esac
|
|
Z2_ERROR_DETAIL_NORMALIZED="$(printf '%s' "$1" | tr '\r\n\t' ' ' | cut -b "1-$Z2_ERROR_DETAIL_MAX_BYTES")"
|
|
}
|
|
|
|
z2_error_detail_normalize() {
|
|
z2_error_detail_normalize_read "$1" || return 1
|
|
printf '%s' "$Z2_ERROR_DETAIL_NORMALIZED"
|
|
}
|
|
|
|
z2_error_detail_is_valid() {
|
|
local LC_ALL=C
|
|
[ "${#1}" -le "$Z2_ERROR_DETAIL_MAX_BYTES" ] 2>/dev/null || return 1
|
|
case "$1" in *[[:cntrl:]]*) return 1 ;; esac
|
|
return 0
|
|
}
|
|
|
|
z2_error_fields_are_valid() {
|
|
local status="$1" domain="$2" stage="$3" code="$4" detail="$5"
|
|
case "$status" in OK|ERROR) ;; *) return 1 ;; esac
|
|
z2_error_token_is_valid "$domain" && z2_error_token_is_valid "$stage" &&
|
|
z2_error_token_is_valid "$code" && z2_error_detail_is_valid "$detail" || return 1
|
|
if [ "$status" = OK ]; then
|
|
[ "$domain" = NONE ] && [ "$stage" = NONE ] && [ "$code" = NONE ] &&
|
|
[ -z "$detail" ]
|
|
else
|
|
[ "$domain" != NONE ] && [ "$stage" != NONE ] && [ "$code" != NONE ] &&
|
|
[ -n "$detail" ]
|
|
fi
|
|
}
|
|
|
|
z2_error_set() {
|
|
local detail
|
|
detail="$(z2_error_detail_normalize "$4")"
|
|
z2_error_fields_are_valid ERROR "$1" "$3" "$2" "$detail" || return 1
|
|
Z2_ERROR_STATUS=ERROR
|
|
Z2_ERROR_DOMAIN="$1"
|
|
Z2_ERROR_CODE="$2"
|
|
Z2_ERROR_STAGE="$3"
|
|
Z2_ERROR_DETAIL="$detail"
|
|
}
|
|
|
|
z2_error_clear() {
|
|
Z2_ERROR_STATUS=OK
|
|
Z2_ERROR_DOMAIN=NONE
|
|
Z2_ERROR_STAGE=NONE
|
|
Z2_ERROR_CODE=NONE
|
|
Z2_ERROR_DETAIL=""
|
|
}
|
|
|
|
z2_error_emit_machine() {
|
|
z2_error_fields_are_valid "${Z2_ERROR_STATUS:-}" "${Z2_ERROR_DOMAIN:-}" \
|
|
"${Z2_ERROR_STAGE:-}" "${Z2_ERROR_CODE:-}" "${Z2_ERROR_DETAIL:-}" || return 1
|
|
printf 'Z2_ERROR_SCHEMA=%s\n' "$Z2_ERROR_SCHEMA_VERSION"
|
|
printf 'Z2_ERROR_STATUS=%s\n' "$Z2_ERROR_STATUS"
|
|
printf 'Z2_ERROR_DOMAIN=%s\n' "$Z2_ERROR_DOMAIN"
|
|
printf 'Z2_ERROR_STAGE=%s\n' "$Z2_ERROR_STAGE"
|
|
printf 'Z2_ERROR_CODE=%s\n' "$Z2_ERROR_CODE"
|
|
printf 'Z2_ERROR_DETAIL=%s\n' "$Z2_ERROR_DETAIL"
|
|
}
|
|
|
|
z2_error_clear
|
|
|
|
# All live privileged state is kept below one fixed root-only directory. The
|
|
# old /data/local/tmp names are migration inputs only and are never normal
|
|
# lifecycle write/delete targets.
|
|
STATE_DIR="${STATE_DIR:-/data/adb/zapret2-state}"
|
|
# PID-suffixed scratch files live in one disposable subdirectory so recovery
|
|
# logic never has to reason about their names: boot recovery and uninstall
|
|
# may sweep the whole directory, and crash residue can never fence anything.
|
|
Z2_STATE_TMP="$STATE_DIR/tmp"
|
|
PIDFILE="$STATE_DIR/nfqws2.pid"
|
|
OWNER_STATE="$STATE_DIR/owner.meta"
|
|
LOGFILE="$STATE_DIR/nfqws2.log"
|
|
LOGFILE_PREVIOUS="$STATE_DIR/nfqws2.log.1"
|
|
LOG_MAX_BYTES=1048576
|
|
CMDLINE_FILE="$STATE_DIR/nfqws2.cmdline"
|
|
COMPILED_ARGV_FILE="$STATE_DIR/nfqws2.argv"
|
|
COMPILED_VALIDATION_RECEIPT="$STATE_DIR/nfqws2.argv.validated"
|
|
STARTUP_LOG="$STATE_DIR/nfqws2.startup.log"
|
|
ERROR_LOG="$STATE_DIR/nfqws2.error"
|
|
DEBUG_LOG="$STATE_DIR/nfqws2-debug.log"
|
|
RUNTIME_OWNER_MARKER="$STATE_DIR/runtime.owner"
|
|
STATUS_SNAPSHOT="$STATE_DIR/status.snapshot"
|
|
RUNTIME_METADATA_MAX_BYTES=262144
|
|
OWNER_STATE_MAX_BYTES=65536
|
|
|
|
RUNTIME_CONFIG="$ZAPRET_DIR/runtime.ini"
|
|
|
|
NFQWS2="$ZAPRET_DIR/nfqws2"
|
|
LISTS_DIR="$ZAPRET_DIR/lists"
|
|
PRESETS_DIR="$ZAPRET_DIR/presets"
|
|
STRATEGY_CATALOGS_DIR="$ZAPRET_DIR/strategy-catalogs"
|
|
|
|
ZAPRET2_OUT="ZAPRET2_OUT"
|
|
ZAPRET2_IN="ZAPRET2_IN"
|
|
ZAPRET2_PROBE="ZAPRET2_PROBE"
|
|
IPTABLES_STATUS="$STATUS_SNAPSHOT"
|
|
LIFECYCLE_LOCK="$STATE_DIR/lifecycle.lock"
|
|
LIFECYCLE_LOCK_OWNER="$LIFECYCLE_LOCK/owner"
|
|
LIFECYCLE_LOCK_REAPER="$STATE_DIR/lifecycle.lock.reaper"
|
|
LIFECYCLE_LOCK_REAPER_RECOVERY="$STATE_DIR/lifecycle.lock.reaper.recovery"
|
|
LIFECYCLE_LOCK_REAPER_RECOVERY_QUARANTINE="$STATE_DIR/lifecycle.lock.reaper.recovery.quarantine"
|
|
LIFECYCLE_LOCK_QUARANTINE="$STATE_DIR/lifecycle.lock.quarantine"
|
|
LIFECYCLE_LOCK_WAIT_SECONDS="${LIFECYCLE_LOCK_WAIT_SECONDS:-60}"
|
|
UNINSTALL_TOMBSTONE="$STATE_DIR/uninstall.tombstone"
|
|
UNINSTALL_TOMBSTONE_VERSION=1
|
|
PURGE_REQUEST="$STATE_DIR/purge.request"
|
|
FULL_ROLLBACK_TRANSACTION="$STATE_DIR/full-rollback.transaction"
|
|
FULL_ROLLBACK_META="$STATE_DIR/full-rollback.meta"
|
|
FULL_ROLLBACK_HOSTS_BACKUP="$STATE_DIR/hosts.rollback.backup"
|
|
# The DNS manager publishes from /data and this module bind-mounts it over
|
|
# /system/etc/hosts; hosts-overlay.sh owns that mount and re-derives the same
|
|
# paths standalone, because it also runs from post-fs-data.sh without common.sh.
|
|
HOSTS_OVERLAY_DIR="${HOSTS_OVERLAY_DIR:-/data/adb/zapret2-hosts}"
|
|
HOSTS_OVERLAY_FILE="$HOSTS_OVERLAY_DIR/hosts"
|
|
HOSTS_OVERLAY_BASE="$HOSTS_OVERLAY_DIR/system-hosts.base"
|
|
HOSTS_OVERLAY_SCRIPT="$SCRIPT_DIR/hosts-overlay.sh"
|
|
FULL_ROLLBACK_VERSION=1
|
|
INSTALL_GENERATION_META="$ZAPRET_DIR/install-generation.meta"
|
|
INSTALL_GENERATION_VERSION=1
|
|
LEGACY_MIGRATION_MARKER="$STATE_DIR/legacy-direct-rules.migrated"
|
|
OWNER_STATE_VERSION=9
|
|
OWNER_STATE_V9_FIELD_SEQUENCE="version|pid|starttime|argv_sha256|qnum|exe|generation|boot_id|phase|install_generation|install_archive_sha256|firewall_tag|out_chain|in_chain|tethering|ports_tcp|ports_udp|stun_ports|tcp_pkt_out|tcp_pkt_in|udp_pkt_out|udp_pkt_in|desync_mark|ipv4_active|ipv6_active|ipv4_connbytes|ipv4_multiport|ipv4_mark|ipv6_connbytes|ipv6_multiport|ipv6_mark|ipv4_rules|ipv6_rules|ipv4_spec|ipv6_spec|firewall_fingerprint"
|
|
OBSOLETE_FIREWALL_WAL="$STATE_DIR/firewall-teardown.wal"
|
|
|
|
export STATE_DIR Z2_STATE_TMP PIDFILE OWNER_STATE LOGFILE LOGFILE_PREVIOUS CMDLINE_FILE COMPILED_ARGV_FILE
|
|
export COMPILED_VALIDATION_RECEIPT
|
|
export STARTUP_LOG ERROR_LOG DEBUG_LOG RUNTIME_OWNER_MARKER STATUS_SNAPSHOT
|
|
export LIFECYCLE_LOCK LIFECYCLE_LOCK_OWNER LIFECYCLE_LOCK_REAPER
|
|
export LIFECYCLE_LOCK_REAPER_RECOVERY LIFECYCLE_LOCK_REAPER_RECOVERY_QUARANTINE
|
|
export LIFECYCLE_LOCK_QUARANTINE UNINSTALL_TOMBSTONE
|
|
export PURGE_REQUEST
|
|
export FULL_ROLLBACK_TRANSACTION FULL_ROLLBACK_META FULL_ROLLBACK_HOSTS_BACKUP
|
|
export INSTALL_GENERATION_META LEGACY_MIGRATION_MARKER
|
|
|
|
CORE_CONFIG_SOURCE="defaults"
|
|
CORE_CONFIG_SOURCE_PATH="built-in defaults"
|
|
RUNTIME_CONFIG_STATUS="unknown"
|
|
RUNTIME_CONFIG_REASON=""
|
|
RUNTIME_CONFIG_ERROR=""
|
|
RUNTIME_CORE_REPAIR_MODE="defaults"
|
|
RUNTIME_CORE_REQUIRED_KEYS="schema_version config_format runtime_source autostart wifi_only tethering debug qnum desync_mark active_preset nfqws_uid log_mode"
|
|
|
|
is_decimal() {
|
|
case "$1" in
|
|
""|*[!0-9]*) return 1 ;;
|
|
*) return 0 ;;
|
|
esac
|
|
}
|
|
|
|
is_canonical_positive_decimal() {
|
|
case "$1" in ""|0*|*[!0-9]*) return 1 ;; *) return 0 ;; esac
|
|
}
|
|
|
|
is_canonical_nonnegative_i64() {
|
|
local value="$1" digits first rest
|
|
case "$value" in 0) return 0 ;; ""|0*|*[!0-9]*) return 1 ;; esac
|
|
digits=${#value}
|
|
[ "$digits" -lt 19 ] 2>/dev/null && return 0
|
|
[ "$digits" -eq 19 ] 2>/dev/null || return 1
|
|
first=${value%"${value#?}"}
|
|
rest=${value#?}
|
|
[ "$first" -lt 9 ] 2>/dev/null && return 0
|
|
[ "$first" -eq 9 ] 2>/dev/null && [ "$rest" -le 223372036854775807 ] 2>/dev/null
|
|
}
|
|
|
|
is_canonical_nfqws_id() {
|
|
local value="$1" digits
|
|
case "$value" in 0) return 0 ;; ""|0*|*[!0-9]*) return 1 ;; esac
|
|
digits=${#value}
|
|
[ "$digits" -lt 10 ] 2>/dev/null && return 0
|
|
[ "$digits" -eq 10 ] 2>/dev/null && [ "$value" -le 2147483647 ] 2>/dev/null
|
|
}
|
|
|
|
# A proof that asks several metadata questions about one file pays one stat
|
|
# exec instead of one per question. path_meta_capture arms a snapshot for
|
|
# exactly one path; the predicates below consume it only while it names their
|
|
# argument, and the proof that armed it retires it on every exit. Arming is
|
|
# best-effort: without stat the predicates keep their own fallbacks, and an
|
|
# overridden predicate simply never consults the snapshot.
|
|
Z2_PATH_META_PATH=""
|
|
Z2_PATH_META_UID=""
|
|
Z2_PATH_META_MODE=""
|
|
Z2_PATH_META_NLINK=""
|
|
Z2_PATH_META_SIZE=""
|
|
|
|
# Transaction-scoped metadata prefetch. stat answers as many paths per fork as
|
|
# it is handed, and on Android a fork from this library costs about as much as
|
|
# the whole rest of a lifecycle phase, so one batched call at the top of a
|
|
# transaction replaces a dozen single-path ones spread through it. The cache is
|
|
# only armed while this process holds the lifecycle lock — the same
|
|
# single-writer invariant Z2_STATE_DIR_PROOF rests on — and every publisher
|
|
# retires the entry it is about to overwrite, so a cached answer never
|
|
# describes a file this transaction has already changed.
|
|
Z2_META_CACHE=""
|
|
|
|
meta_cache_retire_all() {
|
|
Z2_META_CACHE=""
|
|
}
|
|
|
|
meta_cache_forget() {
|
|
local head tail
|
|
case "$Z2_META_CACHE" in
|
|
*"$Z2_NL$1|"*) ;;
|
|
*) return 0 ;;
|
|
esac
|
|
head="${Z2_META_CACHE%%"$Z2_NL$1|"*}"
|
|
tail="${Z2_META_CACHE#*"$Z2_NL$1|"}"
|
|
tail="${tail#*"$Z2_NL"}"
|
|
Z2_META_CACHE="$head$Z2_NL$tail"
|
|
}
|
|
|
|
# Paths that do not exist are simply absent from the answer; the callers that
|
|
# ask about them fall through to their own fresh probe.
|
|
meta_cache_prime() {
|
|
local out
|
|
Z2_META_CACHE=""
|
|
[ "${LOCK_HELD:-0}" != 0 ] || return 0
|
|
[ "$Z2_HAVE_STAT" = 1 ] || return 0
|
|
[ "$#" -gt 0 ] || return 0
|
|
out="$(stat -c '%n|%u|%a|%h|%s' "$@" 2>/dev/null)" || out=""
|
|
[ -n "$out" ] || return 0
|
|
Z2_META_CACHE="$Z2_NL$out$Z2_NL"
|
|
}
|
|
|
|
meta_cache_read() {
|
|
local rest
|
|
case "$Z2_META_CACHE" in
|
|
*"$Z2_NL$1|"*) ;;
|
|
*) return 1 ;;
|
|
esac
|
|
rest="${Z2_META_CACHE#*"$Z2_NL$1|"}"
|
|
rest="${rest%%"$Z2_NL"*}"
|
|
# Android mksh treats an unescaped `|` in a parameter-expansion pattern as
|
|
# alternation, so `${rest%%|*}` matches the empty alternative and returns
|
|
# an empty field. Escape the record delimiter: otherwise every cache lookup
|
|
# silently misses and falls back to its own stat.
|
|
Z2_PATH_META_UID="${rest%%\|*}"; rest="${rest#*\|}"
|
|
Z2_PATH_META_MODE="${rest%%\|*}"; rest="${rest#*\|}"
|
|
Z2_PATH_META_NLINK="${rest%%\|*}"; rest="${rest#*\|}"
|
|
Z2_PATH_META_SIZE="$rest"
|
|
case "$Z2_PATH_META_UID$Z2_PATH_META_MODE$Z2_PATH_META_NLINK$Z2_PATH_META_SIZE" in
|
|
''|*[!0-9]*) return 1 ;;
|
|
esac
|
|
Z2_PATH_META_PATH="$1"
|
|
return 0
|
|
}
|
|
|
|
meta_cache_add() {
|
|
local out
|
|
[ "${LOCK_HELD:-0}" != 0 ] || return 0
|
|
[ "$Z2_HAVE_STAT" = 1 ] || return 0
|
|
[ "$#" -gt 0 ] || return 0
|
|
out="$(stat -c '%n|%u|%a|%h|%s' "$@" 2>/dev/null)" || return 1
|
|
[ -n "$out" ] || return 0
|
|
if [ -n "$Z2_META_CACHE" ]; then
|
|
Z2_META_CACHE="${Z2_META_CACHE}${out}${Z2_NL}"
|
|
else
|
|
Z2_META_CACHE="$Z2_NL$out$Z2_NL"
|
|
fi
|
|
}
|
|
|
|
path_meta_retire() {
|
|
Z2_PATH_META_PATH=""
|
|
}
|
|
|
|
# The exact file set a lifecycle transaction proves things about. Listing it
|
|
# here keeps the batch honest: a path that is not on it simply pays its own
|
|
# stat, and a path that stops existing is absent from the answer.
|
|
prime_transaction_metadata() {
|
|
# BusyBox stat returns a non-zero aggregate status when any operand is
|
|
# absent. Build the exact existing set with shell builtins first, so one
|
|
# missing optional file cannot discard the answers for every present one.
|
|
set --
|
|
[ ! -e "$PIDFILE" ] && [ ! -L "$PIDFILE" ] || set -- "$@" "$PIDFILE"
|
|
[ ! -e "$OWNER_STATE" ] && [ ! -L "$OWNER_STATE" ] || set -- "$@" "$OWNER_STATE"
|
|
[ ! -e "$STATUS_SNAPSHOT" ] && [ ! -L "$STATUS_SNAPSHOT" ] || set -- "$@" "$STATUS_SNAPSHOT"
|
|
[ ! -e "$LOGFILE" ] && [ ! -L "$LOGFILE" ] || set -- "$@" "$LOGFILE"
|
|
[ ! -e "$RUNTIME_OWNER_MARKER" ] && [ ! -L "$RUNTIME_OWNER_MARKER" ] || set -- "$@" "$RUNTIME_OWNER_MARKER"
|
|
[ ! -e "$COMPILED_ARGV_FILE" ] && [ ! -L "$COMPILED_ARGV_FILE" ] || set -- "$@" "$COMPILED_ARGV_FILE"
|
|
[ ! -e "$COMPILED_VALIDATION_RECEIPT" ] && [ ! -L "$COMPILED_VALIDATION_RECEIPT" ] ||
|
|
set -- "$@" "$COMPILED_VALIDATION_RECEIPT"
|
|
[ ! -e "$STARTUP_LOG" ] && [ ! -L "$STARTUP_LOG" ] || set -- "$@" "$STARTUP_LOG"
|
|
[ ! -e "$ERROR_LOG" ] && [ ! -L "$ERROR_LOG" ] || set -- "$@" "$ERROR_LOG"
|
|
[ ! -e "$CMDLINE_FILE" ] && [ ! -L "$CMDLINE_FILE" ] || set -- "$@" "$CMDLINE_FILE"
|
|
[ ! -e "$INSTALL_GENERATION_META" ] && [ ! -L "$INSTALL_GENERATION_META" ] ||
|
|
set -- "$@" "$INSTALL_GENERATION_META"
|
|
meta_cache_prime "$@"
|
|
}
|
|
|
|
|
|
path_meta_capture() {
|
|
local path="$1" meta
|
|
Z2_PATH_META_PATH=""
|
|
meta_cache_read "$path" && return 0
|
|
[ "$Z2_HAVE_STAT" = 1 ] || return 1
|
|
meta="$(stat -c '%u %a %h %s' "$path" 2>/dev/null)" || return 1
|
|
set -- $meta
|
|
[ "$#" -eq 4 ] || return 1
|
|
Z2_PATH_META_UID="$1"
|
|
Z2_PATH_META_MODE="$2"
|
|
Z2_PATH_META_NLINK="$3"
|
|
Z2_PATH_META_SIZE="$4"
|
|
Z2_PATH_META_PATH="$path"
|
|
}
|
|
|
|
# The size question a proof asks right after its metadata questions comes
|
|
# from the same armed snapshot; without one it falls back to the wc read the
|
|
# call sites used to pay per file.
|
|
path_meta_size_read() {
|
|
if [ -n "$Z2_PATH_META_PATH" ] && [ "$Z2_PATH_META_PATH" = "$1" ]; then
|
|
Z2_PATH_SIZE="$Z2_PATH_META_SIZE"
|
|
return 0
|
|
fi
|
|
if meta_cache_read "$1"; then
|
|
Z2_PATH_SIZE="$Z2_PATH_META_SIZE"
|
|
return 0
|
|
fi
|
|
Z2_PATH_SIZE="$(wc -c < "$1" 2>/dev/null)" || return 1
|
|
}
|
|
|
|
path_uid_is_root() {
|
|
local path="$1" uid listing
|
|
if [ -n "$Z2_PATH_META_PATH" ] && [ "$Z2_PATH_META_PATH" = "$path" ]; then
|
|
[ "$Z2_PATH_META_UID" = 0 ]
|
|
return
|
|
fi
|
|
if meta_cache_read "$path"; then
|
|
[ "$Z2_PATH_META_UID" = 0 ]
|
|
return
|
|
fi
|
|
if [ "$Z2_HAVE_STAT" = 1 ]; then
|
|
uid="$(stat -c '%u' "$path" 2>/dev/null)" || return 1
|
|
[ "$uid" = 0 ]
|
|
return
|
|
fi
|
|
listing="$(ls -ldn "$path" 2>/dev/null)" || return 1
|
|
set -- $listing
|
|
[ "$#" -ge 4 ] && [ "$3" = 0 ]
|
|
}
|
|
|
|
# STATE_DIR metadata has exactly one cooperating mutator: this module, under
|
|
# the lifecycle lock — the same single-writer fact the owner read cache rests
|
|
# on (see Z2_OWNER_READ_CACHE). While this process holds the lock, a proven
|
|
# secure directory therefore stays a fact until this process mutates it;
|
|
# ensure_state_dir (the one mutator) and the lock release retire the proof.
|
|
Z2_STATE_DIR_PROOF=""
|
|
|
|
retire_state_dir_proof() {
|
|
Z2_STATE_DIR_PROOF=""
|
|
}
|
|
|
|
state_dir_is_secure_fresh() {
|
|
local metadata listing
|
|
[ -d "$STATE_DIR" ] && [ ! -L "$STATE_DIR" ] || return 1
|
|
if [ "$Z2_HAVE_STAT" = 1 ]; then
|
|
metadata="$(stat -c '%u:%a' "$STATE_DIR" 2>/dev/null)" || return 1
|
|
[ "$metadata" = 0:700 ]
|
|
return
|
|
fi
|
|
path_uid_is_root "$STATE_DIR" || return 1
|
|
listing="$(ls -ldn "$STATE_DIR" 2>/dev/null)" || return 1
|
|
set -- $listing
|
|
case "${1:-}" in drwx------*) return 0 ;; *) return 1 ;; esac
|
|
}
|
|
|
|
state_dir_is_secure() {
|
|
if [ "${LOCK_HELD:-0}" != 0 ] && [ "$Z2_STATE_DIR_PROOF" = valid ]; then
|
|
return 0
|
|
fi
|
|
state_dir_is_secure_fresh || return 1
|
|
[ "${LOCK_HELD:-0}" = 0 ] || Z2_STATE_DIR_PROOF=valid
|
|
return 0
|
|
}
|
|
|
|
ensure_state_dir() {
|
|
umask 077
|
|
# A proven-secure directory is exactly this function's postcondition, and
|
|
# while the lock is held only this process mutates STATE_DIR, so the armed
|
|
# proof makes the create/chmod/verify round a no-op. Otherwise this is the
|
|
# one cooperating mutator: the cached proof dies here, and the fresh
|
|
# postcondition below re-establishes it.
|
|
if [ "${LOCK_HELD:-0}" != 0 ] && [ "$Z2_STATE_DIR_PROOF" = valid ]; then
|
|
return 0
|
|
fi
|
|
retire_state_dir_proof
|
|
if [ -e "$STATE_DIR" ] || [ -L "$STATE_DIR" ]; then
|
|
[ -d "$STATE_DIR" ] && [ ! -L "$STATE_DIR" ] || return 1
|
|
state_dir_is_secure
|
|
return
|
|
else
|
|
mkdir "$STATE_DIR" 2>/dev/null || return 1
|
|
fi
|
|
state_dir_is_secure
|
|
}
|
|
|
|
state_path_is_managed_file() {
|
|
local suffix
|
|
case "$1" in
|
|
"$STATE_DIR"/*)
|
|
suffix="${1#"$STATE_DIR"/}"
|
|
[ -n "$suffix" ] || return 1
|
|
case "$suffix" in
|
|
tmp/*)
|
|
suffix="${suffix#tmp/}"
|
|
[ -n "$suffix" ] || return 1
|
|
case "$suffix" in */*) return 1 ;; esac
|
|
return 0
|
|
;;
|
|
*/*) return 1 ;;
|
|
esac
|
|
return 0
|
|
;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
ensure_state_tmp_dir() {
|
|
umask 077
|
|
if [ ! -e "$Z2_STATE_TMP" ] && [ ! -L "$Z2_STATE_TMP" ]; then
|
|
mkdir "$Z2_STATE_TMP" 2>/dev/null
|
|
fi
|
|
# Validate unconditionally: a losing mkdir race must never be accepted on
|
|
# the strength of [ -d ] alone, which follows a symlink planted between
|
|
# the existence test and the mkdir. umask 077 already gives a new
|
|
# directory 0700; an existing directory with any other mode is unsafe,
|
|
# not something a hot transaction should silently repair.
|
|
[ -d "$Z2_STATE_TMP" ] && [ ! -L "$Z2_STATE_TMP" ] || return 1
|
|
path_meta_capture "$Z2_STATE_TMP"
|
|
path_uid_is_root "$Z2_STATE_TMP" && [ "$Z2_PATH_META_MODE" = 700 ]
|
|
local rc=$?
|
|
path_meta_retire
|
|
return "$rc"
|
|
}
|
|
|
|
# A managed file's ownership is a property of the directory that holds it, not
|
|
# a fact worth re-forking stat for on every proof. STATE_DIR is proven to be a
|
|
# root-owned 0700 directory, so no unprivileged process can create an entry in
|
|
# it or chown one, and while this process holds the lifecycle lock it is the
|
|
# only cooperating root writer — the same single-writer invariant
|
|
# Z2_STATE_DIR_PROOF already rests on. The existence and regular-file questions
|
|
# stay fresh below because they are shell builtins and cost nothing; only the
|
|
# ownership question is answered from the directory proof. Without the lock the
|
|
# per-file stat is still paid, so read-only observers keep today's behavior.
|
|
managed_file_uid_is_root() {
|
|
[ "${LOCK_HELD:-0}" != 0 ] && [ "$Z2_STATE_DIR_PROOF" = valid ] && return 0
|
|
path_uid_is_root "$1"
|
|
}
|
|
|
|
state_file_is_secure() {
|
|
state_dir_is_secure || return 1
|
|
state_path_is_managed_file "$1" || return 1
|
|
[ -f "$1" ] && [ ! -L "$1" ] || return 1
|
|
managed_file_uid_is_root "$1"
|
|
}
|
|
|
|
observer_state_file_is_secure() {
|
|
[ "${OBSERVER_STATE_DIR_VERIFIED:-0}" = 1 ] || return 1
|
|
state_path_is_managed_file "$1" || return 1
|
|
[ -f "$1" ] && [ ! -L "$1" ] || return 1
|
|
path_uid_is_root "$1"
|
|
}
|
|
|
|
state_file_target_is_safe() {
|
|
state_dir_is_secure || return 1
|
|
state_path_is_managed_file "$1" || return 1
|
|
[ ! -e "$1" ] && [ ! -L "$1" ] && return 0
|
|
state_file_is_secure "$1"
|
|
}
|
|
|
|
if ! is_decimal "$LIFECYCLE_LOCK_WAIT_SECONDS" || [ "$LIFECYCLE_LOCK_WAIT_SECONDS" -lt 1 ] 2>/dev/null; then
|
|
LIFECYCLE_LOCK_WAIT_SECONDS=60
|
|
fi
|
|
|
|
is_safe_token() {
|
|
case "$1" in
|
|
""|*[!A-Za-z0-9._-]*) return 1 ;;
|
|
*) return 0 ;;
|
|
esac
|
|
}
|
|
|
|
is_lower_sha256() {
|
|
[ "${#1}" -eq 64 ] 2>/dev/null || return 1
|
|
case "$1" in *[!0-9a-f]*) return 1 ;; *) return 0 ;; esac
|
|
}
|
|
|
|
path_mode_is_0600() {
|
|
local path="$1" mode listing
|
|
if [ -n "$Z2_PATH_META_PATH" ] && [ "$Z2_PATH_META_PATH" = "$path" ]; then
|
|
[ "$Z2_PATH_META_MODE" = 600 ]
|
|
return
|
|
fi
|
|
if meta_cache_read "$path"; then
|
|
[ "$Z2_PATH_META_MODE" = 600 ]
|
|
return
|
|
fi
|
|
if [ "$Z2_HAVE_STAT" = 1 ]; then
|
|
mode="$(stat -c '%a' "$path" 2>/dev/null)" || return 1
|
|
[ "$mode" = 600 ]
|
|
return
|
|
fi
|
|
listing="$(ls -ldn "$path" 2>/dev/null)" || return 1
|
|
set -- $listing
|
|
case "${1:-}" in -rw-------*) return 0 ;; *) return 1 ;; esac
|
|
}
|
|
|
|
path_nlink_is_one() {
|
|
local path="$1" links listing
|
|
if [ -n "$Z2_PATH_META_PATH" ] && [ "$Z2_PATH_META_PATH" = "$path" ]; then
|
|
[ "$Z2_PATH_META_NLINK" = 1 ]
|
|
return
|
|
fi
|
|
if meta_cache_read "$path"; then
|
|
[ "$Z2_PATH_META_NLINK" = 1 ]
|
|
return
|
|
fi
|
|
if [ "$Z2_HAVE_STAT" = 1 ]; then
|
|
links="$(stat -c '%h' "$path" 2>/dev/null)" || return 1
|
|
[ "$links" = 1 ]
|
|
return
|
|
fi
|
|
listing="$(ls -ldn "$path" 2>/dev/null)" || return 1
|
|
set -- $listing
|
|
[ "$#" -ge 2 ] && [ "$2" = 1 ]
|
|
}
|
|
|
|
INSTALL_META_GENERATION=""
|
|
INSTALL_META_ARCHIVE_SHA256=""
|
|
INSTALL_META_CACHED_PATH=""
|
|
read_install_generation_meta() {
|
|
local path="${1:-$INSTALL_GENERATION_META}" key value version="" module="" generation="" archive="" seen="" size
|
|
# The installer writes this record once and nothing rewrites it while the
|
|
# module runs, so the parse is cached. The path checks are cheap and are
|
|
# repeated on every call: callers use this as a postcondition, and a
|
|
# postcondition that skips verification is not one.
|
|
if [ -n "$INSTALL_META_CACHED_PATH" ] && [ "$INSTALL_META_CACHED_PATH" = "$path" ]; then
|
|
path_meta_capture "$path"
|
|
if [ -f "$path" ] && [ ! -L "$path" ] && path_uid_is_root "$path" &&
|
|
path_mode_is_0600 "$path" && path_nlink_is_one "$path"; then
|
|
path_meta_retire
|
|
return 0
|
|
fi
|
|
path_meta_retire
|
|
return 1
|
|
fi
|
|
INSTALL_META_GENERATION=""; INSTALL_META_ARCHIVE_SHA256=""; INSTALL_META_CACHED_PATH=""
|
|
path_meta_capture "$path"
|
|
if [ -f "$path" ] && [ ! -L "$path" ] && path_uid_is_root "$path" &&
|
|
path_mode_is_0600 "$path" && path_nlink_is_one "$path" &&
|
|
path_meta_size_read "$path"; then
|
|
size="$Z2_PATH_SIZE"
|
|
path_meta_retire
|
|
else
|
|
path_meta_retire
|
|
return 1
|
|
fi
|
|
is_decimal "$size" && [ "$size" -gt 0 ] 2>/dev/null && [ "$size" -le 1024 ] 2>/dev/null || return 1
|
|
while :; do
|
|
key=""; value=""
|
|
IFS='=' read -r key value || [ -n "$key$value" ] || break
|
|
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" ;;
|
|
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" ;;
|
|
*) return 1 ;;
|
|
esac
|
|
done < "$path"
|
|
[ "${#seen}" -eq 4 ] 2>/dev/null && [ "$version" = "$INSTALL_GENERATION_VERSION" ] && [ "$module" = "$MODDIR" ] || return 1
|
|
is_safe_token "$generation" && [ "${#generation}" -le 128 ] 2>/dev/null || return 1
|
|
is_lower_sha256 "$archive" || return 1
|
|
INSTALL_META_GENERATION="$generation"; INSTALL_META_ARCHIVE_SHA256="$archive"
|
|
INSTALL_META_CACHED_PATH="$path"
|
|
}
|
|
|
|
RECOVERY_ARTIFACT_DIAGNOSTIC=""
|
|
RECOVERY_ARTIFACT_CLASS="clean"
|
|
RECOVERY_ARTIFACT_FIRST=""
|
|
|
|
CURRENT_BOOT_ID=""
|
|
STALE_OWNER_DIAGNOSTIC=""
|
|
STALE_OWNER_PUBLICATION_RETIRED=0
|
|
BOOT_RECOVERY_DIAGNOSTIC=""
|
|
BOOT_INCOMPATIBLE_STATE_RETIRED=0
|
|
|
|
is_valid_boot_id() {
|
|
local value="$1"
|
|
[ "${#value}" -eq 36 ] 2>/dev/null || return 1
|
|
case "$value" in
|
|
????????-????-????-????-????????????) ;;
|
|
*) return 1 ;;
|
|
esac
|
|
case "$value" in *[!0-9a-f-]*) return 1;; esac
|
|
}
|
|
|
|
read_current_boot_id() {
|
|
local value
|
|
IFS= read -r value < /proc/sys/kernel/random/boot_id 2>/dev/null || return 1
|
|
is_valid_boot_id "$value" || return 1
|
|
CURRENT_BOOT_ID="$value"
|
|
}
|
|
|
|
stale_owner_clean_ownership_proof() {
|
|
local tool family_state canonical_nfqws effective_nfqws candidate checked=""
|
|
effective_nfqws="${AUDIT_NFQWS2_OVERRIDE:-$NFQWS2}"
|
|
scan_exact_owned_nfqws_for_path "$effective_nfqws" >/dev/null 2>&1 || return 1
|
|
[ -z "$OWNED_SCAN_PIDS" ] || return 1
|
|
canonical_nfqws="$MODDIR/zapret2/nfqws2"
|
|
checked="|$effective_nfqws|"
|
|
for candidate in "$canonical_nfqws" "$NFQWS2"; do
|
|
case "$checked" in *"|$candidate|"*) continue;; esac
|
|
scan_exact_owned_nfqws_for_path "$candidate" >/dev/null 2>&1 || return 1
|
|
[ -z "$OWNED_SCAN_PIDS" ] || return 1
|
|
checked="${checked}${candidate}|"
|
|
done
|
|
for tool in $STALE_OWNER_REQUIRED_TOOLS; do command -v "$tool" >/dev/null 2>&1 || return 1; done
|
|
for tool in iptables ip6tables; do
|
|
command -v "$tool" >/dev/null 2>&1 || continue
|
|
owned_family_present "$tool" >/dev/null 2>&1
|
|
family_state=$?
|
|
case "$family_state" in 1) ;; *) return 1;; esac
|
|
done
|
|
return 0
|
|
}
|
|
|
|
caller_holds_exact_lifecycle_lock() {
|
|
case "$LOCK_HELD" in 1|inherited) ;; *) return 1;; esac
|
|
lock_owner_alive || return 1
|
|
[ "$LOCK_FILE_PID" = "$LOCK_OWNER_PID" ] && [ "$LOCK_FILE_START" = "$LOCK_OWNER_START" ] &&
|
|
[ "$LOCK_FILE_TOKEN" = "$LOCK_OWNER_TOKEN" ]
|
|
}
|
|
|
|
recover_stale_owner_publication() {
|
|
local current_boot pidfile_pid
|
|
STALE_OWNER_PUBLICATION_RETIRED=0
|
|
{ [ -e "$OWNER_STATE" ] || [ -L "$OWNER_STATE" ] || [ -e "$PIDFILE" ] || [ -L "$PIDFILE" ]; } || return 0
|
|
# The owner record is the authenticated commit marker. A bare pidfile can
|
|
# never prove that a PID belongs to this installation.
|
|
[ -e "$OWNER_STATE" ] && [ ! -L "$OWNER_STATE" ] && read_owner_state || {
|
|
STALE_OWNER_DIAGNOSTIC="unauthenticated owner publication remains"
|
|
return 1
|
|
}
|
|
if [ -e "$PIDFILE" ] || [ -L "$PIDFILE" ]; then
|
|
[ ! -L "$PIDFILE" ] && state_file_is_secure "$PIDFILE" || {
|
|
STALE_OWNER_DIAGNOSTIC="unsafe pidfile accompanies owner publication"
|
|
return 1
|
|
}
|
|
IFS= read -r pidfile_pid < "$PIDFILE" 2>/dev/null || return 1
|
|
is_decimal "$pidfile_pid" && [ "$pidfile_pid" = "$OWNER_STATE_PID" ] || {
|
|
STALE_OWNER_DIAGNOSTIC="pidfile and owner publication disagree"
|
|
return 1
|
|
}
|
|
fi
|
|
read_current_boot_id || { STALE_OWNER_DIAGNOSTIC="current boot identity is unavailable"; return 1; }
|
|
current_boot="$CURRENT_BOOT_ID"
|
|
if [ "$OWNER_STATE_BOOT_ID" = "$current_boot" ]; then
|
|
verify_nfqws_pid "$OWNER_STATE_PID" "$OWNER_STATE_START" "$OWNER_STATE_ARGV_SHA256" "$OWNER_STATE_QNUM" && return 0
|
|
STALE_OWNER_DIAGNOSTIC="same-boot owner/PID ambiguity remains"
|
|
return 1
|
|
fi
|
|
STALE_OWNER_REQUIRED_TOOLS=iptables
|
|
[ "$OWNER_STATE_IPV6_ACTIVE" = 1 ] &&
|
|
STALE_OWNER_REQUIRED_TOOLS="$STALE_OWNER_REQUIRED_TOOLS ip6tables"
|
|
stale_owner_clean_ownership_proof || {
|
|
STALE_OWNER_DIAGNOSTIC="cross-boot owner recovery lacks a clean process/firewall snapshot"
|
|
return 1
|
|
}
|
|
# Audits before lock acquisition may classify this state, but only the
|
|
# exact lifecycle-lock owner may retire published metadata.
|
|
caller_holds_exact_lifecycle_lock || return 0
|
|
if [ "${BOOT_STALE_RUNTIME_RECOVERY:-0}" = 1 ] &&
|
|
{ [ -e "$STATUS_SNAPSHOT" ] || [ -L "$STATUS_SNAPSHOT" ]; }; then
|
|
state_file_is_secure "$STATUS_SNAPSHOT" &&
|
|
path_mode_is_0600 "$STATUS_SNAPSHOT" && path_nlink_is_one "$STATUS_SNAPSHOT" || {
|
|
STALE_OWNER_DIAGNOSTIC="stale status snapshot is unsafe"
|
|
return 1
|
|
}
|
|
fi
|
|
if [ -e "$PIDFILE" ]; then rm -f "$PIDFILE" || return 1; fi
|
|
if [ "${BOOT_STALE_RUNTIME_RECOVERY:-0}" = 1 ] && [ -e "$STATUS_SNAPSHOT" ]; then
|
|
rm -f "$STATUS_SNAPSHOT" || return 1
|
|
fi
|
|
rm -f "$OWNER_STATE" || return 1
|
|
retire_owner_read_cache
|
|
STALE_OWNER_PUBLICATION_RETIRED=1
|
|
return 0
|
|
}
|
|
|
|
# Formats produced by older module generations have no live writers anymore:
|
|
# build/probe track journals, the firewall teardown WAL, and the legacy
|
|
# direct-rule migration marker with its snapshot artifacts. Reboot is the
|
|
# migration barrier — current code never coexists with a runtime that still
|
|
# writes them — so the only correct handling is deletion, and only the exact
|
|
# lifecycle-lock owner may do it.
|
|
retire_obsolete_state_artifacts() {
|
|
local path restore_noglob=0
|
|
caller_holds_exact_lifecycle_lock || return 0
|
|
case "$-" in *f*) restore_noglob=1; set +f;; esac
|
|
set -- "$OBSOLETE_FIREWALL_WAL" "$LEGACY_MIGRATION_MARKER" \
|
|
"$STATE_DIR"/build-track.* "$STATE_DIR"/probe-track.* \
|
|
"$STATE_DIR"/legacy-rollback.*
|
|
[ "$restore_noglob" = 1 ] && set -f
|
|
for path in "$@"; do
|
|
{ [ -e "$path" ] || [ -L "$path" ]; } || continue
|
|
rm -f "$path" 2>/dev/null || return 1
|
|
done
|
|
if [ "${BOOT_STALE_RUNTIME_RECOVERY:-0}" = 1 ] &&
|
|
{ [ -e "$Z2_STATE_TMP" ] || [ -L "$Z2_STATE_TMP" ]; }; then
|
|
rm -rf "$Z2_STATE_TMP" 2>/dev/null || return 1
|
|
fi
|
|
# Staging residue in the state root is swept by creator liveness on every
|
|
# pass, including boot: the wholesale scratch removal above covers only
|
|
# the scratch directory.
|
|
retire_dead_scratch_files
|
|
}
|
|
|
|
# Scratch names all end in the PID of the process that created them, and the
|
|
# writers refuse a path that already exists — so residue from a killed
|
|
# operation would fence the next firewall transaction forever. Removing only
|
|
# entries whose creator is gone keeps a concurrent preset preview, which holds
|
|
# no lifecycle lock, safe.
|
|
retire_dead_scratch_files() {
|
|
local path base owner restore_noglob=0
|
|
case "$-" in *f*) restore_noglob=1; set +f;; esac
|
|
# Atomic publications stage through "<target>.tmp.<pid>" in the state root,
|
|
# so a process killed between the redirect and the rename leaves residue
|
|
# there too — and an unknown child fences uninstall.
|
|
if [ -d "$Z2_STATE_TMP" ] && [ ! -L "$Z2_STATE_TMP" ]; then
|
|
set -- "$Z2_STATE_TMP"/* "$STATE_DIR"/*.tmp.*
|
|
else
|
|
set -- "$STATE_DIR"/*.tmp.*
|
|
fi
|
|
[ "$restore_noglob" = 1 ] && set -f
|
|
for path in "$@"; do
|
|
{ [ -e "$path" ] || [ -L "$path" ]; } || continue
|
|
base="${path##*/}"
|
|
case "$base" in
|
|
# Staging names are "<target>.tmp.<pid>" and may carry a token or
|
|
# nonce after the PID, so the creator is the component that
|
|
# follows ".tmp." — not the last one.
|
|
*.tmp.*) owner="${base##*.tmp.}"; owner="${owner%%.*}" ;;
|
|
# Scratch names end in the creator PID, optionally with ".error".
|
|
*) owner="${base%.error}"; owner="${owner##*.}" ;;
|
|
esac
|
|
is_decimal "$owner" && [ "$owner" -gt 0 ] 2>/dev/null || continue
|
|
[ ! -d "/proc/$owner" ] || continue
|
|
rm -rf "$path" 2>/dev/null || return 1
|
|
done
|
|
return 0
|
|
}
|
|
|
|
enumerate_recovery_artifacts() {
|
|
local artifact restore_noglob=0 rc=0
|
|
case "$-" in *f*) restore_noglob=1; set +f;; esac
|
|
for artifact in \
|
|
"$UNINSTALL_TOMBSTONE" "$UNINSTALL_TOMBSTONE".tmp "$UNINSTALL_TOMBSTONE".tmp.* "$STATE_DIR"/.uninstall.tombstone.* \
|
|
"$LIFECYCLE_LOCK_REAPER" "$LIFECYCLE_LOCK_REAPER".* \
|
|
"$LIFECYCLE_LOCK_REAPER_RECOVERY" "$LIFECYCLE_LOCK_REAPER_RECOVERY".* \
|
|
"$LIFECYCLE_LOCK_QUARANTINE" "$LIFECYCLE_LOCK_QUARANTINE".* \
|
|
"$STATE_DIR"/lifecycle.lock.candidate.* "$STATE_DIR"/.lifecycle.lock.* \
|
|
"$FULL_ROLLBACK_TRANSACTION" "$FULL_ROLLBACK_TRANSACTION".tmp "$FULL_ROLLBACK_TRANSACTION".tmp.* \
|
|
"$STATE_DIR"/.full-rollback.transaction.* \
|
|
"$FULL_ROLLBACK_META" "$FULL_ROLLBACK_META".tmp "$FULL_ROLLBACK_META".tmp.* \
|
|
"$STATE_DIR"/.full-rollback.meta.* \
|
|
"$FULL_ROLLBACK_HOSTS_BACKUP" "$FULL_ROLLBACK_HOSTS_BACKUP".tmp "$FULL_ROLLBACK_HOSTS_BACKUP".tmp.* \
|
|
"$STATE_DIR"/.hosts.rollback.backup.*; do
|
|
if [ -e "$artifact" ] || [ -L "$artifact" ]; then printf '%s\n' "$artifact" || { rc=1; break; }; fi
|
|
done
|
|
[ "$restore_noglob" = 1 ] && set -f
|
|
return "$rc"
|
|
}
|
|
|
|
# Only an "unsafe" generation is retired by the boot pass. Rollback evidence is
|
|
# durable on purpose and survives every reboot, so telling the user to reboot
|
|
# would send them in circles.
|
|
recovery_block_remedy() {
|
|
case "${RECOVERY_ARTIFACT_CLASS:-}" in
|
|
unsafe) printf '%s' "; reboot to let boot recovery retire it" ;;
|
|
rollback-partial) printf '%s' "; finish the interrupted full rollback, then reboot" ;;
|
|
rollback-complete) printf '%s' "; a completed full rollback is pending — remove the module in your root manager first, then reinstall it" ;;
|
|
*) ;;
|
|
esac
|
|
}
|
|
|
|
audit_recovery_artifacts() {
|
|
local scope="$1" AUDIT_NFQWS2_OVERRIDE="${2:-}" artifact
|
|
local rollback_seen=0 unsafe_seen=0 rollback_meta=0 rollback_tx=0 rollback_extra=0
|
|
RECOVERY_ARTIFACT_DIAGNOSTIC=""
|
|
RECOVERY_ARTIFACT_CLASS=clean
|
|
RECOVERY_ARTIFACT_FIRST=""
|
|
case "$scope" in
|
|
lifecycle|full-rollback|install|uninstall) ;;
|
|
*) RECOVERY_ARTIFACT_DIAGNOSTIC="unknown recovery audit scope"; return 1 ;;
|
|
esac
|
|
retire_obsolete_state_artifacts || {
|
|
RECOVERY_ARTIFACT_CLASS=unsafe
|
|
RECOVERY_ARTIFACT_DIAGNOSTIC="obsolete state artifacts could not be retired"
|
|
return 1
|
|
}
|
|
if ! recover_stale_owner_publication; then
|
|
RECOVERY_ARTIFACT_CLASS=unsafe
|
|
RECOVERY_ARTIFACT_DIAGNOSTIC="$STALE_OWNER_DIAGNOSTIC"
|
|
return 1
|
|
fi
|
|
for artifact in $(enumerate_recovery_artifacts); do
|
|
[ "$scope" = lifecycle ] && [ "$artifact" = "$UNINSTALL_TOMBSTONE" ] && continue
|
|
[ "$scope" = uninstall ] && [ "$artifact" = "$UNINSTALL_TOMBSTONE" ] && continue
|
|
if [ "$scope" = install ] && [ "$artifact" = "$UNINSTALL_TOMBSTONE" ]; then
|
|
if state_file_is_secure "$UNINSTALL_TOMBSTONE" &&
|
|
read_uninstall_tombstone &&
|
|
[ "$UNINSTALL_FILE_MODULE" = "$MODDIR" ] &&
|
|
! uninstall_tombstone_owner_alive; then
|
|
continue
|
|
fi
|
|
[ -n "$RECOVERY_ARTIFACT_FIRST" ] || RECOVERY_ARTIFACT_FIRST="$artifact"
|
|
unsafe_seen=1
|
|
continue
|
|
fi
|
|
[ -n "$RECOVERY_ARTIFACT_FIRST" ] || RECOVERY_ARTIFACT_FIRST="$artifact"
|
|
if [ -L "$artifact" ] || ! path_uid_is_root "$artifact"; then
|
|
unsafe_seen=1
|
|
continue
|
|
fi
|
|
case "$artifact" in
|
|
"$FULL_ROLLBACK_META")
|
|
[ -f "$artifact" ] || unsafe_seen=1
|
|
rollback_seen=1
|
|
rollback_meta=1
|
|
;;
|
|
"$FULL_ROLLBACK_TRANSACTION")
|
|
[ -f "$artifact" ] || unsafe_seen=1
|
|
rollback_seen=1
|
|
rollback_tx=1
|
|
;;
|
|
"$FULL_ROLLBACK_HOSTS_BACKUP")
|
|
[ -f "$artifact" ] || unsafe_seen=1
|
|
rollback_seen=1
|
|
;;
|
|
"$FULL_ROLLBACK_HOSTS_BACKUP".tmp.*)
|
|
rollback_seen=1
|
|
if [ "$scope" = full-rollback ] && read_transaction >/dev/null 2>&1 &&
|
|
case "${artifact##*/}" in
|
|
"${FULL_ROLLBACK_HOSTS_BACKUP##*/}.tmp.${RB_TOKEN:-}."*) true ;;
|
|
*) false ;;
|
|
esac &&
|
|
state_file_is_secure "$artifact" && path_mode_is_0600 "$artifact"; then
|
|
:
|
|
else
|
|
rollback_extra=1
|
|
fi
|
|
;;
|
|
"$FULL_ROLLBACK_META"*|"$FULL_ROLLBACK_TRANSACTION"*|"$FULL_ROLLBACK_HOSTS_BACKUP"*|"$STATE_DIR"/.full-rollback.*|"$STATE_DIR"/.hosts.rollback.*)
|
|
rollback_seen=1
|
|
rollback_extra=1
|
|
;;
|
|
*) unsafe_seen=1 ;;
|
|
esac
|
|
done
|
|
if [ "$unsafe_seen" = 1 ]; then
|
|
RECOVERY_ARTIFACT_CLASS=unsafe
|
|
elif [ "$rollback_seen" = 1 ]; then
|
|
if [ "$rollback_meta" = 1 ] && [ "$rollback_tx" = 0 ] && [ "$rollback_extra" = 0 ]; then
|
|
RECOVERY_ARTIFACT_CLASS=rollback-complete
|
|
else
|
|
RECOVERY_ARTIFACT_CLASS=rollback-partial
|
|
fi
|
|
else
|
|
RECOVERY_ARTIFACT_CLASS=clean
|
|
fi
|
|
if [ "$RECOVERY_ARTIFACT_CLASS" = rollback-partial ] &&
|
|
[ "$scope" = full-rollback ] && [ "$rollback_extra" = 0 ]; then
|
|
return 0
|
|
fi
|
|
case "$RECOVERY_ARTIFACT_CLASS:$scope" in
|
|
clean:*) return 0 ;;
|
|
rollback-complete:lifecycle)
|
|
[ -e "$UNINSTALL_TOMBSTONE" ] && [ ! -L "$UNINSTALL_TOMBSTONE" ] &&
|
|
state_file_is_secure "$UNINSTALL_TOMBSTONE" &&
|
|
uninstall_tombstone_allows_stop || {
|
|
RECOVERY_ARTIFACT_DIAGNOSTIC="completed rollback lifecycle access requires the exact live uninstall owner"
|
|
return 1
|
|
}
|
|
return 0
|
|
;;
|
|
rollback-complete:full-rollback|rollback-complete:install|rollback-complete:uninstall)
|
|
return 0
|
|
;;
|
|
*)
|
|
RECOVERY_ARTIFACT_DIAGNOSTIC="$RECOVERY_ARTIFACT_CLASS recovery state requires its exact owner: ${RECOVERY_ARTIFACT_FIRST:-unknown}"
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
|
|
# The root-manager boot entry point is the only boundary allowed to discard an
|
|
# incompatible state generation without parsing it. The lifecycle lock proves
|
|
# that no current-boot mutation owns the directory, while a kernel reboot has
|
|
# already destroyed every process and netfilter object described by the old
|
|
# files. Keep the exact held lock until normal release, but retire every other
|
|
# project-owned entry so unsupported schemas can never fence a fresh package.
|
|
discard_incompatible_boot_state() {
|
|
local lock_name remaining
|
|
[ "${BOOT_STALE_RUNTIME_RECOVERY:-0}" = 1 ] || return 1
|
|
caller_holds_exact_lifecycle_lock || return 1
|
|
state_dir_is_secure || return 1
|
|
lock_name="${LIFECYCLE_LOCK##*/}"
|
|
case "$lock_name" in ""|*/*) return 1 ;; esac
|
|
find "$STATE_DIR" -mindepth 1 -maxdepth 1 ! -name "$lock_name" \
|
|
-exec rm -rf {} + 2>/dev/null || return 1
|
|
remaining="$(find "$STATE_DIR" -mindepth 1 -maxdepth 1 ! -name "$lock_name" \
|
|
-print -quit 2>/dev/null)" || return 1
|
|
[ -z "$remaining" ] || return 1
|
|
BOOT_INCOMPATIBLE_STATE_RETIRED=1
|
|
return 0
|
|
}
|
|
|
|
# Boot may need to retire state from the previous kernel even when the module
|
|
# is disabled or runtime autostart is off. Recognized current-schema state uses
|
|
# the strict recovery audit. State classified as unsafe is outside the current
|
|
# generation contract and is discarded wholesale by the boot-only boundary.
|
|
recover_boot_stale_runtime_state() {
|
|
local rc=0 diagnostic=""
|
|
BOOT_RECOVERY_DIAGNOSTIC=""
|
|
BOOT_INCOMPATIBLE_STATE_RETIRED=0
|
|
if [ ! -e "$STATE_DIR" ] && [ ! -L "$STATE_DIR" ]; then
|
|
return 0
|
|
fi
|
|
state_dir_is_secure || {
|
|
BOOT_RECOVERY_DIAGNOSTIC="state directory is unsafe"
|
|
return 1
|
|
}
|
|
[ "${LOCK_HELD:-0}" = 0 ] || {
|
|
BOOT_RECOVERY_DIAGNOSTIC="unexpected inherited lifecycle lock"
|
|
return 1
|
|
}
|
|
acquire_lifecycle_lock || {
|
|
BOOT_RECOVERY_DIAGNOSTIC="lifecycle lock is busy or unsafe"
|
|
return 1
|
|
}
|
|
BOOT_STALE_RUNTIME_RECOVERY=1
|
|
if ! audit_recovery_artifacts lifecycle; then
|
|
if [ "$RECOVERY_ARTIFACT_CLASS" = unsafe ] &&
|
|
discard_incompatible_boot_state; then
|
|
diagnostic=""
|
|
else
|
|
rc=1
|
|
diagnostic="${RECOVERY_ARTIFACT_DIAGNOSTIC:-unsafe recovery state}"
|
|
fi
|
|
fi
|
|
BOOT_STALE_RUNTIME_RECOVERY=0
|
|
if ! release_lifecycle_lock; then
|
|
rc=1
|
|
[ -n "$diagnostic" ] || diagnostic="lifecycle lock release failed"
|
|
fi
|
|
if [ "$rc" -ne 0 ]; then
|
|
BOOT_RECOVERY_DIAGNOSTIC="$diagnostic"
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
canonical_mark() {
|
|
local value rest
|
|
MARK_CANONICAL=""
|
|
# printf is an external on the target shell, and a value already in
|
|
# printf's canonical 0x%x form is its own answer.
|
|
case "$1" in
|
|
0x0)
|
|
MARK_CANONICAL="$1"
|
|
return 0
|
|
;;
|
|
0x[1-9a-f]*)
|
|
rest="${1#0x}"
|
|
case "$rest" in
|
|
*[!0-9a-f]*) ;;
|
|
*)
|
|
if [ "${#rest}" -le 8 ] 2>/dev/null; then
|
|
MARK_CANONICAL="$1"
|
|
return 0
|
|
fi
|
|
;;
|
|
esac
|
|
;;
|
|
esac
|
|
value="$(printf '0x%x' "$1" 2>/dev/null)" || return 1
|
|
# Netfilter marks are unsigned 32-bit values. printf also accepts wider and
|
|
# negative shell integers, so reject every canonical result above 8 hex
|
|
# digits instead of letting Android and the runtime disagree later.
|
|
case "$value" in
|
|
0x[0-9a-f]|0x[0-9a-f][0-9a-f]|0x[0-9a-f][0-9a-f][0-9a-f]|0x[0-9a-f][0-9a-f][0-9a-f][0-9a-f]|0x[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]|0x[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]|0x[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]|0x[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f])
|
|
MARK_CANONICAL="$value"
|
|
;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
prepare_private_runtime_file() {
|
|
local path="$1"
|
|
ensure_state_dir || return 1
|
|
meta_cache_forget "$path"
|
|
state_file_target_is_safe "$path" || return 1
|
|
umask 077
|
|
: > "$path" || return 1
|
|
chmod 0600 "$path" 2>/dev/null || return 1
|
|
state_file_is_secure "$path"
|
|
}
|
|
|
|
write_private_runtime_line() {
|
|
local path="$1" value="$2" tmp="$1.tmp.$$" size
|
|
ensure_state_dir || return 1
|
|
meta_cache_forget "$path"
|
|
state_file_target_is_safe "$path" || return 1
|
|
state_path_is_managed_file "$tmp" || return 1
|
|
[ ! -e "$tmp" ] && [ ! -L "$tmp" ] || return 1
|
|
size="${#value}"
|
|
[ "$size" -le "$RUNTIME_METADATA_MAX_BYTES" ] 2>/dev/null || return 1
|
|
umask 077
|
|
z2_emit_line "$value" > "$tmp" || { rm -f "$tmp"; return 1; }
|
|
mv -f "$tmp" "$path" || { rm -f "$tmp"; return 1; }
|
|
}
|
|
|
|
write_runtime_owner_marker() {
|
|
local tmp="$RUNTIME_OWNER_MARKER.tmp.$$"
|
|
ensure_state_dir || return 1
|
|
meta_cache_forget "$RUNTIME_OWNER_MARKER"
|
|
state_file_target_is_safe "$RUNTIME_OWNER_MARKER" || return 1
|
|
state_path_is_managed_file "$tmp" || return 1
|
|
[ ! -e "$tmp" ] && [ ! -L "$tmp" ] || return 1
|
|
umask 077
|
|
{
|
|
echo "version=1"
|
|
echo "module_dir=$MODDIR"
|
|
echo "nfqws=$NFQWS2"
|
|
} > "$tmp" || { rm -f "$tmp"; return 1; }
|
|
mv -f "$tmp" "$RUNTIME_OWNER_MARKER" || { rm -f "$tmp"; return 1; }
|
|
}
|
|
|
|
read_runtime_owner_marker() {
|
|
local key value version="" module="" nfqws=""
|
|
state_file_is_secure "$RUNTIME_OWNER_MARKER" || return 1
|
|
while IFS='=' read -r key value; do
|
|
case "$key" in
|
|
version) version="$value" ;;
|
|
module_dir) module="$value" ;;
|
|
nfqws) nfqws="$value" ;;
|
|
esac
|
|
done < "$RUNTIME_OWNER_MARKER"
|
|
[ "$version" = 1 ] && [ "$module" = "$MODDIR" ] && [ "$nfqws" = "$NFQWS2" ]
|
|
}
|
|
|
|
new_lifecycle_token_read() {
|
|
Z2_NEW_TOKEN=""
|
|
if [ -r /proc/sys/kernel/random/uuid ]; then
|
|
IFS= read -r Z2_NEW_TOKEN < /proc/sys/kernel/random/uuid 2>/dev/null || Z2_NEW_TOKEN=""
|
|
fi
|
|
if ! is_safe_token "$Z2_NEW_TOKEN"; then
|
|
Z2_NEW_TOKEN="z2-$(date +%s 2>/dev/null)-$$-$(proc_starttime "$$" 2>/dev/null || echo 0)"
|
|
fi
|
|
}
|
|
|
|
new_lifecycle_token() {
|
|
new_lifecycle_token_read || return 1
|
|
z2_emit_line "$Z2_NEW_TOKEN"
|
|
}
|
|
|
|
normalize_qnum() {
|
|
local raw="$1" normalized
|
|
QNUM_NORMALIZED=""
|
|
is_decimal "$raw" || return 1
|
|
normalized="$raw"
|
|
while [ "${normalized#0}" != "$normalized" ]; do
|
|
normalized="${normalized#0}"
|
|
done
|
|
[ -n "$normalized" ] || normalized=0
|
|
[ "${#normalized}" -le 5 ] || return 1
|
|
[ "$normalized" -ge 1 ] 2>/dev/null || return 1
|
|
[ "$normalized" -le 65535 ] 2>/dev/null || return 1
|
|
QNUM_NORMALIZED="$normalized"
|
|
return 0
|
|
}
|
|
|
|
runtime_config_exists() {
|
|
local size
|
|
path_meta_capture "$RUNTIME_CONFIG"
|
|
if [ -f "$RUNTIME_CONFIG" ] && [ ! -L "$RUNTIME_CONFIG" ] && [ -r "$RUNTIME_CONFIG" ] &&
|
|
path_uid_is_root "$RUNTIME_CONFIG" && path_nlink_is_one "$RUNTIME_CONFIG" &&
|
|
runtime_config_mode_is_safe "$RUNTIME_CONFIG" &&
|
|
path_meta_size_read "$RUNTIME_CONFIG"; then
|
|
size="$Z2_PATH_SIZE"
|
|
path_meta_retire
|
|
else
|
|
path_meta_retire
|
|
return 1
|
|
fi
|
|
is_decimal "$size" && [ "$size" -gt 0 ] 2>/dev/null &&
|
|
[ "$size" -le "$RUNTIME_METADATA_MAX_BYTES" ] 2>/dev/null
|
|
}
|
|
|
|
runtime_config_mode_is_safe() {
|
|
local path="$1" mode listing
|
|
if [ -n "$Z2_PATH_META_PATH" ] && [ "$Z2_PATH_META_PATH" = "$path" ]; then
|
|
case "$Z2_PATH_META_MODE" in 600|644) return 0;; *) return 1;; esac
|
|
fi
|
|
if [ "$Z2_HAVE_STAT" = 1 ]; then
|
|
mode="$(stat -c '%a' "$path" 2>/dev/null)" || return 1
|
|
case "$mode" in 600|644) return 0;; *) return 1;; esac
|
|
fi
|
|
listing="$(ls -ln "$path" 2>/dev/null)" || return 1
|
|
set -- $listing
|
|
case "${1:-}" in -rw-------*|-rw-r--r--*) return 0;; *) return 1;; esac
|
|
}
|
|
|
|
runtime_config_state_reason() {
|
|
if [ -L "$RUNTIME_CONFIG" ]; then echo "unsafe-symlink"
|
|
elif [ -e "$RUNTIME_CONFIG" ]; then echo "unreadable-or-unsafe"
|
|
else echo "missing"
|
|
fi
|
|
}
|
|
|
|
ensure_runtime_core_config() {
|
|
RUNTIME_CONFIG_STATUS="unknown"
|
|
RUNTIME_CONFIG_REASON=""
|
|
RUNTIME_CONFIG_ERROR=""
|
|
RUNTIME_CORE_REPAIR_MODE="defaults"
|
|
if runtime_config_exists; then
|
|
set_core_config_defaults
|
|
if apply_runtime_core_overrides; then
|
|
RUNTIME_CONFIG_STATUS="loaded"
|
|
return 0
|
|
fi
|
|
RUNTIME_CONFIG_REASON="invalid-or-partial"
|
|
set_core_config_defaults
|
|
else
|
|
RUNTIME_CONFIG_REASON="$(runtime_config_state_reason)"
|
|
# Never replace an existing symlink, directory, device, or unreadable
|
|
# file. Only a missing path or a readable regular runtime can heal.
|
|
if [ -e "$RUNTIME_CONFIG" ] || [ -L "$RUNTIME_CONFIG" ]; then
|
|
RUNTIME_CONFIG_STATUS="unavailable"
|
|
[ -n "$RUNTIME_CONFIG_ERROR" ] || RUNTIME_CONFIG_ERROR="unsafe runtime.ini target"
|
|
return 1
|
|
fi
|
|
set_core_config_defaults
|
|
fi
|
|
if regenerate_runtime_core_config; then
|
|
RUNTIME_CONFIG_STATUS="regenerated"
|
|
set_core_config_defaults
|
|
apply_runtime_core_overrides || {
|
|
RUNTIME_CONFIG_STATUS="unavailable"
|
|
[ -n "$RUNTIME_CONFIG_ERROR" ] || RUNTIME_CONFIG_ERROR="regenerated runtime.ini failed validation"
|
|
return 1
|
|
}
|
|
return 0
|
|
fi
|
|
[ -n "$RUNTIME_CONFIG_ERROR" ] || RUNTIME_CONFIG_ERROR="runtime.ini regeneration failed"
|
|
RUNTIME_CONFIG_STATUS="unavailable"
|
|
return 1
|
|
}
|
|
|
|
regenerate_runtime_core_config() {
|
|
local runtime_tool="$SCRIPT_DIR/runtime-config.sh"
|
|
[ -f "$runtime_tool" ] && [ ! -L "$runtime_tool" ] || return 1
|
|
if runtime_config_exists; then
|
|
sh "$runtime_tool" --repair "$RUNTIME_CONFIG" >/dev/null 2>&1 || return 1
|
|
else
|
|
sh "$runtime_tool" "$RUNTIME_CONFIG" >/dev/null 2>&1 || return 1
|
|
fi
|
|
runtime_config_exists
|
|
}
|
|
|
|
runtime_config_status_message() {
|
|
case "$RUNTIME_CONFIG_STATUS" in
|
|
loaded) echo "runtime.ini is present and authoritative: $RUNTIME_CONFIG" ;;
|
|
regenerated) echo "runtime.ini was regenerated because it was $RUNTIME_CONFIG_REASON: $RUNTIME_CONFIG" ;;
|
|
unavailable) echo "runtime.ini is unavailable ($RUNTIME_CONFIG_REASON): ${RUNTIME_CONFIG_ERROR:-validation failed}" ;;
|
|
*) echo "Runtime config status: $RUNTIME_CONFIG_STATUS" ;;
|
|
esac
|
|
}
|
|
|
|
core_config_source_message() { echo "Core config source: $CORE_CONFIG_SOURCE_PATH"; }
|
|
|
|
# Configuration parsing runs on the boot path and may inspect thousands of
|
|
# catalog lines. Keep trimming in the current shell: spawning sed for every
|
|
# scalar makes validation take minutes on process-heavy Android devices.
|
|
trim_config_value_in_place() {
|
|
CONFIG_VALUE_TRIMMED="$1"
|
|
CONFIG_VALUE_TRIMMED="${CONFIG_VALUE_TRIMMED#"${CONFIG_VALUE_TRIMMED%%[![:space:]]*}"}"
|
|
CONFIG_VALUE_TRIMMED="${CONFIG_VALUE_TRIMMED%"${CONFIG_VALUE_TRIMMED##*[![:space:]]}"}"
|
|
}
|
|
|
|
# Decode one INI/bootstrap scalar without eval, command substitution or escape
|
|
# expansion. Matching outer quotes are removed; unmatched quotes are rejected.
|
|
decode_config_value() {
|
|
local value first last
|
|
CONFIG_VALUE_DECODED=""
|
|
case "$1" in *'
|
|
'*) return 1 ;; esac
|
|
trim_config_value_in_place "$1"
|
|
value="$CONFIG_VALUE_TRIMMED"
|
|
[ -n "$value" ] || { CONFIG_VALUE_DECODED=""; return 0; }
|
|
first="${value%"${value#?}"}"
|
|
last="${value#"${value%?}"}"
|
|
case "$first" in
|
|
\"|\')
|
|
[ "${#value}" -ge 2 ] || return 1
|
|
[ "$last" = "$first" ] || return 1
|
|
value="${value#?}"
|
|
value="${value%?}"
|
|
;;
|
|
*) case "$last" in \"|\') return 1 ;; esac ;;
|
|
esac
|
|
# The character class covers embedded CR/LF and every other control byte
|
|
# without a command substitution for each parsed scalar.
|
|
case "$value" in *[[:cntrl:]]*) return 1 ;; esac
|
|
CONFIG_VALUE_DECODED="$value"
|
|
}
|
|
|
|
apply_core_config_key() {
|
|
local key="$1" value="$2"
|
|
case "$key" in
|
|
autostart|AUTOSTART) case "$value" in 0|1) AUTOSTART="$value" ;; *) return 1;; esac ;;
|
|
wifi_only|WIFI_ONLY) case "$value" in 0|1) WIFI_ONLY="$value" ;; *) return 1;; esac ;;
|
|
tethering|TETHERING) case "$value" in 0|1) TETHERING="$value" ;; *) return 1;; esac ;;
|
|
debug|DEBUG) case "$value" in 0|1) DEBUG="$value" ;; *) return 1;; esac ;;
|
|
qnum|QNUM) normalize_qnum "$value" || return 1; QNUM="$QNUM_NORMALIZED" ;;
|
|
desync_mark|DESYNC_MARK) canonical_mark "$value" || return 1; DESYNC_MARK="$MARK_CANONICAL" ;;
|
|
active_preset|ACTIVE_PRESET)
|
|
is_safe_runtime_file_name "$value" || return 1
|
|
case "$value" in _*|*.txt) ;; *) return 1 ;; esac
|
|
case "$value" in _*) return 1 ;; esac
|
|
ACTIVE_PRESET="$value"
|
|
;;
|
|
nfqws_uid|NFQWS_UID)
|
|
case "$value" in
|
|
*:*)
|
|
case "${value#*:}" in *:*) return 1;; esac
|
|
is_canonical_nfqws_id "${value%%:*}" &&
|
|
is_canonical_nfqws_id "${value#*:}" || return 1
|
|
NFQWS_UID="$value"
|
|
;;
|
|
*) return 1 ;;
|
|
esac
|
|
;;
|
|
log_mode|LOG_MODE) case "$value" in android|file|syslog|none) LOG_MODE="$value" ;; *) return 1;; esac ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
is_safe_file_name_byte_length() {
|
|
local value="$1"
|
|
local byte_length LC_ALL=C
|
|
|
|
# BusyBox ash on Android counts characters rather than bytes in ${#value},
|
|
# even with LC_ALL=C. Keep the common printable-ASCII path shell-native.
|
|
case "$value" in
|
|
*[!\ -~]*)
|
|
byte_length="$(printf '%s' "$value" | LC_ALL=C wc -c)" || return 1
|
|
;;
|
|
*) byte_length="${#value}" ;;
|
|
esac
|
|
case "$byte_length" in
|
|
''|*[!0-9]*) return 1 ;;
|
|
esac
|
|
[ "$byte_length" -le 255 ] 2>/dev/null
|
|
}
|
|
|
|
is_safe_runtime_file_name() {
|
|
local value="$1"
|
|
[ -n "$value" ] && is_safe_file_name_byte_length "$value" || return 1
|
|
trim_config_value_in_place "$value"
|
|
[ "$value" = "$CONFIG_VALUE_TRIMMED" ] || return 1
|
|
[ "$value" != . ] && [ "$value" != .. ] || return 1
|
|
case "$value" in */*|*\\*|*\"*|*\'*) return 1;; esac
|
|
case "$value" in *[[:cntrl:]]*) return 1 ;; esac
|
|
return 0
|
|
}
|
|
|
|
set_core_config_defaults() {
|
|
RUNTIME_SOURCE="builtin-defaults"
|
|
AUTOSTART=1
|
|
WIFI_ONLY=0
|
|
TETHERING=0
|
|
DEBUG=0
|
|
QNUM=200
|
|
DESYNC_MARK=0x40000000
|
|
ACTIVE_PRESET="Default v1 (game filter).txt"
|
|
NFQWS_UID="0:0"
|
|
LOG_MODE="none"
|
|
}
|
|
|
|
# WIFI_ONLY=1 was accepted by older releases even though the current firewall
|
|
# contract has no verified interface selector. Migrate it to the safe mode.
|
|
normalize_unsupported_wifi_only() {
|
|
WIFI_ONLY_LEGACY_NORMALIZED=0
|
|
case "${WIFI_ONLY:-}" in
|
|
0) return 0 ;;
|
|
1)
|
|
WIFI_ONLY=0
|
|
WIFI_ONLY_LEGACY_NORMALIZED=1
|
|
return 0
|
|
;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
apply_runtime_core_overrides() {
|
|
runtime_config_exists || return 1
|
|
local current_section="" line="" cr key value core_sections=0 seen_keys="|" required missing=""
|
|
RUNTIME_CORE_REPAIR_MODE="defaults"
|
|
cr="$Z2_CR"
|
|
while IFS= read -r line || [ -n "$line" ]; do
|
|
line="${line%"$cr"}"
|
|
trim_config_value_in_place "$line"
|
|
line="$CONFIG_VALUE_TRIMMED"
|
|
case "$line" in
|
|
""|"#"*|";"*) continue ;;
|
|
"["*"]")
|
|
current_section="${line#[}"
|
|
current_section="${current_section%]}"
|
|
if [ "$current_section" = core ]; then
|
|
core_sections=$((core_sections + 1))
|
|
[ "$core_sections" -eq 1 ] || {
|
|
RUNTIME_CONFIG_ERROR="runtime.ini contains duplicate [core] sections"
|
|
return 1
|
|
}
|
|
fi
|
|
continue
|
|
;;
|
|
esac
|
|
[ "$current_section" = core ] || continue
|
|
case "$line" in
|
|
*=*)
|
|
trim_config_value_in_place "${line%%=*}"
|
|
key="$CONFIG_VALUE_TRIMMED"
|
|
case "$key" in ""|*[!a-z0-9_-]*)
|
|
RUNTIME_CONFIG_ERROR="invalid runtime.ini [core] key: $key"
|
|
return 1
|
|
;;
|
|
esac
|
|
value="${line#*=}"
|
|
decode_config_value "$value" || {
|
|
RUNTIME_CONFIG_ERROR="invalid quoted value for [core] $key"
|
|
return 1
|
|
}
|
|
value="$CONFIG_VALUE_DECODED"
|
|
;;
|
|
*)
|
|
RUNTIME_CONFIG_ERROR="malformed runtime.ini [core] line"
|
|
return 1
|
|
;;
|
|
esac
|
|
case "$seen_keys" in *"|$key|"*)
|
|
RUNTIME_CONFIG_ERROR="duplicate runtime.ini [core] key: $key"
|
|
return 1
|
|
;;
|
|
esac
|
|
seen_keys="${seen_keys}${key}|"
|
|
case "$key" in
|
|
schema_version)
|
|
[ "$value" = 1 ] || { RUNTIME_CONFIG_ERROR="unsupported runtime.ini schema_version"; return 1; }
|
|
;;
|
|
config_format)
|
|
[ "$value" = runtime-v1 ] || { RUNTIME_CONFIG_ERROR="unsupported runtime.ini config_format"; return 1; }
|
|
;;
|
|
runtime_source)
|
|
case "$value" in ""|*[!A-Za-z0-9._-]*) RUNTIME_CONFIG_ERROR="invalid runtime.ini runtime_source"; return 1;; esac
|
|
RUNTIME_SOURCE="$value"
|
|
;;
|
|
autostart|wifi_only|tethering|debug|qnum|desync_mark|active_preset|nfqws_uid|log_mode)
|
|
apply_core_config_key "$key" "$value" || {
|
|
if [ "$key" = qnum ]; then
|
|
RUNTIME_CONFIG_ERROR="qnum=$value, expected 1..65535"
|
|
else
|
|
RUNTIME_CONFIG_ERROR="$key=$value is invalid"
|
|
fi
|
|
return 1
|
|
}
|
|
;;
|
|
*)
|
|
RUNTIME_CONFIG_ERROR="unsupported runtime.ini [core] key: $key"
|
|
return 1
|
|
;;
|
|
esac
|
|
done < "$RUNTIME_CONFIG"
|
|
|
|
[ "$core_sections" -eq 1 ] || {
|
|
RUNTIME_CONFIG_ERROR="runtime.ini has no [core] section"
|
|
return 1
|
|
}
|
|
for required in $RUNTIME_CORE_REQUIRED_KEYS; do
|
|
case "$seen_keys" in *"|$required|"*) ;; *) missing="${missing}${missing:+,}$required" ;; esac
|
|
done
|
|
if [ -n "$missing" ]; then
|
|
RUNTIME_CONFIG_ERROR="runtime.ini [core] is partial; missing: $missing"
|
|
return 1
|
|
fi
|
|
|
|
if ! normalize_qnum "$QNUM"; then
|
|
RUNTIME_CONFIG_ERROR="invalid [core] qnum '$QNUM' (expected decimal 1..65535)"
|
|
return 1
|
|
fi
|
|
QNUM="$QNUM_NORMALIZED"
|
|
return 0
|
|
}
|
|
|
|
runtime_config_error_code() {
|
|
case "$1" in
|
|
"unsupported runtime.ini schema_version") RUNTIME_CONFIG_ERROR_CODE=UNSUPPORTED_SCHEMA ;;
|
|
"unsupported runtime.ini config_format") RUNTIME_CONFIG_ERROR_CODE=UNSUPPORTED_FORMAT ;;
|
|
"runtime.ini contains duplicate [core] sections") RUNTIME_CONFIG_ERROR_CODE=DUPLICATE_CORE ;;
|
|
"invalid runtime.ini [core] key:"*) RUNTIME_CONFIG_ERROR_CODE=INVALID_CORE_KEY ;;
|
|
"invalid quoted value for [core]"*) RUNTIME_CONFIG_ERROR_CODE=INVALID_QUOTED_VALUE ;;
|
|
"malformed runtime.ini [core] line") RUNTIME_CONFIG_ERROR_CODE=MALFORMED_CORE_LINE ;;
|
|
"duplicate runtime.ini [core] key:"*) RUNTIME_CONFIG_ERROR_CODE=DUPLICATE_CORE_KEY ;;
|
|
"invalid runtime.ini runtime_source") RUNTIME_CONFIG_ERROR_CODE=INVALID_RUNTIME_SOURCE ;;
|
|
"unsupported runtime.ini [core] key:"*) RUNTIME_CONFIG_ERROR_CODE=UNKNOWN_CORE_KEY ;;
|
|
"runtime.ini has no [core] section") RUNTIME_CONFIG_ERROR_CODE=MISSING_CORE ;;
|
|
"runtime.ini [core] is partial;"*) RUNTIME_CONFIG_ERROR_CODE=INCOMPLETE_CORE ;;
|
|
"invalid [core] qnum"*|"invalid [core] value for qnum"|qnum=*", expected 1..65535")
|
|
RUNTIME_CONFIG_ERROR_CODE=INVALID_QNUM
|
|
;;
|
|
"invalid [core] value for"*|*" is invalid")
|
|
RUNTIME_CONFIG_ERROR_CODE=INVALID_CORE_VALUE
|
|
;;
|
|
"runtime.ini is required for read-only status")
|
|
RUNTIME_CONFIG_ERROR_CODE=RUNTIME_MISSING
|
|
;;
|
|
*) RUNTIME_CONFIG_ERROR_CODE=CONFIG_INVALID ;;
|
|
esac
|
|
}
|
|
|
|
load_effective_core_config() {
|
|
set_core_config_defaults
|
|
CORE_CONFIG_SOURCE="defaults"
|
|
CORE_CONFIG_SOURCE_PATH="built-in defaults"
|
|
if ensure_runtime_core_config; then
|
|
CORE_CONFIG_SOURCE="runtime.ini"
|
|
CORE_CONFIG_SOURCE_PATH="$RUNTIME_CONFIG"
|
|
normalize_unsupported_wifi_only || return 1
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# Status and diagnostics never create or migrate configuration and never read
|
|
# bootstrap inputs. A missing, partial, or invalid runtime is reported as such.
|
|
load_effective_core_config_readonly() {
|
|
set_core_config_defaults
|
|
CORE_CONFIG_SOURCE="defaults"
|
|
CORE_CONFIG_SOURCE_PATH="built-in defaults"
|
|
RUNTIME_CONFIG_ERROR=""
|
|
if runtime_config_exists; then
|
|
RUNTIME_CONFIG_REASON=""
|
|
CORE_CONFIG_SOURCE="runtime.ini"
|
|
CORE_CONFIG_SOURCE_PATH="$RUNTIME_CONFIG"
|
|
if ! apply_runtime_core_overrides; then
|
|
RUNTIME_CONFIG_STATUS="unavailable"
|
|
RUNTIME_CONFIG_REASON="invalid-or-partial"
|
|
return 1
|
|
fi
|
|
RUNTIME_CONFIG_STATUS="loaded"
|
|
normalize_unsupported_wifi_only || return 1
|
|
return 0
|
|
fi
|
|
|
|
RUNTIME_CONFIG_STATUS="unavailable"
|
|
RUNTIME_CONFIG_REASON="$(runtime_config_state_reason)"
|
|
RUNTIME_CONFIG_ERROR="runtime.ini is required for read-only status"
|
|
return 1
|
|
}
|
|
|
|
# Field 22 of /proc/PID/stat, parsed with builtins after the last ')'.
|
|
# proc_starttime_read is the fork-free form: hot call sites consume the
|
|
# global instead of paying a command-substitution fork per proof. The
|
|
# printf wrapper below stays for scripts and captures that want a value.
|
|
PROC_STARTTIME=""
|
|
proc_starttime_read() {
|
|
local pid="$1" stat tail
|
|
PROC_STARTTIME=""
|
|
is_decimal "$pid" || return 1
|
|
[ "$pid" -gt 0 ] 2>/dev/null || return 1
|
|
[ -r "/proc/$pid/stat" ] || return 1
|
|
IFS= read -r stat < "/proc/$pid/stat" || return 1
|
|
tail="${stat##*) }"
|
|
set -- $tail
|
|
[ "$#" -ge 20 ] || return 1
|
|
shift 19
|
|
PROC_STARTTIME="$1"
|
|
}
|
|
|
|
proc_starttime() {
|
|
proc_starttime_read "$1" || return 1
|
|
printf '%s\n' "$PROC_STARTTIME"
|
|
}
|
|
|
|
LOCK_HELD=0
|
|
LOCK_OWNER_PID=""
|
|
LOCK_OWNER_START=""
|
|
LOCK_OWNER_TOKEN=""
|
|
LIFECYCLE_ACQUIRE_CANDIDATE=""
|
|
LIFECYCLE_ACQUIRE_TOKEN=""
|
|
|
|
read_lock_owner() {
|
|
LOCK_FILE_PID=""; LOCK_FILE_START=""; LOCK_FILE_TOKEN=""
|
|
LOCK_FILE_KIND=""; LOCK_FILE_BOOT=""; LOCK_FILE_MODULE=""
|
|
state_dir_is_secure || return 1
|
|
[ -d "$LIFECYCLE_LOCK" ] && [ ! -L "$LIFECYCLE_LOCK" ] || return 1
|
|
[ -f "$LIFECYCLE_LOCK_OWNER" ] && [ ! -L "$LIFECYCLE_LOCK_OWNER" ] || return 1
|
|
path_meta_capture "$LIFECYCLE_LOCK_OWNER"
|
|
if path_uid_is_root "$LIFECYCLE_LOCK_OWNER" && path_mode_is_0600 "$LIFECYCLE_LOCK_OWNER" &&
|
|
path_nlink_is_one "$LIFECYCLE_LOCK_OWNER"; then
|
|
path_meta_retire
|
|
else
|
|
path_meta_retire
|
|
return 1
|
|
fi
|
|
local key value sequence="" version="" kind="" boot="" module=""
|
|
while IFS='=' read -r key value; do
|
|
sequence="${sequence}${sequence:+|}$key"
|
|
case "$key" in
|
|
pid) LOCK_FILE_PID="$value" ;;
|
|
starttime) LOCK_FILE_START="$value" ;;
|
|
token) LOCK_FILE_TOKEN="$value" ;;
|
|
version) version="$value" ;;
|
|
kind) kind="$value" ;;
|
|
boot_id) boot="$value" ;;
|
|
module_dir) module="$value" ;;
|
|
*) return 1 ;;
|
|
esac
|
|
done < "$LIFECYCLE_LOCK_OWNER"
|
|
is_decimal "$LOCK_FILE_PID" && [ "$LOCK_FILE_PID" -gt 0 ] 2>/dev/null &&
|
|
is_decimal "$LOCK_FILE_START" && [ "$LOCK_FILE_START" -gt 0 ] 2>/dev/null &&
|
|
is_safe_token "$LOCK_FILE_TOKEN" || return 1
|
|
case "$sequence" in
|
|
pid\|starttime\|token)
|
|
[ -z "$version$kind$boot$module" ] || return 1
|
|
LOCK_FILE_KIND=shell
|
|
;;
|
|
version\|kind\|pid\|starttime\|boot_id\|token\|module_dir)
|
|
[ "$version" = 1 ] && [ "$kind" = android-mutation ] &&
|
|
is_valid_boot_id "$boot" && [ "$module" = "$MODDIR" ] || return 1
|
|
LOCK_FILE_KIND=android-mutation
|
|
LOCK_FILE_BOOT="$boot"
|
|
LOCK_FILE_MODULE="$module"
|
|
;;
|
|
*) return 1 ;;
|
|
esac
|
|
return 0
|
|
}
|
|
|
|
lock_owner_alive() {
|
|
read_lock_owner || return 1
|
|
if [ "$LOCK_FILE_KIND" = android-mutation ]; then
|
|
# Boot identity is part of the Android lease. A proven mismatch is
|
|
# stale even if the numeric PID was reused; an unavailable boot query
|
|
# is unknown and therefore blocks cleanup rather than weakening it.
|
|
read_current_boot_id || return 0
|
|
[ "$LOCK_FILE_BOOT" = "$CURRENT_BOOT_ID" ] || return 1
|
|
fi
|
|
proc_starttime_read "$LOCK_FILE_PID" || return 1
|
|
[ "$PROC_STARTTIME" = "$LOCK_FILE_START" ]
|
|
}
|
|
|
|
# Read-only, constant-cost lifecycle classification. Unlike lock_owner_alive,
|
|
# this preserves the distinction between a live owner, a proven stale owner,
|
|
# and ownership that cannot be authenticated safely.
|
|
classify_lifecycle_lock() {
|
|
local actual
|
|
LIFECYCLE_OBSERVED_STATE=idle
|
|
LIFECYCLE_OBSERVED_KIND=none
|
|
|
|
if [ ! -e "$LIFECYCLE_LOCK" ] && [ ! -L "$LIFECYCLE_LOCK" ]; then
|
|
if [ -e "$STATE_DIR" ] || [ -L "$STATE_DIR" ]; then
|
|
state_dir_is_secure || {
|
|
LIFECYCLE_OBSERVED_STATE=ambiguous
|
|
LIFECYCLE_OBSERVED_KIND=unknown
|
|
}
|
|
fi
|
|
return 0
|
|
fi
|
|
read_lock_owner || {
|
|
LIFECYCLE_OBSERVED_STATE=ambiguous
|
|
LIFECYCLE_OBSERVED_KIND=unknown
|
|
return 0
|
|
}
|
|
LIFECYCLE_OBSERVED_KIND="$LOCK_FILE_KIND"
|
|
if [ "$LOCK_FILE_KIND" = android-mutation ]; then
|
|
read_current_boot_id || {
|
|
LIFECYCLE_OBSERVED_STATE=ambiguous
|
|
return 0
|
|
}
|
|
if [ "$LOCK_FILE_BOOT" != "$CURRENT_BOOT_ID" ]; then
|
|
LIFECYCLE_OBSERVED_STATE=stale
|
|
return 0
|
|
fi
|
|
fi
|
|
proc_starttime_read "$LOCK_FILE_PID" 2>/dev/null || {
|
|
LIFECYCLE_OBSERVED_STATE=stale
|
|
return 0
|
|
}
|
|
if [ "$PROC_STARTTIME" = "$LOCK_FILE_START" ]; then
|
|
LIFECYCLE_OBSERVED_STATE=active
|
|
else
|
|
LIFECYCLE_OBSERVED_STATE=stale
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# Caller-relative identity check for a previously classified live Android lease.
|
|
# This is authentication only: observers never acquire, recover, or release the lock.
|
|
lifecycle_lock_is_owned_by_caller() {
|
|
[ "$LIFECYCLE_OBSERVED_STATE" = active ] &&
|
|
[ "$LOCK_FILE_KIND" = android-mutation ] &&
|
|
[ -n "${ZAPRET2_LIFECYCLE_TOKEN:-}" ] &&
|
|
[ "${ZAPRET2_LIFECYCLE_TOKEN:-}" = "$LOCK_FILE_TOKEN" ] &&
|
|
[ "${ZAPRET2_LIFECYCLE_OWNER_PID:-}" = "$LOCK_FILE_PID" ] &&
|
|
[ "${ZAPRET2_LIFECYCLE_OWNER_START:-}" = "$LOCK_FILE_START" ]
|
|
}
|
|
|
|
read_lifecycle_gate() {
|
|
local key value
|
|
GATE_FILE_PID=""; GATE_FILE_START=""; GATE_FILE_TOKEN=""
|
|
state_file_is_secure "$LIFECYCLE_LOCK_REAPER" || return 1
|
|
while IFS='=' read -r key value; do
|
|
case "$key" in
|
|
pid) GATE_FILE_PID="$value" ;;
|
|
starttime) GATE_FILE_START="$value" ;;
|
|
token) GATE_FILE_TOKEN="$value" ;;
|
|
esac
|
|
done < "$LIFECYCLE_LOCK_REAPER"
|
|
is_decimal "$GATE_FILE_PID" && is_decimal "$GATE_FILE_START" && is_safe_token "$GATE_FILE_TOKEN"
|
|
}
|
|
|
|
lifecycle_gate_alive() {
|
|
read_lifecycle_gate || return 1
|
|
proc_starttime_read "$GATE_FILE_PID" || return 1
|
|
[ "$PROC_STARTTIME" = "$GATE_FILE_START" ]
|
|
}
|
|
|
|
release_lifecycle_gate() {
|
|
local token="$1"
|
|
read_lifecycle_gate || return 1
|
|
[ "$GATE_FILE_PID" = "$$" ] && [ "$GATE_FILE_TOKEN" = "$token" ] || return 1
|
|
rm -f "$LIFECYCLE_LOCK_REAPER" 2>/dev/null
|
|
}
|
|
|
|
read_lifecycle_recovery_gate() {
|
|
local key value
|
|
RECOVERY_FILE_PID=""; RECOVERY_FILE_START=""; RECOVERY_FILE_TOKEN=""
|
|
state_file_is_secure "$LIFECYCLE_LOCK_REAPER_RECOVERY" || return 1
|
|
while IFS='=' read -r key value; do
|
|
case "$key" in
|
|
pid) RECOVERY_FILE_PID="$value" ;;
|
|
starttime) RECOVERY_FILE_START="$value" ;;
|
|
token) RECOVERY_FILE_TOKEN="$value" ;;
|
|
esac
|
|
done < "$LIFECYCLE_LOCK_REAPER_RECOVERY"
|
|
is_decimal "$RECOVERY_FILE_PID" && is_decimal "$RECOVERY_FILE_START" && is_safe_token "$RECOVERY_FILE_TOKEN"
|
|
}
|
|
|
|
lifecycle_recovery_gate_alive() {
|
|
read_lifecycle_recovery_gate || return 1
|
|
proc_starttime_read "$RECOVERY_FILE_PID" || return 1
|
|
[ "$PROC_STARTTIME" = "$RECOVERY_FILE_START" ]
|
|
}
|
|
|
|
release_lifecycle_recovery_gate() {
|
|
local token="$1"
|
|
read_lifecycle_recovery_gate || return 1
|
|
[ "$RECOVERY_FILE_PID" = "$$" ] && [ "$RECOVERY_FILE_TOKEN" = "$token" ] || return 1
|
|
rm -f "$LIFECYCLE_LOCK_REAPER_RECOVERY" 2>/dev/null
|
|
}
|
|
|
|
claim_lifecycle_recovery_gate() {
|
|
local self_start="$1" token="$2" tmp="$LIFECYCLE_LOCK_REAPER_RECOVERY.tmp.$$.$token"
|
|
local stale_pid stale_start stale_token quarantine
|
|
umask 077
|
|
z2_emit_line "pid=$$
|
|
starttime=$self_start
|
|
token=$token" > "$tmp" || return 1
|
|
if ln "$tmp" "$LIFECYCLE_LOCK_REAPER_RECOVERY" 2>/dev/null; then
|
|
rm -f "$tmp"
|
|
return 0
|
|
fi
|
|
rm -f "$tmp"
|
|
read_lifecycle_recovery_gate || return 1
|
|
lifecycle_recovery_gate_alive && return 1
|
|
stale_pid="$RECOVERY_FILE_PID"; stale_start="$RECOVERY_FILE_START"; stale_token="$RECOVERY_FILE_TOKEN"
|
|
sleep 1
|
|
if ! read_lifecycle_recovery_gate || lifecycle_recovery_gate_alive ||
|
|
[ "$RECOVERY_FILE_PID" != "$stale_pid" ] || [ "$RECOVERY_FILE_START" != "$stale_start" ] ||
|
|
[ "$RECOVERY_FILE_TOKEN" != "$stale_token" ]; then
|
|
return 1
|
|
fi
|
|
quarantine="$LIFECYCLE_LOCK_REAPER_RECOVERY_QUARANTINE.$$.$token"
|
|
[ ! -e "$quarantine" ] || return 1
|
|
mv "$LIFECYCLE_LOCK_REAPER_RECOVERY" "$quarantine" 2>/dev/null || return 1
|
|
rm -f "$quarantine" 2>/dev/null || return 1
|
|
return 1
|
|
}
|
|
|
|
claim_lifecycle_gate() {
|
|
local self_start="$1" token="$2" tmp="$LIFECYCLE_LOCK_REAPER.tmp.$$.$token"
|
|
local stale_pid stale_start stale_token
|
|
while :; do
|
|
if [ ! -e "$LIFECYCLE_LOCK_REAPER_RECOVERY" ]; then
|
|
umask 077
|
|
z2_emit_line "pid=$$
|
|
starttime=$self_start
|
|
token=$token" > "$tmp" || return 1
|
|
if ln "$tmp" "$LIFECYCLE_LOCK_REAPER" 2>/dev/null; then
|
|
rm -f "$tmp"
|
|
if [ -e "$LIFECYCLE_LOCK_REAPER_RECOVERY" ]; then
|
|
release_lifecycle_gate "$token" >/dev/null 2>&1 || true
|
|
sleep 1
|
|
continue
|
|
fi
|
|
return 0
|
|
fi
|
|
rm -f "$tmp"
|
|
fi
|
|
lifecycle_gate_alive && return 1
|
|
if claim_lifecycle_recovery_gate "$self_start" "$token"; then
|
|
if read_lifecycle_gate; then
|
|
stale_pid="$GATE_FILE_PID"; stale_start="$GATE_FILE_START"; stale_token="$GATE_FILE_TOKEN"
|
|
sleep 1
|
|
if read_lifecycle_gate && ! lifecycle_gate_alive &&
|
|
[ "$GATE_FILE_PID" = "$stale_pid" ] && [ "$GATE_FILE_START" = "$stale_start" ] &&
|
|
[ "$GATE_FILE_TOKEN" = "$stale_token" ]; then
|
|
rm -f "$LIFECYCLE_LOCK_REAPER" 2>/dev/null
|
|
fi
|
|
else
|
|
# Atomic hard-link publication cannot expose a partial gate.
|
|
# A stable malformed regular gate is therefore abandoned.
|
|
sleep 1
|
|
if [ -f "$LIFECYCLE_LOCK_REAPER" ] && [ ! -L "$LIFECYCLE_LOCK_REAPER" ] &&
|
|
! read_lifecycle_gate; then
|
|
rm -f "$LIFECYCLE_LOCK_REAPER" 2>/dev/null
|
|
fi
|
|
fi
|
|
release_lifecycle_recovery_gate "$token" >/dev/null 2>&1 || true
|
|
continue
|
|
fi
|
|
return 1
|
|
done
|
|
}
|
|
|
|
acquire_lifecycle_lock() {
|
|
local attempts=0 self_start token owner_pid owner_start quarantine candidate
|
|
local stale_kind stale_pid stale_start stale_token stale_boot stale_module
|
|
ensure_state_dir || return 1
|
|
proc_starttime_read "$$" || return 1
|
|
self_start="$PROC_STARTTIME"
|
|
token="${ZAPRET2_LIFECYCLE_TOKEN:-}"
|
|
owner_pid="${ZAPRET2_LIFECYCLE_OWNER_PID:-}"
|
|
owner_start="${ZAPRET2_LIFECYCLE_OWNER_START:-}"
|
|
|
|
# A child launched by either recognized lock holder can safely inherit the
|
|
# exact live lock. Android preset mutations deliberately invoke lifecycle
|
|
# replacement under their cross-process lease, so rejecting that known
|
|
# owner would deadlock the child behind its own parent transaction. The
|
|
# child may never remove the lock: the original holder owns cleanup.
|
|
if is_safe_token "$token" && is_decimal "$owner_pid" && is_decimal "$owner_start" &&
|
|
lock_owner_alive &&
|
|
{ [ "$LOCK_FILE_KIND" = shell ] || [ "$LOCK_FILE_KIND" = android-mutation ]; } &&
|
|
[ "$LOCK_FILE_TOKEN" = "$token" ] &&
|
|
[ "$LOCK_FILE_PID" = "$owner_pid" ] &&
|
|
[ "$LOCK_FILE_START" = "$owner_start" ]; then
|
|
LOCK_HELD=inherited
|
|
LOCK_OWNER_PID="$owner_pid"
|
|
LOCK_OWNER_START="$owner_start"
|
|
LOCK_OWNER_TOKEN="$token"
|
|
prime_transaction_metadata
|
|
return 0
|
|
fi
|
|
|
|
new_lifecycle_token_read || return 1
|
|
token="$Z2_NEW_TOKEN"
|
|
is_safe_token "$token" || return 1
|
|
candidate="$LIFECYCLE_LOCK.candidate.$$.$token"
|
|
LIFECYCLE_ACQUIRE_TOKEN="$token"
|
|
LIFECYCLE_ACQUIRE_CANDIDATE="$candidate"
|
|
[ ! -e "$candidate" ] || return 1
|
|
mkdir "$candidate" 2>/dev/null || return 1
|
|
umask 077
|
|
if ! z2_emit_line "pid=$$
|
|
starttime=$self_start
|
|
token=$token" > "$candidate/owner"; then
|
|
rm -rf "$candidate" 2>/dev/null
|
|
LIFECYCLE_ACQUIRE_CANDIDATE=""; LIFECYCLE_ACQUIRE_TOKEN=""
|
|
return 1
|
|
fi
|
|
while [ "$attempts" -lt "$LIFECYCLE_LOCK_WAIT_SECONDS" ]; do
|
|
if ! claim_lifecycle_gate "$self_start" "$token"; then
|
|
attempts=$((attempts + 1)); sleep 1; continue
|
|
fi
|
|
if [ ! -e "$LIFECYCLE_LOCK" ] && [ ! -L "$LIFECYCLE_LOCK" ]; then
|
|
if mv "$candidate" "$LIFECYCLE_LOCK" 2>/dev/null; then
|
|
release_lifecycle_gate "$token" >/dev/null 2>&1 || true
|
|
candidate=""
|
|
LIFECYCLE_ACQUIRE_CANDIDATE=""; LIFECYCLE_ACQUIRE_TOKEN=""
|
|
LOCK_HELD=1
|
|
LOCK_OWNER_PID="$$"
|
|
LOCK_OWNER_START="$self_start"
|
|
LOCK_OWNER_TOKEN="$token"
|
|
export ZAPRET2_LIFECYCLE_TOKEN="$token"
|
|
export ZAPRET2_LIFECYCLE_OWNER_PID="$$"
|
|
export ZAPRET2_LIFECYCLE_OWNER_START="$self_start"
|
|
prime_transaction_metadata
|
|
return 0
|
|
fi
|
|
release_lifecycle_gate "$token" >/dev/null 2>&1 || true
|
|
elif ! read_lock_owner; then
|
|
# Only an exact recognized owner schema may ever be reaped. A
|
|
# malformed, foreign, or future record remains a hard fail-closed
|
|
# barrier for manual inspection.
|
|
release_lifecycle_gate "$token" >/dev/null 2>&1 || true
|
|
elif lock_owner_alive; then
|
|
release_lifecycle_gate "$token" >/dev/null 2>&1 || true
|
|
else
|
|
# The gate excludes publishers and other reapers. A second stable
|
|
# exact stale-owner observation makes quarantine safe.
|
|
stale_kind="$LOCK_FILE_KIND"; stale_pid="$LOCK_FILE_PID"; stale_start="$LOCK_FILE_START"
|
|
stale_token="$LOCK_FILE_TOKEN"; stale_boot="$LOCK_FILE_BOOT"; stale_module="$LOCK_FILE_MODULE"
|
|
sleep 1
|
|
if read_lock_owner && ! lock_owner_alive &&
|
|
[ "$LOCK_FILE_KIND" = "$stale_kind" ] && [ "$LOCK_FILE_PID" = "$stale_pid" ] &&
|
|
[ "$LOCK_FILE_START" = "$stale_start" ] && [ "$LOCK_FILE_TOKEN" = "$stale_token" ] &&
|
|
[ "$LOCK_FILE_BOOT" = "$stale_boot" ] && [ "$LOCK_FILE_MODULE" = "$stale_module" ]; then
|
|
quarantine="$LIFECYCLE_LOCK_QUARANTINE.$$.$token"
|
|
if [ ! -e "$quarantine" ] && mv "$LIFECYCLE_LOCK" "$quarantine" 2>/dev/null; then
|
|
release_lifecycle_gate "$token" >/dev/null 2>&1 || true
|
|
rm -rf "$quarantine" 2>/dev/null || true
|
|
attempts=$((attempts + 1))
|
|
continue
|
|
fi
|
|
fi
|
|
release_lifecycle_gate "$token" >/dev/null 2>&1 || true
|
|
fi
|
|
attempts=$((attempts + 1))
|
|
sleep 1
|
|
done
|
|
[ -z "$candidate" ] || rm -rf "$candidate" 2>/dev/null
|
|
LIFECYCLE_ACQUIRE_CANDIDATE=""; LIFECYCLE_ACQUIRE_TOKEN=""
|
|
return 1
|
|
}
|
|
|
|
# A caller with an early signal trap can use this while acquire_lifecycle_lock
|
|
# is waiting. Only the exact candidate/gates published by this PID and token
|
|
# are retired; an inherited or foreign lifecycle owner is never released.
|
|
abort_lifecycle_lock_acquire() {
|
|
local candidate="$LIFECYCLE_ACQUIRE_CANDIDATE" token="$LIFECYCLE_ACQUIRE_TOKEN" owner
|
|
if [ "$LOCK_HELD" = 1 ]; then
|
|
release_lifecycle_lock >/dev/null 2>&1 || return 1
|
|
LIFECYCLE_ACQUIRE_CANDIDATE=""; LIFECYCLE_ACQUIRE_TOKEN=""
|
|
return 0
|
|
fi
|
|
if is_safe_token "$token"; then
|
|
release_lifecycle_gate "$token" >/dev/null 2>&1 || true
|
|
release_lifecycle_recovery_gate "$token" >/dev/null 2>&1 || true
|
|
fi
|
|
case "$candidate" in "$LIFECYCLE_LOCK.candidate.$$.$token") ;; *) return 1 ;; esac
|
|
if [ -d "$candidate" ] && [ ! -L "$candidate" ]; then
|
|
owner="$candidate/owner"
|
|
if [ -f "$owner" ] && [ ! -L "$owner" ] && path_uid_is_root "$owner"; then
|
|
rm -f "$owner" 2>/dev/null || return 1
|
|
fi
|
|
rmdir "$candidate" 2>/dev/null || return 1
|
|
elif [ -e "$candidate" ] || [ -L "$candidate" ]; then
|
|
return 1
|
|
fi
|
|
LIFECYCLE_ACQUIRE_CANDIDATE=""; LIFECYCLE_ACQUIRE_TOKEN=""
|
|
return 0
|
|
}
|
|
|
|
release_lifecycle_lock() {
|
|
local quarantine
|
|
# Past this point other writers may mutate publications again.
|
|
retire_owner_read_cache
|
|
retire_state_dir_proof
|
|
retire_proven_process_fact
|
|
meta_cache_retire_all
|
|
[ "$LOCK_HELD" = 1 ] || { LOCK_HELD=0; return 0; }
|
|
# The reaper protocol only ever quarantines a lock whose recorded owner is
|
|
# dead across a double observation. This owner is alive and releasing its
|
|
# own record, so no gate is needed: verify the record is still ours and
|
|
# retire it with one atomic rename.
|
|
if read_lock_owner &&
|
|
[ "$LOCK_FILE_PID" = "$LOCK_OWNER_PID" ] &&
|
|
[ "$LOCK_FILE_START" = "$LOCK_OWNER_START" ] &&
|
|
[ "$LOCK_FILE_TOKEN" = "$LOCK_OWNER_TOKEN" ]; then
|
|
quarantine="$LIFECYCLE_LOCK_QUARANTINE.release.$$.$LOCK_OWNER_TOKEN"
|
|
if [ ! -e "$quarantine" ] && mv "$LIFECYCLE_LOCK" "$quarantine" 2>/dev/null; then
|
|
rm -rf "$quarantine" 2>/dev/null || true
|
|
LOCK_HELD=0
|
|
return 0
|
|
fi
|
|
fi
|
|
# Preserve ownership state on failure so the caller's EXIT trap can retry
|
|
# exact cleanup. Forgetting a still-published owner turns a recoverable
|
|
# release error into a persistent lifecycle barrier.
|
|
return 1
|
|
}
|
|
|
|
module_removal_pending() {
|
|
[ -e "$MODDIR/remove" ] || [ -L "$MODDIR/remove" ]
|
|
}
|
|
|
|
read_uninstall_tombstone() {
|
|
local key value version="" seen_version=0 seen_pid=0 seen_start=0
|
|
local seen_token=0 seen_module=0
|
|
UNINSTALL_FILE_PID=""; UNINSTALL_FILE_START=""
|
|
UNINSTALL_FILE_TOKEN=""; UNINSTALL_FILE_MODULE=""
|
|
state_file_is_secure "$UNINSTALL_TOMBSTONE" && [ -r "$UNINSTALL_TOMBSTONE" ] || return 1
|
|
while IFS='=' read -r key value; do
|
|
case "$key" in
|
|
version)
|
|
[ "$seen_version" = 0 ] || return 1
|
|
version="$value"; seen_version=1
|
|
;;
|
|
pid)
|
|
[ "$seen_pid" = 0 ] || return 1
|
|
UNINSTALL_FILE_PID="$value"; seen_pid=1
|
|
;;
|
|
starttime)
|
|
[ "$seen_start" = 0 ] || return 1
|
|
UNINSTALL_FILE_START="$value"; seen_start=1
|
|
;;
|
|
token)
|
|
[ "$seen_token" = 0 ] || return 1
|
|
UNINSTALL_FILE_TOKEN="$value"; seen_token=1
|
|
;;
|
|
module_dir)
|
|
[ "$seen_module" = 0 ] || return 1
|
|
UNINSTALL_FILE_MODULE="$value"; seen_module=1
|
|
;;
|
|
*) return 1 ;;
|
|
esac
|
|
done < "$UNINSTALL_TOMBSTONE"
|
|
[ "$seen_version:$seen_pid:$seen_start:$seen_token:$seen_module" = 1:1:1:1:1 ] || return 1
|
|
[ "$version" = "$UNINSTALL_TOMBSTONE_VERSION" ] || return 1
|
|
is_decimal "$UNINSTALL_FILE_PID" && [ "$UNINSTALL_FILE_PID" -gt 0 ] 2>/dev/null || return 1
|
|
is_decimal "$UNINSTALL_FILE_START" || return 1
|
|
is_safe_token "$UNINSTALL_FILE_TOKEN" || return 1
|
|
[ "$UNINSTALL_FILE_MODULE" = "$MODDIR" ]
|
|
}
|
|
|
|
uninstall_tombstone_owner_alive() {
|
|
proc_starttime_read "$UNINSTALL_FILE_PID" || return 1
|
|
[ "$PROC_STARTTIME" = "$UNINSTALL_FILE_START" ]
|
|
}
|
|
|
|
uninstall_environment_authorized() {
|
|
is_safe_token "${ZAPRET2_UNINSTALL_TOKEN:-}" &&
|
|
[ "${ZAPRET2_UNINSTALL_TOKEN:-}" = "$UNINSTALL_FILE_TOKEN" ] &&
|
|
is_decimal "${ZAPRET2_UNINSTALL_OWNER_PID:-}" &&
|
|
[ "${ZAPRET2_UNINSTALL_OWNER_PID:-}" = "$UNINSTALL_FILE_PID" ] &&
|
|
is_decimal "${ZAPRET2_UNINSTALL_OWNER_START:-}" &&
|
|
[ "${ZAPRET2_UNINSTALL_OWNER_START:-}" = "$UNINSTALL_FILE_START" ] &&
|
|
lock_owner_alive &&
|
|
[ "$LOCK_FILE_PID" = "$UNINSTALL_FILE_PID" ] &&
|
|
[ "$LOCK_FILE_START" = "$UNINSTALL_FILE_START" ]
|
|
}
|
|
|
|
uninstall_tombstone_allows_start() {
|
|
UNINSTALL_TOMBSTONE_ERROR=""; UNINSTALL_TOMBSTONE_DIAGNOSTIC=""
|
|
if module_removal_pending; then
|
|
UNINSTALL_TOMBSTONE_ERROR="Root-manager module removal marker is present: $MODDIR/remove"
|
|
return 1
|
|
fi
|
|
{ [ -e "$UNINSTALL_TOMBSTONE" ] || [ -L "$UNINSTALL_TOMBSTONE" ]; } || return 0
|
|
UNINSTALL_TOMBSTONE_ERROR="uninstall tombstone blocks start/restart: $UNINSTALL_TOMBSTONE"
|
|
return 1
|
|
}
|
|
|
|
uninstall_tombstone_allows_stop() {
|
|
UNINSTALL_TOMBSTONE_ERROR=""; UNINSTALL_TOMBSTONE_DIAGNOSTIC=""
|
|
{ [ -e "$UNINSTALL_TOMBSTONE" ] || [ -L "$UNINSTALL_TOMBSTONE" ]; } || return 0
|
|
read_uninstall_tombstone || {
|
|
UNINSTALL_TOMBSTONE_ERROR="uninstall tombstone is malformed or unsafe"
|
|
return 1
|
|
}
|
|
uninstall_tombstone_owner_alive || {
|
|
UNINSTALL_TOMBSTONE_ERROR="uninstall tombstone owner is not alive"
|
|
return 1
|
|
}
|
|
uninstall_environment_authorized || {
|
|
UNINSTALL_TOMBSTONE_ERROR="stop caller lacks exact live uninstall ownership"
|
|
return 1
|
|
}
|
|
UNINSTALL_TOMBSTONE_DIAGNOSTIC="stop authorized by exact live uninstall owner"
|
|
return 0
|
|
}
|
|
|
|
# proc_cmdline_sha256_read is the fork-free form, in the same shape as
|
|
# proc_starttime_read: hot call sites consume the global instead of paying a
|
|
# command-substitution fork on top of the one sha256sum already costs. The
|
|
# printf wrapper below stays for captures that want a value.
|
|
PROC_CMDLINE_SHA256=""
|
|
proc_cmdline_sha256_read() {
|
|
local pid="$1" value
|
|
PROC_CMDLINE_SHA256=""
|
|
is_decimal "$pid" || return 1
|
|
[ -r "/proc/$pid/cmdline" ] || return 1
|
|
value="$(sha256sum "/proc/$pid/cmdline" 2>/dev/null)" || return 1
|
|
value="${value%% *}"
|
|
is_lower_sha256 "$value" || return 1
|
|
PROC_CMDLINE_SHA256="$value"
|
|
}
|
|
|
|
proc_cmdline_sha256() {
|
|
proc_cmdline_sha256_read "$1" || return 1
|
|
printf '%s\n' "$PROC_CMDLINE_SHA256"
|
|
}
|
|
|
|
# Exact argv0 needs the NUL separators tr restores; the first line of that
|
|
# expansion is argv0. Cutting it with a parameter expansion instead of sed
|
|
# spares one fork+exec, and proc_argv0_read spares hot call sites the
|
|
# command-substitution fork on top (same shape as proc_starttime_read).
|
|
# PROC_CMDLINE_LINES keeps the whole expansion so the same snapshot can
|
|
# answer the exact-argument question tr|grep used to re-read for.
|
|
PROC_ARGV0=""
|
|
PROC_CMDLINE_LINES=""
|
|
proc_argv0_read() {
|
|
local pid="$1" value nl='
|
|
'
|
|
PROC_ARGV0=""
|
|
PROC_CMDLINE_LINES=""
|
|
is_decimal "$pid" || return 1
|
|
[ -r "/proc/$pid/cmdline" ] || return 1
|
|
value="$(tr '\000' '\n' < "/proc/$pid/cmdline" 2>/dev/null)" || return 1
|
|
PROC_CMDLINE_LINES="$value"
|
|
value="${value%%"$nl"*}"
|
|
[ -n "$value" ] || return 1
|
|
PROC_ARGV0="$value"
|
|
}
|
|
|
|
proc_argv0() {
|
|
proc_argv0_read "$1" || return 1
|
|
printf '%s\n' "$PROC_ARGV0"
|
|
}
|
|
|
|
# Fast, fork-free prefilter for the recovery scan. Shell variables cannot retain
|
|
# NUL separators while reading cmdline, so an exact argv0 is
|
|
# guaranteed to retain this prefix. Prefix collisions are harmless: the
|
|
# candidate still goes through verify_nfqws_pid's full argv0/start/exe proof.
|
|
proc_cmdline_may_match_nfqws() {
|
|
local pid="$1" runtime_nfqws2="${AUDIT_NFQWS2_OVERRIDE:-$NFQWS2}" cmdline=""
|
|
is_decimal "$pid" || return 1
|
|
[ -r "/proc/$pid/cmdline" ] || return 1
|
|
IFS= read -r cmdline < "/proc/$pid/cmdline" 2>/dev/null || [ -n "$cmdline" ] || return 1
|
|
case "$cmdline" in "$runtime_nfqws2"*) return 0 ;; *) return 1 ;; esac
|
|
}
|
|
|
|
OWNER_STATE_PID=""
|
|
OWNER_STATE_START=""
|
|
OWNER_STATE_ARGV_SHA256=""
|
|
OWNER_STATE_QNUM=""
|
|
OWNER_STATE_EXE=""
|
|
OWNER_STATE_GENERATION=""
|
|
OWNER_STATE_PHASE=""
|
|
OWNER_STATE_SCHEMA_VERSION=""
|
|
OWNER_STATE_INSTALL_GENERATION=""
|
|
OWNER_STATE_INSTALL_ARCHIVE_SHA256=""
|
|
OWNER_STATE_TETHERING=0
|
|
OWNER_STATE_PORTS_TCP=""; OWNER_STATE_PORTS_UDP=""; OWNER_STATE_STUN_PORTS=""
|
|
OWNER_STATE_TCP_PKT_OUT=""; OWNER_STATE_TCP_PKT_IN=""
|
|
OWNER_STATE_UDP_PKT_OUT=""; OWNER_STATE_UDP_PKT_IN=""; OWNER_STATE_DESYNC_MARK=""
|
|
OWNER_STATE_IPV4_ACTIVE=0; OWNER_STATE_IPV6_ACTIVE=0
|
|
OWNER_STATE_IPV4_CONNBYTES=0; OWNER_STATE_IPV4_MULTIPORT=0; OWNER_STATE_IPV4_MARK=0
|
|
OWNER_STATE_IPV6_CONNBYTES=0; OWNER_STATE_IPV6_MULTIPORT=0; OWNER_STATE_IPV6_MARK=0
|
|
OWNER_STATE_IPV4_RULES=0; OWNER_STATE_IPV6_RULES=0
|
|
OWNER_STATE_IPV4_SPEC=""; OWNER_STATE_IPV6_SPEC=""; OWNER_STATE_FIREWALL_FINGERPRINT=""
|
|
OWNER_WRITE_READY=0
|
|
|
|
normalize_owner_port_list() {
|
|
local list="$1" item first last old_ifs result="" normalized
|
|
OWNER_PORT_LIST_NORMALIZED=""
|
|
[ -n "$list" ] || return 1
|
|
case "$list" in *[!0-9,:]*|,*|*,|*,,*) return 1;; esac
|
|
old_ifs="$IFS"; IFS=,; set -- $list; IFS="$old_ifs"; [ "$#" -gt 0 ] || return 1
|
|
for item in "$@"; do
|
|
case "$item" in
|
|
*:*) first="${item%%:*}"; last="${item#*:}"; case "$last" in *:*) return 1;; esac
|
|
is_decimal "$first" && is_decimal "$last" || return 1
|
|
while :; do case "$first" in 0?*) first="${first#0}" ;; *) break ;; esac; done
|
|
while :; do case "$last" in 0?*) last="${last#0}" ;; *) break ;; esac; done
|
|
[ "$first" -le 65535 ] 2>/dev/null && [ "$last" -le 65535 ] 2>/dev/null && [ "$first" -le "$last" ] 2>/dev/null || return 1
|
|
normalized="$first:$last" ;;
|
|
*) is_decimal "$item" || return 1; normalized="$item"
|
|
while :; do case "$normalized" in 0?*) normalized="${normalized#0}" ;; *) break ;; esac; done
|
|
[ "$normalized" -le 65535 ] 2>/dev/null || return 1 ;;
|
|
esac
|
|
result="${result}${result:+,}$normalized"
|
|
done
|
|
OWNER_PORT_LIST_NORMALIZED="$result"
|
|
}
|
|
|
|
normalize_owner_optional_port_list() {
|
|
OWNER_PORT_LIST_NORMALIZED=""
|
|
[ -z "$1" ] && return 0
|
|
normalize_owner_port_list "$1"
|
|
}
|
|
|
|
owner_port_rule_count() {
|
|
local old_ifs
|
|
OWNER_PORT_RULE_COUNT=0
|
|
[ -n "$1" ] || return 0
|
|
old_ifs="$IFS"; IFS=,; set -- $1; IFS="$old_ifs"; OWNER_PORT_RULE_COUNT=$#
|
|
[ "$OWNER_PORT_RULE_COUNT" -gt 0 ] || return 1
|
|
}
|
|
|
|
is_safe_firewall_identity() {
|
|
local tag="$1" out="$2" inchain="$3"
|
|
if [ "$tag" = stable0001 ]; then
|
|
[ "$out" = ZAPRET2_OUT ] && [ "$inchain" = ZAPRET2_IN ]
|
|
return
|
|
fi
|
|
case "$tag" in ""|*[!A-Za-z0-9]*) return 1;; esac
|
|
[ "${#tag}" -eq 10 ] 2>/dev/null || return 1
|
|
[ "$out" = "Z2O_$tag" ] && [ "$inchain" = "Z2I_$tag" ] && [ "${#out}" -le 28 ] 2>/dev/null
|
|
}
|
|
|
|
prepare_new_firewall_identity() {
|
|
local token
|
|
token="${ZAPRET2_LIFECYCLE_TOKEN:-}"
|
|
if [ -n "$token" ]; then
|
|
is_safe_token "$token" || return 1
|
|
else
|
|
new_lifecycle_token_read || return 1
|
|
token="$Z2_NEW_TOKEN"
|
|
fi
|
|
FIREWALL_TAG=stable0001
|
|
ZAPRET2_OUT=ZAPRET2_OUT
|
|
ZAPRET2_IN=ZAPRET2_IN
|
|
PENDING_OWNER_GENERATION="$token"
|
|
is_safe_firewall_identity "$FIREWALL_TAG" "$ZAPRET2_OUT" "$ZAPRET2_IN"
|
|
}
|
|
|
|
# Global-return: the spec is compared and embedded, never streamed, and the
|
|
# printf it used to ride on is an external on the target shell.
|
|
owner_build_family_spec_read() {
|
|
OWNER_FAMILY_SPEC="family:$1;active:$2;tag:$OWNER_WRITE_FIREWALL_TAG;outchain:$OWNER_WRITE_OUT_CHAIN;inchain:$OWNER_WRITE_IN_CHAIN;tethering:$OWNER_WRITE_TETHERING;qnum:$OWNER_WRITE_QNUM;tcp:$OWNER_WRITE_PORTS_TCP;udp:$OWNER_WRITE_PORTS_UDP;stun:$OWNER_WRITE_STUN_PORTS;tcp_out:$OWNER_WRITE_TCP_PKT_OUT;tcp_in:$OWNER_WRITE_TCP_PKT_IN;udp_out:$OWNER_WRITE_UDP_PKT_OUT;udp_in:$OWNER_WRITE_UDP_PKT_IN;mark:$OWNER_WRITE_DESYNC_MARK;connbytes:$3;multiport:$4;markcap:$5;rules:$6"
|
|
}
|
|
|
|
owner_spec_fingerprint_read() {
|
|
local value
|
|
OWNER_SPEC_FINGERPRINT=""
|
|
# A late shell-function shim (tests provide one on stripped hosts) must
|
|
# still count as availability, so only the positive probe is cached.
|
|
[ "$Z2_HAVE_SHA256SUM" = 1 ] || command -v sha256sum >/dev/null 2>&1 || return 1
|
|
# The here-document feeds sha256sum the exact bytes the old printf
|
|
# pipeline produced (each spec line newline-terminated) without the
|
|
# extra printf and awk processes.
|
|
value="$(sha256sum 2>/dev/null <<Z2_SPEC_EOF
|
|
$1
|
|
$2
|
|
Z2_SPEC_EOF
|
|
)" || return 1
|
|
value="${value%% *}"
|
|
is_lower_sha256 "$value" || return 1
|
|
OWNER_SPEC_FINGERPRINT="$value"
|
|
}
|
|
|
|
prepare_owner_generation_spec() {
|
|
local ipv4_active="${1:-1}" ipv6_active="${2:-0}" tcp_count udp_count per_direction
|
|
read_install_generation_meta || return 1
|
|
is_safe_firewall_identity "${FIREWALL_TAG:-}" "${ZAPRET2_OUT:-}" "${ZAPRET2_IN:-}" || prepare_new_firewall_identity || return 1
|
|
OWNER_WRITE_FIREWALL_TAG="$FIREWALL_TAG"; OWNER_WRITE_OUT_CHAIN="$ZAPRET2_OUT"; OWNER_WRITE_IN_CHAIN="$ZAPRET2_IN"
|
|
case "${TETHERING:-0}" in 0|1) OWNER_WRITE_TETHERING="${TETHERING:-0}" ;; *) return 1 ;; esac
|
|
normalize_qnum "${QNUM:-}" || return 1; OWNER_WRITE_QNUM="$QNUM_NORMALIZED"
|
|
normalize_owner_optional_port_list "${PORTS_TCP:-}" || return 1; OWNER_WRITE_PORTS_TCP="$OWNER_PORT_LIST_NORMALIZED"
|
|
normalize_owner_optional_port_list "${PORTS_UDP:-}" || return 1; OWNER_WRITE_PORTS_UDP="$OWNER_PORT_LIST_NORMALIZED"
|
|
[ -n "$OWNER_WRITE_PORTS_TCP$OWNER_WRITE_PORTS_UDP" ] || return 1
|
|
# Voice ports are already folded into the compiled UDP union.
|
|
OWNER_WRITE_STUN_PORTS=0
|
|
is_canonical_positive_decimal "${TCP_PKT_OUT:-}" || return 1; OWNER_WRITE_TCP_PKT_OUT="$TCP_PKT_OUT"
|
|
is_canonical_positive_decimal "${TCP_PKT_IN:-}" || return 1; OWNER_WRITE_TCP_PKT_IN="$TCP_PKT_IN"
|
|
is_canonical_positive_decimal "${UDP_PKT_OUT:-}" || return 1; OWNER_WRITE_UDP_PKT_OUT="$UDP_PKT_OUT"
|
|
is_canonical_positive_decimal "${UDP_PKT_IN:-}" || return 1; OWNER_WRITE_UDP_PKT_IN="$UDP_PKT_IN"
|
|
canonical_mark "${DESYNC_MARK:-}" || return 1; OWNER_WRITE_DESYNC_MARK="$MARK_CANONICAL"
|
|
case "$ipv4_active:$ipv6_active" in 1:0|1:1) ;; *) return 1;; esac
|
|
OWNER_WRITE_IPV4_ACTIVE="$ipv4_active"; OWNER_WRITE_IPV6_ACTIVE="$ipv6_active"
|
|
OWNER_WRITE_IPV4_CONNBYTES="${IPV4_CONNBYTES:-1}"; OWNER_WRITE_IPV4_MULTIPORT="${IPV4_MULTIPORT:-1}"; OWNER_WRITE_IPV4_MARK="${IPV4_MARK:-1}"
|
|
OWNER_WRITE_IPV6_CONNBYTES="${IPV6_CONNBYTES:-1}"; OWNER_WRITE_IPV6_MULTIPORT="${IPV6_MULTIPORT:-1}"; OWNER_WRITE_IPV6_MARK="${IPV6_MARK:-1}"
|
|
case "$OWNER_WRITE_IPV4_CONNBYTES:$OWNER_WRITE_IPV4_MULTIPORT:$OWNER_WRITE_IPV4_MARK:$OWNER_WRITE_IPV6_CONNBYTES:$OWNER_WRITE_IPV6_MULTIPORT:$OWNER_WRITE_IPV6_MARK" in *[!01:]*) return 1;; esac
|
|
owner_port_rule_count "$OWNER_WRITE_PORTS_TCP" || return 1
|
|
tcp_count="$OWNER_PORT_RULE_COUNT"
|
|
owner_port_rule_count "$OWNER_WRITE_PORTS_UDP" || return 1
|
|
udp_count="$OWNER_PORT_RULE_COUNT"
|
|
if [ "$OWNER_WRITE_IPV4_MULTIPORT" = 1 ]; then
|
|
per_direction=0; [ -z "$OWNER_WRITE_PORTS_TCP" ] || per_direction=$((per_direction + 1)); [ -z "$OWNER_WRITE_PORTS_UDP" ] || per_direction=$((per_direction + 1))
|
|
else per_direction=$((tcp_count + udp_count)); fi
|
|
OWNER_WRITE_IPV4_RULES=$((per_direction * (1 + OWNER_WRITE_IPV4_CONNBYTES) * ipv4_active))
|
|
if [ "$OWNER_WRITE_IPV6_MULTIPORT" = 1 ]; then
|
|
per_direction=0; [ -z "$OWNER_WRITE_PORTS_TCP" ] || per_direction=$((per_direction + 1)); [ -z "$OWNER_WRITE_PORTS_UDP" ] || per_direction=$((per_direction + 1))
|
|
else per_direction=$((tcp_count + udp_count)); fi
|
|
OWNER_WRITE_IPV6_RULES=$((per_direction * (1 + OWNER_WRITE_IPV6_CONNBYTES) * ipv6_active))
|
|
owner_build_family_spec_read ipv4 "$ipv4_active" "$OWNER_WRITE_IPV4_CONNBYTES" "$OWNER_WRITE_IPV4_MULTIPORT" "$OWNER_WRITE_IPV4_MARK" "$OWNER_WRITE_IPV4_RULES"
|
|
OWNER_WRITE_IPV4_SPEC="$OWNER_FAMILY_SPEC"
|
|
owner_build_family_spec_read ipv6 "$ipv6_active" "$OWNER_WRITE_IPV6_CONNBYTES" "$OWNER_WRITE_IPV6_MULTIPORT" "$OWNER_WRITE_IPV6_MARK" "$OWNER_WRITE_IPV6_RULES"
|
|
OWNER_WRITE_IPV6_SPEC="$OWNER_FAMILY_SPEC"
|
|
owner_spec_fingerprint_read "$OWNER_WRITE_IPV4_SPEC" "$OWNER_WRITE_IPV6_SPEC" || return 1
|
|
OWNER_WRITE_FIREWALL_FINGERPRINT="$OWNER_SPEC_FINGERPRINT"
|
|
OWNER_WRITE_INSTALL_GENERATION="$INSTALL_META_GENERATION"; OWNER_WRITE_INSTALL_ARCHIVE_SHA256="$INSTALL_META_ARCHIVE_SHA256"; OWNER_WRITE_SOURCE_GENERATION=""; OWNER_WRITE_READY=1
|
|
}
|
|
|
|
owner_state_is_current_boot() {
|
|
[ "$OWNER_STATE_SCHEMA_VERSION" = "$OWNER_STATE_VERSION" ] || return 1
|
|
is_valid_boot_id "$OWNER_STATE_BOOT_ID" || return 1
|
|
read_current_boot_id || return 1
|
|
[ "$OWNER_STATE_BOOT_ID" = "$CURRENT_BOOT_ID" ]
|
|
}
|
|
|
|
# One locked transaction has exactly one cooperating writer of the owner
|
|
# publication: the lock holder itself. A completed read is therefore a fact
|
|
# until this process mutates the publication, while Android prices every
|
|
# re-proof in forks (tens of milliseconds per command substitution), so
|
|
# helpers re-asking the already-answered question dominated whole lifecycle
|
|
# transactions. The cache arms only while the lifecycle lock is held and
|
|
# never under a test's binary override; every publication mutation and the
|
|
# lock release retire it.
|
|
Z2_OWNER_READ_CACHE=""
|
|
|
|
retire_owner_read_cache() {
|
|
Z2_OWNER_READ_CACHE=""
|
|
}
|
|
|
|
owner_read_cache_active() {
|
|
[ "${LOCK_HELD:-0}" != 0 ] && [ -z "${AUDIT_NFQWS2_OVERRIDE:-}" ]
|
|
}
|
|
|
|
read_owner_state() {
|
|
if owner_read_cache_active && [ -n "$Z2_OWNER_READ_CACHE" ]; then
|
|
if [ "$Z2_OWNER_READ_CACHE" = valid ]; then
|
|
# The cached fields are stable: OWNER_STATE_* has no writer besides
|
|
# the fresh parse. Only the derived globals are re-primed, because
|
|
# a caller may legitimately have repointed them at the generation
|
|
# it is preparing since the previous read.
|
|
owner_state_prime_derived
|
|
return 0
|
|
fi
|
|
return 1
|
|
fi
|
|
if read_owner_state_fresh; then
|
|
if owner_read_cache_active; then Z2_OWNER_READ_CACHE=valid; fi
|
|
return 0
|
|
fi
|
|
if owner_read_cache_active; then Z2_OWNER_READ_CACHE=invalid; fi
|
|
return 1
|
|
}
|
|
|
|
owner_state_prime_derived() {
|
|
OWNER_WRITE_FIREWALL_TAG="$OWNER_STATE_FIREWALL_TAG"; OWNER_WRITE_OUT_CHAIN="$OWNER_STATE_OUT_CHAIN"; OWNER_WRITE_IN_CHAIN="$OWNER_STATE_IN_CHAIN"
|
|
OWNER_WRITE_TETHERING="$OWNER_STATE_TETHERING"
|
|
OWNER_WRITE_QNUM="$OWNER_STATE_QNUM"
|
|
OWNER_WRITE_PORTS_TCP="$OWNER_STATE_PORTS_TCP"; OWNER_WRITE_PORTS_UDP="$OWNER_STATE_PORTS_UDP"; OWNER_WRITE_STUN_PORTS="$OWNER_STATE_STUN_PORTS"
|
|
OWNER_WRITE_TCP_PKT_OUT="$OWNER_STATE_TCP_PKT_OUT"; OWNER_WRITE_TCP_PKT_IN="$OWNER_STATE_TCP_PKT_IN"
|
|
OWNER_WRITE_UDP_PKT_OUT="$OWNER_STATE_UDP_PKT_OUT"; OWNER_WRITE_UDP_PKT_IN="$OWNER_STATE_UDP_PKT_IN"
|
|
OWNER_WRITE_DESYNC_MARK="$OWNER_STATE_DESYNC_MARK"
|
|
FIREWALL_TAG="$OWNER_STATE_FIREWALL_TAG"; ZAPRET2_OUT="$OWNER_STATE_OUT_CHAIN"; ZAPRET2_IN="$OWNER_STATE_IN_CHAIN"
|
|
}
|
|
|
|
read_owner_state_fresh() {
|
|
local expected_nfqws2="${AUDIT_NFQWS2_OVERRIDE:-$NFQWS2}"
|
|
OWNER_STATE_PID=""; OWNER_STATE_START=""; OWNER_STATE_ARGV_SHA256=""
|
|
OWNER_STATE_QNUM=""; OWNER_STATE_EXE=""; OWNER_STATE_GENERATION=""; OWNER_STATE_BOOT_ID=""; OWNER_STATE_PHASE=""; OWNER_STATE_SCHEMA_VERSION=""
|
|
OWNER_STATE_INSTALL_GENERATION=""; OWNER_STATE_INSTALL_ARCHIVE_SHA256=""
|
|
OWNER_STATE_PORTS_TCP=""; OWNER_STATE_PORTS_UDP=""; OWNER_STATE_STUN_PORTS=""
|
|
OWNER_STATE_TCP_PKT_OUT=""; OWNER_STATE_TCP_PKT_IN=""; OWNER_STATE_UDP_PKT_OUT=""; OWNER_STATE_UDP_PKT_IN=""; OWNER_STATE_DESYNC_MARK=""
|
|
OWNER_STATE_IPV4_ACTIVE=""; OWNER_STATE_IPV6_ACTIVE=""; OWNER_STATE_IPV4_CONNBYTES=""; OWNER_STATE_IPV4_MULTIPORT=""; OWNER_STATE_IPV4_MARK=""
|
|
OWNER_STATE_IPV6_CONNBYTES=""; OWNER_STATE_IPV6_MULTIPORT=""; OWNER_STATE_IPV6_MARK=""; OWNER_STATE_IPV4_RULES=""; OWNER_STATE_IPV6_RULES=""
|
|
OWNER_STATE_IPV4_SPEC=""; OWNER_STATE_IPV6_SPEC=""; OWNER_STATE_FIREWALL_FINGERPRINT=""
|
|
OWNER_STATE_FIREWALL_TAG=""; OWNER_STATE_OUT_CHAIN=""; OWNER_STATE_IN_CHAIN=""; OWNER_STATE_TETHERING=""
|
|
local key value version="" tcp_count udp_count stun_count expected seen_keys="|" field_sequence="" size old_ifs
|
|
path_meta_capture "$OWNER_STATE"
|
|
if state_file_is_secure "$OWNER_STATE" && [ -r "$OWNER_STATE" ] &&
|
|
path_meta_size_read "$OWNER_STATE"; then
|
|
size="$Z2_PATH_SIZE"
|
|
path_meta_retire
|
|
else
|
|
path_meta_retire
|
|
return 1
|
|
fi
|
|
is_decimal "$size" && [ "$size" -gt 0 ] 2>/dev/null &&
|
|
[ "$size" -le "$OWNER_STATE_MAX_BYTES" ] 2>/dev/null || return 1
|
|
while IFS='=' read -r key value; do
|
|
case "$seen_keys" in *"|$key|"*) return 1;; esac
|
|
seen_keys="${seen_keys}${key}|"
|
|
field_sequence="${field_sequence}${field_sequence:+|}$key"
|
|
case "$key" in
|
|
version) version="$value" ;;
|
|
pid) OWNER_STATE_PID="$value" ;;
|
|
starttime) OWNER_STATE_START="$value" ;;
|
|
argv_sha256) OWNER_STATE_ARGV_SHA256="$value" ;;
|
|
qnum) OWNER_STATE_QNUM="$value" ;;
|
|
exe) OWNER_STATE_EXE="$value" ;;
|
|
generation) OWNER_STATE_GENERATION="$value" ;;
|
|
boot_id) OWNER_STATE_BOOT_ID="$value" ;;
|
|
phase) OWNER_STATE_PHASE="$value" ;;
|
|
install_generation) OWNER_STATE_INSTALL_GENERATION="$value" ;;
|
|
install_archive_sha256) OWNER_STATE_INSTALL_ARCHIVE_SHA256="$value" ;;
|
|
firewall_tag) OWNER_STATE_FIREWALL_TAG="$value" ;;
|
|
out_chain) OWNER_STATE_OUT_CHAIN="$value" ;;
|
|
in_chain) OWNER_STATE_IN_CHAIN="$value" ;;
|
|
tethering) OWNER_STATE_TETHERING="$value" ;;
|
|
ports_tcp) OWNER_STATE_PORTS_TCP="$value" ;;
|
|
ports_udp) OWNER_STATE_PORTS_UDP="$value" ;;
|
|
stun_ports) OWNER_STATE_STUN_PORTS="$value" ;;
|
|
tcp_pkt_out) OWNER_STATE_TCP_PKT_OUT="$value" ;;
|
|
tcp_pkt_in) OWNER_STATE_TCP_PKT_IN="$value" ;;
|
|
udp_pkt_out) OWNER_STATE_UDP_PKT_OUT="$value" ;;
|
|
udp_pkt_in) OWNER_STATE_UDP_PKT_IN="$value" ;;
|
|
desync_mark) OWNER_STATE_DESYNC_MARK="$value" ;;
|
|
ipv4_active) OWNER_STATE_IPV4_ACTIVE="$value" ;;
|
|
ipv6_active) OWNER_STATE_IPV6_ACTIVE="$value" ;;
|
|
ipv4_connbytes) OWNER_STATE_IPV4_CONNBYTES="$value" ;;
|
|
ipv4_multiport) OWNER_STATE_IPV4_MULTIPORT="$value" ;;
|
|
ipv4_mark) OWNER_STATE_IPV4_MARK="$value" ;;
|
|
ipv6_connbytes) OWNER_STATE_IPV6_CONNBYTES="$value" ;;
|
|
ipv6_multiport) OWNER_STATE_IPV6_MULTIPORT="$value" ;;
|
|
ipv6_mark) OWNER_STATE_IPV6_MARK="$value" ;;
|
|
ipv4_rules) OWNER_STATE_IPV4_RULES="$value" ;;
|
|
ipv6_rules) OWNER_STATE_IPV6_RULES="$value" ;;
|
|
ipv4_spec) OWNER_STATE_IPV4_SPEC="$value" ;;
|
|
ipv6_spec) OWNER_STATE_IPV6_SPEC="$value" ;;
|
|
firewall_fingerprint) OWNER_STATE_FIREWALL_FINGERPRINT="$value" ;;
|
|
*) return 1 ;;
|
|
esac
|
|
done < "$OWNER_STATE"
|
|
[ "$version" = "$OWNER_STATE_VERSION" ] &&
|
|
[ "$field_sequence" = "$OWNER_STATE_V9_FIELD_SEQUENCE" ] || return 1
|
|
OWNER_STATE_SCHEMA_VERSION="$OWNER_STATE_VERSION"
|
|
is_canonical_positive_decimal "$OWNER_STATE_PID" &&
|
|
is_canonical_nonnegative_i64 "$OWNER_STATE_START" || return 1
|
|
normalize_qnum "$OWNER_STATE_QNUM" || return 1
|
|
OWNER_STATE_QNUM="$QNUM_NORMALIZED"
|
|
[ "$OWNER_STATE_EXE" = "$expected_nfqws2" ] || return 1
|
|
is_lower_sha256 "$OWNER_STATE_ARGV_SHA256" || return 1
|
|
is_safe_token "$OWNER_STATE_GENERATION" || return 1
|
|
is_valid_boot_id "$OWNER_STATE_BOOT_ID" || return 1
|
|
case "$OWNER_STATE_PHASE" in launched|active|stopping|error) ;; *) return 1 ;; esac
|
|
is_safe_token "$OWNER_STATE_INSTALL_GENERATION" && [ "${#OWNER_STATE_INSTALL_GENERATION}" -le 128 ] 2>/dev/null || return 1
|
|
is_lower_sha256 "$OWNER_STATE_INSTALL_ARCHIVE_SHA256" || return 1
|
|
is_safe_firewall_identity "$OWNER_STATE_FIREWALL_TAG" "$OWNER_STATE_OUT_CHAIN" "$OWNER_STATE_IN_CHAIN" || return 1
|
|
case "$OWNER_STATE_TETHERING" in 0|1) ;; *) return 1 ;; esac
|
|
# A cold lifecycle process has no prior OWNER_WRITE_* generation. Prime
|
|
# every derived global solely from the just-validated owner fields;
|
|
# otherwise a valid record is accidentally accepted only in the writer's
|
|
# original shell where these globals happen to remain populated.
|
|
owner_state_prime_derived
|
|
normalize_owner_optional_port_list "$OWNER_STATE_PORTS_TCP" || return 1; [ "$OWNER_PORT_LIST_NORMALIZED" = "$OWNER_STATE_PORTS_TCP" ] || return 1
|
|
normalize_owner_optional_port_list "$OWNER_STATE_PORTS_UDP" || return 1; [ "$OWNER_PORT_LIST_NORMALIZED" = "$OWNER_STATE_PORTS_UDP" ] || return 1
|
|
[ -n "$OWNER_STATE_PORTS_TCP$OWNER_STATE_PORTS_UDP" ] || return 1
|
|
[ "$OWNER_STATE_STUN_PORTS" = 0 ] || return 1
|
|
is_canonical_positive_decimal "$OWNER_STATE_TCP_PKT_OUT" && [ "${#OWNER_STATE_TCP_PKT_OUT}" -le 9 ] 2>/dev/null || return 1
|
|
is_canonical_positive_decimal "$OWNER_STATE_TCP_PKT_IN" && [ "${#OWNER_STATE_TCP_PKT_IN}" -le 9 ] 2>/dev/null || return 1
|
|
is_canonical_positive_decimal "$OWNER_STATE_UDP_PKT_OUT" && [ "${#OWNER_STATE_UDP_PKT_OUT}" -le 9 ] 2>/dev/null || return 1
|
|
is_canonical_positive_decimal "$OWNER_STATE_UDP_PKT_IN" && [ "${#OWNER_STATE_UDP_PKT_IN}" -le 9 ] 2>/dev/null || return 1
|
|
canonical_mark "$OWNER_STATE_DESYNC_MARK" || return 1; [ "$MARK_CANONICAL" = "$OWNER_STATE_DESYNC_MARK" ] || return 1
|
|
case "$OWNER_STATE_IPV4_ACTIVE:$OWNER_STATE_IPV6_ACTIVE:$OWNER_STATE_IPV4_CONNBYTES:$OWNER_STATE_IPV4_MULTIPORT:$OWNER_STATE_IPV4_MARK:$OWNER_STATE_IPV6_CONNBYTES:$OWNER_STATE_IPV6_MULTIPORT:$OWNER_STATE_IPV6_MARK" in *[!01:]*) return 1;; esac
|
|
[ "$OWNER_STATE_IPV4_ACTIVE" = 1 ] || return 1
|
|
is_canonical_nonnegative_i64 "$OWNER_STATE_IPV4_RULES" &&
|
|
is_canonical_nonnegative_i64 "$OWNER_STATE_IPV6_RULES" || return 1
|
|
if [ -n "$OWNER_STATE_PORTS_TCP" ]; then
|
|
old_ifs="$IFS"; IFS=,; set -- $OWNER_STATE_PORTS_TCP; IFS="$old_ifs"; tcp_count=$#
|
|
else tcp_count=0; fi
|
|
if [ -n "$OWNER_STATE_PORTS_UDP" ]; then
|
|
old_ifs="$IFS"; IFS=,; set -- $OWNER_STATE_PORTS_UDP; IFS="$old_ifs"; udp_count=$#
|
|
else udp_count=0; fi
|
|
if [ "$OWNER_STATE_IPV4_MULTIPORT" = 1 ]; then
|
|
expected=0; [ -z "$OWNER_STATE_PORTS_TCP" ] || expected=$((expected + 1)); [ -z "$OWNER_STATE_PORTS_UDP" ] || expected=$((expected + 1))
|
|
else expected=$((tcp_count + udp_count)); fi
|
|
expected=$((expected * (1 + OWNER_STATE_IPV4_CONNBYTES)))
|
|
[ "$OWNER_STATE_IPV4_RULES" = $((expected * OWNER_STATE_IPV4_ACTIVE)) ] || return 1
|
|
if [ "$OWNER_STATE_IPV6_MULTIPORT" = 1 ]; then
|
|
expected=0; [ -z "$OWNER_STATE_PORTS_TCP" ] || expected=$((expected + 1)); [ -z "$OWNER_STATE_PORTS_UDP" ] || expected=$((expected + 1))
|
|
else expected=$((tcp_count + udp_count)); fi
|
|
expected=$((expected * (1 + OWNER_STATE_IPV6_CONNBYTES)))
|
|
[ "$OWNER_STATE_IPV6_RULES" = $((expected * OWNER_STATE_IPV6_ACTIVE)) ] || return 1
|
|
owner_build_family_spec_read ipv4 "$OWNER_STATE_IPV4_ACTIVE" "$OWNER_STATE_IPV4_CONNBYTES" "$OWNER_STATE_IPV4_MULTIPORT" "$OWNER_STATE_IPV4_MARK" "$OWNER_STATE_IPV4_RULES"
|
|
[ "$OWNER_FAMILY_SPEC" = "$OWNER_STATE_IPV4_SPEC" ] || return 1
|
|
owner_build_family_spec_read ipv6 "$OWNER_STATE_IPV6_ACTIVE" "$OWNER_STATE_IPV6_CONNBYTES" "$OWNER_STATE_IPV6_MULTIPORT" "$OWNER_STATE_IPV6_MARK" "$OWNER_STATE_IPV6_RULES"
|
|
[ "$OWNER_FAMILY_SPEC" = "$OWNER_STATE_IPV6_SPEC" ] || return 1
|
|
owner_spec_fingerprint_read "$OWNER_STATE_IPV4_SPEC" "$OWNER_STATE_IPV6_SPEC" || return 1
|
|
[ "$OWNER_SPEC_FINGERPRINT" = "$OWNER_STATE_FIREWALL_FINGERPRINT" ] || return 1
|
|
return 0
|
|
}
|
|
|
|
write_numeric_pidfile() {
|
|
local pid="$1"
|
|
is_decimal "$pid" && [ "$pid" -gt 0 ] 2>/dev/null || return 1
|
|
write_private_runtime_line "$PIDFILE" "$pid"
|
|
}
|
|
|
|
write_owner_state() {
|
|
local pid="$1" start="$2" argv_sha256="$3" qnum="$4" generation="$5" phase="$6"
|
|
local tmp="$OWNER_STATE.tmp.$$" boot_id size
|
|
# Any write attempt retires the cached read fact, whether or not the
|
|
# publication ends up replaced: the next reader re-proves from disk.
|
|
retire_owner_read_cache
|
|
read_current_boot_id || return 1
|
|
boot_id="$CURRENT_BOOT_ID"
|
|
is_canonical_positive_decimal "$pid" && is_canonical_nonnegative_i64 "$start" || return 1
|
|
normalize_qnum "$qnum" || return 1
|
|
qnum="$QNUM_NORMALIZED"
|
|
is_lower_sha256 "$argv_sha256" || return 1
|
|
is_safe_token "$generation" || return 1
|
|
case "$phase" in launched|active|stopping|error) ;; *) return 1 ;; esac
|
|
if [ "${OWNER_WRITE_READY:-0}" != 1 ] || { [ -n "${OWNER_WRITE_SOURCE_GENERATION:-}" ] && [ "$OWNER_WRITE_SOURCE_GENERATION" != "$generation" ]; }; then
|
|
prepare_owner_generation_spec 1 "${IPV6_BUILT:-${IPV6_ACTIVE:-0}}" || return 1
|
|
fi
|
|
[ "$qnum" = "$OWNER_WRITE_QNUM" ] || return 1
|
|
ensure_state_dir || return 1
|
|
state_file_target_is_safe "$OWNER_STATE" || return 1
|
|
state_path_is_managed_file "$tmp" || return 1
|
|
[ ! -e "$tmp" ] && [ ! -L "$tmp" ] || return 1
|
|
umask 077
|
|
# One builtin write: printf is an external on the target shell, and this
|
|
# record is written on every phase transition of the replace path.
|
|
z2_emit_line "version=$OWNER_STATE_VERSION
|
|
pid=$pid
|
|
starttime=$start
|
|
argv_sha256=$argv_sha256
|
|
qnum=$qnum
|
|
exe=$NFQWS2
|
|
generation=$generation
|
|
boot_id=$boot_id
|
|
phase=$phase
|
|
install_generation=$OWNER_WRITE_INSTALL_GENERATION
|
|
install_archive_sha256=$OWNER_WRITE_INSTALL_ARCHIVE_SHA256
|
|
firewall_tag=$OWNER_WRITE_FIREWALL_TAG
|
|
out_chain=$OWNER_WRITE_OUT_CHAIN
|
|
in_chain=$OWNER_WRITE_IN_CHAIN
|
|
tethering=$OWNER_WRITE_TETHERING
|
|
ports_tcp=$OWNER_WRITE_PORTS_TCP
|
|
ports_udp=$OWNER_WRITE_PORTS_UDP
|
|
stun_ports=$OWNER_WRITE_STUN_PORTS
|
|
tcp_pkt_out=$OWNER_WRITE_TCP_PKT_OUT
|
|
tcp_pkt_in=$OWNER_WRITE_TCP_PKT_IN
|
|
udp_pkt_out=$OWNER_WRITE_UDP_PKT_OUT
|
|
udp_pkt_in=$OWNER_WRITE_UDP_PKT_IN
|
|
desync_mark=$OWNER_WRITE_DESYNC_MARK
|
|
ipv4_active=$OWNER_WRITE_IPV4_ACTIVE
|
|
ipv6_active=$OWNER_WRITE_IPV6_ACTIVE
|
|
ipv4_connbytes=$OWNER_WRITE_IPV4_CONNBYTES
|
|
ipv4_multiport=$OWNER_WRITE_IPV4_MULTIPORT
|
|
ipv4_mark=$OWNER_WRITE_IPV4_MARK
|
|
ipv6_connbytes=$OWNER_WRITE_IPV6_CONNBYTES
|
|
ipv6_multiport=$OWNER_WRITE_IPV6_MULTIPORT
|
|
ipv6_mark=$OWNER_WRITE_IPV6_MARK
|
|
ipv4_rules=$OWNER_WRITE_IPV4_RULES
|
|
ipv6_rules=$OWNER_WRITE_IPV6_RULES
|
|
ipv4_spec=$OWNER_WRITE_IPV4_SPEC
|
|
ipv6_spec=$OWNER_WRITE_IPV6_SPEC
|
|
firewall_fingerprint=$OWNER_WRITE_FIREWALL_FINGERPRINT" > "$tmp" || { rm -f "$tmp"; return 1; }
|
|
size="$(wc -c < "$tmp" 2>/dev/null)" || { rm -f "$tmp"; return 1; }
|
|
is_decimal "$size" && [ "$size" -gt 0 ] 2>/dev/null &&
|
|
[ "$size" -le "$OWNER_STATE_MAX_BYTES" ] 2>/dev/null || { rm -f "$tmp"; return 1; }
|
|
mv -f "$tmp" "$OWNER_STATE" || { rm -f "$tmp"; return 1; }
|
|
OWNER_WRITE_READY=0; OWNER_WRITE_SOURCE_GENERATION=""
|
|
}
|
|
|
|
publish_nfqws_owner() {
|
|
local pid="$1" start="$2" qnum="$3" phase="$4" argv_sha256 generation
|
|
is_decimal "$pid" && is_decimal "$start" || return 1
|
|
# This is the single launch-time process proof. verify_nfqws_pid captures
|
|
# the stable start time and argv digest in the same pass; recomputing both
|
|
# before owner publication used to double the expensive /proc traversal.
|
|
verify_nfqws_pid "$pid" "$start" "" "$qnum" capture-argv || return 1
|
|
start="$VERIFIED_STARTTIME"
|
|
argv_sha256="$VERIFIED_ARGV_SHA256"
|
|
generation="${PENDING_OWNER_GENERATION:-}"
|
|
if [ -z "$generation" ]; then
|
|
new_lifecycle_token_read || return 1
|
|
generation="$Z2_NEW_TOKEN"
|
|
fi
|
|
# The authenticated, boot-bound owner is the publication commit marker.
|
|
# Publish it first so a same-boot process interruption can leave at worst
|
|
# an owner-only state, which process preflight can verify exactly. A bare
|
|
# numeric pidfile is intentionally never produced. Cross-boot recovery
|
|
# proves that the corresponding process and kernel firewall state vanished.
|
|
write_owner_state "$pid" "$start" "$argv_sha256" "$qnum" "$generation" "$phase" || return 1
|
|
write_numeric_pidfile "$pid" || return 1
|
|
PUBLISHED_PID="$pid"
|
|
PUBLISHED_START="$start"
|
|
PUBLISHED_ARGV_SHA256="$argv_sha256"
|
|
PUBLISHED_GENERATION="$generation"
|
|
PUBLISHED_FIREWALL_FINGERPRINT="$OWNER_WRITE_FIREWALL_FINGERPRINT"
|
|
PUBLISHED_IPV4_RULES="$OWNER_WRITE_IPV4_RULES"
|
|
PUBLISHED_IPV6_RULES="$OWNER_WRITE_IPV6_RULES"
|
|
PUBLISHED_IPV6_ACTIVE="$OWNER_WRITE_IPV6_ACTIVE"
|
|
PUBLISHED_INSTALL_GENERATION="$OWNER_WRITE_INSTALL_GENERATION"
|
|
PUBLISHED_INSTALL_ARCHIVE_SHA256="$OWNER_WRITE_INSTALL_ARCHIVE_SHA256"
|
|
return 0
|
|
}
|
|
|
|
set_owner_phase() {
|
|
local phase="$1"
|
|
read_owner_state && owner_state_is_current_boot || return 1
|
|
[ "$OWNER_STATE_PHASE" = "$phase" ] && return 0
|
|
reverify_published_nfqws_pid "$OWNER_STATE_PID" "$OWNER_STATE_START" "$OWNER_STATE_ARGV_SHA256" "$OWNER_STATE_QNUM" || return 1
|
|
write_owner_state "$OWNER_STATE_PID" "$OWNER_STATE_START" "$OWNER_STATE_ARGV_SHA256" "$OWNER_STATE_QNUM" "$OWNER_STATE_GENERATION" "$phase"
|
|
}
|
|
|
|
retire_owner_metadata() {
|
|
scan_exact_owned_nfqws >/dev/null 2>&1 || return 1
|
|
[ -z "$OWNED_SCAN_PIDS" ] || return 1
|
|
# Rejected/corrupt PID or owner metadata is repair evidence and must not be
|
|
# silently removed. Verified publication cleanup happens in
|
|
# stop_pidfile_process().
|
|
[ ! -e "$PIDFILE" ] && [ ! -L "$PIDFILE" ] || return 1
|
|
[ ! -e "$OWNER_STATE" ] && [ ! -L "$OWNER_STATE" ] || return 1
|
|
return 0
|
|
}
|
|
|
|
VERIFIED_STARTTIME=""
|
|
VERIFIED_ARGV_SHA256=""
|
|
Z2_NFQWS2_REALPATH_FOR=""
|
|
Z2_NFQWS2_REALPATH=""
|
|
|
|
# A full verification that pinned (pid, starttime) and hashed the argv is a
|
|
# fact for as long as that exact process object exists: the same
|
|
# single-writer argument the owner read cache makes for publications (see
|
|
# Z2_OWNER_READ_CACHE) extends to the identity content of the process the
|
|
# locked transaction itself proved. Re-proofs against the same expectations
|
|
# re-establish liveness with builtins (starttime unchanged, cmdline still
|
|
# non-empty and prefix-matched, which also rules out a zombie) and reuse the
|
|
# proven argv digest instead of re-paying tr, sha256sum and readlink execs.
|
|
# Armed only under the lifecycle lock, never under a test's binary override;
|
|
# retired by every stop attempt and by the lock release.
|
|
Z2_PROVEN_PROCESS_FACT=""
|
|
|
|
retire_proven_process_fact() {
|
|
Z2_PROVEN_PROCESS_FACT=""
|
|
}
|
|
|
|
reverify_published_nfqws_pid() {
|
|
local pid="$1" expected_start="$2" expected_argv_sha256="$3" expected_qnum="$4"
|
|
local cmdline runtime_nfqws2
|
|
if owner_read_cache_active && [ -n "$Z2_PROVEN_PROCESS_FACT" ] &&
|
|
normalize_qnum "$expected_qnum" 2>/dev/null &&
|
|
[ "$Z2_PROVEN_PROCESS_FACT" = "$pid|$expected_start|$expected_argv_sha256|$QNUM_NORMALIZED" ]; then
|
|
runtime_nfqws2="${AUDIT_NFQWS2_OVERRIDE:-$NFQWS2}"
|
|
if proc_starttime_read "$pid" 2>/dev/null &&
|
|
[ "$PROC_STARTTIME" = "$expected_start" ] &&
|
|
kill -0 "$pid" 2>/dev/null &&
|
|
[ -r "/proc/$pid/cmdline" ]; then
|
|
cmdline=""
|
|
IFS= read -r cmdline < "/proc/$pid/cmdline" 2>/dev/null || [ -n "$cmdline" ] || cmdline=""
|
|
case "$cmdline" in
|
|
"$runtime_nfqws2"*)
|
|
VERIFIED_STARTTIME="$expected_start"
|
|
VERIFIED_ARGV_SHA256="$expected_argv_sha256"
|
|
return 0
|
|
;;
|
|
esac
|
|
fi
|
|
retire_proven_process_fact
|
|
fi
|
|
verify_nfqws_pid "$pid" "$expected_start" "$expected_argv_sha256" "$expected_qnum"
|
|
}
|
|
|
|
verify_nfqws_pid() {
|
|
local pid="$1" expected_start="${2:-}" expected_argv_sha256="${3:-}" expected_qnum="${4:-}"
|
|
local capture_argv="${5:-}" before after cmd_exe binary_exe cmd_identity binary_identity
|
|
local actual_argv_sha256="" argv0 runtime_nfqws2
|
|
runtime_nfqws2="${AUDIT_NFQWS2_OVERRIDE:-$NFQWS2}"
|
|
VERIFIED_STARTTIME=""
|
|
VERIFIED_ARGV_SHA256=""
|
|
is_decimal "$pid" || return 1
|
|
[ "$pid" -gt 0 ] 2>/dev/null || return 1
|
|
proc_starttime_read "$pid" || return 1
|
|
before="$PROC_STARTTIME"
|
|
[ -z "$expected_start" ] || [ "$before" = "$expected_start" ] || return 1
|
|
kill -0 "$pid" 2>/dev/null || return 1
|
|
proc_argv0_read "$pid" || return 1
|
|
argv0="$PROC_ARGV0"
|
|
[ "$argv0" = "$runtime_nfqws2" ] || return 1
|
|
if [ -n "$expected_qnum" ]; then
|
|
normalize_qnum "$expected_qnum" || return 1
|
|
# Same exact-line match tr|grep -Fqx performed, answered from the
|
|
# cmdline snapshot proc_argv0_read already paid for. An argument
|
|
# containing a newline splits into lines for both forms alike.
|
|
case "$Z2_NL$PROC_CMDLINE_LINES$Z2_NL" in
|
|
*"$Z2_NL--qnum=$QNUM_NORMALIZED$Z2_NL"*) ;;
|
|
*) return 1 ;;
|
|
esac
|
|
fi
|
|
if [ -n "$expected_argv_sha256" ] || [ "$capture_argv" = capture-argv ]; then
|
|
proc_cmdline_sha256_read "$pid" || return 1
|
|
actual_argv_sha256="$PROC_CMDLINE_SHA256"
|
|
fi
|
|
if [ -n "$expected_argv_sha256" ]; then
|
|
is_lower_sha256 "$expected_argv_sha256" || return 1
|
|
[ "$actual_argv_sha256" = "$expected_argv_sha256" ] || return 1
|
|
fi
|
|
|
|
cmd_exe="$(readlink -f "/proc/$pid/exe" 2>/dev/null)"
|
|
# The expected side of the exe comparison resolves a root-owned module
|
|
# path that no cooperating writer re-points while this process runs, so
|
|
# one readlink per binary path serves every verification; the live side
|
|
# above stays a fresh per-process readlink.
|
|
if [ "$Z2_NFQWS2_REALPATH_FOR" = "$runtime_nfqws2" ]; then
|
|
binary_exe="$Z2_NFQWS2_REALPATH"
|
|
else
|
|
binary_exe="$(readlink -f "$runtime_nfqws2" 2>/dev/null)"
|
|
Z2_NFQWS2_REALPATH_FOR="$runtime_nfqws2"
|
|
Z2_NFQWS2_REALPATH="$binary_exe"
|
|
fi
|
|
# Exact argv0 is mandatory on every platform. /proc/PID/exe strengthens
|
|
# that identity when readlink is available, but its absence on some
|
|
# Android kernels must not weaken or disable the exact argv0 check above.
|
|
if [ -n "$cmd_exe" ] && [ -n "$binary_exe" ]; then
|
|
[ "$cmd_exe" = "$binary_exe" ] || return 1
|
|
# A process in another mount namespace can expose the same textual
|
|
# path while executing a different generation inode. The procfs magic
|
|
# link resolves the live executable object, so compare its stable
|
|
# device/inode identity with the expected module binary as well.
|
|
if [ "$Z2_HAVE_STAT" = 1 ]; then
|
|
cmd_identity="$(stat -Lc '%d:%i' "/proc/$pid/exe" 2>/dev/null)" ||
|
|
return 1
|
|
binary_identity="$(stat -Lc '%d:%i' "$runtime_nfqws2" 2>/dev/null)" ||
|
|
return 1
|
|
[ "$cmd_identity" = "$binary_identity" ] || return 1
|
|
fi
|
|
fi
|
|
proc_starttime_read "$pid" || return 1
|
|
after="$PROC_STARTTIME"
|
|
[ "$before" = "$after" ] || return 1
|
|
VERIFIED_STARTTIME="$after"
|
|
VERIFIED_ARGV_SHA256="$actual_argv_sha256"
|
|
# Only a proof that pinned every identity dimension becomes the reusable
|
|
# fact reverify_published_nfqws_pid consumes.
|
|
if owner_read_cache_active && [ -n "$actual_argv_sha256" ] && [ -n "$expected_qnum" ]; then
|
|
Z2_PROVEN_PROCESS_FACT="$pid|$after|$actual_argv_sha256|$QNUM_NORMALIZED"
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# Builtin replacement for candidate="$(cat file)". Command substitution
|
|
# stripped trailing newlines, so the exact acceptance to preserve is one
|
|
# payload line followed only by blank lines.
|
|
read_single_payload_line() {
|
|
local line
|
|
Z2_PAYLOAD_LINE=""
|
|
[ -r "$1" ] || return 1
|
|
{
|
|
IFS= read -r Z2_PAYLOAD_LINE || [ -n "$Z2_PAYLOAD_LINE" ] || return 1
|
|
while IFS= read -r line || [ -n "$line" ]; do
|
|
[ -z "$line" ] || return 1
|
|
done
|
|
} < "$1" 2>/dev/null
|
|
}
|
|
|
|
read_verified_pidfile() {
|
|
VERIFIED_PID=""
|
|
VERIFIED_PID_START=""
|
|
VERIFIED_PID_ARGV_SHA256=""
|
|
VERIFIED_PID_QNUM=""
|
|
state_file_is_secure "$PIDFILE" && [ -r "$PIDFILE" ] || return 1
|
|
local candidate
|
|
read_single_payload_line "$PIDFILE" || return 1
|
|
candidate="$Z2_PAYLOAD_LINE"
|
|
is_decimal "$candidate" || return 1
|
|
if [ -e "$OWNER_STATE" ]; then
|
|
read_owner_state && owner_state_is_current_boot || return 1
|
|
[ "$OWNER_STATE_PID" = "$candidate" ] || return 1
|
|
reverify_published_nfqws_pid "$candidate" "$OWNER_STATE_START" "$OWNER_STATE_ARGV_SHA256" "$OWNER_STATE_QNUM" || return 1
|
|
VERIFIED_PID_ARGV_SHA256="$OWNER_STATE_ARGV_SHA256"
|
|
VERIFIED_PID_QNUM="$OWNER_STATE_QNUM"
|
|
else
|
|
# A bare numeric PID cannot authenticate a current lifecycle owner.
|
|
return 1
|
|
fi
|
|
VERIFIED_PID="$candidate"
|
|
VERIFIED_PID_START="$VERIFIED_STARTTIME"
|
|
return 0
|
|
}
|
|
|
|
verify_status_snapshot_pid() {
|
|
local candidate before after identity cmd_identity binary_identity
|
|
local runtime_nfqws2="${AUDIT_NFQWS2_OVERRIDE:-$NFQWS2}"
|
|
VERIFIED_PID=""
|
|
VERIFIED_PID_START=""
|
|
VERIFIED_PID_ARGV_SHA256=""
|
|
VERIFIED_PID_QNUM=""
|
|
observer_state_file_is_secure "$PIDFILE" && [ -r "$PIDFILE" ] || return 1
|
|
IFS= read -r candidate < "$PIDFILE" 2>/dev/null || return 1
|
|
is_canonical_positive_decimal "$candidate" || return 1
|
|
[ "$candidate" = "$STATUS_FILE_OWN_PID" ] || return 1
|
|
is_canonical_nonnegative_i64 "$STATUS_FILE_OWN_PID_STARTTIME" || return 1
|
|
is_lower_sha256 "$STATUS_FILE_OWN_ARGV_SHA256" || return 1
|
|
proc_starttime_read "$candidate" || return 1
|
|
before="$PROC_STARTTIME"
|
|
[ "$before" = "$STATUS_FILE_OWN_PID_STARTTIME" ] || return 1
|
|
kill -0 "$candidate" 2>/dev/null || return 1
|
|
# No argv0 prefilter here. proc_cmdline_may_match_nfqws is a fork-free
|
|
# *screening* probe for candidates nothing has authenticated yet — the
|
|
# recovery scan over the whole process table, and the stop wait loop that
|
|
# must also witness a zombie transition. This path already knows which
|
|
# process it is asking about, and the next line authenticates the complete
|
|
# argv against the digest the lifecycle owner published, which is strictly
|
|
# stronger than any prefix of it. The prefilter only re-read the same file
|
|
# first, and a shell `read` from a proc file is byte-at-a-time: on this
|
|
# module's own 21 KB nfqws2 cmdline that screening cost ~35 ms — four times
|
|
# the digest it was screening for — on every status observation.
|
|
proc_cmdline_sha256_read "$candidate" || return 1
|
|
[ "$PROC_CMDLINE_SHA256" = "$STATUS_FILE_OWN_ARGV_SHA256" ] || return 1
|
|
if [ "$Z2_HAVE_STAT" = 1 ]; then
|
|
# One stat covers both paths: the frontend is the same, and a device
|
|
# where either path is unreadable fails the call as a whole.
|
|
identity="$(stat -Lc '%d:%i' "/proc/$candidate/exe" "$runtime_nfqws2" 2>/dev/null)" ||
|
|
return 1
|
|
cmd_identity="${identity%%"$Z2_NL"*}"
|
|
binary_identity="${identity#*"$Z2_NL"}"
|
|
# A single line means stat answered for only one path; comparing it
|
|
# against itself would pass an identity that was never established.
|
|
[ "$cmd_identity" != "$identity" ] || return 1
|
|
[ "$cmd_identity" = "$binary_identity" ] || return 1
|
|
fi
|
|
proc_starttime_read "$candidate" || return 1
|
|
after="$PROC_STARTTIME"
|
|
[ "$before" = "$after" ] || return 1
|
|
VERIFIED_PID="$candidate"
|
|
VERIFIED_PID_START="$after"
|
|
VERIFIED_PID_ARGV_SHA256="$PROC_CMDLINE_SHA256"
|
|
VERIFIED_PID_QNUM="$STATUS_FILE_QNUM"
|
|
return 0
|
|
}
|
|
|
|
read_live_pidfile() {
|
|
LIVE_PIDFILE_PID=""
|
|
state_file_is_secure "$PIDFILE" && [ -r "$PIDFILE" ] || return 1
|
|
local candidate
|
|
read_single_payload_line "$PIDFILE" || return 1
|
|
candidate="$Z2_PAYLOAD_LINE"
|
|
is_decimal "$candidate" || return 1
|
|
[ "$candidate" -gt 0 ] 2>/dev/null || return 1
|
|
kill -0 "$candidate" 2>/dev/null || return 1
|
|
proc_starttime_read "$candidate" 2>/dev/null || return 1
|
|
LIVE_PIDFILE_PID="$candidate"
|
|
return 0
|
|
}
|
|
|
|
# Death polling needs only the identity dimension the kernel keeps immutable
|
|
# for a live process: pid plus starttime, which the full entry proof already
|
|
# pinned. Re-proving argv/exe every 100 ms paid three to four forks per turn
|
|
# and added nothing — argv and exe cannot change while pid+starttime hold. A
|
|
# zombie drops its cmdline, so the builtin prefix probe doubles as the
|
|
# zombie-transition witness the wait loops must observe.
|
|
stop_target_is_gone() {
|
|
local pid="$1" start="$2"
|
|
proc_starttime_read "$pid" 2>/dev/null || return 0
|
|
[ "$PROC_STARTTIME" = "$start" ] || return 0
|
|
proc_cmdline_may_match_nfqws "$pid" || return 0
|
|
return 1
|
|
}
|
|
|
|
stop_verified_nfqws_pid() {
|
|
local pid="$1" start="$2" expected_argv_sha256="${3:-}" expected_qnum="${4:-}" n=0
|
|
# A transaction that already proved this exact process re-establishes only
|
|
# immutable pid/starttime liveness here. Without such a proof the helper
|
|
# transparently performs the complete argv/exe verification.
|
|
reverify_published_nfqws_pid "$pid" "$start" "$expected_argv_sha256" "$expected_qnum" ||
|
|
return 2
|
|
# A stop attempt ends the fact's lifetime: the wait loops below must see
|
|
# every death, including a zombie transition.
|
|
retire_proven_process_fact
|
|
kill -TERM "$pid" 2>/dev/null || return 1
|
|
while [ "$n" -lt 50 ]; do
|
|
if stop_target_is_gone "$pid" "$start"; then return 0; fi
|
|
sleep 0.1
|
|
n=$((n + 1))
|
|
done
|
|
if stop_target_is_gone "$pid" "$start"; then return 0; fi
|
|
kill -KILL "$pid" 2>/dev/null || return 1
|
|
n=0
|
|
while [ "$n" -lt 30 ]; do
|
|
sleep 0.1
|
|
if stop_target_is_gone "$pid" "$start"; then return 0; fi
|
|
n=$((n + 1))
|
|
done
|
|
return 1
|
|
}
|
|
|
|
PROCESS_CLEANUP_PREFLIGHT_PROVEN=0
|
|
PROCESS_PREFLIGHT_PID=""
|
|
PROCESS_PREFLIGHT_START=""
|
|
PROCESS_PREFLIGHT_ARGV_SHA256=""
|
|
PROCESS_PREFLIGHT_QNUM=""
|
|
PROCESS_PREFLIGHT_GENERATION=""
|
|
PROCESS_PREFLIGHT_PHASE=""
|
|
PROCESS_PREFLIGHT_PIDFILE_PRESENT=0
|
|
PROCESS_PREFLIGHT_OWNER_PRESENT=0
|
|
PROCESS_PREFLIGHT_LIVE=0
|
|
|
|
process_snapshot_owner_matches() {
|
|
[ "$PROCESS_PREFLIGHT_OWNER_PRESENT" = 1 ] || {
|
|
[ ! -e "$OWNER_STATE" ] && [ ! -L "$OWNER_STATE" ]
|
|
return
|
|
}
|
|
read_owner_state && owner_state_is_current_boot || return 1
|
|
[ "$OWNER_STATE_PID" = "$PROCESS_PREFLIGHT_PID" ] &&
|
|
[ "$OWNER_STATE_START" = "$PROCESS_PREFLIGHT_START" ] &&
|
|
[ "$OWNER_STATE_ARGV_SHA256" = "$PROCESS_PREFLIGHT_ARGV_SHA256" ] &&
|
|
[ "$OWNER_STATE_QNUM" = "$PROCESS_PREFLIGHT_QNUM" ] &&
|
|
[ "$OWNER_STATE_GENERATION" = "$PROCESS_PREFLIGHT_GENERATION" ] &&
|
|
[ "$OWNER_STATE_PHASE" = "$PROCESS_PREFLIGHT_PHASE" ]
|
|
}
|
|
|
|
process_snapshot_pidfile_matches() {
|
|
local candidate
|
|
[ "$PROCESS_PREFLIGHT_PIDFILE_PRESENT" = 1 ] || {
|
|
[ ! -e "$PIDFILE" ] && [ ! -L "$PIDFILE" ]
|
|
return
|
|
}
|
|
state_file_is_secure "$PIDFILE" && [ -r "$PIDFILE" ] || return 1
|
|
read_single_payload_line "$PIDFILE" || return 1
|
|
candidate="$Z2_PAYLOAD_LINE"
|
|
[ "$candidate" = "$PROCESS_PREFLIGHT_PID" ]
|
|
}
|
|
|
|
stop_pidfile_process() {
|
|
local rc=0
|
|
[ "$PROCESS_CLEANUP_PREFLIGHT_PROVEN" = 1 ] || preflight_owned_process_cleanup || return 1
|
|
process_snapshot_pidfile_matches && process_snapshot_owner_matches || return 1
|
|
if [ "$PROCESS_PREFLIGHT_LIVE" = 1 ]; then
|
|
stop_verified_nfqws_pid "$PROCESS_PREFLIGHT_PID" "$PROCESS_PREFLIGHT_START" \
|
|
"$PROCESS_PREFLIGHT_ARGV_SHA256" "$PROCESS_PREFLIGHT_QNUM" || rc=1
|
|
fi
|
|
# The preflight scan ran under the lifecycle lock. No cooperating root
|
|
# writer can create a second module process before this transaction ends,
|
|
# and /data/adb is not traversable by unprivileged Android processes.
|
|
# Re-scanning the complete process set after the exact proven PID died
|
|
# therefore re-proved a transaction fact without strengthening it.
|
|
if [ "$rc" -eq 0 ]; then
|
|
if [ "$PROCESS_PREFLIGHT_PIDFILE_PRESENT" = 1 ]; then rm -f "$PIDFILE" 2>/dev/null || rc=1; fi
|
|
if [ "$PROCESS_PREFLIGHT_OWNER_PRESENT" = 1 ]; then
|
|
rm -f "$OWNER_STATE" 2>/dev/null || rc=1
|
|
retire_owner_read_cache
|
|
fi
|
|
fi
|
|
return "$rc"
|
|
}
|
|
|
|
scan_exact_owned_nfqws() {
|
|
local procdir pid start restore_noglob=0 cmdline runtime_nfqws2 candidates scan_rc
|
|
OWNED_SCAN_PIDS=""
|
|
runtime_nfqws2="${AUDIT_NFQWS2_OVERRIDE:-$NFQWS2}"
|
|
# One pgrep enumeration replaces a shell walk over every Android process
|
|
# (hundreds to thousands of read iterations). pgrep is only a prefilter:
|
|
# over-matching is harmless because every candidate still passes the same
|
|
# prefix read and strict identity proof below, but under-matching is not,
|
|
# so the fast path is reserved for binary paths whose characters are all
|
|
# regex-literal (dot over-matches, which is safe), and any pgrep failure
|
|
# beyond "no matches" falls back to the exhaustive walk.
|
|
candidates=""
|
|
scan_rc=2
|
|
case "$runtime_nfqws2" in
|
|
''|*[!A-Za-z0-9/._-]*) ;;
|
|
*)
|
|
candidates="$(pgrep -f "^$runtime_nfqws2" 2>/dev/null)"
|
|
scan_rc=$?
|
|
;;
|
|
esac
|
|
if [ "$scan_rc" -gt 1 ]; then
|
|
candidates=""
|
|
case "$-" in *f*) restore_noglob=1; set +f ;; esac
|
|
for procdir in /proc/[0-9]*; do
|
|
candidates="$candidates ${procdir#/proc/}"
|
|
done
|
|
[ "$restore_noglob" = 1 ] && set -f
|
|
fi
|
|
for pid in $candidates; do
|
|
is_decimal "$pid" || continue
|
|
# The prefilter runs for every candidate process, so it is inlined
|
|
# down to its three builtins: readability probe, one read, prefix
|
|
# match (proc_cmdline_may_match_nfqws documents why a prefix is the
|
|
# most a NUL-stripped read can prove). The strict identity proof is
|
|
# reserved for plausible candidates.
|
|
[ -r "/proc/$pid/cmdline" ] || continue
|
|
cmdline=""
|
|
IFS= read -r cmdline < "/proc/$pid/cmdline" 2>/dev/null || [ -n "$cmdline" ] || continue
|
|
case "$cmdline" in "$runtime_nfqws2"*) ;; *) continue ;; esac
|
|
proc_starttime_read "$pid" || continue
|
|
start="$PROC_STARTTIME"
|
|
if [ -n "$Z2_PROVEN_PROCESS_FACT" ] &&
|
|
[ "${OWNER_STATE_PID:-}" = "$pid" ] &&
|
|
[ "${OWNER_STATE_START:-}" = "$start" ] &&
|
|
reverify_published_nfqws_pid "$pid" "$start" \
|
|
"$OWNER_STATE_ARGV_SHA256" "$OWNER_STATE_QNUM"; then
|
|
if [ -n "$OWNED_SCAN_PIDS" ]; then OWNED_SCAN_PIDS="$OWNED_SCAN_PIDS $pid"
|
|
else OWNED_SCAN_PIDS="$pid"; fi
|
|
elif verify_nfqws_pid "$pid" "$start" "" ""; then
|
|
if [ -n "$OWNED_SCAN_PIDS" ]; then OWNED_SCAN_PIDS="$OWNED_SCAN_PIDS $pid"
|
|
else OWNED_SCAN_PIDS="$pid"; fi
|
|
fi
|
|
done
|
|
z2_emit_line "$OWNED_SCAN_PIDS"
|
|
}
|
|
|
|
scan_exact_owned_nfqws_for_path() {
|
|
local AUDIT_NFQWS2_OVERRIDE="$1"
|
|
[ -n "$AUDIT_NFQWS2_OVERRIDE" ] || return 1
|
|
scan_exact_owned_nfqws
|
|
}
|
|
|
|
stop_all_exact_owned_nfqws() {
|
|
local pid start rc=0
|
|
scan_exact_owned_nfqws >/dev/null 2>&1 || return 1
|
|
for pid in $OWNED_SCAN_PIDS; do
|
|
proc_starttime_read "$pid" || continue
|
|
start="$PROC_STARTTIME"
|
|
stop_verified_nfqws_pid "$pid" "$start" "" "" || rc=1
|
|
done
|
|
scan_exact_owned_nfqws >/dev/null 2>&1 || return 1
|
|
[ -z "$OWNED_SCAN_PIDS" ] || rc=1
|
|
return "$rc"
|
|
}
|
|
|
|
stop_all_exact_owned_nfqws_for_path() {
|
|
local AUDIT_NFQWS2_OVERRIDE="$1"
|
|
[ -n "$AUDIT_NFQWS2_OVERRIDE" ] || return 1
|
|
stop_all_exact_owned_nfqws
|
|
}
|
|
|
|
# Refuse teardown when exact process publication cannot account for every
|
|
# module-binary process. In particular, a rejected PID/owner file must never
|
|
# fall through to the broad exact-path scan and kill a listener.
|
|
PROCESS_CLEANUP_PREFLIGHT_ERROR=""
|
|
preflight_owned_process_cleanup() {
|
|
local count=0 pid pidfile_present=0 owner_present=0 argv_sha256 start
|
|
PROCESS_CLEANUP_PREFLIGHT_ERROR=""
|
|
PROCESS_CLEANUP_PREFLIGHT_PROVEN=0
|
|
PROCESS_PREFLIGHT_PID=""; PROCESS_PREFLIGHT_START=""; PROCESS_PREFLIGHT_ARGV_SHA256=""
|
|
PROCESS_PREFLIGHT_QNUM=""; PROCESS_PREFLIGHT_GENERATION=""; PROCESS_PREFLIGHT_PHASE=""
|
|
PROCESS_PREFLIGHT_PIDFILE_PRESENT=0; PROCESS_PREFLIGHT_OWNER_PRESENT=0
|
|
PROCESS_PREFLIGHT_LIVE=0
|
|
{ [ -e "$PIDFILE" ] || [ -L "$PIDFILE" ]; } && pidfile_present=1
|
|
{ [ -e "$OWNER_STATE" ] || [ -L "$OWNER_STATE" ]; } && owner_present=1
|
|
scan_exact_owned_nfqws >/dev/null 2>&1 || {
|
|
PROCESS_CLEANUP_PREFLIGHT_ERROR="exact module process scan is unavailable"
|
|
return 1
|
|
}
|
|
for pid in $OWNED_SCAN_PIDS; do count=$((count + 1)); done
|
|
[ "$count" -le 1 ] || {
|
|
PROCESS_CLEANUP_PREFLIGHT_ERROR="multiple exact module processes are ambiguous"
|
|
return 1
|
|
}
|
|
if [ "$count" = 0 ] && [ "$pidfile_present" = 1 ]; then
|
|
PROCESS_CLEANUP_PREFLIGHT_ERROR="PID publication exists but cannot be matched to an exact live module process"
|
|
return 1
|
|
fi
|
|
if [ "$owner_present" = 1 ]; then
|
|
read_owner_state || {
|
|
PROCESS_CLEANUP_PREFLIGHT_ERROR="owner metadata is malformed or unsafe"
|
|
return 1
|
|
}
|
|
owner_state_is_current_boot || {
|
|
PROCESS_CLEANUP_PREFLIGHT_ERROR="owner metadata is not bound to the current boot"
|
|
return 1
|
|
}
|
|
PROCESS_PREFLIGHT_OWNER_PRESENT=1
|
|
PROCESS_PREFLIGHT_GENERATION="$OWNER_STATE_GENERATION"
|
|
PROCESS_PREFLIGHT_PHASE="$OWNER_STATE_PHASE"
|
|
fi
|
|
if [ "$count" = 1 ]; then
|
|
[ "$owner_present" = 1 ] || {
|
|
PROCESS_CLEANUP_PREFLIGHT_ERROR="live module process lacks an authenticated current owner"
|
|
return 1
|
|
}
|
|
PROCESS_PREFLIGHT_LIVE=1
|
|
pid="$OWNED_SCAN_PIDS"
|
|
proc_starttime_read "$pid" || return 1
|
|
start="$PROC_STARTTIME"
|
|
proc_cmdline_sha256_read "$pid" || return 1
|
|
argv_sha256="$PROC_CMDLINE_SHA256"
|
|
if [ "$pidfile_present" = 1 ]; then
|
|
read_verified_pidfile || {
|
|
PROCESS_CLEANUP_PREFLIGHT_ERROR="live PID publication is corrupt or unverified"
|
|
return 1
|
|
}
|
|
[ "$VERIFIED_PID" = "$pid" ] || {
|
|
PROCESS_CLEANUP_PREFLIGHT_ERROR="PID publication names a different live process"
|
|
return 1
|
|
}
|
|
PROCESS_PREFLIGHT_PIDFILE_PRESENT=1
|
|
PROCESS_PREFLIGHT_QNUM="$VERIFIED_PID_QNUM"
|
|
elif [ "$owner_present" = 1 ]; then
|
|
[ "$OWNER_STATE_PID" = "$pid" ] && [ "$OWNER_STATE_START" = "$start" ] &&
|
|
[ "$OWNER_STATE_ARGV_SHA256" = "$argv_sha256" ] &&
|
|
verify_nfqws_pid "$pid" "$start" "$argv_sha256" "$OWNER_STATE_QNUM" || {
|
|
PROCESS_CLEANUP_PREFLIGHT_ERROR="owner metadata does not match the exact module process"
|
|
return 1
|
|
}
|
|
PROCESS_PREFLIGHT_QNUM="$OWNER_STATE_QNUM"
|
|
fi
|
|
PROCESS_PREFLIGHT_PID="$pid"
|
|
PROCESS_PREFLIGHT_START="$start"
|
|
PROCESS_PREFLIGHT_ARGV_SHA256="$argv_sha256"
|
|
elif [ "$owner_present" = 1 ]; then
|
|
PROCESS_CLEANUP_PREFLIGHT_ERROR="current-boot owner publication has no exact live module process"
|
|
return 1
|
|
fi
|
|
PROCESS_CLEANUP_PREFLIGHT_PROVEN=1
|
|
return 0
|
|
}
|
|
|
|
owner_family_generation_healthy() {
|
|
local tool="$1" family="$2" active connbytes expected multiport
|
|
local PORTS_TCP="$OWNER_STATE_PORTS_TCP" PORTS_UDP="$OWNER_STATE_PORTS_UDP"
|
|
local TCP_PKT_OUT="$OWNER_STATE_TCP_PKT_OUT" TCP_PKT_IN="$OWNER_STATE_TCP_PKT_IN"
|
|
local UDP_PKT_OUT="$OWNER_STATE_UDP_PKT_OUT" UDP_PKT_IN="$OWNER_STATE_UDP_PKT_IN"
|
|
local QNUM="$OWNER_STATE_QNUM" DESYNC_MARK="$OWNER_STATE_DESYNC_MARK"
|
|
# The published generation is re-verified against the topology it recorded,
|
|
# never against the one now configured: a changed tethering setting must
|
|
# surface as a topology replacement, not as a health failure of live rules.
|
|
local TETHERING="$OWNER_STATE_TETHERING"
|
|
is_safe_firewall_identity "$OWNER_STATE_FIREWALL_TAG" \
|
|
"$OWNER_STATE_OUT_CHAIN" "$OWNER_STATE_IN_CHAIN" || return 1
|
|
[ "$OWNER_STATE_OUT_CHAIN" = "$Z2_FW_OUT_CHAIN" ] &&
|
|
[ "$OWNER_STATE_IN_CHAIN" = "$Z2_FW_IN_CHAIN" ] || return 1
|
|
# mark is in every rule the module authors, so a record that says otherwise
|
|
# describes something this module did not publish. multiport is not: a
|
|
# kernel without xt_multiport, or a port list past the fifteen values the
|
|
# parser accepts, is published one rule per interval and recorded as such.
|
|
# Requiring 1 here would refuse to re-verify a generation the module itself
|
|
# created, and the start path would rebuild the ruleset on every run.
|
|
if [ "$family" = ipv4 ]; then
|
|
active="$OWNER_STATE_IPV4_ACTIVE"
|
|
connbytes="$OWNER_STATE_IPV4_CONNBYTES"
|
|
expected="$OWNER_STATE_IPV4_RULES"
|
|
multiport="$OWNER_STATE_IPV4_MULTIPORT"
|
|
[ "$OWNER_STATE_IPV4_MARK" = 1 ] || return 1
|
|
else
|
|
active="$OWNER_STATE_IPV6_ACTIVE"
|
|
connbytes="$OWNER_STATE_IPV6_CONNBYTES"
|
|
expected="$OWNER_STATE_IPV6_RULES"
|
|
multiport="$OWNER_STATE_IPV6_MULTIPORT"
|
|
[ "$OWNER_STATE_IPV6_MARK" = 1 ] || return 1
|
|
fi
|
|
case "$multiport" in 0|1) ;; *) return 1 ;; esac
|
|
if [ "$active" = 0 ]; then
|
|
z2_fw_family_absent "$tool"
|
|
return
|
|
fi
|
|
z2_fw_verify_family "$tool" "$connbytes" "$multiport" || return 1
|
|
[ "$Z2_FW_RULES" = "$expected" ]
|
|
}
|
|
|
|
audit_owned_firewall_for_cleanup() {
|
|
FIREWALL_CLEANUP_PREFLIGHT_ERROR=""
|
|
command -v z2_fw_cleanup_family >/dev/null 2>&1 || {
|
|
FIREWALL_CLEANUP_PREFLIGHT_ERROR="firewall reconciler is unavailable"
|
|
return 1
|
|
}
|
|
z2_fw_tool_available iptables || {
|
|
FIREWALL_CLEANUP_PREFLIGHT_ERROR="IPv4 mangle backend is unavailable"
|
|
return 1
|
|
}
|
|
z2_fw_cleanup_is_unambiguous iptables || {
|
|
FIREWALL_CLEANUP_PREFLIGHT_ERROR="IPv4 stable namespace has a foreign reference"
|
|
return 1
|
|
}
|
|
z2_fw_save_audit iptables || {
|
|
FIREWALL_CLEANUP_PREFLIGHT_ERROR="IPv4 stable namespace audit could not be retained"
|
|
return 1
|
|
}
|
|
# The probe budget is spent here, once. Teardown then follows this
|
|
# decision instead of waiting again: a family whose baseline was never
|
|
# captured cannot be torn down from an audit, so a second wait would only
|
|
# walk into a guaranteed failure.
|
|
FIREWALL_IPV6_UNQUERYABLE=0
|
|
FIREWALL_IPV6_AUDITED_EMPTY=0
|
|
if command -v ip6tables >/dev/null 2>&1; then
|
|
if z2_fw_tool_available ip6tables ||
|
|
! firewall_family_persistently_unavailable ip6tables; then
|
|
if ! z2_fw_cleanup_is_unambiguous ip6tables || ! z2_fw_save_audit ip6tables; then
|
|
FIREWALL_CLEANUP_PREFLIGHT_ERROR="IPv6 stable namespace has a foreign reference"
|
|
return 1
|
|
fi
|
|
# A read that found nothing of ours is a proof, not an absence of
|
|
# one. If the frontend goes busy before teardown, that proof is
|
|
# what separates "we could not look" from "we looked and there was
|
|
# nothing" — and only the former deserves a reservation. Nothing
|
|
# can appear in between: teardown publishes no rules and the
|
|
# lifecycle lock is held across both steps.
|
|
! z2_fw_audit_is_absent ip6tables || FIREWALL_IPV6_AUDITED_EMPTY=1
|
|
else
|
|
FIREWALL_IPV6_UNQUERYABLE=1
|
|
fi
|
|
fi
|
|
# Stable chain names are the ownership boundary. Cleanup is idempotent and
|
|
# never touches another chain or a non-exact built-in anchor.
|
|
return 0
|
|
}
|
|
|
|
# Teardown may skip an IPv6 family it cannot query only when something proves
|
|
# this generation published nothing there. That decision was being made
|
|
# separately by stop, full rollback, start's rollback and the failure snapshot,
|
|
# and the four copies disagreed — so it lives here now, with one priority
|
|
# order: what the running transaction did, then the authenticated owner
|
|
# record, then a snapshot committed with a verified ruleset, then the
|
|
# conservative default.
|
|
#
|
|
# Publishes two answers to two different questions:
|
|
# CLEANUP_IPV6_OWNERSHIP_EXPECTED — may an unqueryable family be skipped?
|
|
# IPV6_PUBLICATION_RECORDED — is there positive evidence we published?
|
|
CLEANUP_IPV6_OWNERSHIP_EXPECTED=1
|
|
IPV6_PUBLICATION_RECORDED=0
|
|
resolve_ipv6_ownership_expectation() {
|
|
local owner_available="${1:-0}"
|
|
CLEANUP_IPV6_OWNERSHIP_EXPECTED=1
|
|
IPV6_PUBLICATION_RECORDED=0
|
|
if [ "${IPV6_TOUCHED:-0}" = 1 ] || [ "${IPV6_BUILT:-0}" = 1 ] ||
|
|
[ "${IPV6_ACTIVE:-0}" = 1 ]; then
|
|
IPV6_PUBLICATION_RECORDED=1
|
|
return 0
|
|
fi
|
|
if [ "$owner_available" = 1 ]; then
|
|
CLEANUP_IPV6_OWNERSHIP_EXPECTED="${OWNER_STATE_IPV6_ACTIVE:-1}"
|
|
IPV6_PUBLICATION_RECORDED="${OWNER_STATE_IPV6_ACTIVE:-0}"
|
|
return 0
|
|
fi
|
|
case "${STATUS_FILE_STATUS:-}" in
|
|
ok|stopped)
|
|
[ "${STATUS_FILE_RULESET_VERIFIED:-0}" = 1 ] &&
|
|
CLEANUP_IPV6_OWNERSHIP_EXPECTED="${STATUS_FILE_IPV6_ACTIVE:-1}"
|
|
IPV6_PUBLICATION_RECORDED="${STATUS_FILE_IPV6_ACTIVE:-0}"
|
|
;;
|
|
?*) IPV6_PUBLICATION_RECORDED="${STATUS_FILE_IPV6_ACTIVE:-0}" ;;
|
|
esac
|
|
return 0
|
|
}
|
|
|
|
# A busy xtables lock looks exactly like a missing table in a single probe, and
|
|
# netd, tethering and VPN apps take that lock constantly. Treating one failed
|
|
# probe as permanent would leave real rules behind, so require the condition to
|
|
# persist. A frontend that is not installed at all can never become queryable.
|
|
FIREWALL_PROBE_ATTEMPTS="${FIREWALL_PROBE_ATTEMPTS:-5}"
|
|
firewall_family_persistently_unavailable() {
|
|
local tool="$1" attempt=0 attempts="$FIREWALL_PROBE_ATTEMPTS"
|
|
# A zero, negative or non-numeric budget would skip the loop entirely and
|
|
# report "permanently unavailable" without probing once — the fail-open
|
|
# this function exists to prevent.
|
|
# Bounded on both sides: the caller holds the lifecycle lock while this
|
|
# runs, so an unbounded budget would hold it for hours.
|
|
is_decimal "$attempts" && [ "$attempts" -ge 1 ] 2>/dev/null &&
|
|
[ "$attempts" -le 60 ] 2>/dev/null || attempts=5
|
|
command -v "$tool" >/dev/null 2>&1 || return 0
|
|
while [ "$attempt" -lt "$attempts" ]; do
|
|
z2_fw_tool_available "$tool" && return 1
|
|
attempt=$((attempt + 1))
|
|
[ "$attempt" -ge "$attempts" ] || sleep 1
|
|
done
|
|
return 0
|
|
}
|
|
|
|
FIREWALL_IPV6_SKIPPED_UNPROVEN=0
|
|
FIREWALL_IPV6_UNQUERYABLE=0
|
|
FIREWALL_IPV6_AUDITED_EMPTY=0
|
|
# Set when this teardown actually removed the IPv6 family: the baseline was
|
|
# captured, so the frontend answered, and the removal batch committed. The
|
|
# checks that run afterwards re-ask the kernel, and a frontend that goes busy
|
|
# in that window would otherwise erase a proof this very transaction produced.
|
|
FIREWALL_IPV6_TEARDOWN_PROVEN=0
|
|
cleanup_owned_firewall() {
|
|
local baseline_mode="${1:-owned}" rc=0 result
|
|
case "$baseline_mode" in owned|audited) ;; *) return 1;; esac
|
|
FIREWALL_IPV6_SKIPPED_UNPROVEN=0
|
|
FIREWALL_IPV6_TEARDOWN_PROVEN=0
|
|
command -v z2_fw_cleanup_family >/dev/null 2>&1 || return 1
|
|
z2_fw_cleanup_family iptables "$baseline_mode"
|
|
result=$?
|
|
[ "$result" = 0 ] || rc=1
|
|
if command -v ip6tables >/dev/null 2>&1; then
|
|
# An audited teardown can only remove what the preflight captured, so a
|
|
# family the preflight could not read is not one this mode can touch.
|
|
# An owned teardown captures its own baseline, so it only needs the
|
|
# frontend to answer now.
|
|
if { [ "$baseline_mode" != audited ] || [ "${FIREWALL_IPV6_UNQUERYABLE:-0}" != 1 ]; } &&
|
|
{ z2_fw_tool_available ip6tables || ! firewall_family_persistently_unavailable ip6tables; }; then
|
|
if z2_fw_cleanup_family ip6tables "$baseline_mode"; then
|
|
FIREWALL_IPV6_TEARDOWN_PROVEN=1
|
|
else
|
|
FIREWALL_CLEANUP_PREFLIGHT_ERROR="IPv6 owned ruleset could not be removed"
|
|
rc=1
|
|
fi
|
|
else
|
|
# This family cannot be proven now, and on this device it cannot be
|
|
# proven later either. Refusing would fence every teardown until a
|
|
# reboot — and the next boot would refuse the same way. So skip it
|
|
# and make the uncertainty travel with the result: the caller
|
|
# reports it, the committed receipt withholds its verification
|
|
# claim, and the reboot clears whatever survived.
|
|
#
|
|
# No reservation is needed when our own record already proves this
|
|
# generation published nothing there, or when this run's own
|
|
# preflight read the family and found nothing of ours in it. The
|
|
# preflight proof counts only for the mode that produced it.
|
|
if [ "${CLEANUP_IPV6_OWNERSHIP_EXPECTED:-1}" != 0 ] &&
|
|
{ [ "$baseline_mode" != audited ] ||
|
|
[ "${FIREWALL_IPV6_AUDITED_EMPTY:-0}" != 1 ]; }; then
|
|
FIREWALL_IPV6_SKIPPED_UNPROVEN=1
|
|
fi
|
|
fi
|
|
fi
|
|
return "$rc"
|
|
}
|
|
|
|
owned_family_present() {
|
|
z2_fw_family_absent "$1"
|
|
case $? in 0) return 1;; 1) return 0;; *) return 2;; esac
|
|
}
|
|
|
|
# Read-only namespace discovery for a failed generation that never reached
|
|
# owner.meta publication. Dynamic chain names are strict module-owned kernel
|
|
# object identities; detecting them is safe even when teardown still requires
|
|
# a stronger journal/owner proof.
|
|
zapret2_namespace_present() {
|
|
local tool="$1" listing
|
|
listing="$("$tool" -t mangle -S 2>/dev/null)" || return 2
|
|
printf '%s\n' "$listing" | awk '
|
|
function owned(name, tag, side, ordinal) {
|
|
if (name == "ZAPRET2_OUT" || name == "ZAPRET2_IN" || name == "ZAPRET2_PROBE") return 1
|
|
if ((index(name,"Z2O_")==1 || index(name,"Z2I_")==1) && length(name)==14) {
|
|
tag=substr(name,5,10)
|
|
return tag !~ /[^A-Za-z0-9]/
|
|
}
|
|
if (index(name,"Z2R_")==1 && length(name)>=17) {
|
|
tag=substr(name,5,10); side=substr(name,16,1); ordinal=substr(name,17)
|
|
return substr(name,15,1)=="_" && tag !~ /[^A-Za-z0-9]/ &&
|
|
(side=="O" || side=="I") && ordinal ~ /^[1-9][0-9]*$/
|
|
}
|
|
return 0
|
|
}
|
|
$1 == "-N" && owned($2) { found=1 }
|
|
$1 == "-A" {
|
|
if (owned($2)) found=1
|
|
for (i=3;i<=NF;i++) if (($i=="-j" || $i=="--jump" || $i=="-g" || $i=="--goto") && owned($(i+1))) found=1
|
|
}
|
|
END { exit found ? 0 : 1 }
|
|
'
|
|
}
|
|
|
|
zapret2_delete_simple_jump_all() {
|
|
local tool="$1" source="$2" target="$3" count=0
|
|
while "$tool" -t mangle -C "$source" -j "$target" >/dev/null 2>&1; do
|
|
[ "$count" -lt 4096 ] 2>/dev/null || return 1
|
|
"$tool" -t mangle -D "$source" -j "$target" >/dev/null 2>&1 || return 1
|
|
count=$((count + 1))
|
|
done
|
|
return 0
|
|
}
|
|
|
|
# The root-manager removal marker is a durable global start fence. Once that marker
|
|
# has been authenticated, uninstall may remove every strictly named Zapret2
|
|
# generation even when its interrupted build journal is unavailable. No broad
|
|
# table flush or rule-number deletion is used: only exact module-created jumps
|
|
# and the reserved chain namespace are touched.
|
|
purge_zapret2_namespace() {
|
|
local tool="$1" listing chains chain rest tag suffix parent pass
|
|
listing="$("$tool" -t mangle -S 2>/dev/null)" || return 1
|
|
chains="$(printf '%s\n' "$listing" | awk '
|
|
function owned(name, tag, side, ordinal) {
|
|
if (name == "ZAPRET2_OUT" || name == "ZAPRET2_IN" || name == "ZAPRET2_PROBE") return 1
|
|
if ((index(name,"Z2O_")==1 || index(name,"Z2I_")==1) && length(name)==14) {
|
|
tag=substr(name,5,10)
|
|
return tag !~ /[^A-Za-z0-9]/
|
|
}
|
|
if (index(name,"Z2R_")==1 && length(name)>=17) {
|
|
tag=substr(name,5,10); side=substr(name,16,1); ordinal=substr(name,17)
|
|
return substr(name,15,1)=="_" && tag !~ /[^A-Za-z0-9]/ &&
|
|
(side=="O" || side=="I") && ordinal ~ /^[1-9][0-9]*$/
|
|
}
|
|
return 0
|
|
}
|
|
$1 == "-N" && owned($2) { print $2 }
|
|
')" || return 1
|
|
|
|
for chain in $chains; do
|
|
case "$chain" in
|
|
Z2R_*)
|
|
rest="${chain#Z2R_}"; tag="${rest%%_*}"; suffix="${rest#*_}"
|
|
case "$suffix" in O[1-9]* ) parent="Z2O_$tag" ;; I[1-9]* ) parent="Z2I_$tag" ;; *) return 1 ;; esac
|
|
zapret2_delete_simple_jump_all "$tool" "$parent" "$chain" || return 1
|
|
;;
|
|
Z2O_*|ZAPRET2_OUT)
|
|
zapret2_delete_simple_jump_all "$tool" OUTPUT "$chain" || return 1
|
|
zapret2_delete_simple_jump_all "$tool" FORWARD "$chain" || return 1
|
|
;;
|
|
Z2I_*|ZAPRET2_IN)
|
|
zapret2_delete_simple_jump_all "$tool" INPUT "$chain" || return 1
|
|
zapret2_delete_simple_jump_all "$tool" FORWARD "$chain" || return 1
|
|
;;
|
|
ZAPRET2_PROBE) ;;
|
|
*) return 1 ;;
|
|
esac
|
|
done
|
|
|
|
for chain in $chains; do
|
|
"$tool" -t mangle -S "$chain" >/dev/null 2>&1 || continue
|
|
"$tool" -t mangle -F "$chain" >/dev/null 2>&1 || return 1
|
|
done
|
|
for pass in 1 2; do
|
|
for chain in $chains; do
|
|
case "$pass:$chain" in
|
|
1:Z2R_*|1:ZAPRET2_PROBE|2:Z2O_*|2:Z2I_*|2:ZAPRET2_OUT|2:ZAPRET2_IN) ;;
|
|
*) continue ;;
|
|
esac
|
|
"$tool" -t mangle -S "$chain" >/dev/null 2>&1 || continue
|
|
"$tool" -t mangle -X "$chain" >/dev/null 2>&1 || return 1
|
|
done
|
|
done
|
|
zapret2_namespace_present "$tool"
|
|
case $? in 1) return 0 ;; *) return 1 ;; esac
|
|
}
|
|
|
|
owned_family_absent() {
|
|
z2_fw_family_absent "$1"
|
|
}
|
|
|
|
# Same shape as z2_error_detail_normalize_read: snapshot values carry no
|
|
# control characters on the replace path, so the pipeline runs only when one
|
|
# actually appears.
|
|
status_safe_value_read() {
|
|
case "$1" in
|
|
*[[:cntrl:]]*) STATUS_SAFE_VALUE="$(printf '%s' "$1" | tr '\r\n' ' ')" ;;
|
|
*) STATUS_SAFE_VALUE="$1" ;;
|
|
esac
|
|
}
|
|
|
|
status_safe_value() {
|
|
status_safe_value_read "$1" || return 1
|
|
printf '%s' "$STATUS_SAFE_VALUE"
|
|
}
|
|
|
|
LOG_READY="${LOG_READY:-0}"
|
|
|
|
prepare_lifecycle_log() {
|
|
local log_size
|
|
ensure_state_dir || return 1
|
|
umask 077
|
|
# Refuse symlinks and special files. Removing a hostile path is not
|
|
# necessary for logging and leaves less room for a replacement race.
|
|
state_file_target_is_safe "$LOGFILE" || return 1
|
|
state_file_target_is_safe "$LOGFILE_PREVIOUS" || return 1
|
|
if [ -f "$LOGFILE" ]; then
|
|
log_size="$(wc -c < "$LOGFILE" 2>/dev/null)" || return 1
|
|
is_decimal "$log_size" || return 1
|
|
if [ "$log_size" -ge "$LOG_MAX_BYTES" ] 2>/dev/null; then
|
|
rm -f "$LOGFILE_PREVIOUS" 2>/dev/null || return 1
|
|
mv -f "$LOGFILE" "$LOGFILE_PREVIOUS" 2>/dev/null || return 1
|
|
chmod 0600 "$LOGFILE_PREVIOUS" 2>/dev/null || return 1
|
|
fi
|
|
fi
|
|
: >> "$LOGFILE" || return 1
|
|
path_meta_capture "$LOGFILE"
|
|
state_file_is_secure "$LOGFILE" && path_mode_is_0600 "$LOGFILE"
|
|
local rc=$?
|
|
path_meta_retire
|
|
[ "$rc" -eq 0 ] || return 1
|
|
LOG_READY=1
|
|
return 0
|
|
}
|
|
|
|
append_lifecycle_log() {
|
|
[ "$LOG_READY" = 1 ] || return 0
|
|
z2_emit_line "$1" >> "$LOGFILE" 2>/dev/null
|
|
}
|
|
|
|
# Wall-clock stamp for log lines and snapshots. date is an external; when the
|
|
# shell exposes EPOCHREALTIME, lines landing within the same second reuse one
|
|
# formatted stamp instead of paying one exec each. Shells without it keep the
|
|
# one-date-per-line behavior.
|
|
z2_log_stamp_read() {
|
|
local epoch="${EPOCHREALTIME:-}"
|
|
epoch="${epoch%%.*}"
|
|
if [ -n "$epoch" ] && [ "$epoch" = "${Z2_LOG_STAMP_EPOCH:-}" ] && [ -n "${Z2_LOG_STAMP:-}" ]; then
|
|
return 0
|
|
fi
|
|
Z2_LOG_STAMP="$(date '+%Y-%m-%d %H:%M:%S')"
|
|
Z2_LOG_STAMP_EPOCH="$epoch"
|
|
}
|
|
|
|
STATUS_FILE_STATUS=""
|
|
STATUS_FILE_QNUM=""
|
|
STATUS_FILE_RULES_TOTAL=0
|
|
STATUS_FILE_NFQUEUE_SUPPORTED=0
|
|
STATUS_FILE_QUEUE_BYPASS_SUPPORTED=0
|
|
STATUS_FILE_CONNBYTES_SUPPORTED=0
|
|
STATUS_FILE_MULTIPORT_SUPPORTED=0
|
|
STATUS_FILE_MARK_SUPPORTED=0
|
|
STATUS_FILE_IPV4_ACTIVE=0
|
|
STATUS_FILE_IPV6_ACTIVE=0
|
|
STATUS_FILE_IPV4_RULES=0
|
|
STATUS_FILE_IPV6_RULES=0
|
|
STATUS_FILE_CHAINS=0
|
|
STATUS_FILE_ANCHORS=0
|
|
STATUS_FILE_RULESET_VERIFIED=0
|
|
STATUS_FILE_OWNER_METADATA_VERIFIED=0
|
|
STATUS_FILE_RULES_EXPECTED=0
|
|
STATUS_FILE_OWN_PID=""
|
|
STATUS_FILE_OWN_PID_STARTTIME=""
|
|
STATUS_FILE_OWN_ARGV_SHA256=""
|
|
STATUS_FILE_OWNER_GENERATION=""
|
|
STATUS_FILE_DIAGNOSTICS=""
|
|
STATUS_FILE_ERROR_SCHEMA=0
|
|
STATUS_FILE_ERROR_STATUS=OK
|
|
STATUS_FILE_ERROR_DOMAIN=NONE
|
|
STATUS_FILE_ERROR_CODE=NONE
|
|
STATUS_FILE_ERROR_STAGE=NONE
|
|
STATUS_FILE_ERROR_DETAIL=""
|
|
|
|
# A rejected snapshot must leave nothing behind: consumers read STATUS_FILE_*
|
|
# whether or not they check the return code, so partially parsed facts from a
|
|
# file this function refused would be indistinguishable from accepted ones.
|
|
read_iptables_status() {
|
|
read_iptables_status_parse "$@" && return 0
|
|
reset_status_file_facts
|
|
return 1
|
|
}
|
|
|
|
reset_status_file_facts() {
|
|
STATUS_FILE_STATUS=""; STATUS_FILE_QNUM=""; STATUS_FILE_RULES_TOTAL=0
|
|
STATUS_FILE_NFQUEUE_SUPPORTED=0; STATUS_FILE_QUEUE_BYPASS_SUPPORTED=0
|
|
STATUS_FILE_CONNBYTES_SUPPORTED=0; STATUS_FILE_MULTIPORT_SUPPORTED=0
|
|
STATUS_FILE_MARK_SUPPORTED=0; STATUS_FILE_IPV4_ACTIVE=0; STATUS_FILE_IPV6_ACTIVE=0
|
|
STATUS_FILE_IPV4_RULES=0; STATUS_FILE_IPV6_RULES=0
|
|
STATUS_FILE_CHAINS=0; STATUS_FILE_ANCHORS=0; STATUS_FILE_RULESET_VERIFIED=0
|
|
STATUS_FILE_OWNER_METADATA_VERIFIED=0; STATUS_FILE_RULES_EXPECTED=0; STATUS_FILE_DIAGNOSTICS=""
|
|
STATUS_FILE_OWN_PID=""; STATUS_FILE_OWN_PID_STARTTIME=""
|
|
STATUS_FILE_OWN_ARGV_SHA256=""; STATUS_FILE_OWNER_GENERATION=""
|
|
STATUS_FILE_ERROR_SCHEMA=0; STATUS_FILE_ERROR_STATUS=OK
|
|
STATUS_FILE_ERROR_DOMAIN=NONE; STATUS_FILE_ERROR_CODE=NONE
|
|
STATUS_FILE_ERROR_STAGE=NONE; STATUS_FILE_ERROR_DETAIL=""
|
|
STATUS_FILE_BOOT_ID=""
|
|
}
|
|
|
|
read_iptables_status_parse() {
|
|
local path="${1:-$IPTABLES_STATUS}"
|
|
STATUS_FILE_STATUS=""; STATUS_FILE_QNUM=""; STATUS_FILE_RULES_TOTAL=0
|
|
STATUS_FILE_NFQUEUE_SUPPORTED=0; STATUS_FILE_QUEUE_BYPASS_SUPPORTED=0
|
|
STATUS_FILE_CONNBYTES_SUPPORTED=0; STATUS_FILE_MULTIPORT_SUPPORTED=0
|
|
STATUS_FILE_MARK_SUPPORTED=0; STATUS_FILE_IPV4_ACTIVE=0; STATUS_FILE_IPV6_ACTIVE=0
|
|
STATUS_FILE_IPV4_RULES=0; STATUS_FILE_IPV6_RULES=0
|
|
STATUS_FILE_CHAINS=0; STATUS_FILE_ANCHORS=0; STATUS_FILE_RULESET_VERIFIED=0
|
|
STATUS_FILE_OWNER_METADATA_VERIFIED=0; STATUS_FILE_RULES_EXPECTED=0; STATUS_FILE_DIAGNOSTICS=""
|
|
STATUS_FILE_OWN_PID=""; STATUS_FILE_OWN_PID_STARTTIME=""
|
|
STATUS_FILE_OWN_ARGV_SHA256=""; STATUS_FILE_OWNER_GENERATION=""
|
|
STATUS_FILE_ERROR_SCHEMA=0; STATUS_FILE_ERROR_STATUS=OK
|
|
STATUS_FILE_ERROR_DOMAIN=NONE; STATUS_FILE_ERROR_CODE=NONE
|
|
STATUS_FILE_ERROR_STAGE=NONE; STATUS_FILE_ERROR_DETAIL=""
|
|
STATUS_FILE_BOOT_ID=""
|
|
[ "$path" = "$IPTABLES_STATUS" ] || return 1
|
|
if [ "${OBSERVER_STATE_DIR_VERIFIED:-0}" = 1 ]; then
|
|
observer_state_file_is_secure "$path" && [ -r "$path" ] || return 1
|
|
else
|
|
state_file_is_secure "$path" && [ -r "$path" ] || return 1
|
|
fi
|
|
local key value
|
|
while IFS='=' read -r key value; do
|
|
case "$key" in
|
|
status) STATUS_FILE_STATUS="$value" ;;
|
|
qnum) STATUS_FILE_QNUM="$value" ;;
|
|
rules_total|total) STATUS_FILE_RULES_TOTAL="$value" ;;
|
|
nfqueue_supported) STATUS_FILE_NFQUEUE_SUPPORTED="$value" ;;
|
|
queue_bypass_supported) STATUS_FILE_QUEUE_BYPASS_SUPPORTED="$value" ;;
|
|
connbytes_supported) STATUS_FILE_CONNBYTES_SUPPORTED="$value" ;;
|
|
multiport_supported) STATUS_FILE_MULTIPORT_SUPPORTED="$value" ;;
|
|
mark_supported) STATUS_FILE_MARK_SUPPORTED="$value" ;;
|
|
ipv4_active) STATUS_FILE_IPV4_ACTIVE="$value" ;;
|
|
ipv6_active) STATUS_FILE_IPV6_ACTIVE="$value" ;;
|
|
ipv4_rules) STATUS_FILE_IPV4_RULES="$value" ;;
|
|
ipv6_rules) STATUS_FILE_IPV6_RULES="$value" ;;
|
|
chains) STATUS_FILE_CHAINS="$value" ;;
|
|
anchors) STATUS_FILE_ANCHORS="$value" ;;
|
|
ruleset_verified) STATUS_FILE_RULESET_VERIFIED="$value" ;;
|
|
owner_metadata_verified) STATUS_FILE_OWNER_METADATA_VERIFIED="$value" ;;
|
|
rules_expected) STATUS_FILE_RULES_EXPECTED="$value" ;;
|
|
own_pid) STATUS_FILE_OWN_PID="$value" ;;
|
|
own_pid_starttime) STATUS_FILE_OWN_PID_STARTTIME="$value" ;;
|
|
own_argv_sha256) STATUS_FILE_OWN_ARGV_SHA256="$value" ;;
|
|
owner_generation) STATUS_FILE_OWNER_GENERATION="$value" ;;
|
|
boot_id) STATUS_FILE_BOOT_ID="$value" ;;
|
|
diagnostics) STATUS_FILE_DIAGNOSTICS="$value" ;;
|
|
error_schema) STATUS_FILE_ERROR_SCHEMA="$value" ;;
|
|
error_status) STATUS_FILE_ERROR_STATUS="$value" ;;
|
|
error_domain) STATUS_FILE_ERROR_DOMAIN="$value" ;;
|
|
error_code) STATUS_FILE_ERROR_CODE="$value" ;;
|
|
error_stage) STATUS_FILE_ERROR_STAGE="$value" ;;
|
|
error_detail) STATUS_FILE_ERROR_DETAIL="$value" ;;
|
|
esac
|
|
done < "$path"
|
|
# The snapshot describes processes and netfilter objects that a reboot
|
|
# destroys, so one from an earlier boot is not stale data to reconcile —
|
|
# it describes nothing that exists. Reject it here and every consumer is
|
|
# correct without needing a separate retirement pass.
|
|
read_current_boot_id || return 1
|
|
[ "$STATUS_FILE_BOOT_ID" = "$CURRENT_BOOT_ID" ] || return 1
|
|
normalize_qnum "$STATUS_FILE_QNUM" && STATUS_FILE_QNUM="$QNUM_NORMALIZED" || STATUS_FILE_QNUM=""
|
|
for value in "$STATUS_FILE_RULES_TOTAL" "$STATUS_FILE_IPV4_RULES" \
|
|
"$STATUS_FILE_IPV6_RULES" "$STATUS_FILE_RULES_EXPECTED" \
|
|
"$STATUS_FILE_CHAINS" "$STATUS_FILE_ANCHORS"; do
|
|
is_decimal "$value" || return 1
|
|
done
|
|
for value in "$STATUS_FILE_NFQUEUE_SUPPORTED" "$STATUS_FILE_QUEUE_BYPASS_SUPPORTED" \
|
|
"$STATUS_FILE_CONNBYTES_SUPPORTED" "$STATUS_FILE_MULTIPORT_SUPPORTED" \
|
|
"$STATUS_FILE_MARK_SUPPORTED" "$STATUS_FILE_IPV4_ACTIVE" "$STATUS_FILE_IPV6_ACTIVE" \
|
|
"$STATUS_FILE_RULESET_VERIFIED" "$STATUS_FILE_OWNER_METADATA_VERIFIED"; do
|
|
case "$value" in 0|1) ;; *) return 1 ;; esac
|
|
done
|
|
if [ -n "$STATUS_FILE_OWN_PID" ] || [ -n "$STATUS_FILE_OWN_PID_STARTTIME" ] ||
|
|
[ -n "$STATUS_FILE_OWN_ARGV_SHA256" ] || [ -n "$STATUS_FILE_OWNER_GENERATION" ]; then
|
|
is_decimal "$STATUS_FILE_OWN_PID" &&
|
|
is_decimal "$STATUS_FILE_OWN_PID_STARTTIME" &&
|
|
is_safe_token "$STATUS_FILE_OWNER_GENERATION" || return 1
|
|
[ -z "$STATUS_FILE_OWN_ARGV_SHA256" ] ||
|
|
is_lower_sha256 "$STATUS_FILE_OWN_ARGV_SHA256" || return 1
|
|
fi
|
|
if [ "$STATUS_FILE_ERROR_SCHEMA" = "$Z2_ERROR_SCHEMA_VERSION" ] &&
|
|
z2_error_fields_are_valid "$STATUS_FILE_ERROR_STATUS" "$STATUS_FILE_ERROR_DOMAIN" \
|
|
"$STATUS_FILE_ERROR_STAGE" "$STATUS_FILE_ERROR_CODE" "$STATUS_FILE_ERROR_DETAIL"; then
|
|
:
|
|
elif [ "$STATUS_FILE_ERROR_SCHEMA" = 0 ]; then
|
|
STATUS_FILE_ERROR_STATUS=OK
|
|
STATUS_FILE_ERROR_DOMAIN=NONE; STATUS_FILE_ERROR_CODE=NONE
|
|
STATUS_FILE_ERROR_STAGE=NONE; STATUS_FILE_ERROR_DETAIL=""
|
|
else
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
restore_status_facts() {
|
|
read_iptables_status >/dev/null 2>&1 || true
|
|
[ -n "${STATUS_QNUM:-}" ] || STATUS_QNUM="${QNUM:-$STATUS_FILE_QNUM}"
|
|
[ -n "${STATUS_QNUM:-}" ] || { read_owner_state >/dev/null 2>&1 && STATUS_QNUM="$OWNER_STATE_QNUM"; }
|
|
STATUS_NFQUEUE_SUPPORTED="${STATUS_NFQUEUE_SUPPORTED:-$STATUS_FILE_NFQUEUE_SUPPORTED}"
|
|
STATUS_QUEUE_BYPASS_SUPPORTED="${STATUS_QUEUE_BYPASS_SUPPORTED:-$STATUS_FILE_QUEUE_BYPASS_SUPPORTED}"
|
|
STATUS_CONNBYTES_SUPPORTED="${STATUS_CONNBYTES_SUPPORTED:-$STATUS_FILE_CONNBYTES_SUPPORTED}"
|
|
STATUS_MULTIPORT_SUPPORTED="${STATUS_MULTIPORT_SUPPORTED:-$STATUS_FILE_MULTIPORT_SUPPORTED}"
|
|
STATUS_MARK_SUPPORTED="${STATUS_MARK_SUPPORTED:-$STATUS_FILE_MARK_SUPPORTED}"
|
|
}
|
|
|
|
write_iptables_status() {
|
|
local state="$1" tmp="$IPTABLES_STATUS.tmp.$$" errors diagnostics
|
|
local error_status="${STATUS_ERROR_STATUS:-OK}"
|
|
local error_domain="${STATUS_ERROR_DOMAIN:-NONE}" error_code="${STATUS_ERROR_CODE:-NONE}"
|
|
local error_stage="${STATUS_ERROR_STAGE:-NONE}" error_detail
|
|
status_safe_value_read "${STATUS_ERRORS:-}"
|
|
errors="$STATUS_SAFE_VALUE"
|
|
status_safe_value_read "${STATUS_DIAGNOSTICS:-}"
|
|
diagnostics="$STATUS_SAFE_VALUE"
|
|
z2_error_detail_normalize_read "${STATUS_ERROR_DETAIL:-}"
|
|
error_detail="$Z2_ERROR_DETAIL_NORMALIZED"
|
|
z2_error_fields_are_valid "$error_status" "$error_domain" "$error_stage" "$error_code" \
|
|
"$error_detail" ||
|
|
return 1
|
|
ensure_state_dir || return 1
|
|
read_current_boot_id || return 1
|
|
state_file_target_is_safe "$IPTABLES_STATUS" || return 1
|
|
[ ! -e "$tmp" ] && [ ! -L "$tmp" ] || return 1
|
|
umask 077
|
|
{
|
|
echo "status=$state"
|
|
echo "boot_id=$CURRENT_BOOT_ID"
|
|
z2_log_stamp_read
|
|
echo "timestamp=$Z2_LOG_STAMP"
|
|
echo "rules_ok=${STATUS_RULES_OK:-0}"
|
|
echo "rules_fail=${STATUS_RULES_FAIL:-0}"
|
|
echo "rules_total=${STATUS_RULES_TOTAL:-0}"
|
|
echo "ok=${STATUS_RULES_OK:-0}"
|
|
echo "fail=${STATUS_RULES_FAIL:-0}"
|
|
echo "total=${STATUS_RULES_TOTAL:-0}"
|
|
echo "errors=$errors"
|
|
echo "own_pid=${STATUS_OWN_PID:-}"
|
|
echo "own_pid_starttime=${STATUS_OWN_PID_STARTTIME:-}"
|
|
echo "own_argv_sha256=${STATUS_OWN_ARGV_SHA256:-}"
|
|
echo "owner_generation=${STATUS_OWNER_GENERATION:-}"
|
|
echo "pid_verified=${STATUS_PID_VERIFIED:-0}"
|
|
echo "owner_metadata_verified=${STATUS_OWNER_METADATA_VERIFIED:-0}"
|
|
echo "ruleset_verified=${STATUS_RULESET_VERIFIED:-0}"
|
|
echo "rules_expected=${STATUS_RULES_EXPECTED:-0}"
|
|
echo "qnum=${STATUS_QNUM:-${QNUM:-}}"
|
|
echo "ipv4_active=${STATUS_IPV4_ACTIVE:-0}"
|
|
echo "ipv6_active=${STATUS_IPV6_ACTIVE:-0}"
|
|
echo "ipv4_rules=${STATUS_IPV4_RULES:-0}"
|
|
echo "ipv6_rules=${STATUS_IPV6_RULES:-0}"
|
|
echo "chains=${STATUS_CHAINS:-0}"
|
|
echo "anchors=${STATUS_ANCHORS:-0}"
|
|
echo "nfqueue_supported=${STATUS_NFQUEUE_SUPPORTED:-0}"
|
|
echo "queue_bypass_supported=${STATUS_QUEUE_BYPASS_SUPPORTED:-0}"
|
|
echo "connbytes_supported=${STATUS_CONNBYTES_SUPPORTED:-0}"
|
|
echo "multiport_supported=${STATUS_MULTIPORT_SUPPORTED:-0}"
|
|
echo "mark_supported=${STATUS_MARK_SUPPORTED:-0}"
|
|
echo "fallback_mode=${STATUS_FALLBACK_MODE:-0}"
|
|
z2_emit_line "error_schema=$Z2_ERROR_SCHEMA_VERSION
|
|
error_status=$error_status
|
|
error_domain=$error_domain
|
|
error_code=$error_code
|
|
error_stage=$error_stage
|
|
error_detail=$error_detail"
|
|
echo "diagnostics=$diagnostics"
|
|
} > "$tmp" || { rm -f "$tmp"; return 1; }
|
|
mv -f "$tmp" "$IPTABLES_STATUS" || { rm -f "$tmp"; return 1; }
|
|
}
|
|
|
|
# Lifecycle mutations already own the expensive process/firewall verification
|
|
# that produces STATUS_*. When explicitly requested by a compatible app, return
|
|
# that committed v6 projection on the same root transport instead of making the
|
|
# app launch zapret-status.sh and re-parse the snapshot immediately afterward.
|
|
emit_committed_status_v6() {
|
|
local state="$1" lifecycle_state="$2" owner_kind="$3"
|
|
local owned process active pid pid_verified pid_start generation owner_verified
|
|
local ipv4 ipv6 rules expected ipv4_rules ipv6_rules ruleset nfqueue queue_bypass
|
|
[ "${ZAPRET2_EMIT_STATUS_V6:-0}" = 1 ] || return 0
|
|
case "$lifecycle_state:$owner_kind" in
|
|
idle:none|owned:android-mutation) ;;
|
|
*) return 1 ;;
|
|
esac
|
|
[ ! -e "$UNINSTALL_TOMBSTONE" ] && [ ! -L "$UNINSTALL_TOMBSTONE" ] ||
|
|
return 1
|
|
module_removal_pending && return 1
|
|
case "$state" in
|
|
ok)
|
|
owned=1; process=1; active=1
|
|
pid="${STATUS_OWN_PID:-}"; pid_verified=1
|
|
pid_start="${STATUS_OWN_PID_STARTTIME:-}"
|
|
generation="${STATUS_OWNER_GENERATION:-}"; owner_verified=1
|
|
ipv4="${STATUS_IPV4_ACTIVE:-0}"; ipv6="${STATUS_IPV6_ACTIVE:-0}"
|
|
rules="${STATUS_RULES_TOTAL:-0}"; expected="${STATUS_RULES_EXPECTED:-0}"
|
|
ipv4_rules="${STATUS_IPV4_RULES:-0}"; ipv6_rules="${STATUS_IPV6_RULES:-0}"
|
|
ruleset=1; nfqueue=1; queue_bypass=1
|
|
;;
|
|
stopped)
|
|
owned=0; process=0; active=0
|
|
pid=""; pid_verified=0; pid_start=""; generation=""; owner_verified=0
|
|
ipv4=0; ipv6=0; rules=0; expected=0; ipv4_rules=0; ipv6_rules=0
|
|
# A stopped receipt reports what this teardown actually proved.
|
|
# Everything measurable is zero either way; the single thing a
|
|
# teardown that had to skip an unqueryable family cannot do is
|
|
# certify the ruleset. Carrying that on the receipt is what lets
|
|
# the operation report its own reservation, instead of staying
|
|
# silent and leaving the caller to infer it from a second,
|
|
# separately-raced observation. Unset defaults to withholding the
|
|
# claim: asserting a verification nobody recorded is the one
|
|
# direction this field must never fail in.
|
|
ruleset="${STATUS_RULESET_VERIFIED:-0}"; nfqueue=0; queue_bypass=0
|
|
;;
|
|
*) return 1 ;;
|
|
esac
|
|
cat <<EOF
|
|
Z2_PROTOCOL=6
|
|
Z2_STATUS=$state
|
|
Z2_OWNED=$owned
|
|
Z2_PROCESS=$process
|
|
Z2_ACTIVE=$active
|
|
Z2_PID=$pid
|
|
Z2_PID_VERIFIED=$pid_verified
|
|
Z2_PID_STARTTIME=$pid_start
|
|
Z2_OWNER_GENERATION=$generation
|
|
Z2_OWNER_METADATA_VERIFIED=$owner_verified
|
|
Z2_QNUM=${STATUS_QNUM:-${QNUM:-}}
|
|
Z2_IPV4=$ipv4
|
|
Z2_IPV6=$ipv6
|
|
Z2_RULES=$rules
|
|
Z2_EXPECTED_RULES=$expected
|
|
Z2_IPV4_RULES=$ipv4_rules
|
|
Z2_IPV6_RULES=$ipv6_rules
|
|
Z2_RULESET_VERIFIED=$ruleset
|
|
Z2_NFQUEUE=$nfqueue
|
|
Z2_QUEUE_BYPASS=$queue_bypass
|
|
Z2_UPDATE_BLOCKED=0
|
|
Z2_UNINSTALL_TOMBSTONE=0
|
|
Z2_LIFECYCLE_STATE=$lifecycle_state
|
|
Z2_LIFECYCLE_OWNER_KIND=$owner_kind
|
|
Z2_CHAINS=${STATUS_CHAINS:-0}
|
|
Z2_ANCHORS=${STATUS_ANCHORS:-0}
|
|
Z2_ERROR_SCHEMA=$Z2_ERROR_SCHEMA_VERSION
|
|
Z2_ERROR_STATUS=OK
|
|
Z2_ERROR_DOMAIN=NONE
|
|
Z2_ERROR_STAGE=NONE
|
|
Z2_ERROR_CODE=NONE
|
|
Z2_ERROR_DETAIL=
|
|
Z2_COMPLETE=1
|
|
EOF
|
|
}
|