moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,105 @@
# Adversarial Verifier Verdict — Installer Arc (Steal #5)
**Date:** 2026-07-10
**Verifier:** adversarial Opus (read-only on code; non-destructive probes only)
**Scope:** 6 commits on `feat/steal-5-installer` over main `b8c65c22`
(`94cc971e` plan · `3715b349` Wave 1 · `ce20af22` liveness fix · `364772ec` Wave 2 CI+docs ·
`31295ad0` CI runner-context fix · `9606414d` exec-bit restore).
**Contract:** `docs/plans/INSTALLER-ARC-2026-07-10.md` (decisions D1-D10).
## VERDICT: APPROVED (with residuals)
No CRITICAL or HIGH findings. The installer's security posture is genuinely strong on the
dimensions that matter most for `curl | bash` software: **no user-controlled value reaches any
shell/eval sink**, the whole script is wrapped in `main()` invoked on the last line (partial
download cannot execute a destructive prefix), idempotency/marker timing is correct, and the CI
smoke job is a real from-scratch Linux E2E that gates regressions (no always-green trap).
Residuals below are 1 MEDIUM + 5 LOW, all non-blocking.
---
## What I tried to break, and what held
### 1. Injection (hostile flag values / wizard answers) — HELD
Traced every user-controlled value (`--dir --port --data-dir --branch --local-source` + the five
`/dev/tty` wizard answers) to every sink:
- `git clone --depth 1 --branch "$BRANCH" "$REPO_URL" "$INSTALL_DIR"`, `mkdir -p "$INSTALL_DIR"`,
`cd "$INSTALL_DIR"`, the `tar` pipelines, `bash …/waggle-server.sh start --port "$PORT" --data-dir "$DATA_DIR"`
— all pass values as **quoted, already-expanded arguments**. Bash does **not** re-parse variable
*contents* for command substitution or word-into-command splitting, so `--data-dir '$(rm -rf ~)'`
is stored and used as a literal directory name; the `$( )` never executes.
- The install marker (`write_marker`, install.sh:267-288) is the one place answers cross into a
file, and it does so exactly per **D6**: `PORT/DATA_DIR/BRANCH` are passed as **environment
variables** into `node -e` and emitted via `JSON.stringify`; the marker path is `argv[1]`. Zero
shell interpolation. This is belt-and-suspenders — no other sink evals user input either.
- `verify_runtime` runs a **constant** node script; there is no `eval` anywhere in either file.
Result: **D6 satisfied and exceeded.** No injection path found.
### 2. `curl | bash` semantics — HELD (design strength)
- **Partial-download execution:** all logic lives in functions; the only top-level statements
before `main` are colour setup, the `trap`, arg-parsing (a no-op when `$@` is empty, as under
`curl|bash`), and OS detection — all harmless. `main` is literally the **last line** (install.sh:367).
A truncated download therefore either syntax-errors before running anything, or defines functions
and hits EOF before `main`**nothing destructive runs**. Correct pattern.
- **/dev/tty absence (CI/docker):** `ask`/`ask_yesno`/`run_wizard` all guard `[ ! -e /dev/tty ]`
(and `--yes`) → silently take defaults. Verified.
- **Ctrl-C mid-install:** `trap on_interrupt INT` restores tty (`stty sane`) and aborts; a partial
dir without a marker is picked up by the resume branch next run.
### 3. Idempotency / resume — HELD
3-way branch (install.sh:324-340) is correct; the marker is written **after** build+verify and
**before** start (install.sh:357), so its presence means "fully built." Failure between clone and
marker → next run resumes (skips clone, re-runs `npm install`+`build:packages`). Re-run with marker
`exit 0` with upgrade hint. `write_marker` timestamp-backs-up any existing marker.
### 4. `waggle-server.sh` process management — mostly HELD (see MEDIUM/LOW residuals)
Health-first liveness is the right call. `stop` **refuses to signal** when `/health` responds but no
pidfile exists (won't kill an unidentified process on the port). `read_pid` sanitizes to digits.
Stale pidfile (dead PID, free port) → `start` proceeds correctly. tasklist/taskkill fallbacks are
correct for the msys/Git-Bash native-PID false-negative case.
### 5. Plan-vs-impl drift — D1-D10 all implemented as specified
Verified each decision against code. Only wording drift: D7 says "pure-bash timeout shim," but the
health poll actually depends on `curl`/`wget` (the script comment even states "no pure-bash HTTP").
See MEDIUM-1.
### 6. CI gating — HELD
`installer-smoke.yml` triggers on push/PR touching `install.sh`, `scripts/waggle-server.sh`, or the
workflow; runs `shellcheck` as a **failing** gate, then a real from-scratch install → boot →
assert `/health` 200 → status → stop → **assert port freed**. No `|| true` / `continue-on-error`
on any assertion (the `|| true` / `|| echo 000` occurrences are all in diagnostics/polling, not
masking a gate). **CI run 29096830297 = success, on HEAD commit `9606414d`** (verified via `gh`).
shellcheck therefore passes on both scripts (proven by the green run).
### 7. CLAUDE.md §7 security — HELD
No hardcoded secrets in any scope file (grepped). No `eval`/dynamic-require. Vault untouched.
`.gitattributes` addition (`*.sh text eol=lf`) is clean and does not disturb existing binary pins.
---
## Residuals (non-blocking)
| # | Sev | File:line | Issue | Break scenario / note |
|---|-----|-----------|-------|-----------------------|
| M-1 | MEDIUM | `scripts/waggle-server.sh:82-91,141-149` | Health poll requires `curl` **or** `wget`; neither is checked by `install.sh` preflight. Contradicts D7's "pure-bash timeout shim." | On a minimal box with neither tool (e.g. some Alpine images), `start` spawns a **healthy** sidecar but `http_ok` returns 2, so `wait_for_health` times out at 60s and reports "did not become healthy" + exits 1 — a false failure. Non-security; clear error + log tail, no corruption. curl/wget is present on essentially every VPS/homelab image and is proven on the CI ubuntu runner, so real-world impact is low. Fix options: `/dev/tcp` fallback in `http_ok`, or add a curl/wget check to install.sh preflight. |
| L-1 | LOW | `install.sh:246-252` | Predictable temp path `/tmp/waggle-verify.$$` for stderr capture (CWE-377). | On a shared multi-user box an attacker who wins a PID-guess race could pre-symlink the path and have the `2>` truncate a file the user can write. Single-user localhost typical; low probability. Fix: `mktemp`. |
| L-2 | LOW | `scripts/waggle-server.sh:214-253` | PID-reuse in `stop`: if our server is down but the recorded integer PID was reused by an unrelated process **not** serving `/health` on our port, `signal_pid TERM/KILL` targets that process. | Standard pidfile-manager risk, single-user localhost; mitigated by the health-first early-return and the "refuse to signal unknown PID" guard. Matches industry norm. |
| L-3 | LOW | `install.sh:78-79`; `waggle-server.sh:58-59` | `--port` is never validated as numeric. | `--port abc` yields a broken-but-safe install (health never comes up → clear timeout). No injection. CLAUDE.md coding-style asks for boundary validation — worth a `[[ "$PORT" =~ ^[0-9]+$ ]]` guard. |
| L-4 | LOW | README.md:60 / getting-started.md:18 | The documented one-liner points at `raw.githubusercontent.com/marolinik/waggle-os/main/install.sh`, but **install.sh is not on `main` yet** (only on `feat/steal-5-installer`). | The docs describe the post-merge reality. Slug `marolinik/waggle-os` is correct and the repo is PUBLIC. **Sequencing dependency:** the merge must land install.sh on `main` for the published one-liner to resolve; the README/getting-started edits should not be published ahead of that merge. Honest, not a lie — but flag the ordering. |
| L-5 | LOW | `install.sh:88-90` | `--help` uses `sed -n '2,40p' "$0"`; under `curl \| bash`, `$0` is `bash`, so `--help` won't print the header. | Cosmetic; nobody pipes `--help`. Works when run as `./install.sh --help`. |
## Informational (out of scope / by design)
- getting-started.md Options 2-3 reference `github.com/marolinik/waggle` (desktop releases + a
`git clone …/waggle.git`) while origin is `marolinik/waggle-os`. These lines are **pre-existing**
(not introduced by this arc) — noted for a future docs pass, not this verdict.
- The resume branch (`install.sh:330-337`) runs `npm install` on pre-existing directory contents
when a non-marked dir exists. This trusts the directory by design (it is the user's own machine);
a pre-seeded malicious `package.json` postinstall would run, but that presupposes attacker write
access to the user's home dir. Acceptable under the local trust model.
---
**Bottom line:** the security-critical properties (no injection, partial-download safety, correct
idempotency, honest git-over-TLS acquisition, real gating CI) all hold. Ship it; address M-1 and
L-1/L-3 as fast-follows, and ensure the merge lands `install.sh` on `main` before the README
one-liner is relied upon (L-4).