An upgrade over a pre-0.7.0 flat /opt/box skips setup-host: box --version says 0.8.0 while box-firewall stays pre-#102 #115

Closed
opened 2026-07-19 21:52:21 +00:00 by dan-claude-bot · 0 comments
dan-claude-bot commented 2026-07-19 21:52:21 +00:00 (Migrated from github.com)

Found during the 0.8.0 release drill on a real Incus host. box --version reports the new version while the host-side scripts stay at the old one, so a fix shipped in the new release is silently not deployed.

What happened, exactly

The drill host had a pre-0.7.0 flat /opt/box tree (a bare 0.6.0 install, not the versions/<v> layout). Installing 0.8.0 over it:

  1. The migration block converts the flat tree into versions/0.6.0/.
  2. That migration runs before had_install is computed.
  3. had_install therefore sees an existing install, and host/setup-host.sh is skipped.
  4. /opt/box/current -> versions/0.8.0 and /usr/local/bin/box are updated correctly.

Result: the CLI is 0.8.0, but every artifact setup-host.sh is responsible for installing stays whatever the old version left behind.

The concrete damage: the #102 fix does not land

/usr/local/sbin/box-firewall is the script box-firewall.service runs. On this host, post-upgrade, line 22 is still:

if command -v ufw >/dev/null && ufw status 2>/dev/null | grep -q "Status: active"; then

That is exactly the shape #102 removed. 0.8.0 ships the fix — host/box-firewall.sh reads ufw status once into a variable — and 0.8.0's own comment states what the old form costs:

grep -q matches the first line and exits immediately, closing the read end while ufw is still writing... ufw then dies of SIGPIPE (141)... under set -o pipefail the PIPELINE reports 141, so the if reads false and a host with UFW plainly active takes the no-UFW branch — installing the nft fallback table and never building the DNS carve-out its persisted rules are counting on. Measured at ~2% per invocation.

So the operator most likely to upgrade — someone on a UFW host who read the #102 entry in the release notes — is precisely the one who does not receive it. box --version tells them they have it.

Verified by diff on the drill host: diff /usr/local/sbin/box-firewall /opt/box/versions/0.8.0/host/box-firewall.sh shows the installed script is the pre-fix one.

Why this is worth fixing rather than documenting

The failure is silent and self-concealing. There is no error, no warning, and the version string actively asserts the opposite. Nothing on the host reports that setup-host was skipped, and drill/doctor.sh reports clean throughout — correctly, since it checks host state (networks, ACLs, nft tables), not whether the host scripts match the installed version.

It also is not limited to box-firewall. Anything setup-host.sh installs or converges is exposed the same way; box-firewall is simply where it was caught, because 0.8.0 happens to change that file.

Scope

Affects any host upgrading from a pre-0.7.0 flat /opt/box to 0.7.0+. Hosts already on the versions/<v> layout are unaffected — for them had_install is true for the right reason and the same skip is correct.

That makes this narrow but not hypothetical: it is the upgrade path for every host installed before the versioned layout shipped, and it was hit on the very first real host the drill touched.

Suggested fix

Compute had_install before the migration block, so a flat tree is recognised as "pre-versioned layout" rather than as "already installed in the current layout". A tree needing migration has by definition never been converged by this version's setup-host.

A cheaper alternative that does not reorder anything: record the version that last ran setup-host (e.g. in INSTALLED_FROM or a sibling file) and re-run it whenever the installed version differs. That also closes the general case — any future release that changes a host-side script gets it deployed — rather than just this migration.

Either way, worth adding an assertion that the installed /usr/local/sbin/box-firewall matches $CURRENT/host/box-firewall.sh, since that is the check that would have caught this without a drill.

Not the same as #107

#107 is about drill/wipe.sh carrying the #102 shape in a script that is safe because it lacks pipefail. This is different: the real, pipefail-carrying box-firewall on a live host, still unfixed after an upgrade that was supposed to fix it.

Refs

