Directory magic trick

Posted on . Updated on .

Despite having spent years and years writing shell scripts and typing commands at Linux or Unix shell prompts, it never ceases to amaze me how subtle details sometimes catch me by surprise and leave me puzzled staring at the screen. Last Friday, about to leave work for the weekend, I lost almost half an hour trying to locate a file that seemed to exist and not exist at the same time.

Now that I clarified what was happening, I’ve simplified the situation and prepared an example for you to see. The following text has been copied verbatim from the terminal, without editing.

$ pwd
$ ( cd .. && ls )
$ ls ..

I’m pretty sure experienced sysadmins already suspect what’s happening, but the situation didn’t ring any bells in my head.

How is it possible that listing the files from the parent directory gives us an unexpected result, different from changing to the parent directory and listing the files?


It happens because the current directory has been accessed through a symlink to another directory. In other words, this is the tree:

$ pwd
$ find . -print0 | xargs -0 ls -ld
drwx------ 4 rg3 users 80 Oct 28 19:32 .
drwx------ 3 rg3 users 60 Oct 28 19:32 ./backstage
drwx------ 2 rg3 users 40 Oct 28 19:32 ./backstage/now-you-dont
drwx------ 2 rg3 users 60 Oct 28 19:32 ./stage
lrwxrwxrwx 1 rg3 users 25 Oct 28 19:32 ./stage/now-you-see-me -> ../backstage/now-you-dont

As you can see, accessing “stage/now-you-see-me” means that we’re really changing to “backstage/now-you-dont”. The parent directory of the latter is “backstage”, and the directory entry “..” points to “backstage”. We can see that with i-node numbers:

$ stat --format=%i backstage
$ stat --format=%i stage/now-you-see-me/..

When running “ls ..”, we’re using that directory entry and we see its contents, a subdirectory called “now-you-dont”.

However, in the original situation we accessed the path through “stage/now-you-see-me” and the shell was aware of that. When we “cd ..”, the shell isn’t going to “stage/now-you-see-me/..”, but removing the last path component and changing to “stage”. When listing the contents, we’re shown the symlink “now-you-see-me”. I’ve straced bash doing it, and these are the important lines:

[pid  7328] stat("/tmp", {st_mode=S_IFDIR|S_ISVTX|0777, st_size=160, ...}) = 0
[pid  7328] stat("/tmp/directory-trick", {st_mode=S_IFDIR|0700, st_size=80, ...}) = 0
[pid  7328] stat("/tmp/directory-trick/stage", {st_mode=S_IFDIR|0700, st_size=60, ...}) = 0
[pid  7328] stat("/tmp/directory-trick/stage/now-you-see-me", {st_mode=S_IFDIR|0700, st_size=40, ...}) = 0
[pid  7328] stat("/tmp/directory-trick/stage/now-you-see-me", {st_mode=S_IFDIR|0700, st_size=40, ...}) = 0
[pid  7328] chdir("/tmp/directory-trick/stage") = 0
[pid  7328] stat(".", {st_mode=S_IFDIR|0700, st_size=60, ...}) = 0

Notice the call to “chdir” and how bash checks every directory component.

In the real situation, the directory structure was a bit longer, the symlink appeared in the middle of it, not at the end, and the interesting file was to be found just at the edge of the case. In other words, everything was a bit harder to notice.

comments powered by Disqus