{ "intent": "Bring hosts_enforcer.sh under the 250-line cap by extracting its bind-mount and Magisk/cache halves into two flat siblings, and cover both at 100% — neither had any test, and together they decide whether the DNS blocklist is actually in force.", "scope": [ "phone_focus_mode/hosts_enforcer.sh - 421 lines down to 207", "phone_focus_mode/hosts_mount.sh - new: is_bind_mounted_correctly, unmount_existing_hosts_mount, make_target_writable_once, assert_bind_mount", "phone_focus_mode/hosts_magisk.sh - new: sync_magisk_module, protect_magisk_module, disable_firefox_doh, flush_browser_dns_caches, ensure_canonical_immutable, restart_netd_for_hosts_cache", "phone_focus_mode/deploy.sh - both siblings added to both hardcoded lists", "phone_focus_mode/lib/tests/ - a harness, four case files and one entry point, all new", "Non-goal: any device deploy. The split is proven with stubs, hashes and behavioural comparison instead." ], "changes": [ "Audited for stdin-draining loops BEFORE extracting, because piping such a loop would silently change behaviour in a way the stubs cannot catch. hosts_enforcer.sh contains no `while read` loop at all, so that risk did not apply here. Also checked that no function writes a global, so there was no cross-half read/write hazard of the kind that kept NET_BUILT in the curfew entry script.", "Placed both libraries as flat siblings, matching dns_iptables.sh, tether_iptables.sh and curfew_net.sh: deploy.sh pushes phone scripts into one directory, so a lib/ path would not exist on the device.", "Added both to deploy.sh's push list and cp list, then verified the two lists correspond exactly with no orphan in either direction — a push without a matching cp stages a file that never lands, and the enforcer would then fail to source it.", "Removed a provably equivalent guard from is_bind_mounted_correctly. Its early `[ ! -f $HOSTS_TARGET ] && return 1` is unreachable as a decision: sha256_of returns empty for a missing file, so the existing -n check already rejects that case. A mutation removing it survived because no test could distinguish it. Equivalent code is a claim of a check that is not being made, so it was deleted rather than papered over with a test that would pass either way.", "Stubbed cp, chattr, mount, umount, am, pgrep, stop, start and sleep on PATH — the process boundary, since these libraries invoke real binaries. Two hardcoded paths cannot be stubbed that way (/proc/self/mounts and the Fenix profile glob), so the staged copies have those substituted for harness-controlled directories while the shipped files keep their literal paths.", "The harness needs a copy the cp stub cannot intercept, so it uses shell redirection, which is a builtin and cannot be shadowed. The mount stub had the same problem and the same fix." ], "verification": [ { "command": "bash phone_focus_mode/lib/tests/test_hosts_libs.sh", "result": "pass", "evidence": "Results: 69 passed, 0 failed. Neither library had a test before." }, { "command": "bash meta/scripts/shell_coverage.sh phone_focus_mode/lib/tests/test_hosts_libs.sh hosts_mount.sh", "result": "pass", "evidence": "hosts_mount.sh: 43/43 lines = 100.00%, exit 0 at the default minimum. First measured run was 91.11%." }, { "command": "bash meta/scripts/shell_coverage.sh phone_focus_mode/lib/tests/test_hosts_libs.sh hosts_magisk.sh", "result": "pass", "evidence": "hosts_magisk.sh: 62/62 lines = 100.00%. First measured run was 83.87%." }, { "command": "mutation run: 17 mutations of the mount and magisk behaviour", "result": "pass", "evidence": "17 killed, 0 survived. Two survived the first run and both were real: the unmount give-up threshold was not pinned to 5 attempts (raising it to 500 changed nothing observable), and the missing-target case was masked. The first was closed with an attempt-count assertion; the second turned out to be equivalent code and was deleted." }, { "command": "ordering assertions inside the suite", "result": "pass", "evidence": "make_target_writable_once is asserted to remount /system ro AFTER rw and to re-set +i AFTER dropping it. Presence assertions alone would pass with either pair reversed, which would leave /system writable or the hosts file unlocked." }, { "command": "bash meta/scripts/verify_shell_split.sh 9f2e208 phone_focus_mode/hosts_enforcer.sh phone_focus_mode/hosts_enforcer.sh phone_focus_mode/hosts_magisk.sh phone_focus_mode/hosts_mount.sh", "result": "pass", "evidence": "Reports only is_bind_mounted_correctly as differing — the function whose equivalent guard was removed. All 19 other functions hash identical, so this is the first split in this session where the extraction itself was fully verbatim." }, { "command": "behavioural comparison of is_bind_mounted_correctly, old body vs new, across every input state", "result": "pass", "evidence": "both-missing NOT / target-missing NOT / match MOUNTED / mismatch NOT — identical from both bodies. The hash difference carries no behaviour change." }, { "command": "shellcheck on each touched file individually (no -x); sh -n on the three phone-side scripts", "result": "pass", "evidence": "All eleven files clean standalone. Two SC2034 findings were resolved by deleting globals the harness defined but nothing read, not by a disable. All three phone scripts parse under ash, which is what runs them on the device." }, { "command": "deploy-list parity check in both directions", "result": "pass", "evidence": "comm over the push list and the cp list returns empty both ways: every pushed file has a matching cp, and every cp has a matching push. hosts_magisk.sh and hosts_mount.sh appear in both." }, { "command": "bash meta/scripts/check_file_length.sh --all", "result": "pass", "evidence": "19 over-cap files, down from 20. hosts_enforcer.sh 207, hosts_mount.sh 91, hosts_magisk.sh 166, and the six test files between 24 and 198 — all under the cap." } ], "risks": [ "UNVERIFIED ON DEVICE. This is the third enforcer split in this session without a deploy behind it, after tether_enforcer.sh, dns_enforcer.sh and curfew_enforcer.sh. hosts_enforcer.sh is the highest-stakes of them: if it fails to source a sibling it exits, and nothing is blocking while the phone looks normal.", "Two new files must reach the device. A stale checkout or a partial push leaves hosts_enforcer.sh unable to source them, which stops the enforcer outright rather than degrading it.", "The tests substitute two hardcoded paths in the staged copies (/proc/self/mounts and the Fenix profile glob). If either path changes in the shipped file without the harness's sed being updated, the substitution silently stops matching and those tests would exercise the real paths — which do not exist here, so the cases would pass vacuously. The mutation run is what would catch that." ], "rollback": [ "git revert the commit. hosts_enforcer.sh returns to a single 421-line file, over the cap and untested, and deploy.sh stops pushing the two siblings.", "After rollback, run sh -n phone_focus_mode/hosts_enforcer.sh expecting exit 0, and confirm grep -c 'hosts_magisk\\|hosts_mount' phone_focus_mode/deploy.sh returns 0.", "A rollback AFTER a deploy leaves two stale files in $REMOTE_DIR. They are inert once nothing sources them, but remove them to keep a later audit honest." ] }