Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
7 changes: 5 additions & 2 deletions internal/std/archive/archive.shellf
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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.
Expand Down
35 changes: 35 additions & 0 deletions test/e2e/plans/adverse-archive.extract-member-dash.shellf
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Loading