30 10 | 2013

Beware of symlinks when testing a file existence

Written by Tanguy

Classified in : Debian, Command line, To remember

A strange problem

Yesterday, I was reported a funny problem with the dokuwiki Debian package's postinst script, which contains a piece of shell script similar to that:

# Check the destination does not already exist
if [ ! -e /the/destination ]
    ln -s /some/file /the/destination

It was failing with that message: ln: failed to create symbolic link '/the/destination': File exists. Even though I had just tested it did not exist!

View of a comete colliding with the Earth

Collision detected…

The explanation

The reason of that failure is quite simple, but easy to forget: test(1), aka [, uses stat(2), which dereferences symlinks when testing for file existence! So, test ! -e /the/destination does not mean “check there is no file there” but rather “check there is either no file or only a dead symlink”.

So, the correct way to write such a piece of code is:

# Check there is not already a file or a (dead) symlink
if [ ! -e /the/destination -a ! -h /the/destination ]
    ln -s /some/file /the/destination


wednesday 30 october 2013 à 16:50 Patrick said : #1

Please note, that the latter code is still incorrect, as it contains a race condition. It is triggered when externally creating the link after the check failed and before the script creates the link.

As so often, it's really hard to get this right with a shell. Depending on your use case, "ln -sf" might be an option.

wednesday 30 october 2013 à 16:52 Zack Weinberg said : #2

Your "improved" code still has a bug: there's a gap between the check and the link creation, where another process could come in and create the file. Better to just create the link but allow it to fail:

ln -s /some/file /the/destination 2> /dev/null || :

or if you want the script to stop when some *other* kind of error pops up

perl -e 'symlink($ARGV[0], $ARGV[1]) or $!{EEXIST} or die "$ARGV[1]: $!\n"'

wednesday 30 october 2013 à 16:53 Zack Weinberg said : #3

... that should have been

<code>perl -e 'symlink($ARGV[0], $ARGV[1]) or $!{EEXIST} or die "$ARGV[1]: $!\n"' /some/file /the/destination</code>

wednesday 30 october 2013 à 17:04 Tanguy said : #4

@Patrick, @Zack Weinberg : Yes, in fact I forgot to mention the context. It is not in writing a generic piece of software, but a Debian package maintainer script, the symlink being to configure a web server for a web-based software. Here, it is not a matter of security, but only of avoiding a failure case if the user already put that link himself with a previous release.

The user may indeed very well find a hard and tricky way to make the installation script fail, but that possibility is not relevant here, because there are already plenty of ways of doing that, the first one being to interrupt the installation with a ^C, a SIGTERM or a SIGKILL.

wednesday 30 october 2013 à 21:34 Guto said : #5

I use readlink to do this kind of verfication:

if ! readlink -e /target/file >/dev/null 2>&1
ln -s /orig/file /target/file

thursday 31 october 2013 à 00:47 russ said : #6

POSIX marks the "-a" option of test obsolescent; it would be better written as "] && [".

thursday 31 october 2013 à 09:47 Tanguy said : #7

@russ : Right, thanks for that information. I do not like it, since it may require to spawn additional processes, but most shells have test built-in, fortunately.

thursday 07 november 2013 à 20:28 Victor Nițu said : #8

If I may add, I guess any other option than `ln -sf` would end up being more expensive than simply forcing the symlinking. I wonder now if there are any reasons for *not* doing the '-sf' move, other than not looking like a hackish script.

Write a comment

What is the third letter of the word unbm? :