checkout-action/tools/ci/test-bash-func.sh
2026-06-04 23:30:30 +09:00

411 lines
12 KiB
Shell
Executable file

#!/usr/bin/env bash
# SPDX-License-Identifier: Apache-2.0 OR MIT
# shellcheck disable=SC2016
set -CeEuo pipefail
IFS=$'\n\t'
trap -- 'printf >&2 "%s\n" "${0##*/}: trapped SIGINT"; exit 1' SIGINT
cd -- "$(dirname -- "$0")"/../..
# Test BASH_FUNC_*%% related behavior and some functions used in action.
#
# See action.yml for more about BASH_FUNC_*%%.
bail() {
printf '::error::%s\n' "$*"
exit 1
}
# Use binaries available at standard location to prevent path interception.
# See resolve_path in action.yml for more.
# NB: Sync with it.
resolve_path() {
for dir in /bin /usr/bin /sbin /usr/sbin; do
if [[ -x "${dir}/$1" ]]; then
printf '%s/%s\n' "${dir}" "$1"
return
fi
done
if [[ -e /etc/NIXOS ]] && [[ -x /run/current-system/sw/bin/"$1" ]]; then
printf '/run/current-system/sw/bin/%s\n' "$1"
elif [[ -e /etc/NIXOS ]] && [[ -x /run/wrappers/bin/"$1" ]]; then
printf '/run/wrappers/bin/%s\n' "$1"
elif [[ -e /etc/nix ]] && [[ -x /root/.nix-profile/bin/"$1" ]]; then
printf '/root/.nix-profile/bin/%s\n' "$1"
fi
}
bash="${1:-$(resolve_path bash)}"
sh="${2:-/bin/sh}"
printf 'bash: %s\n' "${bash}"
printf 'sh: %s\n' "${sh}"
with_func() {
env \
"${name}"="${func}" \
"BASH_FUNC_${name}%%=${func}" \
"BASH_FUNC_${name}()=${func}" \
"__BASH_FUNC<${name}>()=${func}" \
"$@"
}
set -x
# Affected
affected=(
# https://www.gnu.org/software/bash/manual/html_node/Bourne-Shell-Builtins.html
cd
false
getopts
hash
pwd
test
'[' # unaffected when --posix used
true
umask
# https://www.gnu.org/software/bash/manual/html_node/Bash-Builtins.html
alias
bind
builtin
caller
command
declare
echo
enable
help
let
local
logout
mapfile
printf
read
readarray
type
typeset
ulimit
unalias
# https://www.gnu.org/software/bash/manual/html_node/Modifying-Shell-Behavior.html
shopt
# https://www.gnu.org/software/bash/manual/html_node/Job-Control-Builtins.html
bg
fg
jobs
kill
wait
disown
suspend
# https://www.gnu.org/software/bash/manual/html_node/Directory-Stack-Builtins.html
dirs
popd
pushd
# https://www.gnu.org/software/bash/manual/html_node/Bash-History-Builtins.html
fc
history
# https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion-Builtins.html
compgen
complete
compopt
# others
time # altlinux only
)
for name in "${affected[@]}"; do
func='() { exit; }'
script="${name} >/dev/null || :; exit 1"
if [[ "${name}" =~ ^[\[]$ ]] || { [[ "${name}" == 'time' ]] && [[ "${CONTAINER:-}" != 'alt:'* ]]; }; then
with_func "${bash}" --posix -c "${script}" && exit 1
else
with_func "${bash}" --posix -c "${script}"
fi
if { [[ "${name}" =~ ^[\[]$ ]] && [[ "${CONTAINER:-}" =~ ^fedora:(2[0-5])$ ]]; } || { [[ "${name}" == 'time' ]] && [[ "${CONTAINER:-}" != 'alt:'* ]]; }; then
# /bin/bash: error importing function definition for `BASH_FUNC_:'
with_func "${bash}" -c "set -o posix; ${script}" && exit 1
with_func "${bash}" -c "${script}" && exit 1
else
with_func "${bash}" -c "set -o posix; ${script}"
with_func "${bash}" -c "${script}"
fi
printf '%s affected\n' "${name}"
done
# Affected only on non-POSIX mode:
# - POSIX special builtins listed in https://www.gnu.org/software/bash/manual/html_node/Special-Builtins.html.
# Refs: 18 in https://www.gnu.org/software/bash/manual/html_node/Bash-POSIX-Mode.html#Bash-POSIX-Mode-1
# - Function names contain /
# Refs: 19 in https://www.gnu.org/software/bash/manual/html_node/Bash-POSIX-Mode.html#Bash-POSIX-Mode-1
# However, I can reproduce this only on interactive shell...
# $ /bin/sh() { echo a; }; /bin/sh -c 'echo b'
# a
# $ exit # needed on 3.2, unneeded on 5.3
# $ set -o posix
# $ /bin/sh() { echo a; }; /bin/sh -c 'echo b'
# b
non_posix_affected=(
# https://www.gnu.org/software/bash/manual/html_node/Bourne-Shell-Builtins.html
':'
'.'
break
continue
eval
exec
export
exit
readonly
return
shift
times
trap
unset
# https://www.gnu.org/software/bash/manual/html_node/Bash-Builtins.html
source
# https://www.gnu.org/software/bash/manual/html_node/Modifying-Shell-Behavior.html
set
# others
/bin/sh
)
for name in "${non_posix_affected[@]}"; do
func='() { exit; }'
case "${name}" in
exit)
func='() { builtin exit; }'
script='exit 1'
;;
/bin/sh) script="${name} -c '' || true; exit 1" ;;
*) script="${name} >/dev/null || true; exit 1" ;;
esac
with_func "${bash}" --posix -c "${script}" && exit 1
with_func "${bash}" -c "builtin set -o posix; ${script}" && exit 1
if [[ "${name}" =~ ^[:.\[]$ ]] && [[ "${CONTAINER:-}" =~ ^fedora:(2[0-5])$ ]]; then
# /bin/bash: error importing function definition for `BASH_FUNC_:'
with_func "${bash}" -c "${script}" && exit 1
elif [[ "${name}" == '/bin/sh' ]]; then
# /bin/sh: error importing function definition for `/bin/sh'
with_func "${bash}" -c "${script}" && exit 1
else
with_func "${bash}" -c "${script}"
fi
printf '%s affected only on non-POSIX mode\n' "${name}"
done
# Not affected:
# - Reserved words other than time listed in https://www.gnu.org/software/bash/manual/html_node/Reserved-Words.html
# - Separators for Lists of Commands https://www.gnu.org/software/bash/manual/html_node/Lists.html
# TODO: until while coproc select function { } [[ ]] !
func='() { exit; }'
for name in ';' '&'; do
script="true ${name}; exit 1"
with_func "${bash}" --posix -c "${script}" && exit 1
with_func "${bash}" -c "builtin set -o posix; ${script}" && exit 1
with_func "${bash}" -c "${script}" && exit 1
printf '%s not affected\n' "${name}"
done
for name in '&&' '||'; do
script="true ${name} true; exit 1"
with_func "${bash}" --posix -c "${script}" && exit 1
with_func "${bash}" -c "builtin set -o posix; ${script}" && exit 1
with_func "${bash}" -c "${script}" && exit 1
printf '%s not affected\n' "${name}"
done
printf '%s not affected\n' "${name}"
script='if true; then true; elif true; then true; else true; fi'
"${bash}" --posix -c "${script}"
"${bash}" -c "builtin set -o posix; ${script}"
"${bash}" -c "${script}"
script='if true; then true; elif true; then true; else true; fi || true; exit 1'
for name in 'if' 'then' 'elif' 'else' 'fi'; do
with_func "${bash}" --posix -c "${script}" && exit 1
with_func "${bash}" -c "builtin set -o posix; ${script}" && exit 1
with_func "${bash}" -c "${script}" && exit 1
printf '%s not affected\n' "${name}"
done
script='case a in a) true ;; esac'
"${bash}" --posix -c "${script}"
"${bash}" -c "builtin set -o posix; ${script}"
"${bash}" -c "${script}"
for name in 'case' 'in' 'esac'; do
script='case a in a) true ;; esac || true; exit 1'
with_func "${bash}" --posix -c "${script}" && exit 1
with_func "${bash}" -c "builtin set -o posix; ${script}" && exit 1
with_func "${bash}" -c "${script}" && exit 1
printf '%s not affected\n' "${name}"
done
script='for a in a; do true; done'
"${bash}" --posix -c "${script}"
"${bash}" -c "builtin set -o posix; ${script}"
"${bash}" -c "${script}"
script='for a in a; do true; done || true; exit 1'
for name in 'for' 'in' 'do' 'done'; do
with_func "${bash}" --posix -c "${script}" && exit 1
with_func "${bash}" -c "builtin set -o posix; ${script}" && exit 1
with_func "${bash}" -c "${script}" && exit 1
printf '%s not affected\n' "${name}"
done
# Test our unset logic used in action.yml
func1='() { exit; }'
func2=$'() { exit\n }'
with_func() {
env '['="${func1}" 'command'="${func2}" \
'BASH_FUNC_[%%'="${func1}" 'BASH_FUNC_command%%'="${func2}" \
'BASH_FUNC_[()'="${func1}" 'BASH_FUNC_command()'="${func2}" \
'__BASH_FUNC<[>()'="${func1}" '__BASH_FUNC<command>()'="${func2}" \
SH_PATH="${sh}" \
"$@"
}
if type -P pwsh >/dev/null; then
# shellcheck disable=SC2218 # false positive
with_func pwsh tools/ci/test-bash-func-pwsh.ps1
fi
if type -P powershell >/dev/null; then
# shellcheck disable=SC2218 # false positive
with_func powershell tools/ci/test-bash-func-pwsh.ps1
fi
# test alternative approach for non-Windows.
with_func() {
env 'command'="${func2}" \
'BASH_FUNC_command%%'="${func2}" \
'BASH_FUNC_command()'="${func2}" \
'__BASH_FUNC<command>()'="${func2}" \
SH_PATH="${sh}" \
"$@"
}
with_func "${sh}" tools/ci/test-bash-func-sh.sh awk
if [[ -e /proc/self/environ ]]; then
with_func "${sh}" tools/ci/test-bash-func-sh.sh bash-linux
fi
affected_pat=''
for cmd in "${affected[@]}"; do
case "${cmd}" in
# They are unset by env command in shell field.
builtin | '[' | printf | command | read) ;;
*) affected_pat+="|${cmd}" ;;
esac
done
res=$(grep -En '^.*' src/*.sh action.yml | sed -E '/^[^ ]+:( *#| *$)/d' | { grep -E '(^[^ ]+:|[({;&|!]|if|while (IFS=([^ ]*|\$?'\''[^'\'']+'\'') )?) *('"${affected_pat#|}"')([ );]|$)' || true; })
if [[ -n "${res}" ]]; then
printf 'found vulnerable builtin call; use `builtin` command before builtin:\n%s\n' "${res}"
exit 1
fi
# test credential helper function
# NB: Sync credential helper function with main.sh.
helper='
protocol=""
host=""
while IFS= read -r line || [ -n "${line}" ]; do
case "${line}" in
protocol=*) protocol="${line#protocol=}" ;;
host=*) host="${line#host=}" ;;
esac
done
if [ "${protocol}" = "${INPUT_PROTOCOL}" ] && [ "${host}" = "${INPUT_HOSTNAME}" ]; then
printf "protocol=%s\nhost=%s\nusername=x-access-token\npassword=%s\n" "${INPUT_PROTOCOL}" "${INPUT_HOSTNAME}" "${INPUT_TOKEN}"
fi
'
export INPUT_PROTOCOL=https
export INPUT_HOSTNAME=github.com
export INPUT_TOKEN=dummy
set -x
res=$("${sh}" -c "${helper}" <<<'
protocol=https
host=github.com
')
[[ "${res}" == *'password=dummy'* ]] || false
res=$(printf '
protocol=https
host=github.com' | "${sh}" -c "${helper}")
[[ "${res}" == *'password=dummy'* ]] || false
res=$("${sh}" -c "${helper}" <<<'
protocol=http
host=github.com
')
[[ "${res}" != *'password=dummy'* ]] || false
res=$("${sh}" -c "${helper}" <<<'
protocol=https
host=not.github.com
')
[[ "${res}" != *'password=dummy'* ]] || false
# test hex generation used in rand function in main.sh.
set -o posix
# NB: Sync with main.sh.
# Refs: https://stackoverflow.com/a/34329799
od=$(resolve_path od)
[[ -n "${od}" ]] || od=$(type -P od)
od_hex() {
rand=$("${od}" -vN64 -An -tx1)
rand="${rand//[$'\n' ]/}"
if [[ "${#rand}" -ne 128 ]]; then
return 1
fi
}
hexdump=$(resolve_path hexdump)
[[ -n "${hexdump}" ]] || hexdump=$(type -P hexdump || true)
[[ -n "${hexdump}" ]] || [[ ! -x "C:\msys64\usr\bin\hexdump.exe" ]] || hexdump="C:\msys64\usr\bin\hexdump.exe"
hexdump_hex() {
rand=$("${hexdump}" -vn64 -e ' /1 "%02x"')
if [[ "${#rand}" -ne 128 ]]; then
return 1
fi
}
bash_hex() {
if [[ "${BASH_VERSION}" == '3.'* ]] || { [[ "${BASH_VERSION}" == '4.'* ]] && [[ "$(ldd --version 2>&1 || true)" == *'musl'* ]]; }; then
return 1 # old bash is broken with while read loop with stdin containing null (or non ASCII?) byte.
fi
rand=''
while IFS= LC_ALL=C read -rd '' -n1 b; do
printf -v b '%02x' "'${b}"
[[ "${#b}" -eq 2 ]] || continue
rand+="${b}"
[[ "${#rand}" -lt 128 ]] || break
done
if [[ "${#rand}" -ne 128 ]]; then
return 1
fi
}
tmp=$(mktemp)
trap -- 'rm -f -- "${tmp:?}"' EXIT
count=0
bash_rand_match=0
test_hex() {
_=$((count++))
od_hex <"${tmp}"
res="${rand}"
if [[ -n "${hexdump}" ]]; then
hexdump_hex <"${tmp}"
if [[ "${res}" != "${rand}" ]]; then
exit 1
fi
else
case "${CONTAINER:-}" in
windows-11-arm) ;;
*) bail "missing hexdump" ;;
esac
fi
if [[ "${BASH_VERSION}" == '3.'* ]] || { [[ "${BASH_VERSION}" == '4.'* ]] && [[ "$(ldd --version 2>&1 || true)" == *'musl'* ]]; }; then
return
fi
bash_hex <"${tmp}"
if [[ "${res}" == "${rand}" ]]; then
_=$((bash_rand_match++))
fi
}
printf 'cfdc8041987654c6ae1c40bdaa4da84ad7f6dd8a275f506403493d37d704ea647bb7f1ed834e3b21b57ae92840f8d3641a7947ecb53ae6fa1d2a658676613d620' >|"${tmp}"
test_hex
if [[ "${res}" != "63666463383034313938373635346336616531633430626461613464613834616437663664643861323735663530363430333439336433376437303465613634" ]]; then
exit 1
fi
for _i in {0..300}; do
head -c 128 </dev/urandom >|"${tmp}"
test_hex
done
case "${CONTAINER:-}" in
ubuntu-2*)
[[ "${bash_rand_match}" -gt 250 ]] || exit 1
;;
macos-* | alt:p8 | alpine:3.[0-9] | openwrt/rootfs:x86-64-18.06.9)
[[ "${bash_rand_match}" -eq 0 ]] || exit 1
;;
*)
[[ "${bash_rand_match}" -eq "${count}" ]] || exit 1
;;
esac