Handling argc==0 in the kernel
I'd like to have an argument, please
Most developers are familiar with the prototype of the main program as expressed in the C language:
int main(int argc, char *argv[], char *envp[]);
The program is invoked with its command-line arguments in argv and the environment in envp; both are pointers to null-terminated arrays of char * strings. The number of non-null entries in argv is stored in argc. This API is a user-space creation, though; what happens when the kernel first runs a program is a little bit different: on Linux, that program is passed a single pointer to the argv array. The envp array begins immediately after the NULL value that terminates argv. Thus, in a C program, the following statement will be true on entry to main():
envp == argv + argc + 1
By convention, argv[0] is the name of the program that is being executed, and many programs rely on that convention. As it happens, though, this convention is exactly that: a convention, but not a guarantee. The actual contents of argv are entirely under the control of whoever calls execve() to run the program in the first place, and that caller is not required to put the program name in argv[0].
Indeed, the caller is not required to provide argv[0] at all. If the argv array passed to execve() is empty (or the argv pointer is simply NULL), the first pointer in the new program's argv array will be NULL, and the envp array will start immediately thereafter. Unfortunately, Polkit (or, more specifically, the setuid pkexec utility) "knew" that argv[0] would always be present, so it performed its argument processing by iterating over the argv array starting at argv[1]. If there are no arguments at all, argv[1] is the same as envp, so pkexec was iterating through its environment variables instead. Throw in some in-place argument modification (pkexec overwrites its argv array), and pkexec could be convinced to rewrite its environment variables, thus bypassing the sanitizing of those variables done for setuid programs. At that point, the game was over.
This problem is not new, and neither is awareness of it. Ryan Mallon wrote about
it in 2013, noting that "it does allow for some amusing behaviour
from setuid binaries
"; he also evidently sent a Polkit
patch to address it, but that patch was never applied. Even further
back, in 2007, Michael Kerrisk reported the
kernel's behavior as a bug, but the report was closed with little
discussion. So the problem persisted, culminating in the vulnerability
administrators are scrambling to patch now.
Toward a more general fix
Fixing this issue is a simple matter of making pkexec check that argc is at least one. But there are surely other programs out there containing similar assumptions. Given the strength of the argv[0] convention, it is natural to ask whether it makes sense to allow programs to be run with an empty argv array at all. Perhaps it doesn't, but the current API has a lot of history and cannot be changed without a lot of thought.
Ariadne Conill started
the linux-kernel discussion with a patch that would simply disallow
calls to execve() without at least one argv entry.
Offending callers would get an EFAULT return value instead. This would
solve the problem by providing a guarantee that argv would not be
empty, but at the potential cost of introducing problems of its own.
One is that, as Kees Cook discovered,
there is actually a fair amount of code out there that calls
execve() with an empty argv array. Conill wrote
those off as "lazily-written test cases which should be
fixed
", but regressions in lazily-written test cases are still
regressions. Also, as Heikki
Kallasjoki and Rich
Felker both pointed out, an empty argv array is actually
allowed by the POSIX standard.
Felker also suggested an alternative with less potential for regressions: only enforce a non-empty argv at privilege boundaries — when execve() is being called to run a setuid program, in other words. Cook said that he would rather avoid taking the privilege boundary into account if possible, though. He proposed a different solution to the problem: inject an extra null pointer at the end of an empty argv array so that even code that tries to skip argv[0] will notice that there is nothing there. This solution will not work either, as it turns out: the ABI promise is that envp starts immediately after argv, and the extra NULL breaks that promise. There are evidently programs that rely on that layout and would break if it were changed.
Yet another approach, first suggested by Eric Biederman, would be to replace an empty argv with one containing a single pointer to an empty string. This proposal had some support (though nobody has implemented it as of this writing), but also provoked some worries of its own. Perhaps there are programs out there that will respond badly to an empty-string argument, or which do something special when argc is zero. Changing the number of arguments passed to the program run by execve() just looks like it could create surprises.
Cook eventually summarized the situation this way:
Given the code we've found that depends on NULL argv, I think we likely can't make the change outright, so we're down this weird rabbit hole of trying to reject what we can and create work-around behaviors for the cases that currently exist.
That notwithstanding, he then went on to express a preference for the
initial change (simply
disallowing a zero-argument execve() call anyway, albeit with an
EINVAL return rather than EFAULT), with a suggestion to
fix a Valgrind test that is already
known to break with that restriction.
Conill responded with
a new version of the original patch; this time it emits a warning
before failing an empty-argv call to execve() with
EINVAL. Cook acked the
patch, saying: "Let's do it and see what breaks
"; Biederman
concurred:
"Especially since you are signing up to help fix the tests
".
That is where the discussion stands as of this writing, but it is far from
clear that this is how the problem will eventually be addressed. This
patch has, crucially, not yet survived its first encounter with Linus
Torvalds, who may take a dim view of its potential for regressions. It
is an ABI change, after all, and there may well be code out there
that responds badly to it, though the fact that BSD systems already
prohibit an empty argv will, with luck, have already shaken out
most of those.
Should unfortunate regressions arise anyway, a different
solution will almost certainly need to be found.
Index entries for this article | |
---|---|
Kernel | Security/Vulnerabilities |
Kernel | System calls/execve() |
Security | Linux kernel |
Posted Jan 28, 2022 15:21 UTC (Fri)
by maxfragg (subscriber, #122266)
[Link] (9 responses)
Posted Jan 28, 2022 15:42 UTC (Fri)
by johnnyapol (subscriber, #151785)
[Link]
Posted Jan 28, 2022 16:02 UTC (Fri)
by Tobu (subscriber, #24111)
[Link]
Posted Jan 28, 2022 16:15 UTC (Fri)
by jreiser (subscriber, #11027)
[Link]
Posted Jan 28, 2022 17:26 UTC (Fri)
by ariadne (subscriber, #138312)
[Link] (5 responses)
Posted Jan 28, 2022 20:14 UTC (Fri)
by khim (subscriber, #9252)
[Link] (4 responses)
Note that while, at one time, both C standard and POSIX allowed So yes, it's a mess, kinda, because yes, old standards explicitly allowed this, but these standards were fixed. Whether Linux can or should take advantage of that change is another story, of course.
Posted Jan 28, 2022 21:52 UTC (Fri)
by developer122 (guest, #152928)
[Link]
Posted Jan 29, 2022 0:23 UTC (Sat)
by nybble41 (subscriber, #55106)
[Link] (1 responses)
If you do claim to be a Strictly Conforming POSIX Application then you can't call exec with less than one argument. However, a Strictly Conforming POSIX Application may be *invoked* with zero arguments, and is required to handle that case as well to qualify as Strictly Conforming since POSIX does not specify that the argument list passed to main() will be non-empty and you can't assume every other program in the system is also Strictly Conforming.
Posted Jan 29, 2022 1:43 UTC (Sat)
by Paf (subscriber, #91811)
[Link]
Posted Jan 29, 2022 10:43 UTC (Sat)
by larkey (guest, #104463)
[Link]
> Early proposals required that the value of argc passed to main() be "one or greater". This was driven by the same requirement in drafts of the ISO C standard. In fact, historical implementations have passed a value of zero when no arguments are supplied to the caller of the exec functions. This requirement was removed from the ISO C standard and subsequently removed from this volume of POSIX.1-2017 as well.
There's no hard requirement on argc.
> The wording, in particular the use of the word should, requires a Strictly Conforming POSIX Application to pass at least one argument to the exec function, thus guaranteeing that argc be one or greater when invoked by such an application. In fact, this is good practice, since many existing applications reference argv[0] without first checking the value of argc.
A *strictly* conforming application. This is more than just "plain" POSIX.
Posted Jan 28, 2022 15:47 UTC (Fri)
by larkey (guest, #104463)
[Link] (28 responses)
> If the argv array passed to execve() is empty (or the argv pointer is simply NULL), the first pointer in the new program's argv array will be NULL, and the envp array will start immediately thereafter.
POSIX says that argv == NULL isn't allowed:
> The argument argv is an array of character pointers to null-terminated strings. The application shall ensure that the last member of this array is a null pointer.
(https://pubs.opengroup.org/onlinepubs/9699919799.2018edit...)
That's also what the kernel bug (https://bugzilla.kernel.org/show_bug.cgi?id=8408) from years back is about. It is *not* about enforcing argc >= 1 (i.e., argv[0] != NULL).
The patch by Ariadne Conill does *not* address what was reported in the bug, despite claiming so. It enforces that case b) never happens which is a stricter requirement and indeed not required by POSIX, as correctly pointed out by Heikki Kallasjoki and Rich Felker. This is just a recommendation:
> The value in argv[0] should point to a filename string that is
I agree that Linux should not allow argv == NULL, that's definitely against the spec. argv[0] == NULL is a different matter although I think it is a reasonable demand, from a security stand point.
-------
Appendix:
POSIX on shall and should:
> shall
> should
(https://pubs.opengroup.org/onlinepubs/9699919799.2018edit...)
Posted Jan 28, 2022 16:26 UTC (Fri)
by matthias (subscriber, #94967)
[Link] (25 responses)
Probably the confusion is for a simple reason. The two things are more or less the same. If the execve() function is called with argv == NULL, the kernel actually constructs a NULL-terminated array argv with argv[0] == NULL and calls main(argv,0). Thus for the called application, these two things are indistinguishable.
> I agree that Linux should not allow argv == NULL, that's definitely against the spec.
From the examples in the discussions it seems that the "lazily written tests" do exactly this: call execve() with argv == NULL. If Linux disallows this, these tests will break due to an ABI/API change.
> argv[0] == NULL is a different matter although I think it is a reasonable demand, from a security stand point.
Yeah, but disallowing the former and allowing the latter does not make any sense, as the two cases cannot be distinguished by the called application. There would be no advantage from just disallowing argv == NULL.
The problematic case from the security persepctive is allowing argv[0] == NULL (or more precisely argc==0). The problematic case from the compatibility perspective is disallowing argv == NULL in the execve() call. Thus only disallowing argv == NULL in the call gives us the worst from both perspectives. We get all compatibility problems with no increase in security.
Posted Jan 28, 2022 16:35 UTC (Fri)
by larkey (guest, #104463)
[Link] (24 responses)
> Yeah, but disallowing the former and allowing the latter does not make any sense, as the two cases cannot be distinguished by the called application. There would be no advantage from just disallowing argv == NULL.
I disagree here though. Disallowing argv == NULL but allowing argv[0] == NULL would allow for this "iterator" pattern (which is as old as UNIX):
for (**arg = &argv[1]; arg < &argv[argc]; arg++)
Posted Jan 28, 2022 16:57 UTC (Fri)
by corbet (editor, #1)
[Link] (4 responses)
...and that pattern will still fail badly with argv[0]==NULL.
Posted Jan 28, 2022 17:00 UTC (Fri)
by larkey (guest, #104463)
[Link] (3 responses)
But I think that Ariadne's patch should be applied nontheless. It's just that there's much confusion about these two different issues (although related).
Posted Jan 29, 2022 18:56 UTC (Sat)
by adobriyan (subscriber, #30858)
[Link] (2 responses)
for (int i = 1; i < argc; ++i) { const char *arg = argv[i]; ... }
Posted Jan 30, 2022 23:32 UTC (Sun)
by rschroev (subscriber, #4164)
[Link] (1 responses)
Posted Jan 31, 2022 1:01 UTC (Mon)
by jreiser (subscriber, #11027)
[Link]
Because that does not accommodate state changes: deleting, re-arranging, or re-writing pieces of argv (and thus argc); because the GNU getopt routine has not always existed, has not always offered all its current features, and early suffered from bugs; and probably other reasons, too.
Posted Jan 28, 2022 17:12 UTC (Fri)
by matthias (subscriber, #94967)
[Link]
Disallowing argv[0] still allows for this pattern. The security problem arises with applications relying on argc>0 (like pkexec did).
And there is really no reason to disallow argv == NULL (in the execve()-call) while allowing argv[0] == NULL. As Linux constructs the { NULL } array, both things cannot be distinguished from the called application. There is no reason to get all the compatibility problems arising from disallowing argv == NULL without gaining any security benefit. Either we want the get rid of the security issues. Then we have to disallow argv[0] == NULL. Or we can leave everything as it is now.
Posted Jan 29, 2022 8:31 UTC (Sat)
by matthias (subscriber, #94967)
[Link] (17 responses)
I just was wondering if this is UB if argc==0. Probably it is. According to the C standard argv is a argc+1 sized array, where argv[argc] == NULL. If argc is 0, argv[1] uses an index outside of the bounds of the array. Thus even writing argv[1] would be UB. The code argv+1 instead of &argv[1] would be valid, as it is perfectly allowed to construct a pointer pointing to the address immediately after the end of an array. However one is not allowed to dereference it.
I do not really expect this to break even in the case of argc==0 and of course as long as the program is called with argc>0 this code is not UB at all. But a conforming compiler can probably eliminate a check argc!=0 that occurs later on, because the very fact that argv[1] has been used tells the compiler that either argc>0 or the code is UB and the compiler is free to do whatever it wants.
Posted Jan 29, 2022 10:39 UTC (Sat)
by larkey (guest, #104463)
[Link] (12 responses)
But there's no dereferencing? But more plainly perhaps:
for (char **arg = argv + 1; arg < argv+argv; arg++)
Posted Jan 29, 2022 11:05 UTC (Sat)
by matthias (subscriber, #94967)
[Link] (11 responses)
I agree that the compiler will probably generate the exact same code for **arg = &argv[1] and **arg = argv + 1. But in the first case, the compiler is allowed to conclude that the length of the argv array is at least two (including the final NULL value), as argv[1] is UB if the array has length one. I am not saying that any compiler actually does this, but when it comes to UB, compilers are known to do very crazy things, so it is not entirely impossible. And in the case of the main function it is even less likely that the compiler will do anything crazy. The pointer is provided by the OS and the compiler does not know the length of the array (unless it uses the special connection between argv and argc). So the code should probably be fine. But this is one of the things I really do not like about C. The threat of UB is almost everywhere. And at some point in the future some clever compiler will exploit this against you.
Posted Jan 29, 2022 11:22 UTC (Sat)
by mchapman (subscriber, #66589)
[Link]
No, this is not correct.
C specifies that &*a is equivalent to a: "neither [the * operator] nor the & operator is evaluated and the result is as if both were omitted". This expression is valid for any value of a, even a null pointer.
Posted Jan 29, 2022 23:08 UTC (Sat)
by areilly (subscriber, #87829)
[Link] (9 responses)
No: &argv[1] is semantically identical to argv + 1. It is pointer arithmetic and does not cause a dereference.
However: in deference to C support on AS400 (and Unisys? anything with fancy pointer representations) it *is* undefined behaviour for arrays of length zero. It's only legal to construct a pointer value to the single element past the end of the array, whether or not the pointer is subsequently dereferenced. This is also why it's technically illegal to use a post-decremented pointer traversal that accesses the first element of an array. The one-past-the-end rule doesn't extend to one-before-the-beginning. Sigh.
Posted Jan 30, 2022 10:43 UTC (Sun)
by larkey (guest, #104463)
[Link] (8 responses)
Aren't zero-length arrays a GNU extension? Or do you refer to flexible struct members?
Posted Jan 30, 2022 11:23 UTC (Sun)
by areilly (subscriber, #87829)
[Link] (1 responses)
Posted Jan 31, 2022 9:50 UTC (Mon)
by larkey (guest, #104463)
[Link]
Posted Jan 30, 2022 20:21 UTC (Sun)
by NYKevin (subscriber, #129325)
[Link] (5 responses)
In practice, most implementations either return NULL or convert it into a call to malloc(1). But even then, that's a one-byte array, not a one-object array, so you still can't legally add one to it unless the pointer's type has sizeof() == 1.
Posted Jan 30, 2022 20:23 UTC (Sun)
by NYKevin (subscriber, #129325)
[Link]
Or realloc(), I suppose. Point is, you definitely can't dereference it.
Posted Jan 31, 2022 1:20 UTC (Mon)
by jreiser (subscriber, #11027)
[Link] (2 responses)
One of the uses of malloc(0) is to serve as an analogue of the Lisp function (gensym): return a unique value, one that does not point to anything else returned by malloc, both now and for the remaining life of the process. (Yes, the standard requires this.) An implementation for which malloc(0) always returns NULL is not acceptable. So in practice, many implementations begin with something like size += (0==size), i.e. malloc(0) is treated as malloc(1).
Posted Jan 31, 2022 1:31 UTC (Mon)
by ABCD (subscriber, #53650)
[Link]
An implementation for which malloc(0) always returns NULL is expressly permitted by the POSIX standard:
If the size of the space requested is 0, the behavior is implementation-defined: the value returned shall be either a null pointer or a unique pointer.
The C17 standard says something similar:
If the size of the space requested is zero, the behavior is implementation-defined: either a null pointer is returned to indicate an error, or the behavior is as if the size were some nonzero value, except that the returned pointer shall not be used to access an object.
Posted Jan 31, 2022 9:43 UTC (Mon)
by NYKevin (subscriber, #129325)
[Link]
The fact that it happens to vaguely resemble some feature of Lisp, but the standard does not actually require it to fulfill all the requirements of that feature is, frankly, Lisp's problem, not C's problem.
Posted Jan 31, 2022 10:02 UTC (Mon)
by larkey (guest, #104463)
[Link]
Anyway, the POSIX standard pretty clearly says that argv is NULL-terminated, that is, not by itself a zero-length array, but at least
char **argv = { NULL };
Posted Jan 30, 2022 3:24 UTC (Sun)
by nybble41 (subscriber, #55106)
[Link]
Another comment by areilly hinted at this already, but evaluating argv[1] for an argv array of size 1 (where argc == 0) is not undefined behavior. You are allowed to construct (but not dereference) a pointer to the element immediately after the end of an array. (Taking the address of a dereference or array indexing expression does not count as actually dereferencing the pointer: even though argv[1] in an expression by itself would be UB, &argv[1] and &*(argv + 1) are both semantically identical to the pointer arithmetic operation argv + 1. The address-of operator "cancels out" the dereference.) So there is no undefined behavior in the example, even if argc == 0, as &argv[1] and &argv[0] are both valid pointers.
Posted Mar 28, 2022 21:41 UTC (Mon)
by fest3er (guest, #60379)
[Link] (1 responses)
Hmmm. Off-by-one seems to rear its head here. If argc is 0, then argv[] should contain 0+1 elements, thus only argv[0] exists and should be null and argv[1] is out-of-bounds.
Aside, why are people saying that argv is the same as argv[0]? Isn't argv the address of the array, and argv[0] the address of the zeroeth argument (or null to indicate the end of the array)? Thus, shouldn't argv always be a valid pointer to an array and the array always contain at least the terminating NULL pointer?
And when the kernel constructs the argv array, does it use its memory or the user's memory?
Posted Mar 29, 2022 12:51 UTC (Tue)
by jem (subscriber, #24231)
[Link]
Yes, of course. If argc is 0, then the argument vector is empty and only contains the terminating NULL at index 0. You can't expect to find anything of interest in argv[1].
>And when the kernel constructs the argv array, does it use its memory or the user's memory?
The argv array is in the process (user) address space. You can print the address of argv if you are interested to find out where it is located.
Posted Mar 29, 2022 20:16 UTC (Tue)
by nybble41 (subscriber, #55106)
[Link]
From C99 (draft) §6.5.3.2:
> If the operand is the result of a unary * operator, neither that operator nor the & operator is evaluated and the result is as if both were omitted, except that the constraints on the operators still apply and the result is not an lvalue. Similarly, if the operand is the result of a [] operator, neither the & operator nor the unary * that is implied by the [] is evaluated and the result is as if the & operator were removed and the [] operator were changed to a + operator.
So "&argv[1]" is effectively rewritten as "argv+1" and there is no undefined behavior either way. You could even have an expression like "&(*p)" or "&p[0]" where p == NULL and the result will be well-defined (NULL). A conforming compiler cannot assume that argc > 0 based on the presence of "&argv[1]".
Posted Jan 28, 2022 20:07 UTC (Fri)
by khim (subscriber, #9252)
[Link] (1 responses)
You found the right page, just, perhaps, not the right quote. I think that one should close that discussion about meanings of different words: I don't think explanations can be more clear than that. You don't need to dig around for strict meanings of different words, authors of the standard did that for you.
Posted Jan 28, 2022 21:13 UTC (Fri)
by larkey (guest, #104463)
[Link]
Posted Jan 28, 2022 16:08 UTC (Fri)
by judas_iscariote (guest, #47386)
[Link] (7 responses)
BSDs on this case return -EFAULT , other OSs return -EINVAL..so allowing this behaviour is not the common standard..
Posted Jan 28, 2022 16:18 UTC (Fri)
by larkey (guest, #104463)
[Link] (6 responses)
execve("/bin/date", NULL, NULL);
as mentioned in the year-old bug report. But this patch is about
execve("/bin/date", &(char*){NULL}, NULL);
which neither FreeBSD thinks is faulty, nor POSIX requires (i.e., FreeBSD is exactly in line with POSIX here). POSIX however recommends that argv[0] != NULL.
There are two issues here, a) that Linux isn't POSIX conforming, allowing argv == NULL, and b) pkexec relying on argv[0] != NULL which is only a POSIX recommendation. I do think the patch from Ariadne Conill should be applied, but then Linux is decidedly stricter than FreeBSD.
Posted Jan 28, 2022 16:22 UTC (Fri)
by larkey (guest, #104463)
[Link]
https://github.com/freebsd/freebsd-src/commit/773fa8cd136...
Copied from OpenBSD, says the commit message.
Posted Jan 28, 2022 17:33 UTC (Fri)
by ariadne (subscriber, #138312)
[Link] (1 responses)
Posted Jan 28, 2022 17:44 UTC (Fri)
by larkey (guest, #104463)
[Link]
But, at least to me, it was kind of confusing at first to see these two mangled.
Posted Jan 30, 2022 18:44 UTC (Sun)
by anton (subscriber, #25547)
[Link] (2 responses)
Posted Jan 31, 2022 9:58 UTC (Mon)
by larkey (guest, #104463)
[Link] (1 responses)
> The argument argv is an array of character pointers to null-terminated strings. The application shall ensure that the last member of this array is a null pointer.
There is nothing in POSIX saying "it may be an array, but it also may be NULL". If a pointer may be NULL in any C function, this is explicitly noted. Compare the standard on strtol(3):
> A pointer to the final string shall be stored in the object pointed to by endptr, provided that endptr is not a null pointer.
(https://pubs.opengroup.org/onlinepubs/9699919799/function...).
This is making it explicit that endptr may be NULL. The nptr argument, however, may not be NULL i.e.,
strtol(NULL, NULL, 10);
is *not* allowed. In the same vain, you can't just make argv be NULL.
Posted Jan 31, 2022 12:35 UTC (Mon)
by anton (subscriber, #25547)
[Link]
Posted Jan 28, 2022 18:53 UTC (Fri)
by flussence (guest, #85566)
[Link] (29 responses)
Posted Jan 28, 2022 19:16 UTC (Fri)
by NYKevin (subscriber, #129325)
[Link] (8 responses)
* /proc might not be mounted at all, or it might be mounted somewhere other than /proc.
Posted Jan 30, 2022 21:42 UTC (Sun)
by developer122 (guest, #152928)
[Link] (7 responses)
Posted Feb 1, 2022 1:34 UTC (Tue)
by NYKevin (subscriber, #129325)
[Link] (6 responses)
Posted Feb 1, 2022 14:57 UTC (Tue)
by mathstuf (subscriber, #69389)
[Link] (5 responses)
Posted Feb 1, 2022 18:07 UTC (Tue)
by nybble41 (subscriber, #55106)
[Link] (4 responses)
Interesting fact: Even though the /proc/$PID/exe symlink is "broken" you can still open it, producing a reference to the original file. The /proc filesystem has its own special rules for resolving symlinks. However, attempting to restore the file by creating a hard-link with `ln` or `link` gives the error "Invalid cross-device link" even though the original (deleted) file and the target directory are on the same filesystem. (You can *copy* /proc/$PID/exe to a new file, though.)
Posted Feb 7, 2022 9:28 UTC (Mon)
by jwilk (subscriber, #63328)
[Link] (3 responses)
" You probably wanted "
Posted Feb 7, 2022 18:27 UTC (Mon)
by nybble41 (subscriber, #55106)
[Link] (2 responses)
Yes, I tried it both with and without the -L option—I suppose I should have mentioned the difference in the error message. The kernel specifically blocks the creation of a hard-link to a previously deleted (zero-refcount) inode. It does work, however, if the inode still exists in the filesystem due to hard-links, even if the original path was deleted and the /proc/$PID/fd/N symlink is broken.
Posted Feb 10, 2022 21:21 UTC (Thu)
by Jandar (subscriber, #85683)
[Link] (1 responses)
If you an open the deleted /proc/pid/exe, why would linkat(fd, "", AT_FDCWD, "/path/for/file", AT_EMPTY_PATH); not work? A filedescriptor obtained with open( ".", , | O_RDWR ) has to have a zero st_nlink as well.
I have never done an open(,O_TMPFILE) + linkat(fd, "", AT_FDCWD,...) so this may be a misconception.
Posted Feb 11, 2022 0:29 UTC (Fri)
by nybble41 (subscriber, #55106)
[Link]
There are various things you can still do with /proc/$PID/fd/$N which may prove surprising from a security point of view. For example, if a file descriptor is opened read-only and passed to another user over a socket, or inherited by a child process, then normally the recipient can only read from that FD, even if they would have had write access to the original file. However, the target process inheriting that file descriptor can open /proc/self/fd/$N *for writing* and modify the contents of the file, even without access to the original path, so long as permissions on the file itself are compatible:
user$ mkdir my_dir
The overall moral of this example is that if you're relying on restrictive permissions on a parent directory to restrict access to otherwise-writable files it may not work out quite the way you hoped.
Posted Jan 28, 2022 20:06 UTC (Fri)
by dskoll (subscriber, #1630)
[Link] (18 responses)
You don't need to muck about in /proc. The first argument to execve() is the pathname being executed; that could be placed as argv[0].
Posted Jan 28, 2022 20:20 UTC (Fri)
by matthias (subscriber, #94967)
[Link] (1 responses)
If a callee does not inspect argv[0], then any string is good. If a callee expects some non-empty string as argv[0], then calling with argv pointing to { NULL } does not work today. And there is no reason why it should work tomorrow. Doing anything else than aborting with an error would only be to be backwards compatible with the current situation.
The only reason this could break anything that is working now that I can images is, that some callee actually expects to be called with argc==0 and argv pointing to { NULL }. But this would be really weird.
Posted Jan 30, 2022 21:43 UTC (Sun)
by developer122 (guest, #152928)
[Link]
Posted Jan 28, 2022 20:24 UTC (Fri)
by khim (subscriber, #9252)
[Link] (4 responses)
Nope. It couldn't. Since that one, too, may be So it's not bullet-proof and, I think, few examples where this Linux-only misfeature is exploited are not worth inventing crazy schemes.
Posted Jan 28, 2022 21:07 UTC (Fri)
by dskoll (subscriber, #1630)
[Link] (3 responses)
But if
Posted Jan 28, 2022 21:15 UTC (Fri)
by khim (subscriber, #9252)
[Link] (2 responses)
When someone would call `realpath` on it? That never happened in real-world usecase so yeah, you are right, that may a way out. Of course this would also break most tests anyway thus it's not clear if couple of real programs are worth that complexity. Kernel config option looks more and more sensible: we know such programs are rare, but how rare exactly?
Posted Jan 28, 2022 21:26 UTC (Fri)
by dskoll (subscriber, #1630)
[Link] (1 responses)
Posted Jan 29, 2022 2:48 UTC (Sat)
by comex (subscriber, #71521)
[Link]
Posted Jan 28, 2022 20:31 UTC (Fri)
by floppus (guest, #137245)
[Link] (7 responses)
If the kernel were to silently replace argc==0 with argc==1, it seems like using an empty string would be most parsimonious. At least an empty string is guaranteed not to be a valid filename, whereas /proc/self/exe or the name of the executable might or might not be.
Posted Jan 28, 2022 21:11 UTC (Fri)
by dskoll (subscriber, #1630)
[Link]
I think either an empty string or a copy of the first argument to execve is a good choice.
Posted Jan 28, 2022 21:21 UTC (Fri)
by khim (subscriber, #9252)
[Link] (3 responses)
How? All examples I have ever saw are using And empty arguments have tendency to make programs wonky.
Posted Jan 28, 2022 22:12 UTC (Fri)
by NYKevin (subscriber, #129325)
[Link] (2 responses)
setuid programs already need to defend against argv[0] == "" (or any other actual string, for that matter) because they must not trust the caller. POSIX explicitly specifies that the caller can set argv[0] to whatever it wants, and passing an invalid or incorrect argv[0] has been well understood for a long time as A Thing That Can Be Done (even though it's probably a bad idea in most cases). OTOH, the "there is no argv[0]" case is much less obvious and (I think) is not supported at all on some platforms, so it should not be too surprising that at least one setuid program failed to check for it.
So, from a security perspective, changing "the string doesn't exist" to "the string is empty" is the most straightforward way to close the security hole, since the callee is very likely already checking for an empty string (to the extent that it cares about argv[0] at all).
Posted Jan 28, 2022 22:41 UTC (Fri)
by JoeBuck (subscriber, #2330)
[Link]
Posted Feb 10, 2022 4:45 UTC (Thu)
by rlhamil (guest, #6472)
[Link]
That's a much more complex transformation than inserting argv[0]="" if argv is NULL or argv[0] is NULL.
Posted Jan 30, 2022 21:48 UTC (Sun)
by developer122 (guest, #152928)
[Link] (1 responses)
Posted Feb 1, 2022 1:37 UTC (Tue)
by NYKevin (subscriber, #129325)
[Link]
Posted Feb 4, 2022 16:48 UTC (Fri)
by sbaugh (guest, #103291)
[Link] (2 responses)
Not when using execveat(fd, "", ..., AT_EMPTY_PATH)
Posted Feb 4, 2022 17:04 UTC (Fri)
by adobriyan (subscriber, #30858)
[Link]
Posted Feb 5, 2022 1:14 UTC (Sat)
by mchapman (subscriber, #66589)
[Link]
Posted Jan 29, 2022 0:06 UTC (Sat)
by ariadne (subscriber, #138312)
[Link]
Posted Jan 29, 2022 16:45 UTC (Sat)
by ballombe (subscriber, #9523)
[Link]
Posted Jan 31, 2022 2:41 UTC (Mon)
by marcH (subscriber, #57642)
[Link] (10 responses)
First, a brief reminder that some standard requires argv[argc] to be NULL would be IMHO very useful. It's not obvious that it does because why would you need a clumsy NULL terminator when you already have a proper argc?
For a while I wondered "Did he mean the \0 byte terminating argv[argc - 1] instead?". The pointer arithmetic and discussion that follows clarifies but it comes after. Also, pointer arithmetic with a mix of pointers and arrays degrading to pointers is... not fun :-)
Posted Jan 31, 2022 8:53 UTC (Mon)
by anselm (subscriber, #2796)
[Link] (9 responses)
FWIW, my venerable copy of ANSI/ISO 9899-1990 says, in section 5.1.2.2.1:
Perhaps this was changed in the meantime but I would be surprised.
Presumably so you can say char **p; for (p = argv; *p; p++) {…}. One might argue that this is superfluous and that people ought to be doing something like int i; for (i = 0; i < argc; i++) {…} instead, but chances are that in the run-up to ANSI/ISO 9899-1990 there were C implementations that did guarantee argv[argc] == 0 already, and the former idiom was sufficiently widespread that it was easier to mandate this for everyone than to outlaw (or leave vague) something that worked fine for a large enough number of people.
Posted Jan 31, 2022 9:55 UTC (Mon)
by NYKevin (subscriber, #129325)
[Link] (1 responses)
I would tend to assume that the int loop is marginally easier to branch-predict? But I'm somewhat skeptical that that was relevant on the hardware of the time...
Posted Jan 31, 2022 11:19 UTC (Mon)
by mfuzzey (subscriber, #57966)
[Link]
Posted Jan 31, 2022 13:14 UTC (Mon)
by jem (subscriber, #24231)
[Link] (6 responses)
One might also argue that argc is superfluous, and iterating over argv by incrementing a pointer variable, stopping when the terminating NULL is reached, would be the idiomatic C thing to do. After all, this would be analogous to how C's null-terminated strings are handled.
Posted Jan 31, 2022 19:10 UTC (Mon)
by marcH (subscriber, #57642)
[Link] (5 responses)
Yes and that would be very ironic in an article about a pkexec security issue caused by not using argc as termination and fixed by looking at it :-)
Anyway comparing termination in C versus all other programming languages was not my point. Using _both_ argc and NULL at the same time is idiomatic in exactly zero programming language, so that's confusing. So I'm merely suggesting adding one short sentence about that.
Posted Jan 31, 2022 19:42 UTC (Mon)
by nybble41 (subscriber, #55106)
[Link] (4 responses)
One could equally well cast this issue as "caused by not checking the first element of the array for the NULL terminator". It's exactly the same error as if you received a NUL-terminated string and started iterating over it from the second element, assuming the string to be non-empty.
I'm in the Pascal-style string/list camp myself—lengths should be explicit—but using argc instead of checking for NULL is neither necessary nor sufficient to avoid this problem. (To illustrate the latter, consider the case of a program making the same non-empty argv assumption and using a loop of the form "int i = 1; do { ... } while (i++ < argc)" to shave off a "redundant" bounds check.)
Posted Jan 31, 2022 19:57 UTC (Mon)
by marcH (subscriber, #57642)
[Link] (3 responses)
It's "exactly the same" only if your termination looks like "i != argc" but no one ever does that. Everyone does "i < argc" or something like it. I mean no one writes:
for (int i; i++; i!=argc)
> but using argc instead of checking for NULL is neither necessary nor sufficient to avoid this problem.
I haven't looked at the actual code but if pkexec's loop had used an argc-based termination then the loop would have not run at all.
> int i = 1; do { ... } while (i++ < argc)" to shave off a "redundant" bounds check.)
That's bending over backwards to avoid a simpler for loop. I'm sure you can find some real world and valid cases for this but most of the time people just write a simpler for loop.
Posted Jan 31, 2022 22:19 UTC (Mon)
by marcH (subscriber, #57642)
[Link]
Of course I meant no one writes this:
for (int i=start; i!=argc; i++)
Sorry for the noise.
Posted Feb 1, 2022 1:58 UTC (Tue)
by NYKevin (subscriber, #129325)
[Link] (1 responses)
Maybe not in C, but C++ tacitly encourages this sort of nonsense because iterators are not, strictly speaking, required to be ordered (for example, a linked-list iterator may only be comparable for equality, because there is no O(1) way of ordering two iterators). So if you're taking C++ code and porting it to straight C, the most straightforward translation of a foreach loop is to use an inequality comparison instead of a less-than comparison, and it's possible that some programmers who started out as C++ programmers and later learned C may have picked this up as an idiom.
(Incidentally, these people also tend to favor the preincrement operator over the postincrement operator in contexts where there's no semantic difference, because in C++, postincrement can be significantly costlier due to the need to make a copy.)
Posted Feb 1, 2022 12:59 UTC (Tue)
by madscientist (subscriber, #16861)
[Link]
Also even in C, old-school programmers preferred pre-increment because post-increment was less efficient. Of course it was hard to detect that inefficiency, but it was a peg to hang one's preferential hat on and that's all that's needed when faced with two essentially equivalent choices. In C++ with operator overloading it can be measurably less efficient, of course, depending on the type implementation.
Posted Feb 13, 2022 23:46 UTC (Sun)
by Smon (guest, #104795)
[Link]
I am also happy with a kernel parameter or compile flag. When all distributions are using it, it can get the kernel default behavior.
Handling argc==0 in the kernel
Handling argc==0 in the kernel
The way argc / argv[] / envp[] are laid out on the stack by the elf loader is part of the elf ABI (for existing architectures at least).
You might be able to shuffle it in userspace, I guess.
Handling argc==0 in the kernel
Breaking envp == (1+ argc + argv) wlll break all current ELF executables that have been statically linked against glibc. [Asking for a system that is highly robust requires that there be at least a few such programs.] It will also break all executable main programs that have been compressed by upx.
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
argc == 0
both are fixed now and POSIX, in fact, says that explicitly to remove any shadow of doubt:exec
function, thus guaranteeing that argc
be one or greater when invoked by such an application.
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
associated with the process [...]
>
> For an implementation that conforms to POSIX.1-2017, describes a feature or behavior that is mandatory. An application can rely on the existence of the feature or behavior.
>
> For an implementation that conforms to POSIX.1-2017, describes a feature or behavior that is recommended but not mandatory. An application should not rely on the existence of the feature or behavior. An application that relies on such a feature or behavior cannot be assured to be portable across conforming implementations.
Handling argc==0 in the kernel
Handling argc==0 in the kernel
>
> The problematic case from the security persepctive is allowing argv[0] == NULL (or more precisely argc==0). The problematic case from the compatibility perspective is disallowing argv == NULL in the execve() call. Thus only disallowing argv == NULL in the call gives us the worst from both perspectives. We get all compatibility problems with no increase in security.
Allowing argv[0]==NULL would indeed allow the "old as Unix" pattern to work. But it also works fine now in the argv==NULL case. The pattern, perhaps equally old, that fails is:
Handling argc==0 in the kernel
for (arg = argv + 1; *arg; arg++)
/* ... */
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
>Why do people even try to come up with other ways to do things?
Handling argc==0 in the kernel
Handling argc==0 in the kernel
> for (**arg = &argv[1]; arg < &argv[argc]; arg++)
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
>It is legal to call malloc(0), and it is legal for malloc(0) to return a value other than NULL. If it does so, then the only thing you're allowed to do with that value is pass it to free()
malloc(0)
malloc(0)
malloc(0)
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Linux isn't POSIX conforming, allowing argv == NULL
Can you support this claim with actual POSIX wording?
Handling argc==0 in the kernel
In all of this text I see only requirements on the application, not on the OS. An application must not pass argv=NULL, and POSIX does not define define what the OS shall do if the application is non-conforming in this respect. Linux has a certain behaviour, BSD a different one, both conforming AFAICT.
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
* / might not be / (e.g. chroot, containers, etc.), and so /proc might be inaccessible.
* The callee might try to pass the argv[0] string on to some other process, but then /proc/self will be interpreted as that process.
* If you try to fix the above using an absolute PID instead of "self," then you run into PID namespaces.
* There are a variety of situations where the called process's binary is not directly accessible to the called process as a regular file, and /proc/self/exe appears to be a broken symlink. The only way to fix this problem is for the caller to provide a suitable alternative path, if a functional argv[0] is desired at all.
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
ln /proc/$PID/exe foo
" fails with EXDEV
, because it's trying to hardlink the symlink that resides in /proc
.
ln -L /proc/$PID/exe foo
" to dereference the symlink… which still doesn't work, but at least you get a more understandable error message. (The dereferencing happens on the kernel side, so it could work in principle.)
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
user$ chmod 0700 my_dir
user$ cd my_dir
user$ echo test > testfile
user$ chmod 0666 testfile
user$ exec 3< testfile
user$ su -c 'su nobody -s /bin/bash' # can't use sudo here as by default it would close the FD
nobody$ ls -l /proc/self/fd/3
lr-x------ 1 nobody nogroup 64 Feb 10 00:00 /proc/self/fd/3 -> /home/user/my_dir/testfile
nobody$ cat /home/user/my_dir/testfile # no access via my_dir
cat: /home/user/my_dir/testfile: Permission denied
nobody$ cat /proc/self/fd/3
test
nobody$ echo attempt1 1>&3 # writing to the FD itself fails, because it was opened read-only
bash: echo: write error: Bad file descriptor
nobody$ echo attempt2 > /proc/self/fd/3 # but this succeeds!
nobody$ cat /proc/self/fd/3
attempt2
nobody$ mkdir /tmp/other_dir && cd /tmp/other_dir
nobody$ ln -L /proc/self/fd/3 testfile_link # this works despite /proc/sys/fs/protected_hardlinks because we have read and write access to the file
nobody$ ls -l testfile_link
-rw-rw-rw- 3 user user 9 Feb 10 00:00 testfile_link
nobody$ echo attempt3 > testfile_link
nobody$ cat testfile_link
attempt3
nobody$ exit
user$ cat testfile # confirm that the original file was changed
attempt3
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
/proc/self/exe
(and, indeed there are one case where pattern exec("/proc/self/exe", NULL, NULL);
is used).Handling argc==0 in the kernel
execve("/proc/self/exe", NULL, NULL);
succeeds, under what circumstances could /proc/self/exe
not be relied on in the execed program?
Handling argc==0 in the kernel
Handling argc==0 in the kernel
realpath
is an executable; I think you meant readlink(2)
. But my point is this: If the original execve
call succeeds, then "/proc/self/exe"
must still be valid in the exec'd program. if /proc/self/exe
were not valid, then the execve
call would fail.
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
> But if any setuid program actually relies on argv[0] being the name of the program, that's a severe bug (symlinks, TOCTOU, etc., never mind the fact that the caller can always specify any string they want.)
Handling argc==0 in the kernel
argv[0]
only to multiplex binaries. They don't ever look on the actual binary which is specified in argv[0]
, rather, they are only interested in `basename`. Symlinks, TOCTOU and even the fact that caller can specify anything there are irrelevant: for them it's just an argument of the command line which happen to include name of the program.Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
What not a special ldsuid.so loader for SUID program that would sanitize everything before the real program start ?
This way security-conscious distro could ship a ldsuid.so program that implement the security they want.
Handling argc==0 in the kernel
Handling argc==0 in the kernel
First, a brief reminder that some standard requires argv[argc] to be NULL would be IMHO very useful.
argv[argc] shall be a null pointer.
It's not obvious that it does because why would you need a clumsy NULL terminator when you already have a proper argc?
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Handling argc==0 in the kernel
>
> for (int i; i++; i!=argc)
Handling argc==0 in the kernel
Handling argc==0 in the kernel
Only set argv[0] to "" if the binary is setuid (or has other privileges).