127 lines
5.1 KiB
Shell
Executable file
127 lines
5.1 KiB
Shell
Executable file
#!/usr/bin/env bash
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
set -Eeuo pipefail
|
|
|
|
bin=${1:-./zupt}
|
|
repo_root=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd -P)
|
|
case $bin in
|
|
/*) ;;
|
|
*) bin="$(pwd -P)/${bin#./}" ;;
|
|
esac
|
|
tmp=$(mktemp -d "${TMPDIR:-/tmp}/zupt-bench-safety.XXXXXXXX")
|
|
trap 'rm -rf -- "$tmp"' EXIT HUP INT TERM
|
|
|
|
fail() {
|
|
printf 'FAIL: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
# CodeQL #7 reported the old lstat(child) -> recursive pathname operation as
|
|
# cpp/toctou-race-condition. Keep the platform-specific cleanup primitives in
|
|
# the source gate as well as exercising the runtime symlink boundary below.
|
|
cleanup_source=$repo_root/src/zupt_main.c
|
|
grep -Fq 'static int zupt_remove_temp_tree_fd(int directory_fd)' \
|
|
"$cleanup_source" || fail 'POSIX descriptor-relative cleanup is missing'
|
|
grep -Fq 'unlinkat(parent_fd, entry->d_name, 0)' "$cleanup_source" ||
|
|
fail 'POSIX leaf cleanup is not unlinkat-relative'
|
|
grep -Fq 'directory_handle, data.cFileName, 1, 0)' "$cleanup_source" ||
|
|
fail 'Windows recursive cleanup is not handle-relative'
|
|
grep -Fq 'FILE_OPEN_REPARSE_POINT' "$cleanup_source" ||
|
|
fail 'Windows cleanup no longer opens reparse points without following'
|
|
grep -Fq 'zupt_win_delete_cleanup_entry(' "$cleanup_source" ||
|
|
fail 'Windows cleanup lacks identity-checked handle deletion'
|
|
grep -Fq 'current.nFileIndexLow == expected->nFileIndexLow' "$cleanup_source" ||
|
|
fail 'Windows cleanup no longer rejects a close/reopen name exchange'
|
|
if grep -Fq 'RemoveDirectoryW(full)' "$cleanup_source"; then
|
|
fail 'Windows root cleanup restored post-handle pathname deletion'
|
|
fi
|
|
if grep -Fq 'lstat(child' "$cleanup_source" ||
|
|
grep -Fq 'zupt_remove_temp_tree(child' "$cleanup_source"; then
|
|
fail 'temporary cleanup restored a check-then-use pathname traversal'
|
|
fi
|
|
|
|
case $(uname -s 2>/dev/null || printf unknown) in
|
|
MINGW*|MSYS*|CYGWIN*)
|
|
"$bin" bench --compare >/dev/null 2>&1 ||
|
|
fail 'native Windows handle-relative benchmark cleanup failed'
|
|
printf 'SKIP: adversarial POSIX symlink injection is not native on Windows\n'
|
|
printf 'private Windows handle-relative benchmark workspace: PASS\n'
|
|
exit 0
|
|
;;
|
|
esac
|
|
|
|
printf 'benchmark sentinel must remain unchanged\n' > "$tmp/sentinel"
|
|
cp "$tmp/sentinel" "$tmp/sentinel.expected"
|
|
|
|
# The historical implementation derived this public directory from its PID
|
|
# and followed a precreated text.txt symlink. A fresh Bash process has `$$`
|
|
# equal to the PID retained by exec, including on macOS Bash 3.2, so the test
|
|
# recreates that exact attack without guessing another process.
|
|
bash -c '
|
|
set -e
|
|
old_directory="/tmp/zupt_bench_corpus_$$"
|
|
printf "%s\n" "$old_directory" > "$2/old-directory"
|
|
mkdir "$old_directory"
|
|
ln -s "$2/sentinel" "$old_directory/text.txt"
|
|
test -L "$old_directory/text.txt"
|
|
exec "$1" bench --compare >/dev/null 2>&1
|
|
' zupt-benchmark-test "$bin" "$tmp" || fail 'benchmark comparison failed'
|
|
|
|
cmp "$tmp/sentinel.expected" "$tmp/sentinel" ||
|
|
fail 'benchmark followed the historical predictable temporary symlink'
|
|
old_directory=$(sed -n '1p' "$tmp/old-directory")
|
|
case $old_directory in
|
|
/tmp/zupt_bench_corpus_[0-9]*) ;;
|
|
*) fail 'unexpected historical temporary path' ;;
|
|
esac
|
|
if [[ -d $old_directory ]]; then
|
|
mv "$old_directory" "$tmp/historical-remnant"
|
|
fi
|
|
|
|
# Inject a directory symlink into the private workspace while a real benchmark
|
|
# is active. Cleanup must remove the link itself and never visit its target.
|
|
mkdir "$tmp/symlink-target"
|
|
printf 'cleanup sentinel must survive\n' > "$tmp/symlink-target/sentinel"
|
|
cp "$tmp/symlink-target/sentinel" "$tmp/symlink-target.expected"
|
|
dd if=/dev/urandom of="$tmp/injection-input" bs=65536 count=128 2>/dev/null
|
|
|
|
physical_tmp=$(CDPATH='' cd -P -- /tmp && pwd -P)
|
|
: > "$tmp/preexisting-workspaces"
|
|
for candidate in "$physical_tmp"/zupt-bench-*; do
|
|
if [[ -d $candidate && ! -L $candidate ]]; then
|
|
printf '%s\n' "$candidate" >> "$tmp/preexisting-workspaces"
|
|
fi
|
|
done
|
|
|
|
(cd "$tmp" && "$bin" bench injection-input >/dev/null 2>&1) &
|
|
bench_pid=$!
|
|
injected=0
|
|
injected_workspace=
|
|
attempt=0
|
|
while (( attempt < 1000 )); do
|
|
for candidate in "$physical_tmp"/zupt-bench-*; do
|
|
[[ -d $candidate && ! -L $candidate ]] || continue
|
|
if grep -Fqx -- "$candidate" "$tmp/preexisting-workspaces"; then
|
|
continue
|
|
fi
|
|
if ln -s "$tmp/symlink-target" "$candidate/attacker-link" \
|
|
2>/dev/null; then
|
|
injected=1
|
|
injected_workspace=$candidate
|
|
break
|
|
fi
|
|
done
|
|
(( injected == 1 )) && break
|
|
kill -0 "$bench_pid" 2>/dev/null || break
|
|
sleep 0.01
|
|
attempt=$((attempt + 1))
|
|
done
|
|
wait "$bench_pid" || fail 'benchmark with injected symlink failed'
|
|
(( injected == 1 )) || fail 'could not observe the private benchmark workspace'
|
|
if [[ -e $injected_workspace || -L $injected_workspace ]]; then
|
|
fail 'injected workspace was not the benchmark tree that was removed'
|
|
fi
|
|
cmp "$tmp/symlink-target.expected" "$tmp/symlink-target/sentinel" ||
|
|
fail 'temporary cleanup followed an injected directory symlink'
|
|
|
|
printf 'private descriptor/handle-relative benchmark workspace: PASS\n'
|