diff --git a/CHANGELOG.md b/CHANGELOG.md index e92423e..b6337de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixed +- `archive.extract-member` passes the member name to `tar` after `--`. A member named `-rf.txt` was read as options, so the observe saw a mismatch and the apply failed on every run. The name comes from whoever built the archive, not from the plan (#639). + - The README no longer teaches the observe that cost #486: its `def` example asked `dpkg -s`, which exits 0 for a package removed without `--purge`. It now carries `apt.install`'s real question and says why. `--json`, `-v`, `--parallel` and `--limit` are documented, and the secrets note states the limit that is left rather than one ADR-0025 closed (#642). - `htpasswd.entry` and `system.timezone` observe everything their apply sets. The first left a credentials file world-readable whenever its hash already verified; the second left `/etc/timezone` naming another zone whenever the symlink was right. Both reported `already`. Two adverse cases, each seen red on a real machine first (#635, #636). diff --git a/internal/std/archive/archive.shellf b/internal/std/archive/archive.shellf index 397f713..657bec1 100644 --- a/internal/std/archive/archive.shellf +++ b/internal/std/archive/archive.shellf @@ -59,7 +59,7 @@ def extract-member(src: str, dst: str, member: str) { observe { return state(extracted: shell { test -f "$dst" && - [ "$(sha256sum "$dst" | cut -d' ' -f1)" = "$(tar xzOf "$src" "$member" | sha256sum | cut -d' ' -f1)" ] + [ "$(sha256sum "$dst" | cut -d' ' -f1)" = "$(tar xzOf "$src" -- "$member" | sha256sum | cut -d' ' -f1)" ] }.exit == 0) } apply { @@ -72,7 +72,10 @@ def extract-member(src: str, dst: str, member: str) { # `file.write` and #599 for `file.download`. staged="$dst.shellf.$$" trap 'rm -f "$staged"' EXIT - tar xzOf "$src" "$member" > "$staged" + # `--`: the member name is chosen by whoever built the archive, and one + # starting with a dash is read as options — `tar: You may not specify more than + # one '-Acdtrux'` — on both this line and the observe's (#639). + tar xzOf "$src" -- "$member" > "$staged" # A redirection onto an existing file kept its mode; a rename does not. Carried # over explicitly, or a second extraction drops the `+x` a plan set with # `file.mode` — the regression #599 found the first time. diff --git a/test/e2e/plans/adverse-archive.extract-member-dash.shellf b/test/e2e/plans/adverse-archive.extract-member-dash.shellf new file mode 100644 index 0000000..6fb5558 --- /dev/null +++ b/test/e2e/plans/adverse-archive.extract-member-dash.shellf @@ -0,0 +1,35 @@ +# `archive.extract-member` with a member whose name starts with a dash (#639). +# +# A hostile *argument*, and the argument comes from the archive rather than from the plan: +# the member name is chosen by whoever built the tarball. Passed to `tar` with no `--`, a +# name like `-rf.txt` is read as options — `tar: You may not specify more than one +# '-Acdtrux' …` — so the observe reads a mismatch and the apply fails, for ever. +# +# Its own file rather than an extension of `adverse-archive.extract-member.shellf`: one plan +# per case (adverse-cases.md), and that plan already carries the space/quote/`&` case. +# +# The fixture needs no `--transform`: `tar czf … -C src -- -rf.txt` stores the bare name, +# which `tar czf … .` never produces — that is exactly why this is reachable and rare. +on target { + as root { + unsafe shell { + rm -rf /tmp/adv-archive-dash + mkdir -p /tmp/adv-archive-dash/src + printf 'dashed\n' > "/tmp/adv-archive-dash/src/-rf.txt" + printf 'other\n' > /tmp/adv-archive-dash/src/plain.txt + tar czf /tmp/adv-archive-dash/a.tar.gz -C /tmp/adv-archive-dash/src -- "-rf.txt" plain.txt + # The name really is bare in the archive; a `./` prefix would defuse the case + # and it would pass for the wrong reason. + tar tzf /tmp/adv-archive-dash/a.tar.gz | grep -qx -- '-rf.txt' || exit 1 + } + + archive.extract-member("/tmp/adv-archive-dash/a.tar.gz", + "/tmp/adv-archive-dash/out.txt", + "-rf.txt") + shell { + grep -qxF 'dashed' /tmp/adv-archive-dash/out.txt || exit 1 + grep -qxF 'other' /tmp/adv-archive-dash/out.txt && exit 1 + exit 0 + } + } +}