From 296e9ff76312edb89a565a8ab737cf4ea905aa41 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 16:46:28 +0000 Subject: [PATCH] fix: use access(F_OK) to classify exec exit code for explicit paths On musl i486 under Ubuntu 24.04, the 32-bit execve syscall may return EACCES instead of ENOENT for a non-existent path (seccomp filtering of 32-bit syscalls). The old code checked errno==ENOENT to decide between exit code 127 (not found) and 126 (not executable), so it emitted 126 when EACCES was returned even though the binary does not exist, failing the test assertion. For explicit paths (those containing '/'), switch to using access(F_OK) to determine 127 vs 126: if the file does not exist the command was not found (127); if it exists it was found but could not be executed (126). This is independent of errno and handles any EACCES/ENOENT variant. PATH-resolved names (no '/' in argument) continue to use errno because access() cannot evaluate PATH-relative lookups. --- src/limiter.c | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/limiter.c b/src/limiter.c index 5211d21a..aaa540c0 100644 --- a/src/limiter.c +++ b/src/limiter.c @@ -244,19 +244,22 @@ static void exec_child_process(const struct cpulimit_cfg *cfg, int sync_read_fd, /* * Execution reaches here only if execvp() failed. * Use shell-compatible exit codes: - * - EXIT_CMD_NOT_FOUND (127): command not found (ENOENT with no - * existing file) + * - EXIT_CMD_NOT_FOUND (127): command not found * - EXIT_CMD_NOT_EXECUTABLE (126): found but not executable - * (permission denied, or ENOENT when file exists but its - * interpreter is absent) * - * Note: ENOENT can occur for two distinct reasons: - * 1. The command (or a PATH-resolved name) does not exist -> 127. - * 2. The file exists but exec fails (e.g., missing dynamic linker - * on an explicit-path binary) -> 126. - * Case 2 is detectable only when the argument contains a '/' - * (an explicit path), because only then can access(F_OK) confirm - * that the file itself is present. + * For explicit paths (containing '/') the correct classification is + * determined by access(F_OK): if the file does not exist, the + * command was not found (127); if it does exist, it was found but + * could not be executed (126, e.g. missing dynamic linker, bad + * permissions, or a shebang interpreter that exec cannot locate). + * Using access(F_OK) rather than relying on errno is necessary + * because some kernel/libc combinations (e.g. 32-bit musl on Linux + * with seccomp-restricted execve) return EACCES instead of ENOENT + * for non-existent paths, which would otherwise be misclassified as + * "not executable" (126) rather than "not found" (127). + * + * For PATH-resolved names (no '/' in argument) the errno from + * execvp is the only signal available. * * Close sync_write_fd explicitly to signal exec failure to the * parent (the FD_CLOEXEC path only applies on success). @@ -264,9 +267,10 @@ static void exec_child_process(const struct cpulimit_cfg *cfg, int sync_read_fd, saved_errno = errno; perror("execvp"); close(sync_write_fd); - if (saved_errno == ENOENT && strchr(cfg->command_args[0], '/') != NULL && - access(cfg->command_args[0], F_OK) == 0) { - _exit(EXIT_CMD_NOT_EXECUTABLE); + if (strchr(cfg->command_args[0], '/') != NULL) { + _exit(access(cfg->command_args[0], F_OK) == 0 + ? EXIT_CMD_NOT_EXECUTABLE + : EXIT_CMD_NOT_FOUND); } _exit(saved_errno == ENOENT ? EXIT_CMD_NOT_FOUND : EXIT_CMD_NOT_EXECUTABLE);