Adversarial review of 9db25ba60 found four residual defects in the
auto-update asset install, plus a doc line it left contradicting itself.
- install_auto_update_assets copied the bundled helper into the staged
mktemp file with an unchecked cp, and both call sites invoke the
function under `if !`, which suppresses errexit for its whole body. A
failed copy (ENOSPC, EIO) fell through to
configure_auto_update_script_repo, whose awk emits a lone GITHUB_REPO=
line for empty input, so a shebang-less one-line stub replaced the
working helper with a "script" that only ever exits 0 - silently
disabling unattended updates. Check the cp, and refuse the swap unless
the staged helper is non-empty and starts with #!.
- Both units were rendered with a bare truncating `cat > "$unit"` whose
status was never checked, and the function's last statement is
safe_systemctl daemon-reload, which returns 0 by design. A failing
write truncated a working unit and still reported success. Render each
unit to ${path}.tmp and commit it with a checked rename, so a failure
leaves the installed unit byte-identical.
- The widened ReadWritePaths could not reach deployed boxes: the unit
that grants the write access is itself the file that has to be
rewritten, and on an existing install the sandbox running the
installer excludes /etc/systemd/system and /usr/local/bin (EROFS). The
Go update pipeline cannot carry it either - pulse.service runs as
User=pulse with its own ProtectSystem=strict over the install and
config dirs only. So probe each destination directory up front and,
when one is blocked, re-exec this already-signature-verified installer
through systemd-run with a new internal --repair-auto-update-units
entry point: PID 1 forks the transient unit, so it starts in the host
mount namespace instead of inheriting the sandbox. The installer is
copied into the install dir first because the calling unit's
PrivateTmp=yes hides its /tmp copy from PID 1. The escape needs root
and systemd-run, and never recurses.
- Keep the ReadWritePaths entries as directory grants: every write now
commits with a rename from a sibling staging file, and rename needs
write access on the containing directory, so the file-level entries
systemd would otherwise accept cannot work. Document the tradeoff in
the unit and the subsystem contract instead.
The deployment-installability contract still claimed the update sandbox
leaves "only the install dir, config dir and /tmp" writable, which the
paragraph the same file gained in 9db25ba60 contradicts; the same stale
rationale had been copied into two test comments.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>