Skip to content

Commit

Permalink
run-command: treat inaccessible directories as ENOENT
Browse files Browse the repository at this point in the history
When execvp reports EACCES, it can be one of two things:

  1. We found a file to execute, but did not have
     permissions to do so.

  2. We did not have permissions to look in some directory
     in the $PATH.

In the former case, we want to consider this a
permissions problem and report it to the user as such (since
getting this for something like "git foo" is likely a
configuration error).

In the latter case, there is a good chance that the
inaccessible directory does not contain anything of
interest. Reporting "permission denied" is confusing to the
user (and prevents our usual "did you mean...?" lookup). It
also prevents git from trying alias lookup, since we do so
only when an external command does not exist (not when it
exists but has an error).

This patch detects EACCES from execvp, checks whether we are
in case (2), and if so converts errno to ENOENT. This
behavior matches that of "bash" (but not of simpler shells
that use execvp more directly, like "dash").

Test stolen from Junio.

Signed-off-by: Jeff King <peff@peff.net>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
  • Loading branch information
peff authored and gitster committed Apr 5, 2012
1 parent 1696d72 commit 38f865c
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 3 deletions.
2 changes: 2 additions & 0 deletions cache.h
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -1261,4 +1261,6 @@ extern struct startup_info *startup_info;
/* builtin/merge.c */ /* builtin/merge.c */
int checkout_fast_forward(const unsigned char *from, const unsigned char *to); int checkout_fast_forward(const unsigned char *from, const unsigned char *to);


int sane_execvp(const char *file, char *const argv[]);

#endif /* CACHE_H */ #endif /* CACHE_H */
2 changes: 1 addition & 1 deletion exec_cmd.c
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ int execv_git_cmd(const char **argv) {
trace_argv_printf(nargv, "trace: exec:"); trace_argv_printf(nargv, "trace: exec:");


/* execvp() can only ever return if it fails */ /* execvp() can only ever return if it fails */
execvp("git", (char **)nargv); sane_execvp("git", (char **)nargv);


trace_printf("trace: exec failed: %s\n", strerror(errno)); trace_printf("trace: exec failed: %s\n", strerror(errno));


Expand Down
66 changes: 64 additions & 2 deletions run-command.c
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -18,6 +18,68 @@ static inline void dup_devnull(int to)
} }
#endif #endif


static char *locate_in_PATH(const char *file)
{
const char *p = getenv("PATH");
struct strbuf buf = STRBUF_INIT;

if (!p || !*p)
return NULL;

while (1) {
const char *end = strchrnul(p, ':');

strbuf_reset(&buf);

/* POSIX specifies an empty entry as the current directory. */
if (end != p) {
strbuf_add(&buf, p, end - p);
strbuf_addch(&buf, '/');
}
strbuf_addstr(&buf, file);

if (!access(buf.buf, F_OK))
return strbuf_detach(&buf, NULL);

if (!*end)
break;
p = end + 1;
}

strbuf_release(&buf);
return NULL;
}

static int exists_in_PATH(const char *file)
{
char *r = locate_in_PATH(file);
free(r);
return r != NULL;
}

int sane_execvp(const char *file, char * const argv[])
{
if (!execvp(file, argv))
return 0; /* cannot happen ;-) */

/*
* When a command can't be found because one of the directories
* listed in $PATH is unsearchable, execvp reports EACCES, but
* careful usability testing (read: analysis of occasional bug
* reports) reveals that "No such file or directory" is more
* intuitive.
*
* We avoid commands with "/", because execvp will not do $PATH
* lookups in that case.
*
* The reassignment of EACCES to errno looks like a no-op below,
* but we need to protect against exists_in_PATH overwriting errno.
*/
if (errno == EACCES && !strchr(file, '/'))
errno = exists_in_PATH(file) ? EACCES : ENOENT;
return -1;
}

static const char **prepare_shell_cmd(const char **argv) static const char **prepare_shell_cmd(const char **argv)
{ {
int argc, nargc = 0; int argc, nargc = 0;
Expand Down Expand Up @@ -56,7 +118,7 @@ static int execv_shell_cmd(const char **argv)
{ {
const char **nargv = prepare_shell_cmd(argv); const char **nargv = prepare_shell_cmd(argv);
trace_argv_printf(nargv, "trace: exec:"); trace_argv_printf(nargv, "trace: exec:");
execvp(nargv[0], (char **)nargv); sane_execvp(nargv[0], (char **)nargv);
free(nargv); free(nargv);
return -1; return -1;
} }
Expand Down Expand Up @@ -278,7 +340,7 @@ int start_command(struct child_process *cmd)
} else if (cmd->use_shell) { } else if (cmd->use_shell) {
execv_shell_cmd(cmd->argv); execv_shell_cmd(cmd->argv);
} else { } else {
execvp(cmd->argv[0], (char *const*) cmd->argv); sane_execvp(cmd->argv[0], (char *const*) cmd->argv);
} }
if (errno == ENOENT) { if (errno == ENOENT) {
if (!cmd->silent_exec_failure) if (!cmd->silent_exec_failure)
Expand Down
13 changes: 13 additions & 0 deletions t/t0061-run-command.sh
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -34,4 +34,17 @@ test_expect_success POSIXPERM 'run_command reports EACCES' '
grep "fatal: cannot exec.*hello.sh" err grep "fatal: cannot exec.*hello.sh" err
' '


test_expect_success POSIXPERM 'unreadable directory in PATH' '
mkdir local-command &&
test_when_finished "chmod u+rwx local-command && rm -fr local-command" &&
git config alias.nitfol "!echo frotz" &&
chmod a-rx local-command &&
(
PATH=./local-command:$PATH &&
git nitfol >actual
) &&
echo frotz >expect &&
test_cmp expect actual
'

test_done test_done

0 comments on commit 38f865c

Please sign in to comment.