Thursday, August 14, 2008

Back To Basics: Avoiding Recursive Alias Disasters On Linux And Unix

Hey there,

Today, we're going to step into the wayback machine and take a look at something very basic that, if ignored or forgotten, can potentially have devastating side-effects. And, I'm not referring to the way I used to drink in college ;) This has more to do with an op-ed piece we ran a while back about paying attention to the small things. In fact, it has exactly to do with that.

The topic for today's blast of hot air is "infinite recursion." Specifically, we'll be looking at a kind of insidious way of getting stung by that issue through the use of shell aliases. They seem harmless, and they do make it so you can, for instance, type "sit-stay" instead of "while :;do print -n "*";sleep 15;done" when you want to walk away from your terminal and not disconnect your SSH session, but they do have a down side, which is fairly easy to exploit. In my experience, these cpu-killers happen most often "by accident" rather than with any malicious intent. Nevertheless, their very existence cries out for caution (Tomorrow I might write some fiction to get all these fifty cent phrases out of my system. Although some folks may contend that I've done enough of that on this blog already ;)

The infinite recursion problem with aliases closely parallels a very simple exploit that can be run in a simple shell script. The ends are the same, and the means are closely related. For instance, if you create a shell script called "ls," with the contents:


ls;sleep 1500000000

and manage to get that in a user's PATH before the real /bin/ls (or /usr/bin/ls), it'll ratchet up the number of open processes and filehandles very quickly. If let sit, it will take down any machine of any size eventually. This end can actually be accomplished even more simply than that, but we're not trying to encourage reckless behaviour; just rubbernecking a little ;)

The problem you can get into with simple shell aliases is when you have an alias for a command (like "ls") that actually already exists. I will usually make my aliases unique (like "sit-stay," above, which substitutes for an asterisk-printing while loop), but not everyone does. And, sometimes, an alias that is unique can become the opposite if you change OS's. For instance "ll" doesn't exist on Solaris, but it does on HP-UX.

It's very important that, when you create an alias which is named the same as a system binary (or shell built-in) that you compensate for that fact. It's actually made very easy to protect yourself, from yourself, by pretty much every shell I've ever worked with. At the most basic level, you can instruct your shell to not even attempt alias expansion of whatever you type on the command line by simply enclosing it in quotes (single or double should both work - If not, let me know what shell you're using. It would be interesting to have a list of shells that make this distinction). So, while:

host # cd /tmp

would (and I'm oversimplifying this by a preposterous degree) cause the shell to check, in order, if "cd" was an alias, a shell built-in, a function, an actual binary in our PATH or an erroneous entry (stopping at whatever point returns true first), typing:

host # "cd" /tmp

would remove the "alias check" from the equation. Problem solved and/or potential for damage neutralized. Just like the simple shell script above, if you created an alias for "cd" (which might differ from this shell to yours) like this one:

host # alias cd="cd $@"

you could have a problem on your hands. Lord knows why you'd want to do this, since you'd still be typing "cd wherever" either way, but I'm just being obtuse to make a point ;) In this situation, every time someone types something like:

host # cd /my/home/dir

The shell will determine that "cd" is an alias before it processes the command and, while it's processing the alias it will see that the actual command (to which the alias refers) contains an alias, which it will process, etc, etc, and, before you know it, system resources will become depleted to the point that no new processes can be forked. The machine, however, will be completely forked ;)

The preferred way (or so they tell me) is to encapsulate your alias in a function of the type _NAME (for the command NAME) (even though you, theoretically, only have to make sure your alias has the "real" command in quotes):

function _cd {
"cd" $@
alias cd="_cd"

If you're lazy, like me, you'll just do this, instead:

alias cd='"cd" $@'

And, of course, if you want to have your alias/function print out where you've cd'ed to, you'll need to keep in mind that the return code (errno) for cd will be overwritten when you do the print or echo. This is easy enough to handle by grabbing the value of $?, and we use it in a lot of our scripts:

function _cd {
"cd $@"
echo $PWD
return $return
alias cd="_cd"

and you shouldn't have to worry about recursive calling of the "cd" (or any) command ever again. Assuming you always follow these precautions when dreaming up your aliases :)


, Mike

Please note that this blog accepts comments via email only. See our Mission And Policy Statement for further details.