On 3/22/23 15:45, Laszlo Ersek wrote:
On 3/21/23 18:28, Eric Blake wrote:
> it is indeed a bug in busybox now that POSIX is moving towards
> standardizing realpath, so I've filed it:
>
https://bugs.busybox.net/show_bug.cgi?id=15466
I've found another busybox bug.
The "/bin/sh" utility is provided by busybox as well (via the usual
symlinking).
Per POSIX, if
execvp(file, { argv[0], argv[1], ..., NULL })
were to fail with -1/ENOEXEC, then execvp() must retry "as if" with
execv(<shell path>, { argv[0], file, argv[1], ..., NULL })
In other words, if direct execution of "file" failed because "file"
"has the appropriate access permission but has an unrecognized format", then
execvp() is required to try executing "file" as a shell script. For that,
<shell path> is left unspecified by POSIX, but the arguments of the shell are
specified:
- Argv[0] remains the same. That is, what we wanted "file" to know itself as,
is what we now want *the shell executable* to know itself as.
- argv[1] becomes "file" -- this is the script that the shell is supposed to
run.
- argv[2] and onwards become positional parameters $1, $2, ... for the shell script.
And the argv[0] specification is what's violated by busybox, because if argv[0] is
anything other than "sh", then the busybox binary doesn't recognize itself
as the shell!
The simplest way to demonstrate the bug is this:
bash-5.2$ ( exec -a foobar /bin/sh <<< "echo hello" )
foobar: applet not found
And then, another way to demonstrate the same busybox issue... lets us, in fact, discover
a musl bug in turn!!!
Consider the following C program (called "test-execvp.c"; the binary is called
"test-execvp"):
-------------
#include <stdio.h>
#include <unistd.h>
int main(void)
{
char *args[] = { "foobar", NULL };
execvp("hello.sh", args);
perror("execvp");
return 1;
}
-------------
The file "hello.sh" resides in the current directory (same directory where
"test-execvp" resides). Furthermore it has execute permission, and the following
contents:
-------
echo hello
-------
Now consider the following command (from bash):
$ PATH=.:$PATH test-execvp
What is supposed to happen is this:
(1) bash shall find test-execvp in the current directory per PATH,
(2) execvp() shall find "hello.sh" in the current directory per PATH,
(3) execvp() shall hit an internal failure -1/ENOEXEC,
(4) execvp() shall then invoke the shell (under an unspecified pathname),
(5) the shell shall get "foobar" for its argv[0], and "hello.sh" for
its argv[1]
(6) we shall see "hello" on the standard output.
That's exactly what happens on Linux/glibc. (Note: this result has absolutely nothing
to do with my execvpe() implementation, or libnbd in the first place.)
Now, according to my above description of the busybox bug, we're tempted to believe
that step (6) fails on Alpine Linux (using musl + busybox). We expect the busybox binary
to be launched, via the /bin/sh symlink, in step (4), and we expect it to fail after step
(5), due to it not recognizing "foobar" as an "applet name".
It turns out however that step (4) does not happen. musl does not handle ENOEXEC:
It's getting crazier by the hour.
I thought to create a reproducer for busybox, in spite of musl breaking down at an earlier
point (see above). For that, I *statically linked* "test-execvp" on RHEL-9.1
(using glibc), and then executed the binary in the Alpine Linux container. This should
eliminate musl from the picture, and exercise the ENOEXEC fallback (from glibc), invoking
/bin/sh (aka busybox) under argv[0] "foobar", and trigger the "unknown
applet" bug in busybox.
However, this does not happen. Instead, I get "hello" printed. How is that
possible?
The solution is that glibc *too* has a bug, and that bug hides the busybox bug. Namely, in
glibc, going back to historical commit
commit 6a032d81581978187f562e5533a32e0a6a3d352b (tag: cvs/libc-960210)
Author: Roland McGrath <roland(a)gnu.org>
Date: Sat Feb 10 10:00:27 1996 +0000
Sat Feb 10 04:18:48 1996 Roland McGrath <roland(a)churchy.gnu.ai.mit.edu>
* posix/execvp.c: If execv fails with ENOEXEC, run the shell on
the file.
Fri Feb 9 11:46:45 1996 Roland McGrath <roland(a)churchy.gnu.ai.mit.edu>
* time/Makefile (CFLAGS-zdump.c, CFLAGS-zic.c, CFLAGS-ialloc.c,
CFLAGS-scheck.c): Use -DNOID instead of -Wno-unused.
* hurd/Makefile (user-interfaces): Added hurd/tioctl.
(note the date: 1996!), the POSIX-mandated fallback
execv(<shell path>, { argv[0], file, argv[1], ..., NULL })
is not being done. Instead, the following is done:
execv(<shell path>, { <shell path>, file, argv[1], ..., NULL })
In other words, the original argv[0] is not preserved, but is replaced by <shell
path>. (Look for _PATH_BSHELL in said historical glibc commit, and also in today's
glibc file "posix/execvpe.c".)
This can be demonstrated with:
$ PATH=.:$PATH strace -etrace=execve test-execvp
execve("./test-execvp", ["test-execvp"], 0x7ffc0d1e5248 /* 85 vars */)
= 0
execve("./hello.sh", ["foobar"], 0x7ffc528e14a8 /* 85 vars */) = -1
ENOEXEC (Exec format error)
execve("/bin/sh", ["/bin/sh", "./hello.sh"], 0x7ffc528e14a8
/* 85 vars */) = 0
hello
+++ exited with 0 +++
The third execve() call should be:
execve("/bin/sh", ["foobar", "./hello.sh"], 0x7ffc528e14a8
/* 85 vars */) = 0
^^^^^^^^
Laszlo