diff --git a/CHANGELOG.md b/CHANGELOG.md index d3e6428..0b1c33e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,27 @@ +## v0.146.1 — R-861 review fixes: the signed update flips a root-owned copy; no Wants=/continuations in mount units; the escrow read follows no symlink anywhere (2026-10-05) + +Released by `scripts/release-agent.sh`: binary sha256 `badd6c9a2e40c8bfe856d2d1a203443b21b7eb92ecc35d6090ab44518d4d082a`, +config bundle sha256 `42333e969028867ad8142335e6c1bc4040eec231de0d8d330c2d4b2cf7bc3442`. **Supersedes v0.146.0, which +was released but never vouched or delivered to any box.** The same order applies: signed `agent_update` first, then the +signed `agent_config_update`. + +A background security review of the v0.146.0 commit found three holes in the new code; each is fixed and red-proved +(`felhom.eu/documentation/audits/hub-safety-2026-10-05/partF/red-proof.txt`, S1–S3): + +- **S1 — a race in the signed update.** `felhom-os-apply` hashed the agent's staged file and then let the A/B wrapper copy + it BY PATH; the agent owns that directory and could swap the file in between. Now the root step reads the file ONCE + (`read_staged_once`: O_NOFOLLOW, fstat, owner, size), hashes those bytes, writes them to a root-owned directory + (`/var/lib/felhom-os-apply/agent-update/`) and hands ONLY that copy to `felhom-selfupdate-guarded apply`, which now + refuses any other directory, a symlink, or a file not owned by root. Tests: `AgentUpdate` (+1), + `SelfupdateWrapperConfinement`. +- **S2 — an allowlist escape in `felhom-priv-apply`.** `[Unit]` accepted `Wants=`/`Requires=`/`Before=` naming any unit, so + a mount unit could start e.g. `reboot.target`. `[Unit]` now holds only `Description` and `After=local-fs-pre.target` + (what the renderers write), and any line ending in a backslash (a systemd continuation this parser would read + differently) is refused. Tests `test_U2_wants_starts_another_unit`, `test_U2_continuation_line`. +- **S3 — a path traversal in the escrow read.** `O_NOFOLLOW` guards only the last component; a symlinked DIRECTORY in the + agent's own state dir still redirected the root read. `readStagedNoFollow` now walks the path from `/` with + `openat(O_NOFOLLOW)` per component. Test `TestAttach_RefusesASymlinkedDirectory`. + ## v0.146.0 — the agent's root grants narrowed: exact sudo patterns, a root content checker, fixed files from the bundle, the signed update checked as root (R-861) (2026-10-05) Released by `scripts/release-agent.sh`: binary sha256 `b860af465076041e07f35fed1b12d64ae2b2985d8995f0ce167418d39c2b00d5`,