From 779ace0c8702ce74abccaffd66541f1de84f9c69 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Fri, 17 Jul 2026 08:25:51 +0200 Subject: [PATCH] fix(hosts-guard): stop setup aborting silently, so the guards actually enable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit setup_hosts_guard.sh died partway on every run under `set -euo pipefail`, which is why hosts-guard.path / hosts-bind-mount.service / nsswitch-guard.path sat installed-but-disabled since 2026-02-02 and resolved-guard.path was never installed at all — /etc/hosts had no watcher to restore it if tampered. Two aborts, both silent: 1. `local_dot=$(grep -E '^\s*DNSOverTLS\s*=' ... | tail -1 | sed ...)` — grep exits 1 when the key is absent, pipefail propagates, the assignment returns 1, set -e kills the script. DNSOverTLS is absent from a default resolved.conf, so this fired on EVERY run, right before resolved-guard.path was enabled. The fix-if-missing branch below it was unreachable because detecting "missing" aborted first. Same trap on the ReadEtcHosts probe. check_and_enable_services.sh already guarded its copy with `|| true`. 2. `systemctl restart systemd-resolved` — resolved is MASKED here (dnsmasq serves DNS), so the restart failed and aborted the script on its last step, after everything was already configured. Now skipped with a note. Before: exit 1, resolved-guard disabled, nsswitch-guard start-limit-hit. After: exit 0; hosts-guard.path, hosts-bind-mount.service, nsswitch-guard.path and resolved-guard.path all enabled+active. DNS still resolves; getent ahosts on doubleclick/googlesyndication/facebook returns 0.0.0.0; /etc/hosts still immutable at 176888 lines. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01JsCs1GUyiinA7bg68YvAh1 --- ...-hosts-guard-silent-aborts-2026-07-17.json | 26 +++++++++++++++++++ .../hosts/guard/setup_hosts_guard.sh | 25 +++++++++++++++--- 2 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 docs/superpowers/evidence/fix-hosts-guard-silent-aborts-2026-07-17.json diff --git a/docs/superpowers/evidence/fix-hosts-guard-silent-aborts-2026-07-17.json b/docs/superpowers/evidence/fix-hosts-guard-silent-aborts-2026-07-17.json new file mode 100644 index 00000000..c81899ef --- /dev/null +++ b/docs/superpowers/evidence/fix-hosts-guard-silent-aborts-2026-07-17.json @@ -0,0 +1,26 @@ +{ + "intent": "setup_hosts_guard.sh aborted partway on every run under `set -euo pipefail`, silently leaving the hosts guards half-configured or disabled. hosts-guard.path, hosts-bind-mount.service and nsswitch-guard.path were installed 2026-02-02 but disabled, and resolved-guard.path/enforce-resolved.sh were never installed at all — so /etc/hosts had no watcher to restore it if tampered. Make the installer run to completion so all four guards actually get enabled.", + "scope": [ + "linux_configuration/scripts/periodic_background/hosts/guard/setup_hosts_guard.sh", + "Non-goals: blocklist content, dnsmasq config, the other drifted services (thesis_tracker, focus_mode, guest_mode_removal, workout_locker) which the author will repair separately" + ], + "changes": [ + "Added `|| true` to the ReadEtcHosts and DNSOverTLS detection assignments. grep exits 1 when a key is absent; with set -euo pipefail that aborted the whole script — silently skipping the very fix-if-missing branch below it. DNSOverTLS is absent from a default resolved.conf, so the run died there on EVERY invocation, right before resolved-guard.path was enabled. (check_and_enable_services.sh already guarded its copy of the same grep with `|| true`; this brings the installer in line.)", + "Guarded the final `systemctl restart systemd-resolved`: systemd-resolved is MASKED on this host (dnsmasq serves DNS), so restarting it failed and set -e aborted the script on its last step — after everything was already configured, making a successful run report failure. Now skips with a note when resolved is masked/disabled." + ], + "verification": [ + { + "command": "sudo bash setup_hosts_guard.sh; systemctl is-enabled/is-active for the four guard units; getent ahosts on blocked domains; bash -n + shellcheck --severity=warning", + "result": "pass", + "evidence": "Before: exit 1, resolved-guard.path disabled/inactive, nsswitch-guard.path failed (unit-start-limit-hit). After: exit 0, and hosts-guard.path / hosts-bind-mount.service / nsswitch-guard.path / resolved-guard.path are all enabled+active. DNS still resolves (dnsmasq untouched); getent ahosts doubleclick.net / googlesyndication.com / facebook.com all return 0.0.0.0; /etc/hosts still immutable with 176888 lines. parse OK, shellcheck clean." + } + ], + "risks": [ + "The guards are now actually live: /etc/hosts has a read-only bind mount and a path watcher that reverts edits, and nsswitch.conf/resolved.conf are enforced against canonical snapshots. Editing those files now requires the documented unlock path (sudo unlock-hosts) or `setup_hosts_guard.sh --uninstall`.", + "nsswitch-guard hit 'unit-start-limit-hit' once during the first run (the enforcement rewrites the file it watches, re-triggering itself). It settled to active, but a rapid drift/restore cycle could trip systemd's start limit again." + ], + "rollback": [ + "sudo bash setup_hosts_guard.sh --uninstall (disables the units and removes them; canonical snapshots are left in place).", + "Validate after rollback: the four units are gone/disabled, /etc/hosts is no longer bind-mounted (findmnt /etc/hosts is empty), and DNS still resolves." + ] +} diff --git a/linux_configuration/scripts/periodic_background/hosts/guard/setup_hosts_guard.sh b/linux_configuration/scripts/periodic_background/hosts/guard/setup_hosts_guard.sh index 869ee71f..9db4b6d4 100755 --- a/linux_configuration/scripts/periodic_background/hosts/guard/setup_hosts_guard.sh +++ b/linux_configuration/scripts/periodic_background/hosts/guard/setup_hosts_guard.sh @@ -456,8 +456,13 @@ if [[ $ENABLE_RESOLVED -eq 1 ]]; then # Ensure ReadEtcHosts=yes in resolved.conf before snapshotting if [[ -f "$RESOLVED_CONF" ]]; then + # `|| true` is REQUIRED: grep exits 1 when the key is absent, and under + # `set -euo pipefail` that aborts the whole script — silently skipping the + # very fix below that exists to handle the absent case. Without it the + # "key missing" branch is unreachable. (check_and_enable_services.sh + # already guards its copy of this grep the same way.) local_read_hosts=$(grep -E '^\s*ReadEtcHosts\s*=' "$RESOLVED_CONF" 2>/dev/null | - tail -1 | sed 's/.*=\s*//' | tr -d '[:space:]') + tail -1 | sed 's/.*=\s*//' | tr -d '[:space:]') || true if [[ "$local_read_hosts" != "yes" ]]; then msg "Fixing ReadEtcHosts in resolved.conf (was: '$local_read_hosts')" chattr -i "$RESOLVED_CONF" 2>/dev/null || true @@ -472,8 +477,12 @@ if [[ $ENABLE_RESOLVED -eq 1 ]]; then fi # Ensure DNSOverTLS is not set to yes or opportunistic + # `|| true`: same trap as above. DNSOverTLS is absent from a default + # resolved.conf, so without this the script aborted here on every run — + # right before resolved-guard.path was enabled, which is why that guard + # was never installed. local_dot=$(grep -E '^\s*DNSOverTLS\s*=' "$RESOLVED_CONF" 2>/dev/null | - tail -1 | sed 's/.*=\s*//' | tr -d '[:space:]') + tail -1 | sed 's/.*=\s*//' | tr -d '[:space:]') || true if [[ -n "$local_dot" && "$local_dot" != "no" ]]; then msg "Disabling DNSOverTLS in resolved.conf (was: '$local_dot')" chattr -i "$RESOLVED_CONF" 2>/dev/null || true @@ -514,8 +523,16 @@ if [[ $ENABLE_RESOLVED -eq 1 ]]; then "$INSTALL_ENFORCE_RESOLVED" || warn "resolved enforcement returned non-zero" fi - # Restart resolved to pick up corrected config - run systemctl restart systemd-resolved + # Restart resolved to pick up corrected config. + # systemd-resolved is not necessarily the resolver: on this host it is MASKED + # and dnsmasq serves DNS. Restarting a masked unit fails, and under `set -e` + # that aborted the script on its very last step — after everything was already + # configured, so the run reported failure despite having succeeded. + if systemctl is-enabled systemd-resolved.service &>/dev/null; then + run systemctl restart systemd-resolved || warn "Failed to restart systemd-resolved" + else + note "systemd-resolved is masked/disabled — skipping restart (DNS is served elsewhere, e.g. dnsmasq)" + fi else note "Skipping resolved.conf protection (--skip-resolved)" fi