On Fri, Sep 16, 2022 at 02:41:30PM +0100, Josh Triplett wrote: > Currently, execve allocates an mm and parses argv and envp before > checking if the path exists. However, the common case of a $PATH search > may have several failed calls to exec before a single success. Do a > filename lookup for the purposes of returning ENOENT before doing more > expensive operations. > > This does not create a TOCTTOU race, because this can only happen if the > file didn't exist at some point during the exec call, and that point is > permitted to be when we did our lookup. > > To measure performance, I ran 2000 fork and execvpe calls with a > seven-element PATH in which the file was found in the seventh directory > (representative of the common case as /usr/bin is the seventh directory > on my $PATH), as well as 2000 fork and execve calls with an absolute > path to an existing binary. I recorded the minimum time for each, to > eliminate noise from context switches and similar. > > Without fast-path: > fork/execvpe: 49876ns > fork/execve: 32773ns > > With fast-path: > fork/execvpe: 36890ns > fork/execve: 32069ns > > The cost of the additional lookup seems to be in the noise for a > successful exec, but it provides a 26% improvement for the path search > case by speeding up the six failed execs. > > Signed-off-by: Josh Triplett <josh@xxxxxxxxxxxxxxxx> > --- > > Discussed this at Plumbers with Kees Cook; turned out to be even more of > a win than anticipated. > > fs/exec.c | 10 ++++++++++ > 1 file changed, 10 insertions(+) > > diff --git a/fs/exec.c b/fs/exec.c > index 9a5ca7b82bfc..fe786aeb2f1b 100644 > --- a/fs/exec.c > +++ b/fs/exec.c > @@ -1881,6 +1881,16 @@ static int do_execveat_common(int fd, struct filename *filename, > if (IS_ERR(filename)) > return PTR_ERR(filename); > > + /* Fast-path ENOENT for $PATH search failures, before we alloc an mm or > + * parse arguments. */ > + if (fd == AT_FDCWD && flags == 0 && filename->name[0] == '/') { > + struct path path; > + retval = filename_lookup(AT_FDCWD, filename, 0, &path, NULL); > + if (retval == -ENOENT) Oh, actually, I see the 0-day problem. This should be: if (retval < 0) > + goto out_ret; > + path_put(&path); Otherwise this put will happen for an non-successful lookup that wans't ENOENT. I've fixed this in my tree. -Kees > + } > + > /* > * We move the actual failure in case of RLIMIT_NPROC excess from > * set*uid() to execve() because too many poorly written programs > -- > 2.37.2 > -- Kees Cook