Skip to content

Commit 9a603fb

Browse files
authored
fix: make Linux host updates atomic (#14)
Apply verified Linux runtime files, release source, units, restart, health gate, and rollback in one transient root transaction. Safely bridge the v0.0.11 updater namespace, create required LXC roots before systemd starts, and release 0.0.12.\n\nSigned-off-by: Joseph Yaksich <gitcommit90@users.noreply.github.com>
1 parent ab484a7 commit 9a603fb

12 files changed

Lines changed: 283 additions & 35 deletions

CHANGELOG.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.0.12] - 2026-07-26
11+
12+
### Fixed
13+
14+
- Linux host updates now apply the verified runtime files, release symlink,
15+
systemd units, restart, health check, and rollback inside one bounded
16+
transient root transaction. This lets a v0.0.11 host escape its obsolete
17+
`ProtectSystem=strict` mount namespace before atomically replacing host
18+
files, while future updater units grant write access only to the required
19+
parent trees.
20+
- Fresh Linux installs create every LXC state/cache root named by the service
21+
unit before systemd builds its private mount namespace, so the first launch
22+
no longer depends on a channel computer having already been provisioned.
23+
1024
## [0.0.11] - 2026-07-26
1125

1226
### Added
@@ -334,7 +348,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
334348
notarization, stapled tickets, Gatekeeper verification, persistent
335349
Application Support, and isolated Apple container machines.
336350

337-
[Unreleased]: https://github.com/gitcommit90/1Helm/compare/v0.0.11...HEAD
351+
[Unreleased]: https://github.com/gitcommit90/1Helm/compare/v0.0.12...HEAD
352+
[0.0.12]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.12
338353
[0.0.11]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.11
339354
[0.0.10]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.10
340355
[0.0.9]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.9

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ A fresh data directory opens first-run setup. The source runtime defaults to
257257
| `PORT` | `8123` | HTTP/WebSocket control-plane port. |
258258
| `CTRL_DATA_DIR` | `./data` | Databases, routing state, uploads, and narrow workspace mirrors. |
259259
| `HELM_CHANNEL_COMPUTER_BACKEND` | `apple` on macOS, `lxc` on Linux, `wsl` on Windows | Host isolation backend; `native` and `mock` are explicit development/test overrides. |
260-
| `HELM_CHANNEL_MACHINE_IMAGE` | `local/1helm-channel-machine:0.0.11` | Versioned channel-machine image contract. |
260+
| `HELM_CHANNEL_MACHINE_IMAGE` | `local/1helm-channel-machine:0.0.12` | Versioned channel-machine image contract. |
261261

262262
### Agent-first JSON CLI
263263

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "1helm",
33
"productName": "1Helm",
4-
"version": "0.0.11",
4+
"version": "0.0.12",
55
"private": true,
66
"type": "module",
77
"license": "AGPL-3.0-only",

