diff --git a/NEWS b/NEWS index 3c39c636d..ace21901c 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,15 @@ For full details, see the git log at: https://github.com/ksh93/ksh Any uppercase BUG_* names are modernish shell bug IDs. +2021-04-13: + +- Fixed a few bugs that could cause ksh to show the wrong error message and/or + return the wrong exit status if a command couldn't be executed. In + scenarios where the command was found in the PATH but it was not executable, + ksh now returns with exit status 126. Otherwise, ksh will return with exit + status 127 (such as if the command isn't found or if the command name is + too long). + 2021-04-12: - Corrected a memory fault when an attempt was made to unset the default diff --git a/src/cmd/ksh93/data/msg.c b/src/cmd/ksh93/data/msg.c index c27d788bd..f9982554e 100644 --- a/src/cmd/ksh93/data/msg.c +++ b/src/cmd/ksh93/data/msg.c @@ -83,6 +83,9 @@ const char e_logout[] = "Use 'exit' to terminate this shell"; const char e_exec[] = "%s: cannot execute"; const char e_pwd[] = "cannot access parent directories"; const char e_found[] = "%s: not found"; +#ifdef ENAMETOOLONG +const char e_toolong[] = "%s: file name too long"; +#endif const char e_defined[] = "%s: function not defined"; const char e_nointerp[] = "%s: interpreter not found"; const char e_subscript[] = "%s: subscript out of range"; diff --git a/src/cmd/ksh93/include/shell.h b/src/cmd/ksh93/include/shell.h index 4619838fa..740b9592d 100644 --- a/src/cmd/ksh93/include/shell.h +++ b/src/cmd/ksh93/include/shell.h @@ -147,6 +147,9 @@ typedef union Shnode_u Shnode_t; /* error messages */ extern const char e_found[]; +#ifdef ENAMETOOLONG +extern const char e_toolong[]; +#endif extern const char e_format[]; extern const char e_number[]; extern const char e_restricted[]; diff --git a/src/cmd/ksh93/include/version.h b/src/cmd/ksh93/include/version.h index 7e1cf57e2..b3d0fd828 100644 --- a/src/cmd/ksh93/include/version.h +++ b/src/cmd/ksh93/include/version.h @@ -20,7 +20,7 @@ #define SH_RELEASE_FORK "93u+m" /* only change if you develop a new ksh93 fork */ #define SH_RELEASE_SVER "1.0.0-alpha" /* semantic version number: https://semver.org */ -#define SH_RELEASE_DATE "2021-04-12" /* must be in this format for $((.sh.version)) */ +#define SH_RELEASE_DATE "2021-04-13" /* must be in this format for $((.sh.version)) */ #define SH_RELEASE_CPYR "(c) 2020-2021 Contributors to ksh " SH_RELEASE_FORK /* Scripts sometimes field-split ${.sh.version}, so don't change amount of whitespace. */ diff --git a/src/cmd/ksh93/sh/path.c b/src/cmd/ksh93/sh/path.c index 2c58ebfcf..6d6ad491d 100644 --- a/src/cmd/ksh93/sh/path.c +++ b/src/cmd/ksh93/sh/path.c @@ -769,6 +769,8 @@ Pathcomp_t *path_absolute(Shell_t *shp,register const char *name, Pathcomp_t *pp { sh_sigcheck(shp); shp->bltin_dir = 0; + /* In this loop, oldpp is the current pointer. + pp is the next pointer. */ while(oldpp=pp) { pp = path_nextcomp(shp,pp,name,0); @@ -901,10 +903,10 @@ Pathcomp_t *path_absolute(Shell_t *shp,register const char *name, Pathcomp_t *pp np->nvflag = n; } } + if(f<0 && errno!=ENOENT) + noexec = errno; if(!pp || f>=0) break; - if(errno!=ENOENT) - noexec = errno; } if(f<0) { @@ -1007,7 +1009,8 @@ noreturn void path_exec(Shell_t *shp,register const char *arg0,register char *ar char **envp; const char *opath; Pathcomp_t *libpath, *pp=0; - int slash=0; + int slash=0, not_executable=0; + pid_t spawnpid; nv_setlist(local,NV_EXPORT|NV_IDENT|NV_ASSIGN,0); envp = sh_envgen(); if(strchr(arg0,'/')) @@ -1038,18 +1041,40 @@ noreturn void path_exec(Shell_t *shp,register const char *arg0,register char *ar } else opath = arg0; - path_spawn(shp,opath,argv,envp,libpath,0); + spawnpid = path_spawn(shp,opath,argv,envp,libpath,0); + if(spawnpid==-1 && shp->path_err!=ENOENT) + { + /* + * A command was found but it couldn't be executed. + * POSIX specifies that the shell should continue to search for the + * command in PATH and return 126 only when it can't find an executable + * file in other elements of PATH. + */ + not_executable = shp->path_err; + } while(pp && (pp->flags&PATH_FPATH)) pp = path_nextcomp(shp,pp,arg0,0); } while(pp); /* force an exit */ ((struct checkpt*)shp->jmplist)->mode = SH_JMPEXIT; - if((errno=shp->path_err)==ENOENT) + errno = not_executable ? not_executable : shp->path_err; + switch(errno) + { + /* the first two cases return exit status 127 (the command wasn't in the PATH) */ + case ENOENT: errormsg(SH_DICT,ERROR_exit(ERROR_NOENT),e_found,arg0); - else + UNREACHABLE(); +#ifdef ENAMETOOLONG + case ENAMETOOLONG: + errormsg(SH_DICT,ERROR_exit(ERROR_NOENT),e_toolong,arg0); + UNREACHABLE(); +#endif + /* other cases return exit status 126 (the command was found, but wasn't executable) */ + default: errormsg(SH_DICT,ERROR_system(ERROR_NOEXEC),e_exec,arg0); - UNREACHABLE(); + UNREACHABLE(); + } } pid_t path_spawn(Shell_t *shp,const char *opath,register char **argv, char **envp, Pathcomp_t *libpath, int spawn) @@ -1187,6 +1212,8 @@ pid_t path_spawn(Shell_t *shp,const char *opath,register char **argv, char **env return(pid); switch(shp->path_err = errno) { + case EISDIR: + return -1; case ENOEXEC: #if SHOPT_SUID_EXEC case EPERM: diff --git a/src/cmd/ksh93/sh/xec.c b/src/cmd/ksh93/sh/xec.c index 69af7ffc6..34b3f1a66 100644 --- a/src/cmd/ksh93/sh/xec.c +++ b/src/cmd/ksh93/sh/xec.c @@ -3622,6 +3622,11 @@ static pid_t sh_ntfork(Shell_t *shp,const Shnode_t *t,char *argv[],int *jobid,in case ENOENT: errormsg(SH_DICT,ERROR_exit(ERROR_NOENT),e_found+4); UNREACHABLE(); +#ifdef ENAMETOOLONG + case ENAMETOOLONG: + errormsg(SH_DICT,ERROR_exit(ERROR_NOENT),e_toolong+4); + UNREACHABLE(); +#endif default: errormsg(SH_DICT,ERROR_system(ERROR_NOEXEC),e_exec+4); UNREACHABLE(); diff --git a/src/cmd/ksh93/tests/path.sh b/src/cmd/ksh93/tests/path.sh index b8aaf6777..b61c228bd 100755 --- a/src/cmd/ksh93/tests/path.sh +++ b/src/cmd/ksh93/tests/path.sh @@ -180,18 +180,7 @@ builtin -d date 2> /dev/null if [[ $(PATH=:/usr/bin; date) != 'hello' ]] then err_exit "leading : in path not working" fi -( - PATH=$PWD: - builtin chmod - print 'print cannot execute' > noexec - chmod 644 noexec - if [[ ! -x noexec ]] - then noexec > /dev/null 2>&1 - else exit 126 - fi -) -status=$? -[[ $status == 126 ]] || err_exit "exit status of non-executable is $status -- 126 expected" + builtin -d rm 2> /dev/null chmod=$(whence chmod) rm=$(whence rm) @@ -734,5 +723,185 @@ PATH=$savePATH [[ -z $got ]] || err_exit "PATH search inconsistent after changing PATH in subshare (got $(printf %q "$got"))" # ====== -exit $((Errors<125?Errors:125)) +# POSIX: If a command is found but isn't executable, the exit status should be 126. +# The tests are arranged as follows: +# Test *A runs commands with the -c execve(2) optimization. +# Test *B runs commands with spawnveg (i.e., with posix_spawn(3) or vfork(2)). +# Test *C runs commands with fork(2) in an interactive shell. +# Test *D runs commands with 'command -x'. +# Test *E runs commands with 'exec'. +# https://github.com/att/ast/issues/485 +rm -rf noexecute +print 'print cannot execute' > noexecute +mkdir emptydir cmddir +exp=126 +PATH=$PWD $SHELL -c 'noexecute' > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 1A: exit status of non-executable command wrong" \ + "(expected $exp, got $got)" +PATH=$PWD $SHELL -c 'noexecute; exit $?' > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 1B: exit status of non-executable command wrong" \ + "(expected $exp, got $got)" +PATH=$PWD $SHELL -ic 'noexecute; exit $?' > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 1C: exit status of non-executable command wrong" \ + "(expected $exp, got $got)" +PATH=$PWD $SHELL -c 'command -x noexecute; exit $?' > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 1D: exit status of non-executable command wrong" \ + "(expected $exp, got $got)" +PATH=$PWD $SHELL -c 'exec noexecute' > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 1E: exit status of exec'd non-executable command wrong" \ + "(expected $exp, got $got)" +# Add an empty directory where the command isn't found. +PATH=$PWD:$PWD/emptydir $SHELL -c 'noexecute' > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 2A: exit status of non-executable command wrong" \ + "(expected $exp, got $got)" +PATH=$PWD:$PWD/emptydir $SHELL -c 'noexecute; exit $?' > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 2B: exit status of non-executable command wrong" \ + "(expected $exp, got $got)" +PATH=$PWD:$PWD/emptydir $SHELL -ic 'noexecute; exit $?' > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 2C: exit status of non-executable command wrong" \ + "(expected $exp, got $got)" +PATH=$PWD:$PWD/emptydir $SHELL -c 'command -x noexecute; exit $?' > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 2D: exit status of non-executable command wrong" \ + "(expected $exp, got $got)" +PATH=$PWD:$PWD/emptydir $SHELL -c 'exec noexecute' > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 2E: exit status of exec'd non-executable command wrong" \ + "(expected $exp, got $got)" + +# If an executable command is found after a non-executable command, skip the non-executable one. +print 'true' > cmddir/noexecute +chmod +x cmddir/noexecute +exp=0 +PATH=$PWD:$PWD/cmddir $SHELL -c 'noexecute' +got=$? +[[ $exp == $got ]] || err_exit "Test 3A: failed to run executable command after encountering non-executable command" \ + "(expected $exp, got $got)" +PATH=$PWD:$PWD/cmddir $SHELL -c 'noexecute; exit $?' +got=$? +[[ $exp == $got ]] || err_exit "Test 3B: failed to run executable command after encountering non-executable command" \ + "(expected $exp, got $got)" +PATH=$PWD:$PWD/cmddir $SHELL -ic 'noexecute; exit $?' +got=$? +[[ $exp == $got ]] || err_exit "Test 3C: failed to run executable command after encountering non-executable command" \ + "(expected $exp, got $got)" +PATH=$PWD:$PWD/cmddir $SHELL -c 'command -x noexecute; exit $?' +got=$? +[[ $exp == $got ]] || err_exit "Test 3D: failed to run executable command after encountering non-executable command" \ + "(expected $exp, got $got)" +PATH=$PWD:$PWD/cmddir $SHELL -c 'exec noexecute' +got=$? +[[ $exp == $got ]] || err_exit "Test 3E: failed to run exec'd executable command after encountering non-executable command" \ + "(expected $exp, got $got)" + +# Same test as above, but with a directory of the same name in the PATH. +rm "$PWD/noexecute" +mkdir "$PWD/noexecute" +PATH=$PWD:$PWD/cmddir $SHELL -c 'noexecute' > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 4A: failed to run executable command after encountering directory with same name in PATH" \ + "(expected $exp, got $got)" +PATH=$PWD:$PWD/cmddir $SHELL -c 'noexecute; exit $?' > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 4B: failed to run executable command after encountering directory with same name in PATH" \ + "(expected $exp, got $got)" +PATH=$PWD:$PWD/cmddir $SHELL -ic 'noexecute; exit $?' > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 4C: failed to run executable command after encountering directory with same name in PATH" \ + "(expected $exp, got $got)" +PATH=$PWD:$PWD/cmddir $SHELL -c 'command -x noexecute; exit $?' > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 4D: failed to run executable command after encountering directory with same name in PATH" \ + "(expected $exp, got $got)" +PATH=$PWD:$PWD/cmddir $SHELL -c 'exec noexecute' > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 4E: failed to run exec'd executable command after encountering directory with same name in PATH" \ + "(expected $exp, got $got)" +# Don't treat directories as commands. +# https://github.com/att/ast/issues/757 +mkdir cat +PATH=".:$PATH" cat < /dev/null || err_exit "Test 4F: directories should not be treated as executables" + +# Test attempts to run directories located in the PATH. +exp=126 +PATH=$PWD $SHELL -c 'noexecute' > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 5A: exit status of non-executable command wrong" \ + "(expected $exp, got $got)" +PATH=$PWD $SHELL -c 'noexecute; exit $?' > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 5B: exit status of non-executable command wrong" \ + "(expected $exp, got $got)" +PATH=$PWD $SHELL -ic 'noexecute; exit $?' > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 5C: exit status of non-executable command wrong" \ + "(expected $exp, got $got)" +PATH=$PWD $SHELL -c 'command -x noexecute; exit $?' > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 5D: exit status of non-executable command wrong" \ + "(expected $exp, got $got)" +PATH=$PWD $SHELL -c 'exec noexecute' > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 5E: exit status of exec'd non-executable command wrong" \ + "(expected $exp, got $got)" + +# Tests for attempting to run a non-existent command. +exp=127 +PATH=/dev/null $SHELL -c 'nonexist' > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 6A: exit status of non-existent command wrong" \ + "(expected $exp, got $got)" +PATH=/dev/null $SHELL -c 'nonexist; exit $?' > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 6B: exit status of non-existent command wrong" \ + "(expected $exp, got $got)" +PATH=/dev/null $SHELL -ic 'nonexist; exit $?' > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 6C: exit status of non-existent command wrong" \ + "(expected $exp, got $got)" +PATH=/dev/null $SHELL -c 'command -x nonexist; exit $?' > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 6D: exit status of non-existent command wrong" \ + "(expected $exp, got $got)" +PATH=/dev/null $SHELL -c 'exec nonexist' > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 6E: exit status of exec'd non-existent command wrong" \ + "(expected $exp, got $got)" + +# Tests for attempting to use a command name that's too long. +# To make the error messages readable, the long string is replaced +# with 'LONG_CMD_NAME' in the err_exit output. +long_cmd=$(awk -v ORS= 'BEGIN { for(i=0;i<500;i++) print "xxxxxxxxxx"; }') +exp=127 +PATH=$PWD $SHELL -c "$long_cmd" > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 7A: exit status or error message for command with long name wrong" \ + "(expected $exp, got $got)" +PATH=$PWD $SHELL -c "$long_cmd; exit \$?" > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 7B: exit status or error message for command with long name wrong" \ + "(expected $exp, got $got)" +PATH=$PWD $SHELL -ic "$long_cmd; exit \$?" > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 7C: exit status or error message for command with long name wrong" \ + "(expected $exp, got $got)" +PATH=$PWD $SHELL -c "command -x $long_cmd; exit \$?" > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 7D: exit status or error message for command with long name wrong" \ + "(expected $exp, got $got)" +PATH=$PWD $SHELL -c "exec $long_cmd" > /dev/null 2>&1 +got=$? +[[ $exp == $got ]] || err_exit "Test 7E: exit status or error message for exec'd command with long name wrong" \ + "(expected $exp, got $got)" + +# ====== +exit $((Errors<125?Errors:125)) diff --git a/src/lib/libast/include/ast.h b/src/lib/libast/include/ast.h index 65305ef65..47ffc57a7 100644 --- a/src/lib/libast/include/ast.h +++ b/src/lib/libast/include/ast.h @@ -95,25 +95,20 @@ struct _sfio_s; * exit() support -- this matches shell exit codes */ -#define EXIT_BITS 8 /* # exit status bits */ +#define EXIT_BITS 8 /* # exit status bits */ -#define EXIT_USAGE 2 /* usage exit code */ -#define EXIT_QUIT ((1<<(EXIT_BITS))-1) /* parent should quit */ -#define EXIT_NOTFOUND ((1<<(EXIT_BITS-1))-1) /* command not found */ -#define EXIT_NOEXEC ((1<<(EXIT_BITS-1))-2) /* other exec error */ +#define EXIT_USAGE 2 /* usage exit code */ +#define EXIT_QUIT 255 /* parent should quit */ +#define EXIT_NOTFOUND 127 /* command not found */ +#define EXIT_NOEXEC 126 /* other exec error */ -#define EXIT_CODE(x) ((x)&((1<>EXIT_BITS)) - -#define EXITED_CORE(x) (((x)&((1<> 8)) +#define EXITED_CORE(x) (((x) & (256 | 128)) == (256 | 128) || ((x) & (128 | 64)) == (128 | 64)) +#define EXITED_TERM(x) ((x) & (256 | 128)) /* * astconflist() flags diff --git a/src/lib/libast/include/wait.h b/src/lib/libast/include/wait.h index 0dff61930..4280379fd 100644 --- a/src/lib/libast/include/wait.h +++ b/src/lib/libast/include/wait.h @@ -65,23 +65,23 @@ __STDPP__directive pragma pp:nohide wait waitpid #endif #ifndef WIFEXITED -#define WIFEXITED(x) (!((x)&((1<<(EXIT_BITS-1))-1))) +#define WIFEXITED(x) (!((x) & EXIT_NOTFOUND)) #endif #ifndef WEXITSTATUS -#define WEXITSTATUS(x) (((x)>>EXIT_BITS)&((1<> 8) & EXIT_QUIT) #endif #ifndef WIFSIGNALED -#define WIFSIGNALED(x) (((x)&((1<<(EXIT_BITS-1))-1))!=0) +#define WIFSIGNALED(x) (((x) & EXIT_NOTFOUND) != 0) #endif #ifndef WTERMSIG -#define WTERMSIG(x) ((x)&((1<<(EXIT_BITS-1))-1)) +#define WTERMSIG(x) ((x) & EXIT_NOTFOUND) #endif #ifndef WIFSTOPPED -#define WIFSTOPPED(x) (((x)&((1<