File: test-fork-safe-execvpe.sh

package info (click to toggle)
libnbd 1.24.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 10,956 kB
  • sloc: ansic: 55,063; ml: 12,364; sh: 8,817; python: 4,757; makefile: 3,036; perl: 165; cpp: 24
file content (307 lines) | stat: -rwxr-xr-x 10,870 bytes parent folder | download
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