I have found an easily reproducible bug in bash that causes it to crash and have attached a fix.
joey@charmander:~$ wait
joey@charmander:~$ ^C
joey@charmander:~$ true&
[1] 6961
joey@charmander:~$ *** stack smashing detected ***: bash terminated
Aborted (core dumped)
All you have to do is run wait, press ctrl+C and enter (notice how the ^C is entered on the line as if it were a character instead of jumping to the next line), and immediately run any command in the background.
What's happening here? In builtins/wait.def we have the code:
code = setjmp (wait_intr_buf);
if (code)
{
status = 128 + wait_signal_received;
WAIT_RETURN (status);
}
The problem is that longjmp is being called to wait_intr_buf after the function already returned, so the stack is garbage. This happens because of this code in trap.c:
if (this_shell_builtin && (this_shell_builtin == wait_builtin))
{
wait_signal_received = sig;
...
}
and the CHECK_WAIT_INTR macro called in jobs.c and defined in quit.h:
#define CHECK_WAIT_INTR \
do { \
if (wait_signal_received && this_shell_builtin && (this_shell_builtin == wait_builtin)) \
longjmp (wait_intr_buf, 1); \
} while (0)
The root of the problem is that this_shell_builtin is not updated when a command finishes running.
So if we put it all together:
When wait is called this_shell_builtin is set to wait_builtin. The wait call finishes, and we send the program SIGINT. Since this_shell_builtin is still builtin_wait, wait_signal_received is set. We run another command in the background (since it's run in the background it doesn't change this_shell_builtin), and when the child command exits the waitchld command in jobs.c is called which calls CHECK_WAIT_INTR which longjumps back into the wait function that returned long ago. CRASH!
The fix is relatively simple: just unset this_shell_builtin when commands finish running. Here's a patch:
diff --git a/builtins/exit.def b/builtins/exit.def
index 34728eb..2bdf078 100644
--- a/builtins/exit.def
+++ b/builtins/exit.def
@@ -126,11 +126,10 @@ exit_or_logout (list)
if (stopmsg)
{
- /* This is NOT superfluous because EOF can get here without
- going through the command parser. Set both last and this
- so that either `exit', `logout', or ^D will work to exit
- immediately if nothing intervenes. */
- this_shell_builtin = last_shell_builtin = exit_builtin;
+ /* This is NOT superfluous because EOF can get here without going
+ through the command parser. Set last_shell_builtin so that if you
+ try to exit again immediately after ^D the program will exit. */
+ last_shell_builtin = exit_builtin;
return (EXECUTION_FAILURE);
}
}
diff --git a/execute_cmd.c b/execute_cmd.c
index 9cebaef..5959ed2 100644
--- a/execute_cmd.c
+++ b/execute_cmd.c
@@ -4073,7 +4073,6 @@ execute_simple_command (simple_command, pipe_in, pipe_out, async, fds_to_close)
if (words->word->word[0] == '%' && already_forked == 0)
{
this_command_name = async ? "bg" : "fg";
- last_shell_builtin = this_shell_builtin;
this_shell_builtin = builtin_address (this_command_name);
result = (*this_shell_builtin) (words);
goto return_result;
@@ -4105,7 +4104,6 @@ execute_simple_command (simple_command, pipe_in, pipe_out, async, fds_to_close)
{
run_unwind_frame ("simple-command");
this_command_name = "fg";
- last_shell_builtin = this_shell_builtin;
this_shell_builtin = builtin_address ("fg");
started_status = start_job (job, 1);
@@ -4128,7 +4126,6 @@ run_builtin:
if (func == 0 && builtin == 0)
builtin = find_shell_builtin (this_command_name);
- last_shell_builtin = this_shell_builtin;
this_shell_builtin = builtin;
if (builtin || func)
@@ -4240,6 +4237,10 @@ run_builtin:
}
discard_unwind_frame ("simple-command");
this_command_name = (char *)NULL; /* points to freed memory now */
+
+ last_shell_builtin = this_shell_builtin;
+ this_shell_builtin = (sh_builtin_func_t*)NULL;
+
return (result);
}
-----------------------
Since this_shell_builtin is zeroed after every function, handling of setting last_shell_builtin was moved to right before that. The only place that uses last_shell_builtin is in builtin_exit, which checks if the last call was exit in order to force quit even if there are stopped jobs. This code only needs to set last_shell_builtin, not this_shell_builtin, so the code and comment there were updated accordingly.
This is my first time contributing, so let me know if I need to do something further to get the patch merged.