summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDenys Vlasenko <vda.linux@googlemail.com>2025-08-12 17:55:48 +0200
committerDenys Vlasenko <vda.linux@googlemail.com>2025-08-12 17:56:58 +0200
commit5ecbed0e2660f74f3690de52387a7c2f3ea3f2d2 (patch)
treeadc06cd42a26b8f76260ed1ea38c538218d1457e
parentab1de7df999c43f12be26d90e6a143ecfa966630 (diff)
hush: do not segfault on "for </dev/null v in..."
This is not accepted by bash, we may also disallow this, but for now, at least do not crash Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
-rw-r--r--shell/hush.c233
-rw-r--r--shell/hush_test/hush-redir/redir_and_constructs1.right2
-rwxr-xr-xshell/hush_test/hush-redir/redir_and_constructs1.tests2
3 files changed, 124 insertions, 113 deletions
diff --git a/shell/hush.c b/shell/hush.c
index 254f39943..fe3d77c65 100644
--- a/shell/hush.c
+++ b/shell/hush.c
@@ -3914,9 +3914,11 @@ static struct pipe *new_pipe(void)
return pi;
}
-/* Command (member of a pipe) is complete, or we start a new pipe
- * if ctx->command is NULL.
- * No errors possible here.
+/* Parsing of command (member of a pipe) is completed.
+ * If it's not null, a new empty command structure is added
+ * to the current pipe, and ctx->command is set to it.
+ * Return the current number of already parsed commands in the pipe.
+ * No errors are possible here.
*/
static int done_command(struct parse_context *ctx)
{
@@ -3932,17 +3934,16 @@ static int done_command(struct parse_context *ctx)
ctx->pending_redirect = NULL;
}
#endif
-
if (command) {
if (IS_NULL_CMD(command)) {
- debug_printf_parse("done_command: skipping null cmd, num_cmds=%d\n", pi->num_cmds);
+ debug_printf_parse("done_command: skipping null cmd, num_cmds:%d\n", pi->num_cmds);
goto clear_and_ret;
}
pi->num_cmds++;
debug_printf_parse("done_command: ++num_cmds=%d\n", pi->num_cmds);
//debug_print_tree(ctx->list_head, 20);
} else {
- debug_printf_parse("done_command: initializing, num_cmds=%d\n", pi->num_cmds);
+ debug_printf_parse("done_command: initializing, num_cmds:%d\n", pi->num_cmds);
}
/* Only real trickiness here is that the uncommitted
@@ -3953,28 +3954,33 @@ static int done_command(struct parse_context *ctx)
memset(command, 0, sizeof(*command));
#if ENABLE_HUSH_LINENO_VAR
command->lineno = G.parse_lineno;
- debug_printf_parse("command->lineno = G.parse_lineno (%u)\n", G.parse_lineno);
+ debug_printf_parse("command->lineno=G.parse_lineno (%u)\n", G.parse_lineno);
#endif
- return pi->num_cmds; /* used only for 0/nonzero check */
+ return pi->num_cmds;
}
+/* Parsing of a pipe is completed.
+ * Finish prsing current command via done_command().
+ * (If the pipe is not empty, but done_command() did not change the number
+ * of commands in pipe, return value is 1. Used for catching syntax errors)
+ */
static int done_pipe(struct parse_context *ctx, pipe_style type)
{
- int not_null_pipe;
+ int num_cmds;
int oldnum;
- int nullcommand;
+ int last_cmd_is_null;
debug_printf_parse("done_pipe entered, followup %d\n", type);
/* Close previous command */
oldnum = ctx->pipe->num_cmds;
- not_null_pipe = done_command(ctx);
+ num_cmds = done_command(ctx);
/* This is true if this was a non-empty pipe,
* but done_command didn't add a new member to it.
* Usually it is a syntax error.
* Examples: "date | | ...", "date | ; ..."
*/
- nullcommand = (oldnum && not_null_pipe == oldnum);
+ last_cmd_is_null = (oldnum != 0 && num_cmds == oldnum);
#if HAS_KEYWORDS
ctx->pipe->pi_inverted = ctx->ctx_inverted;
@@ -4017,7 +4023,7 @@ static int done_pipe(struct parse_context *ctx, pipe_style type)
ctx->list_head = ctx->pipe = pi;
/* for cases like "cmd && &", do not be tricked by last command
* being null - the entire {...} & is NOT null! */
- not_null_pipe = 1;
+ num_cmds = 1;
} else {
no_conv:
ctx->pipe->followup = type;
@@ -4026,7 +4032,7 @@ static int done_pipe(struct parse_context *ctx, pipe_style type)
/* Without this check, even just <enter> on command line generates
* tree of three NOPs (!). Which is harmless but annoying.
* IOW: it is safe to do it unconditionally. */
- if (not_null_pipe
+ if (num_cmds != 0
#if ENABLE_HUSH_IF
|| ctx->ctx_res_w == RES_FI
#endif
@@ -4041,8 +4047,8 @@ static int done_pipe(struct parse_context *ctx, pipe_style type)
) {
struct pipe *new_p;
debug_printf_parse("done_pipe: adding new pipe: "
- "not_null_pipe:%d ctx->ctx_res_w:%d\n",
- not_null_pipe, ctx->ctx_res_w);
+ "num_cmds:%d ctx->ctx_res_w:%d\n",
+ num_cmds, ctx->ctx_res_w);
new_p = new_pipe();
ctx->pipe->next = new_p;
ctx->pipe = new_p;
@@ -4071,8 +4077,8 @@ static int done_pipe(struct parse_context *ctx, pipe_style type)
done_command(ctx);
//debug_print_tree(ctx->list_head, 10);
}
- debug_printf_parse("done_pipe return:%d\n", nullcommand);
- return nullcommand;
+ debug_printf_parse("done_pipe return: last_cmd_is_null:%d\n", last_cmd_is_null);
+ return last_cmd_is_null;
}
static void initialize_context(struct parse_context *ctx)
@@ -4255,7 +4261,10 @@ static const struct reserved_combo* reserved_word(struct parse_context *ctx)
}
#endif /* HAS_KEYWORDS */
-/* Word is complete, look at it and update parsing context.
+/* Parsing of a word is complete.
+ * Look at it and update current command:
+ * update current command's argv/cmd_type/etc, fill in redirect name and type,
+ * check reserved-ness and assignment-ness, etc...
* Normal return is 0. Syntax errors return 1.
* Note: on return, word is reset, but not o_free'd!
*/
@@ -4292,7 +4301,7 @@ static int done_word(struct parse_context *ctx)
// as written:
// <<EOF$t
// <<EOF$((1))
-// <<EOF`true` [this case also makes heredoc "quoted", a-la <<"EOF". Probably bash-4.3.43 bug]
+// <<EOF`true` [bash 4.3.43 bug: this case also makes heredoc "quoted", a-la <<"EOF". Fixed by 5.2.15]
ctx->pending_redirect->rd_filename = xstrdup(ctx->word.data);
/* Cater for >\file case:
@@ -4309,38 +4318,41 @@ static int done_word(struct parse_context *ctx)
}
debug_printf_parse("word stored in rd_filename: '%s'\n", ctx->word.data);
ctx->pending_redirect = NULL;
- } else {
+ goto ret;
+ }
+
#if HAS_KEYWORDS
# if ENABLE_HUSH_CASE
- if (ctx->ctx_dsemicolon
- && strcmp(ctx->word.data, "esac") != 0 /* not "... pattern) cmd;; esac" */
- ) {
- /* already done when ctx_dsemicolon was set to 1: */
- /* ctx->ctx_res_w = RES_MATCH; */
- ctx->ctx_dsemicolon = 0;
- } else
+ if (ctx->ctx_dsemicolon
+ && strcmp(ctx->word.data, "esac") != 0 /* not "... pattern) cmd;; esac" */
+ ) {
+ /* already done when ctx_dsemicolon was set to 1: */
+ /* ctx->ctx_res_w = RES_MATCH; */
+ ctx->ctx_dsemicolon = 0;
+ } else
# endif
# if defined(CMD_TEST2_SINGLEWORD_NOGLOB)
- if (command->cmd_type == CMD_TEST2_SINGLEWORD_NOGLOB
- && strcmp(ctx->word.data, "]]") == 0
- ) {
- /* allow "[[ ]] >file" etc */
- command->cmd_type = CMD_SINGLEWORD_NOGLOB;
- } else
+ if (command->cmd_type == CMD_TEST2_SINGLEWORD_NOGLOB
+ && strcmp(ctx->word.data, "]]") == 0
+ ) {
+ /* allow "[[ ]] >file" etc */
+ command->cmd_type = CMD_SINGLEWORD_NOGLOB;
+ } else
# endif
- if (!command->argv /* if it's the first word... */
+ if (!command->argv /* if it's the first word... */
+ && !command->redirects /* and no redirects yet... try: </dev/null ! true; echo $? */
# if ENABLE_HUSH_LOOPS
- && ctx->ctx_res_w != RES_FOR /* ...not after FOR or IN */
- && ctx->ctx_res_w != RES_IN
+ && ctx->ctx_res_w != RES_FOR /* ...not after FOR or IN */
+ && ctx->ctx_res_w != RES_IN
# endif
# if ENABLE_HUSH_CASE
- && ctx->ctx_res_w != RES_CASE
+ && ctx->ctx_res_w != RES_CASE
# endif
- ) {
- const struct reserved_combo *reserved;
- reserved = reserved_word(ctx);
- debug_printf_parse("checking for reserved-ness: %d\n", !!reserved);
- if (reserved) {
+ ) {
+ const struct reserved_combo *reserved;
+ reserved = reserved_word(ctx);
+ debug_printf_parse("checking for reserved-ness: %d\n", !!reserved);
+ if (reserved) {
# if ENABLE_HUSH_LINENO_VAR
/* Case:
* "while ...; do
@@ -4348,80 +4360,79 @@ static int done_word(struct parse_context *ctx)
* If we don't close the pipe _now_, immediately after "do", lineno logic
* sees "cmd" as starting at "do" - i.e., at the previous line.
*/
- if (0
- IF_HUSH_IF(|| reserved->res == RES_THEN)
- IF_HUSH_IF(|| reserved->res == RES_ELIF)
- IF_HUSH_IF(|| reserved->res == RES_ELSE)
- IF_HUSH_LOOPS(|| reserved->res == RES_DO)
- ) {
- done_pipe(ctx, PIPE_SEQ);
- }
-# endif
- o_reset_to_empty_unquoted(&ctx->word);
- debug_printf_parse("done_word return %d\n",
- (ctx->ctx_res_w == RES_SNTX));
- return (ctx->ctx_res_w == RES_SNTX);
+ if (0
+ IF_HUSH_IF(|| reserved->res == RES_THEN)
+ IF_HUSH_IF(|| reserved->res == RES_ELIF)
+ IF_HUSH_IF(|| reserved->res == RES_ELSE)
+ IF_HUSH_LOOPS(|| reserved->res == RES_DO)
+ ) {
+ done_pipe(ctx, PIPE_SEQ);
}
+# endif
+ o_reset_to_empty_unquoted(&ctx->word);
+ debug_printf_parse("done_word return %d\n",
+ (ctx->ctx_res_w == RES_SNTX));
+ return (ctx->ctx_res_w == RES_SNTX);
+ }
# if defined(CMD_TEST2_SINGLEWORD_NOGLOB)
- if (strcmp(ctx->word.data, "[[") == 0) {
- command->cmd_type = CMD_TEST2_SINGLEWORD_NOGLOB;
- } else
+ if (strcmp(ctx->word.data, "[[") == 0) {
+ command->cmd_type = CMD_TEST2_SINGLEWORD_NOGLOB;
+ } else
# endif
# if defined(CMD_SINGLEWORD_NOGLOB)
- if (0
- /* In bash, local/export/readonly are special, args
- * are assignments and therefore expansion of them
- * should be "one-word" expansion:
- * $ export i=`echo 'a b'` # one arg: "i=a b"
- * compare with:
- * $ ls i=`echo 'a b'` # two args: "i=a" and "b"
- * ls: cannot access i=a: No such file or directory
- * ls: cannot access b: No such file or directory
- * Note: bash 3.2.33(1) does this only if export word
- * itself is not quoted:
- * $ export i=`echo 'aaa bbb'`; echo "$i"
- * aaa bbb
- * $ "export" i=`echo 'aaa bbb'`; echo "$i"
- * aaa
- */
- IF_HUSH_LOCAL( || strcmp(ctx->word.data, "local") == 0)
- IF_HUSH_EXPORT( || strcmp(ctx->word.data, "export") == 0)
- IF_HUSH_READONLY(|| strcmp(ctx->word.data, "readonly") == 0)
- ) {
- command->cmd_type = CMD_SINGLEWORD_NOGLOB;
- }
+ if (0
+ /* In bash, local/export/readonly are special, args
+ * are assignments and therefore expansion of them
+ * should be "one-word" expansion:
+ * $ export i=`echo 'a b'` # one arg: "i=a b"
+ * compare with:
+ * $ ls i=`echo 'a b'` # two args: "i=a" and "b"
+ * ls: cannot access i=a: No such file or directory
+ * ls: cannot access b: No such file or directory
+ * Note: bash 3.2.33(1) does this only if export word
+ * itself is not quoted:
+ * $ export i=`echo 'aaa bbb'`; echo "$i"
+ * aaa bbb
+ * $ "export" i=`echo 'aaa bbb'`; echo "$i"
+ * aaa
+ */
+ IF_HUSH_LOCAL( || strcmp(ctx->word.data, "local") == 0)
+ IF_HUSH_EXPORT( || strcmp(ctx->word.data, "export") == 0)
+ IF_HUSH_READONLY(|| strcmp(ctx->word.data, "readonly") == 0)
+ ) {
+ command->cmd_type = CMD_SINGLEWORD_NOGLOB;
+ }
# else
- { /* empty block to pair "if ... else" */ }
+ { /* empty block to pair "if ... else" */ }
# endif
- }
+ }
#endif /* HAS_KEYWORDS */
- if (command->group) {
- /* "{ echo foo; } echo bar" - bad */
- syntax_error_at(ctx->word.data);
- debug_printf_parse("done_word return 1: syntax error, "
- "groups and arglists don't mix\n");
- return 1;
- }
+ if (command->group) {
+ /* "{ echo foo; } echo bar" - bad */
+ syntax_error_at(ctx->word.data);
+ debug_printf_parse("done_word return 1: syntax error, "
+ "groups and arglists don't mix\n");
+ return 1;
+ }
- /* If this word wasn't an assignment, next ones definitely
- * can't be assignments. Even if they look like ones. */
- if (ctx->is_assignment != DEFINITELY_ASSIGNMENT
- && ctx->is_assignment != WORD_IS_KEYWORD
- ) {
- ctx->is_assignment = NOT_ASSIGNMENT;
- } else {
- if (ctx->is_assignment == DEFINITELY_ASSIGNMENT) {
- command->assignment_cnt++;
- debug_printf_parse("++assignment_cnt=%d\n", command->assignment_cnt);
- }
- debug_printf_parse("ctx->is_assignment was:'%s'\n", assignment_flag[ctx->is_assignment]);
- ctx->is_assignment = MAYBE_ASSIGNMENT;
+ /* If this word wasn't an assignment, next ones definitely
+ * can't be assignments. Even if they look like ones. */
+ if (ctx->is_assignment != DEFINITELY_ASSIGNMENT
+ && ctx->is_assignment != WORD_IS_KEYWORD
+ ) {
+ ctx->is_assignment = NOT_ASSIGNMENT;
+ } else {
+ if (ctx->is_assignment == DEFINITELY_ASSIGNMENT) {
+ command->assignment_cnt++;
+ debug_printf_parse("++assignment_cnt=%d\n", command->assignment_cnt);
}
- debug_printf_parse("ctx->is_assignment='%s'\n", assignment_flag[ctx->is_assignment]);
- command->argv = add_string_to_strings(command->argv, xstrdup(ctx->word.data));
- debug_print_strings("word appended to argv", command->argv);
+ debug_printf_parse("ctx->is_assignment was:'%s'\n", assignment_flag[ctx->is_assignment]);
+ ctx->is_assignment = MAYBE_ASSIGNMENT;
}
+ debug_printf_parse("ctx->is_assignment='%s'\n", assignment_flag[ctx->is_assignment]);
+ command->argv = add_string_to_strings(command->argv, xstrdup(ctx->word.data));
+ debug_print_strings("word appended to argv", command->argv);
#if ENABLE_HUSH_LOOPS
if (ctx->ctx_res_w == RES_FOR) {
@@ -4446,8 +4457,8 @@ static int done_word(struct parse_context *ctx)
}
#endif
+ ret:
o_reset_to_empty_unquoted(&ctx->word);
-
debug_printf_parse("done_word return 0\n");
return 0;
}
@@ -4770,8 +4781,7 @@ static struct pipe *parse_stream(char **pstring,
struct in_str *input,
int end_trigger);
-/* Returns number of heredocs not yet consumed,
- * or -1 on error.
+/* Returns number of heredocs not yet consumed, or -1 on error.
*/
static int parse_group(struct parse_context *ctx,
struct in_str *input, int ch)
@@ -4832,12 +4842,9 @@ static int parse_group(struct parse_context *ctx,
if (command->argv /* word [word]{... */
|| ctx->word.length /* word{... */
|| ctx->word.has_quoted_part /* ""{... */
- ) {
- syntax_error(NULL);
+ )
debug_printf_parse("parse_group return -1: "
"syntax error, groups and arglists don't mix\n");
- return -1;
- }
#endif
IF_HUSH_FUNCTIONS(skip:)
diff --git a/shell/hush_test/hush-redir/redir_and_constructs1.right b/shell/hush_test/hush-redir/redir_and_constructs1.right
new file mode 100644
index 000000000..232cd8734
--- /dev/null
+++ b/shell/hush_test/hush-redir/redir_and_constructs1.right
@@ -0,0 +1,2 @@
+hush: can't execute '!': No such file or directory
+127:127
diff --git a/shell/hush_test/hush-redir/redir_and_constructs1.tests b/shell/hush_test/hush-redir/redir_and_constructs1.tests
new file mode 100755
index 000000000..a92731e04
--- /dev/null
+++ b/shell/hush_test/hush-redir/redir_and_constructs1.tests
@@ -0,0 +1,2 @@
+# Reserved words are not recognized after redirects
+</dev/null ! true; echo 127:$?