Found during the release: 0.8.0 drill (#114). Related: #102 (the SIGPIPE race itself), #107 (the same shape in the drill), #71 (the global-install requirement that makes /opt/box the operative tree).

Found during the 0.8.0 release drill on a real Incus host. **`box --version` reports the new version while the host-side scripts stay at the old one**, so a fix shipped in the new release is silently not deployed. ## What happened, exactly The drill host had a **pre-0.7.0 flat `/opt/box` tree** (a bare 0.6.0 install, not the `versions/<v>` layout). Installing 0.8.0 over it: 1. The migration block converts the flat tree into `versions/0.6.0/`. 2. That migration runs **before** `had_install` is computed. 3. `had_install` therefore sees an existing install, and `host/setup-host.sh` is **skipped**. 4. `/opt/box/current -> versions/0.8.0` and `/usr/local/bin/box` are updated correctly. Result: the CLI is 0.8.0, but every artifact `setup-host.sh` is responsible for installing stays whatever the old version left behind. ## The concrete damage: the #102 fix does not land `/usr/local/sbin/box-firewall` is the script `box-firewall.service` runs. On this host, post-upgrade, line 22 is still: ```bash if command -v ufw >/dev/null && ufw status 2>/dev/null | grep -q "Status: active"; then ``` That is exactly the shape #102 removed. 0.8.0 ships the fix — `host/box-firewall.sh` reads `ufw status` once into a variable — and 0.8.0's own comment states what the old form costs: > `grep -q` matches the first line and exits immediately, closing the read end while ufw is still writing... ufw then dies of SIGPIPE (141)... under `set -o pipefail` the PIPELINE reports 141, so the `if` reads false and **a host with UFW plainly active takes the no-UFW branch** — installing the nft fallback table and never building the DNS carve-out its persisted rules are counting on. Measured at **~2% per invocation**. So the operator most likely to upgrade — someone on a UFW host who read the #102 entry in the release notes — is precisely the one who does not receive it. `box --version` tells them they have it. Verified by diff on the drill host: `diff /usr/local/sbin/box-firewall /opt/box/versions/0.8.0/host/box-firewall.sh` shows the installed script is the pre-fix one. ## Why this is worth fixing rather than documenting The failure is **silent and self-concealing**. There is no error, no warning, and the version string actively asserts the opposite. Nothing on the host reports that `setup-host` was skipped, and `drill/doctor.sh` reports `clean` throughout — correctly, since it checks host *state* (networks, ACLs, nft tables), not whether the host *scripts* match the installed version. It also is not limited to `box-firewall`. Anything `setup-host.sh` installs or converges is exposed the same way; `box-firewall` is simply where it was caught, because 0.8.0 happens to change that file. ## Scope Affects any host upgrading from a **pre-0.7.0 flat `/opt/box`** to 0.7.0+. Hosts already on the `versions/<v>` layout are unaffected — for them `had_install` is true for the right reason and the same skip is correct. That makes this narrow but not hypothetical: it is the upgrade path for every host installed before the versioned layout shipped, and it was hit on the very first real host the drill touched. ## Suggested fix Compute `had_install` **before** the migration block, so a flat tree is recognised as "pre-versioned layout" rather than as "already installed in the current layout". A tree needing migration has by definition never been converged by this version's `setup-host`. A cheaper alternative that does not reorder anything: record the version that last ran `setup-host` (e.g. in `INSTALLED_FROM` or a sibling file) and re-run it whenever the installed version differs. That also closes the general case — any future release that changes a host-side script gets it deployed — rather than just this migration. Either way, worth adding an assertion that the installed `/usr/local/sbin/box-firewall` matches `$CURRENT/host/box-firewall.sh`, since that is the check that would have caught this without a drill. ## Not the same as #107 #107 is about `drill/wipe.sh` carrying the #102 *shape* in a script that is safe because it lacks `pipefail`. This is different: the real, `pipefail`-carrying `box-firewall` on a live host, still unfixed after an upgrade that was supposed to fix it. ## Refs Found during the `release: 0.8.0` drill (#114). Related: #102 (the SIGPIPE race itself), #107 (the same shape in the drill), #71 (the global-install requirement that makes `/opt/box` the operative tree).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/box#115
No description provided.