Skip to content

Commit 67301fd

Browse files
gitcommit90claude
andauthored
fix(linux): host updates fail installing unit files via /dev/stdin (#64)
The first live 1Helm host update (0.0.39 -> 0.0.40) failed and rolled back. The cause was not the health check (the server was coming up fine) but an earlier step: install-linux-units.sh writes four files - the tmpfiles.d config and the three systemd units - with install -m 0644 /dev/stdin DEST <<EOF ... EOF That reopens fd 0 through /proc. It works when the script runs from an operator shell (every fresh install), but fails with "install: No such file or directory" when the script runs inside a systemd-run oneshot - which is exactly and only how the UPDATE path invokes it. So every fresh install succeeded and the first update could never have succeeded. Under set -e the ENOENT aborted the apply transaction, which correctly rolled back to the prior healthy release. Each heredoc is now captured to a temp file and installed from there via a small install_stdin helper - robust in every execution context, and the literal unit contents and mode are unchanged. Adds a regression guard that fails if any command line in install-linux-units.sh installs from /dev/stdin again. Verified non-vacuous: reintroducing the pattern fails the test with this message. Because apply-linux-release.sh runs the TARGET release's copy of this script, the fix only takes effect once shipped, so it must go out in the next release for updates to that release to succeed. Co-authored-by: Joseph Yaksich <gitcommit90@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent c2690ba commit 67301fd

2 files changed

Lines changed: 22 additions & 4 deletions

File tree

site/public/install-linux-units.sh

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,26 @@ fi
3333
|| { echo "The verified 1Helm release is missing its host lifecycle scripts." >&2; exit 1; }
3434
id "$SERVICE_USER" >/dev/null 2>&1 || { echo "The 1Helm service account does not exist." >&2; exit 1; }
3535

36+
# `install /dev/stdin DEST <<EOF` reopens fd 0 via /proc, which fails with
37+
# "No such file or directory" when this script runs under systemd-run (the path
38+
# a host UPDATE takes, as opposed to a fresh install run from an operator
39+
# shell). Capture the heredoc to a real temp file first, then install that.
40+
install_stdin() {
41+
local mode="$1" dest="$2" tmp
42+
tmp="$(mktemp)"
43+
cat >"$tmp"
44+
install -o root -g root -m "$mode" "$tmp" "$dest"
45+
rm -f "$tmp"
46+
}
47+
3648
install -o root -g root -m 0755 "$RELEASE_ROOT/site/public/update-host.sh" "$INSTALL_ROOT/update-host.sh"
3749
install -o root -g root -m 0755 "$RELEASE_ROOT/site/public/uninstall-host.sh" "$INSTALL_ROOT/uninstall-host.sh"
3850

3951
# ProtectSystem=strict resolves ReadWritePaths before it runs the service. Keep
4052
# Podman's host-only scratch roots present both now and after every reboot so a
4153
# completely fresh machine never fails mount-namespace setup before 1Helm can
4254
# invoke its root-owned runtime helper.
43-
install -m 0644 /dev/stdin /etc/tmpfiles.d/1helm-oci.conf <<'EOF'
55+
install_stdin 0644 /etc/tmpfiles.d/1helm-oci.conf <<'EOF'
4456
d /run/1helm-oci 0755 root root -
4557
d /run/1helm-oci/tmp 1777 root root -
4658
d /run/containers 0755 root root -
@@ -50,7 +62,7 @@ d /run/netns 0755 root root -
5062
EOF
5163
systemd-tmpfiles --create /etc/tmpfiles.d/1helm-oci.conf
5264

53-
install -m 0644 /dev/stdin /etc/systemd/system/1helm.service <<EOF
65+
install_stdin 0644 /etc/systemd/system/1helm.service <<EOF
5466
[Unit]
5567
Description=1Helm durable agent workspace
5668
After=network-online.target
@@ -93,7 +105,7 @@ Delegate=yes
93105
WantedBy=multi-user.target
94106
EOF
95107

96-
install -m 0644 /dev/stdin /etc/systemd/system/1helm-update.service <<EOF
108+
install_stdin 0644 /etc/systemd/system/1helm-update.service <<EOF
97109
[Unit]
98110
Description=Install a verified 1Helm host update
99111
After=network-online.target
@@ -112,7 +124,7 @@ ProtectSystem=strict
112124
ReadWritePaths=$INSTALL_ROOT $STATE_ROOT /run/1helm-oci /usr/libexec /usr/lib/1helm-oci /etc/1helm /etc/default /etc/systemd/system /etc/sudoers.d /etc/subuid /etc/subgid
113125
EOF
114126

115-
install -m 0644 /dev/stdin /etc/systemd/system/1helm-update.path <<EOF
127+
install_stdin 0644 /etc/systemd/system/1helm-update.path <<EOF
116128
[Unit]
117129
Description=Watch for Captain-authorized 1Helm host updates
118130

test/site.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,12 @@ test("installer assets are explicit and syntax-valid", () => {
274274
const updater = readFileSync(`${root}/site/public/update-host.sh`, "utf8");
275275
const linuxUnits = readFileSync(`${root}/site/public/install-linux-units.sh`, "utf8");
276276
const releaseApply = readFileSync(`${root}/site/public/apply-linux-release.sh`, "utf8");
277+
// `install /dev/stdin DEST <<EOF` reopens fd 0 through /proc and fails with
278+
// ENOENT when this script runs inside a systemd-run oneshot - the exact path a
279+
// host UPDATE takes, while fresh installs run it from an operator shell where
280+
// it works. That is why the first live update failed and every fresh install
281+
// passed. Writing unit files must not depend on reopening stdin.
282+
assert.doesNotMatch(linuxUnits, /^\s*install\b[^\n]*\/dev\/stdin/m, "unit files must not be installed by reopening /dev/stdin (breaks under systemd-run)");
277283
assert.match(updater, /browser_download_url/);
278284
assert.match(linuxUnits, /Environment=HELM_APP_ROOT=\$INSTALL_ROOT\/current/, "Linux explicitly exposes the active packaged root to runtime resource resolvers");
279285
assert.match(updater, /\^sha256:\[a-f0-9\]\{64\}\$/, "the Linux updater requires GitHub's exact SHA-256 asset digest");

0 commit comments

Comments
 (0)