Take care editing bash scripts

Imagine I write the following bash script and call it delay.sh. What do you suppose happens when I run ./delay.sh?

#!/bin/bash
sleep 30
#rm -rf --no-preserve-root /
echo "Time's up!"

It looks like it will wait for 30 seconds and then print a message to screen. There are no tricks here—that’s exactly what it does. There’s a dangerous-looking command in the middle but it will have no effect because it’s commented out.

Imagine I run it again and I’m getting bored waiting for the sleep. Thirty seconds is far too long. I open a second terminal, change sleep 30 to sleep 3, then save the file. What do you suppose happens now?

Well, after the 30 seconds elapses, the running script deletes all of my files. This happens because bash reads the content of the script in chunks as it executes, tracking where it’s up to with a byte offset. When I delete one character from the sleep line, the “next command” offset points at the r in #rm instead of the #. From the interpreter’s point of view, the # shifts backwards onto the previous line so it runs the unfortunate command.

This can be confirmed by observing the system calls that bash makes on Linux. Here’s the output of strace bash delay.sh, annotated and trimmed.

# Open the script
openat(AT_FDCWD, "delay.sh", O_RDONLY)  = 3

# Parse the first line (up to 80 characters)
read(3, "#!/bin/bash\nsleep 30\n#echo \"Don'"..., 80) = 64

# Go back to the beginning
lseek(3, 0, SEEK_SET)                   = 0

# Shift it over to file descriptor 255
dup2(3, 255)                            = 255

# Read a chunk of 64 bytes to get the command
read(255, "#!/bin/bash\nsleep 30\n#echo \"Don'"..., 64) = 64

# Position the cursor back to the end of the command we're about to run
# Offset 21 is the `#`
lseek(255, -43, SEEK_CUR)               = 21

# Do the sleep
wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 2072

# Before the wait4 returned, the file was edited from `30` to `3`

# Read a 64 byte chunk to get the next command
# For this demo I replaced the dangerous command with an echo
read(255, "echo \"Don't execute me\"\necho \"Ti"..., 64) = 42

# Bash decided to execute both echos at once without reading again
# Clearly there's some nuance here
write(1, "Don't execute me\n", 17)      = 17
write(1, "Time's up!\n", 11)            = 11

# Read another chunk and discover that we're at EOF
read(255, "", 64)                       = 0

So be careful running a bash script that may be currently executing. It could execute an invalid command, or do something very surprising.


Post a comment

All comments are held for moderation; basic HTML formatting accepted.

Name: (required)
E-mail: (required, not published)
Website: (optional)