1

I use Bash 4.3.48(1) and I ran the following command pattern on a testing VPS machine:

rm -rf ${drt}/${pma}*

This command deleted the entire operating system (Ubuntu). That was evident by executing cd / which returned nothing but the following error:

bash: cd /: No such file or directory

In a deeper look, this happened because both variables in the original command above, were not declared:

  1. I created a file named ~/repoName/assignments_variables.sh that contains a list of exported variables (including drt and pma).

  2. Instead running source /etc/bash.bashrc, I ran /etc/bash.bashrc, which was wrong, because that file is only relevant to the current session (though it can be used to subsequently run files that by themselves execute data in subsessions).


Now that it's clear to me that the bad path is the general cause for the problem, I'd like to dive a bit deeper and ask this:

Why did the rm -rf ignored the wrong variable-expansions and kept going to the /*? I know that rm -rf should delete a dir only if it finds it, hence shouldn't rely on parts of the path of a non-existent dir (like /* that caused deletion of the operating system). How could I improve the rm -rf command to cover possible similar cases of a bad path (due to wrong variable-expansions) in the future?

Is there some Bash directive to make sure that Bash will never expand empty variables (until I undo that directive)?


Let me emphasize: I usually work not only with backups but with double backups. That was really a testing environment with x3 backups in background.

3
  • I published a similar question but deleted it. User: jesse_b commented there. I'd like jesse to read this one. Can someone tag jesse_b somehow? Thanks. Feb 11, 2018 at 4:55
  • 4
    /* is /bin /boot /dev /etc /home /lib /lib64 /lost+found /media /mnt /opt /proc /root /run /sbin /srv /sys /tmp /usr /var, not a non-existent directory. Feb 11, 2018 at 5:03
  • @user9303970: Sorry I had just noticed a syntax error in your previous question but as Michael Homer and isaac pointed out, it looks like you actually have wiped your system.
    – jesse_b
    Feb 11, 2018 at 13:29

3 Answers 3

5

What is expanded if the variables don't exist?
Let's try (note the echo):

$ unset drt pma
$ echo rm -rf ${drt}/${pma}*
rm -rf /bin /boot /dev /etc /hello /home /initrd.img /lib /lib32 /lib64 /libx32 /media /mnt /opt /proc /root /run /sbin /srv /sys /tmp /usr /var /vmlinuz /vmlinuz.old

that is the whole system (as you have already confirmed).
The problem is the expansion done by the shell.

prevention

How do we prevent that from happening?
By preventing the shell from expanding empty variables.
The expansion of ${var:?message} yields message if var is empty or unset.
And stops execution (critical error). Test it:

$ echo rm -rf "${drt:?Missing variable drt}"/"${pma:?Missing variable pma}"*

And this solution is POSIX compatible.

1
  • 4
    Not running as root also helps a bit.
    – Kusalananda
    Feb 11, 2018 at 7:22
3

As has been pointed out, the two variables drt and pma were not present in your script's environment, which meant that the shell expanded these to empty strings. The resulting command was rm -rf /* (with * further expanded to whatever was available in the root directory).

The root directory itself will not be removed by running rm -rf /* as root. Also, I (and Jesse_b who pointed it out to me) am slightly confused by the error message

bash: cd /: No such file or directory

The only way I'm able to provoke this error message myself is by running the command "cd /" (note the quotation marks). Had / really been removed, the error message would have said

bash: cd: /: No such file or directory

isaac has already given some tips for how to prevent you from doing things like this in the future, but I thought that I'd just chip in with my awn advice.

  • Don't work at a root shell prompt.
  • If writing scripts that requires root permissions, use sudo in the script on the specific commands that actually require these elevated permissions. Don't run the whole script as root, especially not during development.
  • You usually do not need to touch /etc/bash.bashrc for any reason, and you should never have to explicitly source it. Environment variables can be created in
    • a user's own $HOME/.bashrc (if the script is to be run from an interactive session), or
    • in the script itself, or
    • in a separate file (written for the script) sourced by the script explicitly, or
    • in a file pointed to by the BASH_ENV environment variable.

Another safety measure that would have saved you in this case is running your script under set -u (nounset) and possibly also set -e (errexit). The nounset shell option will threat the expansion of an unset variable as an error while the errexit shell option will exit the shell session immediately if a command exits with a non-zero exit status.

The script

#!/bin/bash -ue

echo "$hello"
echo "world"

will never output world but will instead provoke the message

script.sh: line 3: hello: unbound variable

before terminating. (It's the -u that does the terminating, -e is doesn't do anything in this simple example).

2

Variables not set interpolate to nothing so what was run was the glob rm -rf /* which is all things under / which is bad as you found out.

Add error checking. And, also, quote the "$variables" because otherwise the shell does a split-glob on them and then stupid things can happen when the $variables contain a space or other problematical characters. Especially when a rm -rf is involved.

if [[ -z "$drt" ]]; then
    echo >&2 "drt variable not set ??"
    exit 1
fi
if [[ -z "$pma" ]]; then
    echo >&2 "pma variable not set ??"
    exit 1
fi
rm -rf "${drt}"/"${pma}"*

(Or use a less problematical language for programming in...)

3
  • I'd use a function to reduce the tedious boilerplate code repetition. e.g. error() { ec="$1"; shift; echo >&2 "$@" ; exit "$ec" ; }. Then: [ -z "$drt" ] && error 1 "drt variable not set ??". Also there's no need to quote the variables separately. The * wildcard needs to be outside of the quoted string so that the shell can expand it, but the two variables can be in one quoted string. e.g. "${drt}/${pma}"* or just "$drt/$pma"*
    – cas
    Feb 11, 2018 at 5:17
  • @cas that's what I do in other languages; anything that complicated I do not write in sh
    – thrig
    Feb 11, 2018 at 15:56
  • to me, that's a way of reducing the complexity (or at least the visual complexity) of a script. and shorten it, from 4 lines per test to 1 line.
    – cas
    Feb 11, 2018 at 21:16

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .