Funny the bugs one finds when reading the source code to a program... I was
looking at the usage of INPUT_REDIRECT and TRANSLATE_REDIRECT in
execute_null_command() and thought “why compare against 8 values, when by
re-ordering enum r_instruction in command.h, a simple range check would
suffice?” Which led me down a rabbit hole. Why *only* those particular
redirections?

When a null command is invoked, its redirections are performed in the
current shell and then unwound.
Unfortunately this unwinding can be ineffective in some cases.

$ cat /tmp/foo
#!/bin/bash
limit=${1:-100}
255>| bar
for (( fd = 10 ; fd <= limit ; ++fd )) do
  eval "$fd>| bar"
done
ls -mU /proc/$$/fd
ls -log /proc/$$/fd/99

$ bash /tmp/foo
0, 1, 2, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45,
46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64,
65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83,
84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101,
255,
lr-x------ 1 64 2025-02-08 11:58:52 +1000 /proc/1723/fd/99 -> /tmp/foo

It's important to note that these are empty commands, not ‘exec’, and that
all the filedescriptors higher than 10 are open reading the script ‘foo’
(not writing ‘bar’).

I know that I've constructed an intentionally pathological case, and I
wouldn't be so concerned about it except that if pushed a bit harder it can
cause Bash to crash: increase the iteration limit to 1000, and in all my
tests it crashes (SIGSEGV or SIGABRT) somewhere between 257 and 530,
sometimes preceded by messages along the lines of:

> *save_bash_input: buffer already exists for new fd 275*

and/or

>
> *malloc: subst.c:4004: assertion botchedmalloc: block on free list
> clobbered*


I've tested and found this can crash multiple versions of Bash ranging from
3.0-rc1 to 5.3-alpha, but they vary as to which fd number finally pushes
them over the edge.

I ran Bash under gdb, and it appears that when saving a reserved
filedescriptor (such as the one used to read the script), it gets confused
and dups twice, but afterwards only cleans up one. The first redirection
 255>|bar
causes the original to be duped to both 10 and 11, but only 10 is closed
after the command.

I was initially experimenting to see whether the kind of redirection would
influence whether to set forcefork in execute_null_command(), since that
*appeared* to be the case, and that's why I used the “>|” redirection
operator. However some of my tests used other redirection operators and it
didn't seem any less prone to failure.

-Martin

Reply via email to