magisk-zapret2/zapret2/scripts/runtime-config.sh
loop-uh e14c46afb1 Publish the DNS hosts file from /data instead of the module tree
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.
2026-07-28 00:56:30 +03:00

308 lines
12 KiB
Shell
Executable file

#!/system/bin/sh
# Create or repair the v1 preset runtime from built-in defaults. This is not
# migration: no bootstrap input is read. Repair preserves non-core section
# content so all callers share one recovery contract.
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd -P)"
ZAPRET_DIR="$(dirname "$SCRIPT_DIR")"
# Sourced before any argument parsing on purpose: common.sh may re-exec this
# script under the standalone-ash interpreter with the original "$@", so the
# argument vector must still be intact when that happens.
ZAPRET2_LAZY_FIREWALL_RECONCILER=1
. "$SCRIPT_DIR/common.sh"
runtime_canonical_sha256() {
awk '
{
sub(/\r$/, "")
lines[NR]=$0
}
END {
last=NR
while (last > 0 && lines[last] == "") last--
for (line_number=1; line_number <= last; line_number++) {
print lines[line_number]
}
}
' "$1" | sha256sum
}
mode=init
candidate_path=""
expected_digest=""
case "${1:-}" in
--inspect-machine) mode=inspect-machine; shift ;;
--commit-candidate)
mode=commit-candidate
shift
candidate_path="${1:-}"
[ "$#" -gt 0 ] && shift
expected_digest="${1:-}"
[ "$#" -gt 0 ] && shift
;;
--repair) mode=repair; shift ;;
esac
output_path="${1:-$ZAPRET_DIR/runtime.ini}"
output_dir="$(dirname "$output_path")"
[ "$#" -le 1 ] || {
echo "ERROR: usage: runtime-config.sh [--inspect-machine|--repair] [runtime.ini]" >&2
echo " runtime-config.sh --commit-candidate CANDIDATE EXPECTED_SHA256 [runtime.ini]" >&2
exit 2
}
[ -d "$output_dir" ] && [ ! -L "$output_dir" ] || {
echo "ERROR: runtime directory is missing or unsafe" >&2; exit 1;
}
case "$output_path" in "$output_dir/runtime.ini") ;; *) echo "ERROR: unsafe runtime path" >&2; exit 1 ;; esac
if [ "$mode" = commit-candidate ]; then
commit_error() {
z2_error_set CONFIG "$1" RUNTIME_COMMIT "$2" ||
z2_error_set CONFIG RUNTIME_COMMIT_FAILED RUNTIME_COMMIT \
"runtime.ini candidate commit failed"
z2_error_emit_machine
exit 1
}
case "$candidate_path" in
"$output_path.candidate."*) ;;
*) commit_error UNSAFE_RUNTIME_CANDIDATE "runtime candidate path is unsafe" ;;
esac
candidate_suffix="${candidate_path#"$output_path.candidate."}"
candidate_pid="${candidate_suffix%%.*}"
candidate_nonce="${candidate_suffix#*.}"
[ "$candidate_nonce" != "$candidate_suffix" ] &&
[ "${candidate_nonce#*.}" = "$candidate_nonce" ] &&
is_decimal "$candidate_pid" && is_decimal "$candidate_nonce" ||
commit_error UNSAFE_RUNTIME_CANDIDATE "runtime candidate identity is unsafe"
for stale_candidate in "$output_path.candidate."*; do
[ -e "$stale_candidate" ] || [ -L "$stale_candidate" ] || continue
[ "$stale_candidate" != "$candidate_path" ] || continue
stale_suffix="${stale_candidate#"$output_path.candidate."}"
stale_pid="${stale_suffix%%.*}"
stale_nonce="${stale_suffix#*.}"
[ "$stale_nonce" != "$stale_suffix" ] &&
[ "${stale_nonce#*.}" = "$stale_nonce" ] &&
is_decimal "$stale_pid" && is_decimal "$stale_nonce" &&
[ -f "$stale_candidate" ] && [ ! -L "$stale_candidate" ] &&
path_uid_is_root "$stale_candidate" &&
path_nlink_is_one "$stale_candidate" ||
commit_error UNSAFE_RUNTIME_CANDIDATE \
"stale runtime candidate is unsafe"
rm -f "$stale_candidate" ||
commit_error RUNTIME_COMMIT_FAILED \
"stale runtime candidate cleanup failed"
done
trap 'rm -f "$candidate_path" 2>/dev/null' EXIT HUP INT TERM
[ "${#expected_digest}" -eq 64 ] &&
case "$expected_digest" in *[!0-9a-f]*) false ;; *) true ;; esac ||
commit_error INVALID_RUNTIME_IDENTITY "expected runtime identity is invalid"
[ -f "$candidate_path" ] && [ ! -L "$candidate_path" ] && [ -r "$candidate_path" ] ||
commit_error UNSAFE_RUNTIME_CANDIDATE \
"runtime candidate is not a readable regular file"
candidate_meta="$(stat -c '%u:%a:%h:%s' "$candidate_path" 2>/dev/null)" ||
commit_error RUNTIME_METADATA_UNAVAILABLE "runtime candidate metadata is unavailable"
old_ifs=$IFS
IFS=:
set -- $candidate_meta
IFS=$old_ifs
[ "$#" -eq 4 ] && [ "$1" = 0 ] && [ "$3" = 1 ] ||
commit_error UNSAFE_RUNTIME_CANDIDATE \
"runtime candidate ownership is unsafe"
case "$2" in 600|644) ;; *)
commit_error UNSAFE_RUNTIME_CANDIDATE "runtime candidate mode is unsafe"
;;
esac
case "$4" in ''|*[!0-9]*)
commit_error UNSAFE_RUNTIME_CANDIDATE "runtime candidate size is invalid"
;;
esac
[ "$4" -gt 0 ] 2>/dev/null && [ "$4" -le 262144 ] 2>/dev/null ||
commit_error UNSAFE_RUNTIME_CANDIDATE \
"runtime candidate size is outside the allowed envelope"
RUNTIME_CONFIG="$candidate_path"
set_core_config_defaults
RUNTIME_CONFIG_ERROR=""
apply_runtime_core_overrides ||
commit_error CONFIG_INVALID "${RUNTIME_CONFIG_ERROR:-runtime candidate is invalid}"
[ -f "$output_path" ] && [ ! -L "$output_path" ] &&
path_uid_is_root "$output_path" && path_nlink_is_one "$output_path" ||
commit_error UNSAFE_RUNTIME_FILE "runtime.ini target is unsafe"
target_meta_before="$(stat -c '%d:%i:%u:%a:%h:%s' "$output_path" 2>/dev/null)" ||
commit_error RUNTIME_METADATA_UNAVAILABLE "runtime.ini metadata is unavailable"
case "$target_meta_before" in *":0:600:1:"*|*":0:644:1:"*) ;; *)
commit_error UNSAFE_RUNTIME_FILE "runtime.ini target metadata is unsafe"
;;
esac
target_digest_before="$(runtime_canonical_sha256 "$output_path" 2>/dev/null)"
target_digest_before="${target_digest_before%% *}"
[ "$target_digest_before" = "$expected_digest" ] ||
commit_error RUNTIME_SOURCE_CHANGED \
"runtime.ini changed since the edit began"
chmod 0644 "$candidate_path" 2>/dev/null ||
commit_error RUNTIME_COMMIT_FAILED "runtime candidate chmod failed"
target_meta_after="$(stat -c '%d:%i:%u:%a:%h:%s' "$output_path" 2>/dev/null)"
target_digest_after="$(runtime_canonical_sha256 "$output_path" 2>/dev/null)"
target_digest_after="${target_digest_after%% *}"
[ "$target_meta_after" = "$target_meta_before" ] &&
[ "$target_digest_after" = "$expected_digest" ] ||
commit_error RUNTIME_SOURCE_CHANGED \
"runtime.ini changed during candidate validation"
mv -f "$candidate_path" "$output_path" ||
commit_error RUNTIME_COMMIT_FAILED "runtime candidate publication failed"
sync || commit_error RUNTIME_COMMIT_FAILED "runtime candidate sync failed"
trap - EXIT HUP INT TERM
z2_error_clear
z2_error_emit_machine
exit 0
fi
if [ "$mode" = inspect-machine ]; then
# Reuse the exact lifecycle parser. This entry point is read-only. The
# envelope fields are opaque to the APK; only their shape and bounds are
# stable.
RUNTIME_CONFIG="$output_path"
if ! runtime_config_exists; then
if [ ! -e "$RUNTIME_CONFIG" ] && [ ! -L "$RUNTIME_CONFIG" ]; then
z2_error_set CONFIG RUNTIME_MISSING RUNTIME_OPEN \
"runtime.ini is missing"
else
z2_error_set CONFIG UNSAFE_RUNTIME_FILE RUNTIME_OPEN \
"runtime.ini is not a safe root-owned regular file"
fi
z2_error_emit_machine
exit 0
fi
runtime_meta_before="$(stat -c '%d:%i:%u:%a:%h:%s' "$RUNTIME_CONFIG" 2>/dev/null)" || {
z2_error_set CONFIG RUNTIME_METADATA_UNAVAILABLE RUNTIME_OPEN \
"runtime.ini metadata is unavailable"
z2_error_emit_machine
exit 0
}
runtime_digest_before="$(runtime_canonical_sha256 "$RUNTIME_CONFIG" 2>/dev/null)" || {
z2_error_set CONFIG RUNTIME_READ_FAILED RUNTIME_OPEN \
"runtime.ini cannot be hashed"
z2_error_emit_machine
exit 0
}
runtime_digest_before="${runtime_digest_before%% *}"
set_core_config_defaults
RUNTIME_CONFIG_ERROR=""
if ! apply_runtime_core_overrides; then
runtime_config_error_code "$RUNTIME_CONFIG_ERROR"
z2_error_set CONFIG "$RUNTIME_CONFIG_ERROR_CODE" RUNTIME_PARSE \
"$RUNTIME_CONFIG_ERROR" ||
z2_error_set CONFIG CONFIG_INVALID RUNTIME_PARSE \
"runtime.ini validation failed"
z2_error_emit_machine
exit 0
fi
runtime_meta_after="$(stat -c '%d:%i:%u:%a:%h:%s' "$RUNTIME_CONFIG" 2>/dev/null)"
runtime_digest_after="$(runtime_canonical_sha256 "$RUNTIME_CONFIG" 2>/dev/null)"
runtime_digest_after="${runtime_digest_after%% *}"
if [ -z "$runtime_meta_after" ] || [ "$runtime_meta_after" != "$runtime_meta_before" ] ||
[ "$runtime_digest_after" != "$runtime_digest_before" ]; then
z2_error_set CONFIG RUNTIME_CHANGED RUNTIME_READ \
"runtime.ini changed while it was being inspected"
z2_error_emit_machine
exit 0
fi
z2_error_clear
z2_error_emit_machine
printf 'Z2_RUNTIME_SHA256\t%s\n' "$runtime_digest_after"
printf 'Z2_RUNTIME_CORE\tschema_version\t1\n'
printf 'Z2_RUNTIME_CORE\tconfig_format\truntime-v1\n'
printf 'Z2_RUNTIME_CORE\truntime_source\t%s\n' "$RUNTIME_SOURCE"
printf 'Z2_RUNTIME_CORE\tautostart\t%s\n' "$AUTOSTART"
printf 'Z2_RUNTIME_CORE\twifi_only\t%s\n' "$WIFI_ONLY"
printf 'Z2_RUNTIME_CORE\ttethering\t%s\n' "$TETHERING"
printf 'Z2_RUNTIME_CORE\tdebug\t%s\n' "$DEBUG"
printf 'Z2_RUNTIME_CORE\tqnum\t%s\n' "$QNUM"
printf 'Z2_RUNTIME_CORE\tdesync_mark\t%s\n' "$DESYNC_MARK"
printf 'Z2_RUNTIME_CORE\tactive_preset\t%s\n' "$ACTIVE_PRESET"
printf 'Z2_RUNTIME_CORE\tnfqws_uid\t%s\n' "$NFQWS_UID"
printf 'Z2_RUNTIME_CORE\tlog_mode\t%s\n' "$LOG_MODE"
exit 0
fi
if [ -e "$output_path" ] || [ -L "$output_path" ]; then
[ "$mode" = repair ] && [ -f "$output_path" ] && [ ! -L "$output_path" ] &&
[ -r "$output_path" ] || {
echo "ERROR: unsafe runtime target" >&2; exit 1;
}
runtime_meta="$(stat -c '%u:%a:%h:%s' "$output_path" 2>/dev/null)" || {
echo "ERROR: runtime metadata is unavailable" >&2; exit 1;
}
old_ifs=$IFS
IFS=:
set -- $runtime_meta
IFS=$old_ifs
[ "$#" -eq 4 ] && [ "$1" = 0 ] && [ "$3" = 1 ] || {
echo "ERROR: unsafe runtime ownership" >&2; exit 1;
}
case "$2" in 600|644) ;; *) echo "ERROR: unsafe runtime mode" >&2; exit 1 ;; esac
case "$4" in ''|*[!0-9]*) echo "ERROR: invalid runtime size" >&2; exit 1 ;; esac
[ "$4" -gt 0 ] 2>/dev/null && [ "$4" -le 262144 ] 2>/dev/null || {
echo "ERROR: runtime size is outside the repair envelope" >&2; exit 1;
}
fi
tmp="$output_path.tmp.$$"
preserved="$output_path.sections.$$"
[ ! -e "$tmp" ] && [ ! -L "$tmp" ] && [ ! -e "$preserved" ] && [ ! -L "$preserved" ] || exit 1
trap 'rm -f "$tmp" "$preserved" 2>/dev/null' EXIT HUP INT TERM
umask 077
if [ "$mode" = repair ] && [ -f "$output_path" ]; then
awk '
BEGIN { in_core=0; keep=0 }
/^[[:space:]]*\[[^]]+\][[:space:]]*$/ {
section=$0
gsub(/^[[:space:]]*\[/, "", section)
gsub(/\][[:space:]]*$/, "", section)
normalized=tolower(section)
if (normalized == "core") { in_core=1; keep=0; next }
in_core=0
keep=1
}
keep && !in_core { print }
' "$output_path" > "$preserved" || exit 1
else
: > "$preserved" || exit 1
fi
{
echo '# Zapret2 preset runtime'
echo '[core]'
echo 'schema_version=1'
echo 'config_format=runtime-v1'
if [ "$mode" = repair ]; then
echo 'runtime_source=self-healed-runtime'
else
echo 'runtime_source=builtin-defaults'
fi
echo 'autostart=1'
echo 'wifi_only=0'
echo 'tethering=0'
echo 'debug=0'
echo 'qnum=200'
echo 'desync_mark=0x40000000'
echo 'active_preset=Default v1 (game filter).txt'
echo 'nfqws_uid=0:0'
echo 'log_mode=none'
if [ -s "$preserved" ]; then
echo
cat "$preserved"
fi
} > "$tmp" || exit 1
rm -f "$preserved" || exit 1
chmod 0644 "$tmp" 2>/dev/null || exit 1
mv -f "$tmp" "$output_path" || exit 1
tmp=
preserved=
trap - EXIT HUP INT TERM
if [ "$mode" = repair ]; then
echo "Repaired runtime config: $output_path"
else
echo "Initialized runtime config: $output_path"
fi