[cs631apue] exec error

Jan Schaumann jschauma at stevens.edu
Wed Nov 27 14:34:04 EST 2019


Hem Shah <hshah42 at stevens.edu> wrote:
> main(int argc, char **argv) {
> 
>         (void) setenv("something", "something", 1);
>         printf("testing\n");
>         (void) execl("/cgi-bin/something.cgi", (char *) 0);
>         printf("%s\n", strerror(errno));

Ok, so I dug in a bit, and I think this is what's
going on:

First, the obvious:

*Always* use '-Wall -Werror'; with that, the code
above wouldn't compile, since execl(3) needs at least
one more argument

Next, even for test code, try to develop the muscle
memory of *always* including all error checking.  That
is, write your test case as

	if (setenv("something", "something", 1) < 0) {
		fprintf(stderr, "Unable to setenv: %s\n", strerror(errno));
	}
	printf("testing\n");
	(void)execl("/cgi-bin/something.cgi", "something", (char *) 0);
	fprintf(stderr, "Unable to exec: %s\n", strerror(errno));


Ok, but so what happens when you compile without -Wall
-Werror and with the invalid invocation, i.e.,

execl(PATH, (char *)0);

?

To understand that, we need to take a look at what
execl(3) actually does.  execl(3) is implemented in
/usr/src/lib/libc/gen/execl.c, and it looks like so:

int
execl(const char *name, const char *arg, ...)
{
        int r;
        va_list ap;
        char **argv;
        int i;

        va_start(ap, arg);
        for (i = 2; va_arg(ap, char *) != NULL; i++)
                continue;
        va_end(ap);

        if ((argv = alloca(i * sizeof (char *))) == NULL) {
                errno = ENOMEM;
                return -1;
        }

        va_start(ap, arg);
        argv[0] = __UNCONST(arg);
        for (i = 1; (argv[i] = va_arg(ap, char *)) != NULL; i++)
                continue;
        va_end(ap);

        r = execve(name, argv, environ);
        return r;
}

(To better understand what this looks like during
execution, let's use the debugger.  Now since the C
library is not compiled with debugging symbols, you
can't easily step into execl(3), but you can copy
this code into your program, call it e.g., "myexecl"
and call that instead, thereby allowing you to use the
debugger to step through it before it calls
execve(2).)

Anyway, so execl(3) iterates over the variable number
of arguments starting after 'arg' to determine how
many there are (i), then allocates a pointer on the
stack (via alloca(3)) that's i elements large; then it
populates that argv with the given arguments befoer
calling execve(2), passing the newly constructed argv
as well as the extern environ pointer.

So it depends on the processing of the variadic
arguments to build the argv; the last argument in this
list MUST be "(char *)0", as noted in the manual page.
When you do not pass any third argument, then you are
in undefined territory.

This is because the way variadic functions work is
roughly as follows:

As we've seen, the arguments to a function are of
course passed in, but that doesn't include the
variable arguments after the last.  That is, if you
use the debugger, you will always see the call to
execl(3) to be e.g.,:

Breakpoint 1, myexecl (name=0x400ddb "/tmp/env", arg=0x0) at e.c:20

That is, variable arguments must be made available to
the function in some other way, since obviously
they're not named.  What 'va_start' does to accomplish
this is: it takes the address of the last known
argument ('arg' in our case), then 'va_arg' increments
the address by the correct size for the type of
argument ('char *' in our case) to pick the next
argument.  (Recall that our stack grows down from the
high address; the function frame is pushed onto the
stack and the arguments are pushed on top from right
to left, so that incrementing the memory address of
the last known arg gets walks you up the address space
with the last argument specified being in the highest
address.)

'va_arg' will stop when the value at the address it
arrives at is 0x0 (cast to 'char *', as that is the
type of arguments these varargs accept).

If you do not specify any arguments after 'arg', then
'va_arg' may yield different invalid memory locations
before it eventually finds a '0x0'; this is platform
and even compiler specific - i.e., undefined.

In our case, you will find that 'va_args' fills the
argv it constructs with a bunch of values:

(gdb) n
32              for (i = 1; (argv[i] = va_arg(ap, char *)) != NULL; i++) 
(gdb) n
36              r = execve(name, argv, environ);
(gdb) p i
$6 = 3
(gdb) p argv[3]
$7 = 0x0
(gdb) p argv[2]
$8 = 0x601218 <__progname> "\025O0\377\177\177"
(gdb) p argv[1]
$9 = 0x7f7fff304988 "\033O0\377\177\177"
(gdb) p argv[0]
$10 = 0x0

That is, the argv it constructs looks like so:

argv[0] = NULL    # This will be the progname of the command you invoke.
argv[1] = garbage # This will be passed to your program as the first argument
argv[2] = garbage # This will be passed to your program as the second argument
argv[3] = NULL    # This marks the end of argv

With this in place, you should then not be surprised
that your execve(2) with that argv fails with EFAULT,
which the manual page for execve(2) describes:

[EFAULT]      The new process file is not as long as indicated by
              the size values in its header; or path, argv, or envp
              point to an illegal address.



Ok, so far, so good (bad).  Now what seems weird is
that when you don't call setenv(3) the program
succeeds, but setenv(3) is a red herring here: without
calling setenv(3), your stack is left unmodified and
va_args happens to get a terminating value; calling
setenv(3) simply causes another frame to be pushed on
the stack and happens to then yield different values.

You can try by calling other functions before calling
execl(3): some functions, depending on how they are
called, will leave the stack with NULL in the
registers that 'va_arg' then happens to access, others
will not.  This is also why your call to printf(3)
after setenv(3) allows the program to execute: by
flushing the stdio buffer, you end up clearing the
registers in question (it appears).  Try calling
printf(3) without a "\n", and you may be again left
with an EFAULT.


Long story short: always use '-Wall -Werror'! :-)

And make sure you understand what the arguments to the
different exec(3) functions are and do and why you
need them.

-Jan


More information about the cs631apue mailing list