site/public/apply-linux-release.sh

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Apply one already downloaded, SHA-verified, built, and retained 1Helm Linux
5+
# release as a single root transaction outside an older updater unit's mount
6+
# namespace. No URL, command, or arbitrary destination is accepted here.
7+
8+
INSTALL_ROOT="/opt/1helm"
9+
RELEASES_ROOT="$INSTALL_ROOT/releases"
10+
APP_ROOT="$INSTALL_ROOT/current"
11+
NODE_LINK="$INSTALL_ROOT/node-current"
12+
STATE_ROOT="/var/lib/1helm"
13+
SERVICE_USER="1helm"
14+
SERVICE_NAME="1helm.service"
15+
PORT="8123"
16+
RELEASE_ROOT="$(readlink -f "${1:-}" 2>/dev/null || true)"
17+
TARGET_VERSION="${2:-}"
18+
STATUS_FILE="$STATE_ROOT/host-update-status.json"
19+
HOST_CONTRACT_PATHS=(
20+
/usr/libexec/1helm-lxc-runtime
21+
/usr/libexec/1helm-lxc-net
22+
/etc/1helm/lxc-unprivileged.conf
23+
/etc/1helm/lxc-idmap
24+
/etc/sudoers.d/1helm-lxc-runtime
25+
/etc/default/lxc-net
26+
/etc/subuid
27+
/etc/subgid
28+
/etc/systemd/system/1helm-lxc-net.service
29+
/etc/systemd/system/1helm.service
30+
/etc/systemd/system/1helm-update.service
31+
/etc/systemd/system/1helm-update.path
32+
/opt/1helm/update-host.sh
33+
/opt/1helm/uninstall-host.sh
34+
)
35+
HOST_UNITS=(1helm-lxc-net.service 1helm.service 1helm-update.path)
36+
TRANSACTION_ACTIVE=0
37+
ROLLING_BACK=0
38+
TEMP_ROOT=""
39+
PREVIOUS_RELEASE=""
40+
41+
[[ "${EUID}" -eq 0 ]] || { echo "The Linux release transaction must run as root." >&2; exit 1; }
42+
[[ "$RELEASE_ROOT" == "$RELEASES_ROOT/"* && -d "$RELEASE_ROOT" ]] \
43+
|| { echo "The Linux release transaction requires a verified retained release." >&2; exit 1; }
44+
[[ "$TARGET_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \
45+
|| { echo "The Linux release transaction requires an exact version." >&2; exit 1; }
46+
PACKAGE_VERSION="$("$NODE_LINK/bin/node" -p 'require(process.argv[1]).version' "$RELEASE_ROOT/package.json" 2>/dev/null || true)"
47+
[[ "$PACKAGE_VERSION" == "$TARGET_VERSION" ]] \
48+
|| { echo "The retained release does not match the requested version." >&2; exit 1; }
49+
[[ -x "$RELEASE_ROOT/site/public/install-lxc-runtime.sh" \
50+
&& -x "$RELEASE_ROOT/site/public/install-linux-units.sh" \
51+
&& -x "$RELEASE_ROOT/site/public/update-host.sh" \
52+
&& -x "$RELEASE_ROOT/site/public/uninstall-host.sh" \
53+
&& -x "$RELEASE_ROOT/scripts/1helm-lxc-runtime" \
54+
&& -x "$RELEASE_ROOT/scripts/1helm-lxc-net" ]] \
55+
|| { echo "The verified release is missing its Linux host contract." >&2; exit 1; }
56+
57+
TEMP_ROOT="$(mktemp -d)"
58+
chmod 0700 "$TEMP_ROOT"
59+
60+
json_string() {
61+
"$NODE_LINK/bin/node" -e 'process.stdout.write(JSON.stringify(process.argv[1] || ""))' "$1"
62+
}
63+
64+
write_status() {
65+
local state="$1" message="$2" error="${3:-}" candidate="$TEMP_ROOT/status.json"
66+
printf '{"mode":"linux-systemd","status":%s,"version":%s,"checked_at":%s,"message":%s,"error":%s}\n' \
67+
"$(json_string "$state")" "$(json_string "$TARGET_VERSION")" "$(( $(date +%s) * 1000 ))" \
68+
"$(json_string "$message")" "$([[ -n "$error" ]] && json_string "$error" || printf null)" >"$candidate"
69+
chown "$SERVICE_USER:$SERVICE_USER" "$candidate"
70+
chmod 0600 "$candidate"
71+
mv -f -- "$candidate" "$STATUS_FILE"
72+
}
73+
74+
snapshot_host_contract() {
75+
install -d -m 0700 "$TEMP_ROOT/files" "$TEMP_ROOT/units"
76+
local path encoded unit
77+
for path in "${HOST_CONTRACT_PATHS[@]}"; do
78+
if [[ -e "$path" || -L "$path" ]]; then
79+
encoded="${path#/}"
80+
install -d -m 0700 "$TEMP_ROOT/files/$(dirname "$encoded")"
81+
cp -a -- "$path" "$TEMP_ROOT/files/$encoded"
82+
fi
83+
done
84+
for unit in "${HOST_UNITS[@]}"; do
85+
systemctl is-enabled "$unit" >"$TEMP_ROOT/units/$unit.enabled" 2>/dev/null || true
86+
systemctl is-active "$unit" >"$TEMP_ROOT/units/$unit.active" 2>/dev/null || true
87+
done
88+
}
89+
90+
rollback_host_contract() {
91+
local path encoded unit enabled active restored_healthy=1
92+
ROLLING_BACK=1
93+
systemctl disable --now 1helm-update.path 1helm-lxc-net.service >/dev/null 2>&1 || true
94+
systemctl stop "$SERVICE_NAME" >/dev/null 2>&1 || true
95+
for path in "${HOST_CONTRACT_PATHS[@]}"; do
96+
encoded="${path#/}"
97+
rm -f -- "$path"
98+
if [[ -e "$TEMP_ROOT/files/$encoded" || -L "$TEMP_ROOT/files/$encoded" ]]; then
99+
install -d -m 0755 "$(dirname "$path")"
100+
cp -a -- "$TEMP_ROOT/files/$encoded" "$path"
101+
fi
102+
done
103+
if [[ -n "$PREVIOUS_RELEASE" && -d "$PREVIOUS_RELEASE" ]]; then
104+
ln -s "$PREVIOUS_RELEASE" "$TEMP_ROOT/rollback-current"
105+
mv -Tf "$TEMP_ROOT/rollback-current" "$APP_ROOT"
106+
fi
107+
systemctl daemon-reload
108+
for unit in "${HOST_UNITS[@]}"; do
109+
enabled="$(cat "$TEMP_ROOT/units/$unit.enabled" 2>/dev/null || true)"
110+
active="$(cat "$TEMP_ROOT/units/$unit.active" 2>/dev/null || true)"
111+
[[ "$enabled" == "enabled" ]] && systemctl enable "$unit" >/dev/null 2>&1 || true
112+
[[ "$active" == "active" ]] && systemctl start "$unit" >/dev/null 2>&1 || true
113+
done
114+
if [[ "$(cat "$TEMP_ROOT/units/$SERVICE_NAME.active" 2>/dev/null || true)" == "active" ]]; then
115+
restored_healthy=0
116+
for _ in {1..300}; do
117+
if curl -fsS "http://127.0.0.1:$PORT/api/setup/status" >/dev/null; then restored_healthy=1; break; fi
118+
sleep 0.2
119+
done
120+
fi
121+
TRANSACTION_ACTIVE=0
122+
ROLLING_BACK=0
123+
[[ "$restored_healthy" -eq 1 ]]
124+
}
125+
126+
cleanup_transaction() {
127+
local command_status=$?
128+
trap - EXIT
129+
if [[ "$TRANSACTION_ACTIVE" -eq 1 && "$ROLLING_BACK" -eq 0 ]]; then
130+
if rollback_host_contract; then
131+
write_status "error" "1Helm v$TARGET_VERSION failed its host health transaction; the prior healthy release was restored." "Host update failed and was rolled back." || true
132+
else
133+
write_status "error" "1Helm v$TARGET_VERSION failed and the prior host could not be proven healthy after rollback." "Host update and rollback health check failed." || true
134+
fi
135+
fi
136+
[[ -z "$TEMP_ROOT" ]] || rm -rf -- "$TEMP_ROOT"
137+
exit "$command_status"
138+
}
139+
trap cleanup_transaction EXIT
140+
141+
PREVIOUS_RELEASE="$(readlink -f "$APP_ROOT" 2>/dev/null || true)"
142+
[[ "$PREVIOUS_RELEASE" == "$RELEASES_ROOT/"* && -d "$PREVIOUS_RELEASE" ]] \
143+
|| { echo "The currently installed 1Helm release is not inside the verified release store." >&2; exit 1; }
144+
snapshot_host_contract
145+
TRANSACTION_ACTIVE=1
146+
write_status "installing" "The host verified v$TARGET_VERSION and is applying one atomic runtime and application transaction."
147+
HELM_HOST_APPLY_DELEGATED=1 "$RELEASE_ROOT/site/public/install-lxc-runtime.sh" "$RELEASE_ROOT"
148+
ln -s "$RELEASE_ROOT" "$TEMP_ROOT/current"
149+
mv -Tf "$TEMP_ROOT/current" "$APP_ROOT"
150+
HELM_HOST_APPLY_DELEGATED=1 "$RELEASE_ROOT/site/public/install-linux-units.sh" "$RELEASE_ROOT"
151+
write_status "restarting" "The host installed v$TARGET_VERSION and is restarting 1Helm."
152+
systemctl restart "$SERVICE_NAME"
153+
healthy=0
154+
for _ in {1..300}; do
155+
if curl -fsS "http://127.0.0.1:$PORT/api/setup/status" >/dev/null; then healthy=1; break; fi
156+
sleep 0.2
157+
done
158+
[[ "$healthy" -eq 1 ]] || { echo "1Helm v$TARGET_VERSION failed its host health check." >&2; exit 1; }
159+
TRANSACTION_ACTIVE=0
160+
write_status "current" "This 1Helm host is running v$TARGET_VERSION."

site/public/install-linux-units.sh

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,24 @@ NODE_LINK="$INSTALL_ROOT/node-current"
1212
STATE_ROOT="/var/lib/1helm"
1313
SERVICE_USER="1helm"
1414

15+
# Bridge upgrades from v0.0.11's too-narrow ProtectSystem=strict namespace.
16+
# The retained release has already been SHA-verified and built by the root
17+
# updater; only its fixed unit installer can be delegated.
18+
if [[ "${HELM_HOST_APPLY_DELEGATED:-}" != "1" ]] \
19+
&& awk -F: '$1 == "0" && $3 ~ /(^|\/)1helm-update\.service(\/|$)/ { found=1 } END { exit found ? 0 : 1 }' /proc/self/cgroup; then
20+
RESOLVED_RELEASE="$(readlink -f "$RELEASE_ROOT" 2>/dev/null || true)"
21+
[[ "$RESOLVED_RELEASE" == /opt/1helm/releases/* && -d "$RESOLVED_RELEASE" ]] \
22+
|| { echo "The updater can delegate only a verified retained 1Helm release." >&2; exit 1; }
23+
DELEGATE_UNIT="1helm-linux-units-apply-${RANDOM}-$$"
24+
exec systemd-run --quiet --collect --wait --pipe --unit="$DELEGATE_UNIT" \
25+
--property=Type=oneshot --property=NoNewPrivileges=false --property=PrivateTmp=true --property=ProtectHome=true \
26+
--setenv=HELM_HOST_APPLY_DELEGATED=1 \
27+
"$RESOLVED_RELEASE/site/public/install-linux-units.sh" "$RESOLVED_RELEASE"
28+
fi
29+
1530
[[ "${EUID}" -eq 0 ]] || { echo "The Linux service installer must run as root." >&2; exit 1; }
1631
[[ -n "$RELEASE_ROOT" && -d "$RELEASE_ROOT" ]] || { echo "A verified 1Helm release directory is required." >&2; exit 1; }
17-
[[ -x "$RELEASE_ROOT/site/public/update-host.sh" && -x "$RELEASE_ROOT/site/public/migrate-linux-host-contract.sh" && -x "$RELEASE_ROOT/site/public/uninstall-host.sh" ]] \
32+
[[ -x "$RELEASE_ROOT/site/public/update-host.sh" && -x "$RELEASE_ROOT/site/public/apply-linux-release.sh" && -x "$RELEASE_ROOT/site/public/migrate-linux-host-contract.sh" && -x "$RELEASE_ROOT/site/public/uninstall-host.sh" ]] \
1833
|| { echo "The verified 1Helm release is missing its host lifecycle scripts." >&2; exit 1; }
1934
id "$SERVICE_USER" >/dev/null 2>&1 || { echo "The 1Helm service account does not exist." >&2; exit 1; }
2035

@@ -67,7 +82,10 @@ NoNewPrivileges=false
6782
PrivateTmp=true
6883
ProtectHome=true
6984
ProtectSystem=strict
70-
ReadWritePaths=$INSTALL_ROOT $STATE_ROOT /var/lib/1helm-lxc /var/cache/1helm-lxc /run/lxc /usr/libexec/1helm-lxc-runtime /usr/libexec/1helm-lxc-net /etc/1helm /etc/default/lxc-net /etc/systemd/system/1helm-lxc-net.service /etc/systemd/system/1helm.service /etc/systemd/system/1helm-update.service /etc/systemd/system/1helm-update.path /etc/sudoers.d/1helm-lxc-runtime /etc/subuid /etc/subgid
85+
# Runtime and unit files are installed by atomic rename and removed during a
86+
# failed-update rollback, so their exact parent directories—not merely the old
87+
# files—must be writable inside this root-owned transaction.
88+
ReadWritePaths=$INSTALL_ROOT $STATE_ROOT /var/lib/1helm-lxc /var/cache/1helm-lxc /run/lxc /usr/libexec /etc/1helm /etc/default /etc/systemd/system /etc/sudoers.d /etc/subuid /etc/subgid
7189
EOF
7290

7391
install -m 0644 /dev/stdin /etc/systemd/system/1helm-update.path <<EOF

site/public/install-lxc-runtime.sh

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ set -euo pipefail
99
APP_SOURCE="${1:-}"
1010
INSTALL_ROOT="/opt/1helm"
1111
RUNTIME_ROOT="$INSTALL_ROOT/runtime/lxc"
12+
LXC_ROOT="/var/lib/1helm-lxc"
13+
LXC_PATH="$LXC_ROOT/containers"
14+
CACHE_BASE="/var/cache/1helm-lxc"
1215
HELPER_PATH="/usr/libexec/1helm-lxc-runtime"
1316
NETWORK_HELPER_PATH="/usr/libexec/1helm-lxc-net"
1417
CONFIG_PATH="/etc/1helm/lxc-unprivileged.conf"
@@ -17,6 +20,52 @@ SUDOERS_PATH="/etc/sudoers.d/1helm-lxc-runtime"
1720
SERVICE_USER="1helm"
1821
IMAGE_BUILD="20260723_07:42"
1922

23+
# v0.0.11's updater unit made the exact destination files writable under
24+
# ProtectSystem=strict. Atomic replacement still requires write access to each
25+
# parent directory, so an upgrade from that unit must hand the complete,
26+
# digest-qualified release transaction to a short-lived root unit outside the
27+
# old mount namespace. Fresh installs and newer updater units run directly.
28+
if [[ "${HELM_HOST_APPLY_DELEGATED:-}" != "1" ]] \
29+
&& awk -F: '$1 == "0" && $3 ~ /(^|\/)1helm-update\.service(\/|$)/ { found=1 } END { exit found ? 0 : 1 }' /proc/self/cgroup; then
30+
RELEASE_ROOT="$(readlink -f "$APP_SOURCE" 2>/dev/null || true)"
31+
[[ "$RELEASE_ROOT" == /opt/1helm/releases/* && -d "$RELEASE_ROOT" ]] \
32+
|| { echo "The updater can delegate only a verified retained 1Helm release." >&2; exit 1; }
33+
[[ -x "$RELEASE_ROOT/site/public/apply-linux-release.sh" ]] \
34+
|| { echo "The retained release is missing its atomic Linux transaction." >&2; exit 1; }
35+
TARGET_VERSION="$(/opt/1helm/node-current/bin/node -p 'require(process.argv[1]).version' "$RELEASE_ROOT/package.json" 2>/dev/null || true)"
36+
[[ "$TARGET_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \
37+
|| { echo "The retained release has no valid package version." >&2; exit 1; }
38+
LEGACY_UPDATER_PID="$(systemctl show --property=MainPID --value 1helm-update.service 2>/dev/null || true)"
39+
legacy_updater_pid_is_exact() {
40+
local pid="${1:-}" main_pid command_line
41+
[[ "$pid" =~ ^[0-9]+$ ]] && ((pid > 1)) && [[ -r "/proc/$pid/cgroup" && -r "/proc/$pid/cmdline" ]] || return 1
42+
main_pid="$(systemctl show --property=MainPID --value 1helm-update.service 2>/dev/null || true)"
43+
[[ "$main_pid" == "$pid" ]] || return 1
44+
awk -F: '$1 == "0" && $3 ~ /(^|\/)1helm-update\.service(\/|$)/ { found=1 } END { exit found ? 0 : 1 }' "/proc/$pid/cgroup" || return 1
45+
command_line="$(tr '\0' '\n' <"/proc/$pid/cmdline")"
46+
grep -Fxq '/opt/1helm/update-host.sh' <<<"$command_line"
47+
}
48+
legacy_updater_pid_is_exact "$LEGACY_UPDATER_PID" \
49+
|| { echo "The legacy updater handoff could not validate its exact systemd main process." >&2; exit 1; }
50+
DELEGATE_UNIT="1helm-release-apply-${TARGET_VERSION//./-}-${RANDOM}-$$"
51+
if systemd-run --quiet --collect --wait --pipe --unit="$DELEGATE_UNIT" \
52+
--property=Type=oneshot --property=NoNewPrivileges=false --property=PrivateTmp=true --property=ProtectHome=true \
53+
"$RELEASE_ROOT/site/public/apply-linux-release.sh" "$RELEASE_ROOT" "$TARGET_VERSION"; then
54+
exit 0
55+
fi
56+
# The delegated transaction has already restored the exact prior release,
57+
# reloaded its units, proved its HTTP health, and written the visible error.
58+
# Stop only the still-identical legacy updater main process with SIGKILL so
59+
# its EXIT trap cannot attempt a second rollback from the obsolete namespace
60+
# or overwrite that stronger evidence.
61+
if legacy_updater_pid_is_exact "$LEGACY_UPDATER_PID"; then
62+
kill -KILL "$LEGACY_UPDATER_PID"
63+
else
64+
echo "The failed release was rolled back, but the legacy updater process identity changed before it could be stopped." >&2
65+
fi
66+
exit 1
67+
fi
68+
2069
case "$(uname -m)" in
2170
x86_64|amd64)
2271
IMAGE_ARCH="amd64"
@@ -50,6 +99,12 @@ for command in curl sha256sum lxc-create lxc-attach lxc-info lxc-start lxc-stop
5099
done
51100
python3 -c 'import ensurepip' >/dev/null 2>&1 || { echo "Python venv support is unavailable after host setup." >&2; exit 1; }
52101

102+
# The service unit names these exact writable roots. They must exist before
103+
# systemd creates the service's mount namespace, even before the first channel
104+
# computer is provisioned.
105+
install -d -o root -g root -m 0711 "$LXC_ROOT" "$LXC_PATH"
106+
install -d -o root -g root -m 0700 "$CACHE_BASE"
107+
53108
TEMP_ROOT="$(mktemp -d)"
54109
trap 'rm -rf -- "$TEMP_ROOT"' EXIT
55110
ASSET_URL="https://images.linuxcontainers.org/images/ubuntu/noble/$IMAGE_ARCH/default/$IMAGE_BUILD"

site/public/update-host.sh

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ PACKAGE_VERSION="$("$NODE_LINK/bin/node" -p 'require(process.argv[1]).version' "
225225
|| fail "The verified Linux artifact version does not match its release tag."
226226
[[ -x "$STAGE/site/public/update-host.sh" ]] \
227227
|| fail "The verified Linux artifact is missing its host updater."
228-
[[ -x "$STAGE/site/public/migrate-linux-host-contract.sh" && -x "$STAGE/site/public/install-lxc-runtime.sh" && -x "$STAGE/site/public/install-linux-units.sh" && -x "$STAGE/site/public/uninstall-host.sh" && -x "$STAGE/scripts/1helm-lxc-runtime" && -x "$STAGE/scripts/1helm-lxc-net" ]] \
228+
[[ -x "$STAGE/site/public/apply-linux-release.sh" && -x "$STAGE/site/public/migrate-linux-host-contract.sh" && -x "$STAGE/site/public/install-lxc-runtime.sh" && -x "$STAGE/site/public/install-linux-units.sh" && -x "$STAGE/site/public/uninstall-host.sh" && -x "$STAGE/scripts/1helm-lxc-runtime" && -x "$STAGE/scripts/1helm-lxc-net" ]] \
229229
|| fail "The verified Linux artifact is missing its isolated LXC runtime contract."
230230
chown -R "$SERVICE_USER:$SERVICE_USER" "$STAGE"
231231

@@ -247,26 +247,15 @@ else
247247
fi
248248
chown -R "$SERVICE_USER:$SERVICE_USER" "$RELEASE_ROOT"
249249

250-
PREVIOUS_RELEASE="$(readlink -f "$APP_ROOT" 2>/dev/null || true)"
251-
[[ "$PREVIOUS_RELEASE" == "$RELEASES_ROOT/"* && -d "$PREVIOUS_RELEASE" ]] || PREVIOUS_RELEASE=""
252-
snapshot_host_contract
253-
TRANSACTION_ACTIVE=1
254-
"$RELEASE_ROOT/site/public/install-lxc-runtime.sh" "$RELEASE_ROOT" \
255-
|| fail "The verified 1Helm release could not install its isolated LXC runtime."
256-
ln -s "$RELEASE_ROOT" "$TEMP_ROOT/current"
257-
mv -Tf "$TEMP_ROOT/current" "$APP_ROOT"
258-
"$RELEASE_ROOT/site/public/install-linux-units.sh" "$RELEASE_ROOT" \
259-
|| fail "The verified 1Helm release could not migrate its Linux service contract."
260-
261-
write_status "restarting" "$TARGET_VERSION" "The host installed v$TARGET_VERSION and is restarting 1Helm."
262-
systemctl restart "$SERVICE_NAME"
263-
healthy=0
264-
for _ in {1..300}; do
265-
if curl -fsS "http://127.0.0.1:$PORT/api/setup/status" >/dev/null; then healthy=1; break; fi
266-
sleep 0.2
267-
done
268-
if [[ "$healthy" -ne 1 ]]; then
269-
fail "1Helm v$TARGET_VERSION failed its host health check; the previous release was restored when available."
250+
# One verified transient root transaction owns the runtime files, current
251+
# symlink, unit migration, restart, health check, and rollback together. This
252+
# also escapes v0.0.11's obsolete ProtectSystem=strict mount namespace before
253+
# the first host file is replaced.
254+
APPLY_UNIT="1helm-release-apply-${TARGET_VERSION//./-}-$$"
255+
if ! systemd-run --quiet --collect --wait --pipe --unit="$APPLY_UNIT" \
256+
--property=Type=oneshot --property=NoNewPrivileges=false --property=PrivateTmp=true --property=ProtectHome=true \
257+
"$RELEASE_ROOT/site/public/apply-linux-release.sh" "$RELEASE_ROOT" "$TARGET_VERSION"; then
258+
echo "1Helm v$TARGET_VERSION failed its atomic host transaction; the transaction's visible status records whether rollback was proven healthy." >&2
259+
exit 1
270260
fi
271-
TRANSACTION_ACTIVE=0
272-
write_status "current" "$TARGET_VERSION" "This 1Helm host is running v$TARGET_VERSION."
261+
exit 0

0 commit comments

Comments
 (0)