1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307
|
#!/usr/bin/env bash
# nbd client library in userspace
# Copyright (C) 2013-2023 Red Hat Inc.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
. ../tests/functions.sh
set -e
set -x
# The "realpath" utility is not in POSIX, but Linux, FreeBSD and
# OpenBSD all have it.
requires "$REALPATH" /
# Fails on macOS with:
# ./test-fork-safe-execvpe.sh: line 58: 84314 Killed: 9 PATH=$pathctl "$execvpe" "$@" > out 2> err
# 'PATH= /Users/rjones/d/libnbd/lib/test-fork-safe-execvpe bin/f expr 1 + 1' should have succeeded
requires_not test "$(uname)" = "Darwin"
# Determine the absolute pathname of the execvpe helper binary.
bname=$(basename -- "$0" .sh)
dname=$(dirname -- "$0")
if ! execvpe=$($REALPATH -- "$dname/$bname"); then
# Work around <https://bugs.busybox.net/show_bug.cgi?id=15466>. For example,
# Alpine Linux in the libnbd CI uses BusyBox.
execvpe=$($REALPATH "$dname/$bname")
fi
# This is an elaborate way to control the PATH variable around the $execvpe
# helper binary as narrowly as possible.
#
# If $1 is "_", then the $execvpe helper binary is invoked with PATH unset.
# Otherwise, the binary is invoked with PATH set to $1.
#
# $2 and onward are passed to $execvpe; $2 becomes "program-to-exec" for the
# helper and $3 becomes argv[0] for the program executed by the helper.
#
# (Outside of the "run" function below, "run0" should only ever be called for
# working around BusyBox. (Alpine Linux in the libnbd CI uses BusyBox.)
# BusyBox's utilities "expr", "sh" etc. are just symlinks to the central
# "busybox" binary executable, and that executable keys its behavior off the
# name under which it is invoked -- that is, $3 here. See
# <https://bugs.busybox.net/show_bug.cgi?id=15481>.)
#
# The command itself (including the PATH setting) is written to "cmd" (for error
# reporting purposes only); the standard output and error are saved in "out" and
# "err" respectively; the exit status is written to "status". This function
# should never fail; if it does, then that's a bug in this unit test script, or
# the disk is full etc.
run0()
{
local pathctl=$1
local exit_status
shift 1
if test _ = "$pathctl"; then
printf 'unset PATH; %s %s\n' "$execvpe" "$*" >cmd
set +e
(
unset PATH
"$execvpe" "$@" >out 2>err
)
exit_status=$?
set -e
else
printf 'PATH=%s %s %s\n' "$pathctl" "$execvpe" "$*" >cmd
set +e
PATH=$pathctl "$execvpe" "$@" >out 2>err
exit_status=$?
set -e
fi
printf '%d\n' $exit_status >status
}
# Does the same as "run0", but $2 becomes *both* "program-to-exec" for the the
# $execvpe helper binary *and* argv[0] for the program executed by the helper.
run()
{
local pathctl=$1
local program=$2
shift 1
run0 "$pathctl" "$program" "$@"
}
# After "run" returns, the following three functions can verify the result.
#
# Check if the helper binary failed in nbd_internal_execvpe_init().
#
# $1 is the error number (a macro name such as ENOENT) that's expected of
# nbd_internal_execvpe_init().
init_fail()
{
local expected_error="nbd_internal_execvpe_init: $1"
local cmd=$(< cmd)
local err=$(< err)
local status=$(< status)
if test 0 -eq "$status"; then
printf "'%s' should have failed\\n" "$cmd" >&2
return 1
fi
if test x"$err" != x"$expected_error"; then
printf "'%s' should have failed with '%s', got '%s'\\n" \
"$cmd" "$expected_error" "$err" >&2
return 1
fi
}
# Check if the helper binary failed in nbd_internal_fork_safe_execvpe().
#
# $1 is the output (the list of candidate pathnames) that
# nbd_internal_execvpe_init() is expected to produce; with inner <newline>
# characters replaced with <comma> characters, and the last <newline> stripped.
#
# $2 is the error number (a macro name such as ENOENT) that's expected of
# nbd_internal_fork_safe_execvpe().
execve_fail()
{
local expected_output=$1
local expected_error="nbd_internal_fork_safe_execvpe: $2"
local cmd=$(< cmd)
local out=$(< out)
local err=$(< err)
local status=$(< status)
if test 0 -eq "$status"; then
printf "'%s' should have failed\\n" "$cmd" >&2
return 1
fi
if test x"$err" != x"$expected_error"; then
printf "'%s' should have failed with '%s', got '%s'\\n" \
"$cmd" "$expected_error" "$err" >&2
return 1
fi
out=${out//$'\n'/,}
if test x"$out" != x"$expected_output"; then
printf "'%s' should have output '%s', got '%s'\\n" \
"$cmd" "$expected_output" "$out" >&2
return 1
fi
}
# Check if the helper binary and the program executed by it succeeded.
#
# $1 is the output (the list of candidate pathnames) that
# nbd_internal_execvpe_init() is expected to produce, followed by any output
# expected of the program that's executed by the helper; with inner <newline>
# characters replaced with <comma> characters, and the last <newline> stripped.
success()
{
local expected_output=$1
local cmd=$(< cmd)
local out=$(< out)
local status=$(< status)
if test 0 -ne "$status"; then
printf "'%s' should have succeeded\\n" "$cmd" >&2
return 1
fi
out=${out//$'\n'/,}
if test x"$out" != x"$expected_output"; then
printf "'%s' should have output '%s', got '%s'\\n" \
"$cmd" "$expected_output" "$out" >&2
return 1
fi
}
# Create a temporary directory and change the working directory to it.
tmpd=$(mktemp -d)
cleanup_fn rm -r -- "$tmpd"
cd "$tmpd"
# If the "file" parameter of execvpe() is an empty string, then we must fail --
# in nbd_internal_execvpe_init() -- regardless of PATH.
run _ ""; init_fail ENOENT
run "" ""; init_fail ENOENT
run . ""; init_fail ENOENT
# Create subdirectories for triggering non-fatal internal error conditions of
# execvpe(). (Almost) every subdirectory will contain one entry, called "f".
#
# Create a directory that's empty.
mkdir empty
# Create a directory with a named pipe (FIFO) in it.
mkdir fifo
mkfifo fifo/f
# Create a directory with a directory in it.
mkdir subdir
mkdir subdir/f
# Create a directory with a non-executable file in it.
mkdir nxregf
touch nxregf/f
# Create a symlink loop.
ln -s symlink symlink
# Create a directory with a (most likely) binary executable in it.
mkdir bin
expr_pathname=$(command -p -v expr)
cp -- "$expr_pathname" bin/f
# Create a directory with an executable shell script that does not contain a
# shebang (#!). The script will print $0 and $1, and not depend on PATH for it.
mkdir sh
printf "command -p printf '%%s %%s\\\\n' \"\$0\" \"\$1\"\\n" >sh/f
chmod u+x sh/f
# In the tests below, invoke each "f" such that the "file" parameter of
# execvpe() contain a <slash> character.
#
# Therefore, PATH does not matter. Set it to the empty string. (Which in this
# implementation would cause nbd_internal_execvpe_init() to fail with ENOENT, if
# the "file" parameter didn't contain a <slash>.)
run "" empty/f; execve_fail empty/f ENOENT
run "" fifo/f; execve_fail fifo/f EACCES
run "" subdir/f; execve_fail subdir/f EACCES
run "" nxregf/f; execve_fail nxregf/f EACCES
run "" nxregf/f/; execve_fail nxregf/f/ ENOTDIR
run "" symlink/f; execve_fail symlink/f ELOOP
# This runs "expr 1 + 1".
run0 "" bin/f expr 1 + 1; success bin/f,2
# This triggers the ENOEXEC branch in nbd_internal_fork_safe_execvpe().
# nbd_internal_fork_safe_execvpe() will first try
#
# execve("sh/f", {"sh/f", "arg1", NULL}, envp)
#
# hitting ENOEXEC. Then it will successfully call
#
# execve("/bin/sh", {"sh/f", "sh/f", "arg1", NULL}, envp)
#
# The shell script will get "sh/f" for $0 and "arg1" for $1, and print those
# out.
run0 "" sh/f sh arg1; success sh/f,"sh/f arg1"
# In the tests below, the "file" parameter of execvpe() will not contain a
# <slash> character.
#
# Show that PATH matters that way -- first, trigger an ENOENT in
# nbd_internal_execvpe_init() by setting PATH to the empty string.
run "" expr 1 + 1; init_fail ENOENT
# Fall back to confstr(_CS_PATH) in nbd_internal_execvpe_init(), by unsetting
# PATH. Verify the generated candidates by invoking "getconf PATH" here, and
# appending "/expr" to each prefix.
expected_output=$(
path=$(command -p getconf PATH)
IFS=:
for p in $path; do
printf '%s/%s\n' $p expr
done
command -p expr 1 + 1
)
run _ expr 1 + 1; success "${expected_output//$'\n'/,}"
# Continue with tests where the "file" parameter of execvpe() does not contain a
# <slash> character, but now set PATH to explicit prefix lists.
#
# Show that, if the last candidate fails execve() with an error number that
# would not be fatal otherwise, we do get that error number.
run empty:fifo:subdir:nxregf:symlink f
execve_fail empty/f,fifo/f,subdir/f,nxregf/f,symlink/f ELOOP
# Put a single prefix in PATH, such that it leads to a successful execution.
# This exercises two things at the same time: (a) that
# nbd_internal_execvpe_init() produces *one* candidate (i.e., that no <colon> is
# seen), and (b) that nbd_internal_fork_safe_execvpe() succeeds for the *last*
# candidate. Repeat the test with "expr" (called "f" under "bin") and the shell
# script (called "f" under "sh", triggering the ENOEXEC branch).
run0 bin f expr 1 + 1; success bin/f,2
run0 sh f sh arg1; success sh/f,"sh/f arg1"
# Demonstrate that the order of candidates matters. The first invocation finds
# "expr" (called "f" under "bin"), the second invocation finds the shell script
# under "sh" (triggering the ENOEXEC branch).
run0 empty:bin:sh f expr 1 + 1; success empty/f,bin/f,sh/f,2
run0 empty:sh:bin f sh arg1; success empty/f,sh/f,bin/f,"sh/f arg1"
# Check the expansion of zero-length prefixes in PATH to ".", plus the
# (non-)insertion of the "/" separator.
run a/: f; execve_fail a/f,./f ENOENT
run :a/ f; execve_fail ./f,a/f ENOENT
run : f; execve_fail ./f,./f ENOENT
pushd bin
run0 : f expr 1 + 1; success ./f,./f,2
popd
run :a/:::b/: f; execve_fail ./f,a/f,./f,./f,b/f,./f ENOENT
|