There is a local privilege escalation in Polkit (formerly PolicyKit, and used in plenty of places where privilege escalation is needed). It was found by Qualys, and carefully documented on the oss-sec mailing list. It has hit the mainstream media fairly hard, too – probably because it follows closely on unrelated log4j and faker.js issues in Open-Source-land. I’m not a security specialist by a long shot (not by at least 3 light-seconds, even), but let’s take a brief look at execve() in FreeBSD.

The description below applies to FreeBSD’s execve() as it is today. This morning. Right now when I write it, because there is a review to fix the root cause of the issue which will surely roll out quickly. In the meantime I landed updates to the polkit package last night, although navigating-the-security-notifications parts have not finished yet.

In UNIX talk, when one process wants to start another, there is a “fork-exec dance”. The one process makes a copy of itself, proceeding as two separate processes, and then exec is called, which replaces a process by a different one from another executable. The most common program (process) that starts other processes is the shell. When you do this in a terminal:

$ ls

Aside from all the other things going on, the shell is going to fork itself and then one of the two will replace itself by the executable for ls. The ls executable then runs, does its thing, and exits. Afterwards, there is one shell process left.

The current API for exec – for replacing the running process by different one from another executable – is execve(). Here is a snippet from the manpage:

     int
     execve(const char *path, char *const argv[], char *const envp[]);

     The execve() system call transforms the calling process into a new
     process.  The new process is constructed from an ordinary file, whose
     name is pointed to by path, called the new process file.
     
     The argument argv is a pointer to a null-terminated array of character
     pointers to null-terminated character strings.  These strings construct
     the argument list to be made available to the new process.  At least one
     argument must be present in the array; by custom, the first element
     should be the name of the executed program (for example, the last
     component of path).

Here is a complete program that uses execve(). The intention is that this program just replaces itself with ls. Running ls with no arguments usually lists the files in the current directory. With arguments, there’s command-line option processing and then the named files or directories are listed.

#include <unistd.h>

int main(int argc, char **argv)
{
    char * const bogus_argv[2] = {NULL, NULL};
    char * const bogus_envp[2] = {"PS1=$", NULL};
    int r = execve("/bin/ls", bogus_argv, bogus_envp);
    return 0;
}

Let’s take a look at some of the lines of this program.

    char * const bogus_argv[2] = {NULL, NULL};

Here we have an array of pointers, ready to act as the argv for the program we’re going to run. Note that both pointers are set to NULL, so we’re violating the constraint mentioned in the manpage: here there are zero arguments in the null-terminated array. But there’s two NULLs, so we can at least pretend that we’re compliant: if there were a non-null pointer which would customarily point to the name of the program, the array would look like {"ls", NULL}. So we have a suitable null-terminator in place.

    char * const bogus_envp[2] = {"PS1=$", NULL};

This is a very minimal environment. PS1 is the name of the variable – it’s the shell prompt. The value $ is what you traditionally have in the POSIX shell.

    int r = execve("/bin/ls", bogus_argv, bogus_envp);

The current process (this program whose source I show above) is replaced by the executable which is in the file /bin/ls – that’s the ls program we know and love. We pass in the bogus argv, which violates the constraints described for execve(), and the very simple environment.

Personally I expected ls to run and list the current directory. After all, even without the name-of-the-program as the first argument, the argv array is null-terminated. Nope.

$ cc t.c && ./a.out 
: PS1=$: No such file or directory

Somehow we’re reaching the environment pointers, which are interpreted as arguments to ls. Replace the first NULL in the assignment to bogus_argv by anything else, including "", and the program behaves as I expected. Not with a bunch of NULLs, though. Making the bogus_argv longer, e.g. with 16 NULLs, doesn’t help: all of them are ignored and the environment leaks into the arguments of ls.

Takeaway

OpenBSD did it right in 2015 (quoting a Twitter message from Bryan Steele, @canadianbryan):

date: 2015/02/07 08:47:49;  author: tedu;  state: Exp;  lines: +7 -1;
forbid execve() with argc == 0. prompted by a millert email.
ok deraadt miod

FreeBSD is going to do it right (in review), and maybe even Linux is going to do it right (via Ariadne Conill, @ariadneconill on Twitter, this patch).