mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:27:02 +01:00
Harden phone_focus_mode CPU use; whitelist wake_alarm_sync
The on-device focus enforcer was overheating the phone by rebuilding firewall rules every tick. State-gate the work instead: - dns_enforcer.sh: only rebuild the DNS chain when it's missing, detached from OUTPUT, or the wrong rule count (chain_intact/expected_rule_count), instead of flushing+reinserting ~50-100 rules every tick (~50% netd CPU). - config.sh: throttle CURFEW_NET_REASSERT_INTERVAL 1s -> 15s (1s forked iptables every second all night, overheating); also whitelist com.kuhy.wake_alarm_sync (general + curfew-night lists). - focus_daemon.sh: build the blocked-system-apps file once at startup and memoize is_allowed() per tick. - deploy.sh: drop `su --mount-master` (removed in Magisk v26+). - SKILL.md: document the state-change-gate polling pattern. Verification: shellcheck clean; no-polling-antipatterns + evidence/contract hooks pass. Evidence + contract artifacts included. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RRCdR4D8SaVrN2xraMQxM
This commit is contained in:
parent
2d0d69b461
commit
fc4b504093
@ -140,6 +140,41 @@ Target: a 1-Hz script should take < 2 ms per invocation on a modern desktop.
|
||||
A 5-second-interval script can afford ~20 ms.
|
||||
If you're over budget, count the `execve` with `strace -c` and remove forks.
|
||||
|
||||
### R9. Never flush+rebuild firewall rules unconditionally in a poll loop
|
||||
|
||||
`iptables -F chain` followed by dozens of `iptables -A` calls every tick is
|
||||
one of the most expensive fork patterns possible — each rule insert is its
|
||||
own fork, plus it contends the xtables lock with the kernel's own netfilter
|
||||
subsystem (netd on Android, firewalld/nftables on Linux). This is a specific
|
||||
enough, and costly enough, instance of R6-style unconditional recomputation
|
||||
to call out on its own: a real incident (`phone_focus_mode/dns_enforcer.sh`,
|
||||
see `feedback-iptables-hash-gate-before-rebuild` in this user's memory)
|
||||
rebuilt ~100 rules every 20s with no state check at all, and measured out
|
||||
to pegging `netd` at a sustained ~50% CPU, contributing to a phone
|
||||
overheating.
|
||||
|
||||
Fix: check whether the current state is already correct before touching
|
||||
iptables at all —
|
||||
|
||||
```bash
|
||||
# Cheap check (2-3 forks): is the chain already right?
|
||||
chain_intact() {
|
||||
"$1" -C OUTPUT -j "$2" >/dev/null 2>&1 || return 1
|
||||
[ "$("$1" -S "$2" 2>/dev/null | grep -c '^-A')" = "$3" ]
|
||||
}
|
||||
|
||||
# Only pay for the expensive flush+rebuild (50-100+ forks) when it's wrong.
|
||||
chain_intact iptables MY_CHAIN "$expected_count" || rebuild_my_chain
|
||||
```
|
||||
|
||||
This preserves self-healing (a flush by the kernel or another process is
|
||||
still detected and repaired on the next tick) while skipping the ~95% of
|
||||
ticks where nothing changed. If diagnosing whether this pattern is already
|
||||
live in a running system, `kill -STOP`/`kill -CONT` the suspect script's PID
|
||||
and compare `/proc/<pid>/stat` utime+stime jiffy deltas of the netfilter
|
||||
daemon before/after — don't trust `dumpsys cpuinfo` (Android) or similar
|
||||
cached snapshot tools, some builds don't refresh them between calls.
|
||||
|
||||
## Python-specific rules (for daemons, not hot-loop callees)
|
||||
|
||||
- Use `pathlib.Path.read_text()` / `read_bytes()` — one syscall, no subprocess.
|
||||
@ -158,6 +193,8 @@ If you're over budget, count the `execve` with `strace -c` and remove forks.
|
||||
- `sensors`, `acpi`, `free`, `lspci`, `iwgetid` in a per-second script
|
||||
- `python …` / `node …` invoked per tick
|
||||
- No `set -u` (silent typo bugs compound over thousands of ticks)
|
||||
- `iptables -F` / `ip6tables -F` followed by unconditional `-A` rebuilds with
|
||||
no preceding "is this already correct?" check (see R9)
|
||||
|
||||
## Verification checklist before shipping
|
||||
|
||||
@ -175,3 +212,5 @@ If you're over budget, count the `execve` with `strace -c` and remove forks.
|
||||
- `linux_configuration/i3-configuration/i3blocks/cpu_monitor.sh` — zero-fork via `/proc/loadavg` + `/sys/class/hwmon`.
|
||||
- `linux_configuration/i3-configuration/i3blocks/motherboard_temp.sh` — zero-fork via `/sys/class/hwmon`.
|
||||
- `linux_configuration/scripts/system-maintenance/systemd/monitors.slice` — resource-cap slice.
|
||||
- `phone_focus_mode/dns_enforcer.sh::chain_intact()`/`expected_rule_count()` — R9's hash/count-gate pattern, fixed 2026-07-03 after it was found pegging Android's `netd` at ~50% CPU.
|
||||
- `phone_focus_mode/hosts_enforcer.sh::sync_magisk_module` — the original hash-before-write pattern R9 mirrors.
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
{
|
||||
"title": "phone_focus_mode CPU/overheat hardening",
|
||||
"objective": "Eliminate redundant per-tick firewall and rule rebuilds in the on-device focus enforcer that were pegging netd and overheating the phone, without weakening enforcement or self-healing, and whitelist the wake_alarm_sync companion app.",
|
||||
"acceptance_criteria": [
|
||||
"DNS chain is rebuilt only when missing, detached from OUTPUT, or the wrong rule count; identical rebuilds are skipped.",
|
||||
"Curfew net reassert runs every 15s instead of every 1s.",
|
||||
"Blocked-system-apps file is built once at startup; is_allowed() is memoized per tick.",
|
||||
"com.kuhy.wake_alarm_sync is present in both the general and curfew-night whitelists.",
|
||||
"shellcheck --severity=warning passes on all four scripts and the no-polling-antipatterns hook passes."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Replacing the interim iptables stopgap with a netd/eBPF per-UID firewall.",
|
||||
"Any change to the Linux-side focus daemon (this is the Android/ADB enforcer)."
|
||||
],
|
||||
"verifier": "shellcheck --severity=warning + pre-commit no-polling-antipatterns hook + on-device netd CPU observation"
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
{
|
||||
"intent": "Stop the on-device phone_focus_mode enforcer from overheating the phone by eliminating redundant per-tick firewall/rule rebuilds, while preserving self-healing enforcement; also whitelist the new wake_alarm_sync app so it can set tomorrow's alarm at night.",
|
||||
"scope": [
|
||||
"phone_focus_mode/config.sh",
|
||||
"phone_focus_mode/dns_enforcer.sh",
|
||||
"phone_focus_mode/focus_daemon.sh",
|
||||
"phone_focus_mode/deploy.sh",
|
||||
".github/skills/efficient-polling-scripts/SKILL.md",
|
||||
"Non-goal: replacing the interim iptables stopgap with a proper netd/eBPF per-UID firewall (tracked as a follow-up)"
|
||||
],
|
||||
"changes": [
|
||||
"config.sh: throttle CURFEW_NET_REASSERT_INTERVAL 1s -> 15s. A 1s interval forked `iptables -L` every second all night, pegging netd at ~30% CPU and overheating the phone; 15s matches HOSTS/LAUNCHER pacing. Deliberate tradeoff: curfew leak window grows from <=1s to <=15s.",
|
||||
"config.sh: add com.kuhy.wake_alarm_sync to the general and curfew-night app whitelists so the wake-alarm phone companion can run (it writes the alarm time to a GitHub Gist).",
|
||||
"dns_enforcer.sh: state-gate the DNS chain rebuild. fill_chain_v4/v6 flush+reinsert ~50-100 rules each call; running that unconditionally every tick pegged netd at ~50% CPU on the xtables lock even though the rule set never changes. New chain_intact()/expected_rule_count() only rebuild when the chain is missing, untethered from OUTPUT, or the wrong size; self-healing is preserved.",
|
||||
"focus_daemon.sh: build the blocked-system-apps file once at startup instead of every enable sweep, and memoize is_allowed() lookups once per main-loop tick.",
|
||||
"deploy.sh: drop `su --mount-master` (removed in Magisk v26+); plain `su -c` runs in the correct mount namespace for our use cases.",
|
||||
"SKILL.md: document the state-change-gate pattern (only rebuild firewall rules when tampered) in the efficient-polling-scripts skill."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "shellcheck --severity=warning phone_focus_mode/config.sh phone_focus_mode/deploy.sh phone_focus_mode/dns_enforcer.sh phone_focus_mode/focus_daemon.sh",
|
||||
"result": "pass",
|
||||
"evidence": "Exit 0, no warnings or errors"
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run (staged phone_focus_mode scripts + SKILL.md)",
|
||||
"result": "pass",
|
||||
"evidence": "shellcheck, no-polling-antipatterns, codespell and generic hooks pass"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"Curfew net leak window grows from <=1s to <=15s during the reassert interval; acceptable vs. thermal/CPU cost.",
|
||||
"State-gate on the DNS chain relies on chain_intact() correctly detecting tampering (existence + attachment + rule count); a subtle mismatch that always reads intact would skip a legitimate rebuild.",
|
||||
"Dropping --mount-master assumes the deployed Magisk is v26+."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit to restore unconditional rebuilds and the 1s interval.",
|
||||
"After rollback, re-run deploy.sh and confirm the DNS/curfew chains reassert and the whitelist no longer contains wake_alarm_sync."
|
||||
]
|
||||
}
|
||||
@ -96,11 +96,15 @@ export CURFEW_NET_IPT_CHAIN="FOCUS_CURFEW_NET"
|
||||
# Android's netd periodically reasserts the whole `filter` table via
|
||||
# iptables-restore, which atomically erases our custom chain (proven on-device:
|
||||
# the chain vanishes ~every 5s). The main enforcer tick (5s) rebuilds it but
|
||||
# leaves up-to-5s leak windows. This fast watchdog re-pins the chain every
|
||||
# CURFEW_NET_REASSERT_INTERVAL seconds *from a cached UID list* (no pm fork),
|
||||
# shrinking the leak to <=1s. The proper fix is netd's own per-UID firewall
|
||||
# (ndc/eBPF), tracked as a follow-up; this is the interim stopgap.
|
||||
export CURFEW_NET_REASSERT_INTERVAL=1
|
||||
# leaves up-to-5s leak windows. This watchdog re-pins the chain every
|
||||
# CURFEW_NET_REASSERT_INTERVAL seconds *from a cached UID list* (no pm fork).
|
||||
# A 1s interval was measured on-device to peg netd at ~30% CPU continuously
|
||||
# (an `iptables -L` fork every second, all night), overheating the phone -
|
||||
# throttled to 15s (matches HOSTS_CHECK_INTERVAL/LAUNCHER_CHECK_INTERVAL
|
||||
# pacing) as a deliberate tradeoff: leak window grows from <=1s to <=15s.
|
||||
# The proper fix is netd's own per-UID firewall (ndc/eBPF), tracked as a
|
||||
# follow-up; this remains an interim stopgap either way.
|
||||
export CURFEW_NET_REASSERT_INTERVAL=15
|
||||
export CURFEW_NET_UID_CACHE="$STATE_DIR/curfew_net_uids.txt"
|
||||
# Manual test toggle: `focus_ctl.sh curfew-test-on` writes this file to force
|
||||
# curfew ACTIVE regardless of clock, so the whole stack can be validated during
|
||||
@ -380,6 +384,9 @@ eu.kanade.tachiyomi.sy
|
||||
# --- Development ---
|
||||
com.github.android
|
||||
|
||||
# --- Wake alarm sync (writes alarm time to GitHub Gist for PC sync) ---
|
||||
com.kuhy.wake_alarm_sync
|
||||
|
||||
# --- Media / podcasts ---
|
||||
ac.mdiq.podcini.X
|
||||
is.xyz.mpv
|
||||
@ -468,6 +475,9 @@ com.kunzisoft.keepass.libre
|
||||
# Smart home (control lights before sleep)
|
||||
com.xiaomi.smarthome
|
||||
|
||||
# --- Wake alarm sync (must work at night to set tomorrow's alarm) ---
|
||||
com.kuhy.wake_alarm_sync
|
||||
|
||||
# --- Good-for-you apps (not scroll traps; kept per your request) ---
|
||||
# Remove any of these if you want a stricter night.
|
||||
com.stronglifts.app
|
||||
|
||||
@ -137,13 +137,12 @@ connect_adb() {
|
||||
}
|
||||
|
||||
# Wrapper: run a root shell command on the phone.
|
||||
# Uses --mount-master so the command sees (and can modify) the global mount
|
||||
# namespace — required for any status checks that inspect the hosts bind
|
||||
# mount, /data/adb/focus_mode files, or for starting daemons.
|
||||
# --mount-master was removed in Magisk v26+; plain su -c works and still
|
||||
# runs in the correct mount namespace for our use cases.
|
||||
adb_root() {
|
||||
local command_text="$1"
|
||||
|
||||
printf '%s\n' "$command_text" | adb_cmd shell su --mount-master -c "sh -s"
|
||||
printf '%s\n' "$command_text" | adb_cmd shell su -c "sh -s"
|
||||
}
|
||||
|
||||
compute_file_hash() {
|
||||
|
||||
@ -167,12 +167,49 @@ fill_chain_v6() {
|
||||
done
|
||||
}
|
||||
|
||||
# ---- State-change gate ----
|
||||
#
|
||||
# fill_chain_v4/v6 flush and reinsert ~50-100 iptables/ip6tables rules each
|
||||
# call. Running that unconditionally every $DNS_CHECK_INTERVAL was measured
|
||||
# to peg netd at ~50% CPU on-device (contending the xtables lock) even
|
||||
# though the rule set never changes at runtime. Only rebuild when the chain
|
||||
# is actually missing, untethered from OUTPUT, or the wrong size (i.e.
|
||||
# someone/something - netd, a root shell, a flush - tampered with it).
|
||||
# Self-healing behavior described in the header comment is preserved; only
|
||||
# redundant identical rebuilds are skipped.
|
||||
|
||||
expected_rule_count() {
|
||||
# 2 fixed DoT rules (tcp+udp/853) + 4 rules per configured DoH/DNS IP.
|
||||
local n=2 ip
|
||||
for ip in "$@"; do
|
||||
[ -z "$ip" ] && continue
|
||||
[ "${ip#\#}" != "$ip" ] && continue
|
||||
n=$((n + 4))
|
||||
done
|
||||
echo "$n"
|
||||
}
|
||||
|
||||
chain_intact() {
|
||||
local ipt="$1" expected="$2" actual
|
||||
"$ipt" -C OUTPUT -j "$DNS_IPT_CHAIN" >/dev/null 2>&1 || return 1
|
||||
actual="$("$ipt" -S "$DNS_IPT_CHAIN" 2>/dev/null | grep -c '^-A')"
|
||||
[ "$actual" = "$expected" ]
|
||||
}
|
||||
|
||||
enforce_iptables() {
|
||||
if command -v iptables >/dev/null 2>&1; then
|
||||
ensure_chain iptables && fill_chain_v4
|
||||
if chain_intact iptables "$(expected_rule_count $DNS_DOH_IPV4)"; then
|
||||
:
|
||||
elif ensure_chain iptables && fill_chain_v4; then
|
||||
log "iptables (v4) DNS chain rebuilt (was missing/tampered)"
|
||||
fi
|
||||
fi
|
||||
if command -v ip6tables >/dev/null 2>&1; then
|
||||
ensure_chain ip6tables && fill_chain_v6
|
||||
if chain_intact ip6tables "$(expected_rule_count $DNS_DOH_IPV6)"; then
|
||||
:
|
||||
elif ensure_chain ip6tables && fill_chain_v6; then
|
||||
log "ip6tables (v6) DNS chain rebuilt (was missing/tampered)"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@ -85,6 +85,14 @@ build_sysprotect_file() {
|
||||
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//' > "$STATE_DIR/sysprotect.txt"
|
||||
}
|
||||
|
||||
# $BLOCKED_SYSTEM_APPS is a static config value (never changes without a
|
||||
# daemon restart), so - like build_sysprotect_file above - this only needs
|
||||
# to run once at startup, not every enable_focus_mode() sweep.
|
||||
build_blocked_sys_file() {
|
||||
echo "$BLOCKED_SYSTEM_APPS" | grep -v '^[[:space:]]*#' | grep -v '^[[:space:]]*$' \
|
||||
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//' > "$STATE_DIR/blocked_sys.txt"
|
||||
}
|
||||
|
||||
reconcile_disabled_apps() {
|
||||
[ -f "$DISABLED_APPS_FILE" ] || return
|
||||
|
||||
@ -137,6 +145,7 @@ init() {
|
||||
build_whitelist_file
|
||||
build_night_whitelist_file
|
||||
build_sysprotect_file
|
||||
build_blocked_sys_file
|
||||
refresh_default_handlers
|
||||
rotate_log
|
||||
|
||||
@ -213,11 +222,25 @@ is_curfew_now() {
|
||||
# (test hook) or inside the time window. The is_allowed() switch below consults
|
||||
# this; because is_allowed() only runs during the focus-mode sweep/reconcile,
|
||||
# curfew automatically takes effect only at home and is a no-op when away.
|
||||
#
|
||||
# Memoized once per main-loop tick (reset at the top of main()'s while loop):
|
||||
# is_allowed() calls this once per enabled package in the sweep, and
|
||||
# is_curfew_now forks `date` - on-device this meant up to ~N extra forks per
|
||||
# 10s tick for N enabled apps, all recomputing a value that cannot change
|
||||
# within a single tick. Result is cached in _CURFEW_TICK_RESULT.
|
||||
curfew_active() {
|
||||
[ "${NIGHT_CURFEW_ENABLED:-0}" = "1" ] || return 1
|
||||
[ -e "$CURFEW_OVERRIDE_FILE" ] && return 1
|
||||
[ -e "$CURFEW_FORCE_FILE" ] && return 0
|
||||
is_curfew_now
|
||||
if [ -n "$_CURFEW_TICK_CACHED" ]; then
|
||||
[ "$_CURFEW_TICK_RESULT" = "1" ]
|
||||
return
|
||||
fi
|
||||
_CURFEW_TICK_CACHED=1
|
||||
_CURFEW_TICK_RESULT=0
|
||||
if [ "${NIGHT_CURFEW_ENABLED:-0}" = "1" ] && [ ! -e "$CURFEW_OVERRIDE_FILE" ]; then
|
||||
if [ -e "$CURFEW_FORCE_FILE" ] || is_curfew_now; then
|
||||
_CURFEW_TICK_RESULT=1
|
||||
fi
|
||||
fi
|
||||
[ "$_CURFEW_TICK_RESULT" = "1" ]
|
||||
}
|
||||
|
||||
# ---- Check if package is allowed (whitelist or system-protected) ----
|
||||
@ -319,10 +342,9 @@ enable_focus_mode() {
|
||||
# list so a newly-promoted handler is never disabled.
|
||||
refresh_default_handlers
|
||||
|
||||
# Build blocked system app list (used both at entry and for periodic sweep)
|
||||
# Blocked system app list is static; built once in init() (see
|
||||
# build_blocked_sys_file), not rebuilt on every sweep.
|
||||
local blocked_sys="$STATE_DIR/blocked_sys.txt"
|
||||
echo "$BLOCKED_SYSTEM_APPS" | grep -v '^[[:space:]]*#' | grep -v '^[[:space:]]*$' \
|
||||
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//' > "$blocked_sys"
|
||||
|
||||
# Periodic rescan catches third-party apps the user re-enabled (e.g. via
|
||||
# Play Store or `pm enable` in a terminal) since the last tick.
|
||||
@ -472,6 +494,9 @@ main() {
|
||||
init
|
||||
|
||||
while true; do
|
||||
# Invalidate the per-tick curfew_active() memo (see its definition).
|
||||
_CURFEW_TICK_CACHED=""
|
||||
|
||||
location="$(get_location)"
|
||||
|
||||
if [ -n "$location" ]; then
|
||||
|
||||
Loading…
Reference in New Issue
Block a user