Fix: Close runtime provenance residuals
This commit is contained in:
@@ -139,25 +139,48 @@ _archive_member_is_safe() {
|
||||
|| "$member" == "$expected_top/"* ]]
|
||||
}
|
||||
|
||||
_bun_zip_entry_types_match() {
|
||||
local archive="$1" archive_top="$2" details entry_types
|
||||
details="$(unzip -Z -s "$archive")" || return 1
|
||||
entry_types="$(awk -v directory="$archive_top/" -v binary="$archive_top/bun" '
|
||||
$NF == directory || $NF == binary { print substr($1, 1, 1), $NF }
|
||||
' <<<"$details")" || return 1
|
||||
[[ "$entry_types" == "d $archive_top/"$'\n'"- $archive_top/bun" ]]
|
||||
}
|
||||
|
||||
_tree_links_stay_inside() {
|
||||
local root="$1" link resolved
|
||||
while IFS= read -r -d '' link; do
|
||||
resolved="$(realpath -m -- "$link")" || return 1
|
||||
[[ "$resolved" == "$root" || "$resolved" == "$root/"* ]] || return 1
|
||||
done < <(find "$root" -type l -print0)
|
||||
local root="$1" link resolved scan_fd scan_pid scan_status=0 invalid=0
|
||||
# Retain and wait for find's PID: a loop fed directly by process substitution
|
||||
# cannot otherwise distinguish an empty tree from a failed traversal.
|
||||
exec {scan_fd}< <(find "$root" -type l -print0)
|
||||
scan_pid=$!
|
||||
while IFS= read -r -d '' link <&"$scan_fd"; do
|
||||
resolved="$(realpath -m -- "$link")" || { invalid=1; continue; }
|
||||
[[ "$resolved" == "$root" || "$resolved" == "$root/"* ]] || invalid=1
|
||||
done
|
||||
exec {scan_fd}<&-
|
||||
wait "$scan_pid" || scan_status=$?
|
||||
(( scan_status == 0 && invalid == 0 ))
|
||||
}
|
||||
|
||||
_tree_hardlinks_stay_inside() {
|
||||
local root="$1" device inode link_count key
|
||||
local root="$1" device inode link_count key scan_fd scan_pid scan_status=0
|
||||
local invalid=0
|
||||
local -A names_in_tree=() inode_links=()
|
||||
while read -r device inode link_count; do
|
||||
exec {scan_fd}< <(find "$root" -type f -printf '%D %i %n\n')
|
||||
scan_pid=$!
|
||||
while read -r device inode link_count <&"$scan_fd"; do
|
||||
key="$device:$inode"
|
||||
names_in_tree["$key"]=$(( ${names_in_tree[$key]:-0} + 1 ))
|
||||
inode_links["$key"]="$link_count"
|
||||
done < <(find "$root" -type f -printf '%D %i %n\n')
|
||||
for key in "${!names_in_tree[@]}"; do
|
||||
[[ "${names_in_tree[$key]}" == "${inode_links[$key]}" ]] || return 1
|
||||
done
|
||||
exec {scan_fd}<&-
|
||||
wait "$scan_pid" || scan_status=$?
|
||||
(( scan_status == 0 )) || return 1
|
||||
for key in "${!names_in_tree[@]}"; do
|
||||
[[ "${names_in_tree[$key]}" == "${inode_links[$key]}" ]] || invalid=1
|
||||
done
|
||||
(( invalid == 0 ))
|
||||
}
|
||||
|
||||
_atomic_symlink() (
|
||||
@@ -348,6 +371,8 @@ _install_bun() (
|
||||
listing="$(unzip -Z1 "$archive")" || { rm -rf -- "$stage"; return 1; }
|
||||
[[ "$listing" == "$archive_top/"$'\n'"$archive_top/bun" ]] \
|
||||
|| { rm -rf -- "$stage"; return 1; }
|
||||
_bun_zip_entry_types_match "$archive" "$archive_top" \
|
||||
|| { rm -rf -- "$stage"; return 1; }
|
||||
while IFS= read -r member; do
|
||||
_archive_member_is_safe "$member" "$archive_top" \
|
||||
|| { rm -rf -- "$stage"; return 1; }
|
||||
@@ -1064,7 +1089,7 @@ _install_claude_code() {
|
||||
"$staged_repo" /etc/yum.repos.d/claude-code.repo || status=$?
|
||||
if (( status == 0 )); then
|
||||
sudo dnf install -y --repo=claude-code --repo=fedora --repo=updates \
|
||||
claude-code || status=$?
|
||||
--from-repo=claude-code claude-code || status=$?
|
||||
fi
|
||||
rm -rf -- "$work"
|
||||
return "$status"
|
||||
|
||||
@@ -379,6 +379,27 @@ for arch_spec in \
|
||||
"$artifact_root/build/codex-resources"
|
||||
rm -f -- "$artifact_root/build/codex-package.json"
|
||||
done
|
||||
python3 - "$artifact_root/bun-symlink.zip" "$artifact_root/bun-special.zip" \
|
||||
"$artifact_root/bun-directory.zip" <<'PY'
|
||||
import stat
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
for destination, directory_type, entry_type, content in (
|
||||
(sys.argv[1], stat.S_IFDIR, stat.S_IFLNK, b"../../outside-bun"),
|
||||
(sys.argv[2], stat.S_IFDIR, stat.S_IFSOCK, b'#!/usr/bin/env bash\nprintf "1.4.0\\n"\n'),
|
||||
(sys.argv[3], stat.S_IFREG, stat.S_IFREG, b'#!/usr/bin/env bash\nprintf "1.4.0\\n"\n'),
|
||||
):
|
||||
with zipfile.ZipFile(destination, "w") as archive:
|
||||
directory = zipfile.ZipInfo("bun-linux-x64/")
|
||||
directory.create_system = 3
|
||||
directory.external_attr = (directory_type | 0o755) << 16
|
||||
archive.writestr(directory, b"")
|
||||
binary = zipfile.ZipInfo("bun-linux-x64/bun")
|
||||
binary.create_system = 3
|
||||
binary.external_attr = (entry_type | 0o755) << 16
|
||||
archive.writestr(binary, content)
|
||||
PY
|
||||
mkdir -p "$artifact_root/build/wrong-node/bin" "$artifact_root/build/wrong-codex"
|
||||
printf '#!/usr/bin/env bash\nprintf "v24.20.0\\n"\n' > "$artifact_root/build/wrong-node/bin/node"
|
||||
printf '#!/usr/bin/env bash\nprintf "codex-cli 0.150.1\\n"\n' > "$artifact_root/build/wrong-codex/codex"
|
||||
@@ -391,10 +412,12 @@ printf 'unexpected\n' > "$artifact_root/build/bun-linux-x64/extra"
|
||||
chmod +x "$artifact_root/build/bun-linux-x64/bun"
|
||||
(cd "$artifact_root/build" && zip -q "$artifact_root/bun-bad.zip" \
|
||||
bun-linux-x64/ bun-linux-x64/bun bun-linux-x64/extra)
|
||||
printf 'reviewed rustdesk fixture\n' > "$artifact_root/rustdesk.rpm"
|
||||
cp "$test_tmp/unsigned-fixture.rpm" "$artifact_root/rustdesk.rpm"
|
||||
printf '#!/usr/bin/env bash\nprintf "executed\\n" > "$OUTSIDE_EXECUTED"\nprintf "v24.20.0\\n"\n' \
|
||||
> "$artifact_root/outside-node"
|
||||
chmod +x "$artifact_root/outside-node"
|
||||
printf '#!/usr/bin/env bash\nprintf "executed\\n" > "$OUTSIDE_EXECUTED"\nprintf "codex-cli 0.150.1\\n"\n' \
|
||||
> "$artifact_root/outside-codex"
|
||||
chmod +x "$artifact_root/outside-node" "$artifact_root/outside-codex"
|
||||
node_escape_top='node-v24.20.0-linux-x64'
|
||||
mkdir -p "$artifact_root/build/$node_escape_top/bin"
|
||||
ln -s "$artifact_root/outside-node" "$artifact_root/build/$node_escape_top/bin/node"
|
||||
@@ -442,6 +465,68 @@ if [[ "${STUB_NODE_ESCAPE:-}" == hardlink && "$*" == *'-xJf'* ]]; then
|
||||
rm -f -- "$node"
|
||||
ln "$ARTIFACT_ROOT/outside-node" "$node"
|
||||
fi
|
||||
if [[ -n "${STUB_TRAVERSAL_ERROR:-}" && "$*" == *'-x'* ]]; then
|
||||
destination=''
|
||||
arguments=("$@")
|
||||
for ((index = 0; index < ${#arguments[@]}; index++)); do
|
||||
if [[ "${arguments[index]}" == -C ]]; then
|
||||
destination="${arguments[index + 1]}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
[[ -n "$destination" ]] || exit 73
|
||||
case "$STUB_TRAVERSAL_ERROR" in
|
||||
Node)
|
||||
selected="$destination/node-v24.20.0-linux-x64/bin/node"
|
||||
outside="$ARTIFACT_ROOT/outside-node"
|
||||
mkdir "$destination/node-v24.20.0-linux-x64/.unreadable"
|
||||
chmod 000 "$destination/node-v24.20.0-linux-x64/.unreadable"
|
||||
;;
|
||||
Codex)
|
||||
selected="$destination/bin/codex"
|
||||
outside="$ARTIFACT_ROOT/outside-codex"
|
||||
;;
|
||||
esac
|
||||
rm -f -- "$selected"
|
||||
ln -s -- "$outside" "$selected"
|
||||
fi
|
||||
STUB
|
||||
|
||||
cat > "$case_root/bin/find" <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
if [[ -n "${STUB_TRAVERSAL_ERROR:-}" && "${1:-}" == *'.stage.'* ]]; then
|
||||
printf 'find:traversal-error:%s\n' "$STUB_TRAVERSAL_ERROR" >> "$COMMAND_LOG"
|
||||
if [[ "$STUB_TRAVERSAL_ERROR" == Node ]]; then
|
||||
root="$1"
|
||||
shift
|
||||
exec /usr/bin/find "$root/.unreadable" "$@"
|
||||
fi
|
||||
printf 'malformed traversal output'
|
||||
exit 74
|
||||
fi
|
||||
exec /usr/bin/find "$@"
|
||||
STUB
|
||||
|
||||
cat > "$case_root/bin/unzip" <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
if [[ -n "${STUB_BUN_ATTRIBUTE:-}" && "${1:-}" == -q ]]; then
|
||||
destination=''
|
||||
while (($#)); do
|
||||
if [[ "$1" == -d ]]; then
|
||||
destination="$2"
|
||||
break
|
||||
fi
|
||||
shift
|
||||
done
|
||||
[[ -n "$destination" ]] || exit 72
|
||||
mkdir -p "$destination/bun-linux-x64"
|
||||
cp "$ARTIFACT_ROOT/bun-x86_64.bin" "$destination/bun-linux-x64/bun"
|
||||
chmod +x "$destination/bun-linux-x64/bun"
|
||||
exit 0
|
||||
fi
|
||||
exec /usr/bin/unzip "$@"
|
||||
STUB
|
||||
|
||||
cat > "$case_root/bin/mv" <<'STUB'
|
||||
@@ -548,7 +633,8 @@ case "$url" in
|
||||
;;
|
||||
*node-v24.20.0-linux-arm64.tar.xz) cp "$ARTIFACT_ROOT/node-aarch64.tar.xz" "$output" ;;
|
||||
*bun-linux-x64.zip)
|
||||
cp "$ARTIFACT_ROOT/${STUB_BAD_LAYOUT:+bun-bad.zip}" "$output" 2>/dev/null \
|
||||
cp "$ARTIFACT_ROOT/${STUB_BUN_ATTRIBUTE:+bun-$STUB_BUN_ATTRIBUTE.zip}" "$output" 2>/dev/null \
|
||||
|| cp "$ARTIFACT_ROOT/${STUB_BAD_LAYOUT:+bun-bad.zip}" "$output" 2>/dev/null \
|
||||
|| cp "$ARTIFACT_ROOT/bun-x86_64.zip" "$output"
|
||||
;;
|
||||
*bun-linux-aarch64.zip) cp "$ARTIFACT_ROOT/bun-aarch64.zip" "$output" ;;
|
||||
@@ -570,6 +656,15 @@ if [[ "${STUB_DIGEST_MISMATCH:-}" == 1 ]]; then
|
||||
printf '%064d %s\n' 0 "$file"
|
||||
exit 0
|
||||
fi
|
||||
if [[ "${STUB_TRAVERSAL_ERROR:-}" == Node ]] \
|
||||
&& cmp -s "$file" "$ARTIFACT_ROOT/outside-node"; then
|
||||
printf '%s %s\n' 89af8424dd53e560b1933f87ba650d8bf57c83ca5a04600eefb31f416aabbae7 "$file"
|
||||
exit 0
|
||||
elif [[ "${STUB_TRAVERSAL_ERROR:-}" == Codex ]] \
|
||||
&& cmp -s "$file" "$ARTIFACT_ROOT/outside-codex"; then
|
||||
printf '%s %s\n' abf1bb1643a79f73aa78ee627e111e02d4f8c98f25813a0cf6ce277709664386 "$file"
|
||||
exit 0
|
||||
fi
|
||||
for spec in \
|
||||
'node-x86_64.tar.xz 2f2c0da162318f0de47665410c7c8c2ed3d36c8f3105de4bbc61176c70a7cbf2' \
|
||||
'node-aarch64.tar.xz 5f4ddab610c1ab2016b3c227cebdbf6d9495161487e4739c7b90090595f465f7' \
|
||||
@@ -578,12 +673,18 @@ for spec in \
|
||||
'bun-x86_64.zip 2d03fb5fb83ac8b567aca0a281b2ce1a1a19d488f56c2968d88c3f25e92fe452' \
|
||||
'bun-aarch64.zip 4b1a332ee861983eb93bcfe6f770fff94e3e31b2c388bdaea3c8ed35e58eed0e' \
|
||||
'bun-bad.zip 2d03fb5fb83ac8b567aca0a281b2ce1a1a19d488f56c2968d88c3f25e92fe452' \
|
||||
'bun-symlink.zip 2d03fb5fb83ac8b567aca0a281b2ce1a1a19d488f56c2968d88c3f25e92fe452' \
|
||||
'bun-special.zip 2d03fb5fb83ac8b567aca0a281b2ce1a1a19d488f56c2968d88c3f25e92fe452' \
|
||||
'bun-directory.zip 2d03fb5fb83ac8b567aca0a281b2ce1a1a19d488f56c2968d88c3f25e92fe452' \
|
||||
'codex-x86_64.tar.gz 00aba704f029f6dc0d948be407a756e0c97cc840132fd691353b2c6b0a505b17' \
|
||||
'codex-aarch64.tar.gz 1ecac3f87823efb98153233b076ea3d6e34a7a8cebe43c5285dc5f79e1514639' \
|
||||
'codex-bad.tar.gz 00aba704f029f6dc0d948be407a756e0c97cc840132fd691353b2c6b0a505b17' \
|
||||
'rustdesk.rpm eb1b053ac5b2f774f2271f7fbbfd2ea475899f7a55135c5e172bc54b9388f108'; do
|
||||
read -r fixture digest <<<"$spec"
|
||||
if cmp -s "$file" "$ARTIFACT_ROOT/$fixture"; then
|
||||
if [[ "$fixture" == rustdesk.rpm ]]; then
|
||||
stat -c '%d:%i' "$file" > "$VERIFIED_RUSTDESK_INODE"
|
||||
fi
|
||||
printf '%s %s\n' "$digest" "$file"
|
||||
exit 0
|
||||
fi
|
||||
@@ -695,11 +796,24 @@ printf 'sudo:%s\n' "${logged[*]}" >> "$COMMAND_LOG"
|
||||
if [[ "$original" == *'/rustdesk.rpm'* ]]; then
|
||||
[[ "$original" == *'--setopt=localpkg_gpgcheck=0'* \
|
||||
&& "$original" != *'--setopt=localpkg_gpgcheck=1'* ]] || exit 70
|
||||
rustdesk_path=''
|
||||
for argument in "$@"; do
|
||||
[[ "$(basename -- "$argument")" != rustdesk.rpm ]] || rustdesk_path="$argument"
|
||||
done
|
||||
[[ "$rustdesk_path" == "$TMPDIR"/tmp.*/rustdesk.rpm \
|
||||
&& -s "$VERIFIED_RUSTDESK_INODE"
|
||||
&& "$(stat -c '%d:%i' "$rustdesk_path")" == "$(<"$VERIFIED_RUSTDESK_INODE")" ]] \
|
||||
&& cmp -s "$rustdesk_path" "$UNSIGNED_RPM" || exit 72
|
||||
/usr/bin/rpm -qp --queryformat '%{NAME}\n' "$rustdesk_path" >/dev/null || exit 73
|
||||
signature_status="$(/usr/bin/rpmkeys --checksig --verbose "$rustdesk_path")" || exit 74
|
||||
[[ "$signature_status" == *'Header SHA256 digest: OK'* \
|
||||
&& "$signature_status" == *'Payload SHA256 digest: OK'* \
|
||||
&& "${signature_status,,}" != *signature* ]] || exit 75
|
||||
fi
|
||||
if [[ "$original" == *' pnpm' || "$original" == *' claude-code' ]]; then
|
||||
case "$original" in
|
||||
'dnf install -y --repo=fedora --repo=updates pnpm'|\
|
||||
'dnf install -y --repo=claude-code --repo=fedora --repo=updates claude-code') ;;
|
||||
'dnf install -y --repo=claude-code --repo=fedora --repo=updates --from-repo=claude-code claude-code') ;;
|
||||
*) exit 71 ;;
|
||||
esac
|
||||
fi
|
||||
@@ -869,6 +983,7 @@ run_installer_function() {
|
||||
make_stub_commands "$case_root"
|
||||
: > "$case_root/commands.log"
|
||||
printf '0\n' > "$case_root/install-counter"
|
||||
: > "$case_root/verified-rustdesk-inode"
|
||||
printf 'preserved\n' > "$case_root/flatpak-state"
|
||||
: > "$case_root/softly-failed"
|
||||
case "${STUB_SEED_OLD:-}" in
|
||||
@@ -1046,6 +1161,8 @@ run_installer_function() {
|
||||
STUB_FLATPAK_STATE="$case_root/flatpak-state" \
|
||||
STUB_FLATPAK_REPO="$case_root/flatpak-repo" \
|
||||
STUB_INSTALL_COUNTER="$case_root/install-counter" \
|
||||
VERIFIED_RUSTDESK_INODE="$case_root/verified-rustdesk-inode" \
|
||||
UNSIGNED_RPM="$test_tmp/unsigned-fixture.rpm" \
|
||||
LC_ALL="${STUB_CALLER_LOCALE:-C}" \
|
||||
HOME="$case_root/home" \
|
||||
NVM_DIR="$case_root/home/.nvm" \
|
||||
@@ -1230,6 +1347,24 @@ for escape_kind in symlink hardlink; do
|
||||
assert_no_runtime_staging "$name"
|
||||
done
|
||||
|
||||
# A traversal command that cannot read/describe the staged tree is itself a
|
||||
# trust failure. Node traverses an unreadable subtree and Codex emits malformed
|
||||
# output before failing; scanners that lose the status execute the outside tool.
|
||||
for traversal_spec in 'Node install_node' 'Codex install_codex'; do
|
||||
read -r component function_name <<<"$traversal_spec"
|
||||
name="${component,,}-traversal-error"
|
||||
reset_installer_fixture
|
||||
STUB_TRAVERSAL_ERROR="$component" expect_failure \
|
||||
run_installer_function "$name" "$function_name"
|
||||
grep -qFx "find:traversal-error:$component" \
|
||||
"$test_tmp/cases/$name/commands.log" \
|
||||
|| fail "$name did not exercise the traversal error"
|
||||
[[ ! -e "$test_tmp/cases/$name/outside-executed" ]] \
|
||||
|| fail "$name executed a target hidden by a failed traversal"
|
||||
assert_soft_failure "$name" "$component"
|
||||
assert_no_runtime_staging "$name"
|
||||
done
|
||||
|
||||
# A bad digest or interrupted transfer cannot replace the previously active
|
||||
# tool and cannot leave reusable bytes behind.
|
||||
for failure_mode in digest interrupted; do
|
||||
@@ -1366,6 +1501,20 @@ for layout_case in \
|
||||
assert_no_runtime_staging "$name"
|
||||
done
|
||||
|
||||
# ZIP names alone do not establish entry type. The extraction adapter
|
||||
# deliberately materializes all crafted entries as regular executable files,
|
||||
# so only central-directory attribute validation can reject them pre-extraction.
|
||||
for attribute_kind in symlink special directory; do
|
||||
name="bun-$attribute_kind-attribute"
|
||||
reset_installer_fixture
|
||||
STUB_BUN_ATTRIBUTE="$attribute_kind" expect_failure \
|
||||
run_installer_function "$name" install_bun
|
||||
assert_soft_failure "$name" Bun
|
||||
[[ ! -e "$test_tmp/cases/$name/home/.bun/versions/1.4.0" ]] \
|
||||
|| fail "$name activated a ZIP entry with non-regular metadata"
|
||||
assert_no_runtime_staging "$name"
|
||||
done
|
||||
|
||||
# Legacy version-only and mismatched collisions are preserved and reported;
|
||||
# neither can be deleted and recreated or treated as installer provenance.
|
||||
for collision_mode in legacy collision; do
|
||||
@@ -1503,7 +1652,7 @@ rpm:release
|
||||
gpg:fingerprint:31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE
|
||||
sudo:install:claude-code.asc:/etc/pki/rpm-gpg/RPM-GPG-KEY-claude-code-panama
|
||||
sudo:install:claude-code.repo:/etc/yum.repos.d/claude-code.repo
|
||||
sudo:dnf install -y --repo=claude-code --repo=fedora --repo=updates claude-code
|
||||
sudo:dnf install -y --repo=claude-code --repo=fedora --repo=updates --from-repo=claude-code claude-code
|
||||
EXPECTED
|
||||
)"
|
||||
cmp -s "$installer_fixture/setup/provenance/keys/claude-code.asc" \
|
||||
|
||||
Reference in New Issue
Block a user