Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 34 additions & 6 deletions userland/apps/shell/src/line_edit.c
Original file line number Diff line number Diff line change
Expand Up @@ -355,9 +355,7 @@ static void handle_tab(line_edit_state* s, const char* prompt) {
int name_prefix_len;

if (cmd_mode) {
/* Command mode: search /bin/ for matching executables */
strncpy(dir_path, "/bin", sizeof(dir_path) - 1);
dir_path[sizeof(dir_path) - 1] = '\0';
/* Command mode: search every PATH directory for executables */
int cap = tok_len < COMPLETE_NAME_MAX - 1 ? tok_len : COMPLETE_NAME_MAX - 1;
memcpy(name_prefix, prefix, (size_t)cap);
name_prefix[cap] = '\0';
Expand Down Expand Up @@ -395,12 +393,42 @@ static void handle_tab(line_edit_state* s, const char* prompt) {
completion_entry* entries = malloc(COMPLETE_MAX * sizeof(completion_entry));
if (!entries) return;

int count = collect_candidates(dir_path, name_prefix, name_prefix_len,
entries, COMPLETE_MAX);
int count = 0;
if (cmd_mode) {
const char* path = getenv("PATH");
if (!path || !*path) path = "/bin";

while (*path && count < COMPLETE_MAX) {
const char* sep = strchr(path, ':');
int dir_len = sep ? (int)(sep - path) : (int)strlen(path);

if (dir_len > 0 && dir_len < (int)sizeof(dir_path)) {
memcpy(dir_path, path, (size_t)dir_len);
dir_path[dir_len] = '\0';
count += collect_candidates(dir_path, name_prefix, name_prefix_len,
entries + count, COMPLETE_MAX - count);
}

if (!sep) break;
path = sep + 1;
}
} else {
count = collect_candidates(dir_path, name_prefix, name_prefix_len,
entries, COMPLETE_MAX);
}

if (count > 1)
if (count > 1) {
qsort(entries, (size_t)count, sizeof(completion_entry), completion_cmp);

/* Drop duplicate names collected from multiple PATH directories */
int unique = 1;
for (int i = 1; i < count; i++) {
if (strcmp(entries[i].name, entries[unique - 1].name) != 0)
entries[unique++] = entries[i];
}
count = unique;
}

if (count == 0) {
/* no matches */
free(entries);
Expand Down
23 changes: 23 additions & 0 deletions userland/apps/shell/src/shell.c
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,31 @@ static int reap_status(int status) {
return status;
}

/*
* Resolve a bare command name against PATH, falling back to /bin.
* Names containing '/' are used as-is.
*/
static const char* resolve_cmd(const char* name, char* path_buf, int buf_size) {
if (strchr(name, '/')) return name;

const char* path = getenv("PATH");
if (!path || !*path) path = "/bin";

while (*path) {
const char* sep = strchr(path, ':');
int dir_len = sep ? (int)(sep - path) : (int)strlen(path);

if (dir_len > 0) {
int n = snprintf(path_buf, buf_size, "%.*s/%s", dir_len, path, name);
if (n > 0 && n < buf_size && access(path_buf, X_OK) == 0)
return path_buf;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PATH match accepts any node

Medium Severity · Logic Bug

resolve_cmd treats a successful access(..., X_OK) as proof it found the command and stops walking PATH. On this OS, access/faccessat succeeds for any resolvable node, including directories and non-executables, so an earlier shadowing name prevents later valid binaries from being tried and surfaces as command not found.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3bb54d8. Configure here.

}

if (!sep) break;
path = sep + 1;
}

/* Keep the historical candidate so failure messaging is unchanged */
int n = snprintf(path_buf, buf_size, "/bin/%s", name);
return (n > 0 && n < buf_size) ? path_buf : name;
}
Expand Down
Loading