I’ve been working on a Fuse filesystem project for quite some time. Now that OS X v10.9 is out, one of my major roadblocks has been removed. But I’ve still been experiencing some difficulty getting OS X to treat my filesystem like a first-class citizen. So to debug it I’ve been banging on it with Tuxera’s port of Pawel Jakub Dawidek’s POSIX file system test suite.

During this process, I made some baselines of my non-FUSE HFS+ boot volume and I came across a “fun” HFS+ surprise:

An Unremovable File

On an HFS+ filesystem you can make a symlink that can’t (easily) be removed…

# be root, for example with: sudo -i
str1=$(python -c "print '1' * 255")
str2=$(python -c "print '2' * 255")
str3=$(python -c "print '3' * 255")
str4=$(python -c "print '4' * 253")
mkdir -p  $str1/$str2/$str3/$str4
ln -s ftw $str1/$str2/$str3/$str4/L

Now we’ve created a tree that can’t be removed. None of these commands will work on OS X v10.9:

# still as root...
unlink 1*/2*/3*/4*/L
unlink $str1/$str2/$str3/$str4/L
rm -rf 1*
rm -rf $str1
rm -rf $str1/$str2/$str3/$str4
rm -rf $str1/$str2/$str3/$str4/L
(cd $str1/$str2/$str3/$str4; unlink L)
(cd $str1/$str2/$str3/$str4; rm -rf L)

They all boil down to the following error. (Note that I’m abbreviating the path components with “[ … ]” here for blog readability)

root# pwd
/private/tmp/111[ ... ]111/222[ ... ]222/333[ ... ]333/444[ ... ]444
root# ls
L
root# rm -f L
rm: L: No space left on device
root# df -H
Filesystem      Size   Used  Avail Capacity   iused     ifree %iused  Mounted on
/dev/disk1      250G   108G   142G    44%  26385563  34601956   43%   /
[...]

To make sure this is really happening, I’ve made a quick program to make the direct syscall. I’ll place it at /tmp/fixit.c:

#include <unistd.h>
#include <stdio.h>
#include <errno.h>
	
int main(int argc, char* argv[]) {
	printf("Unlink returned %i\n", unlink("L"));
	perror("Error was");
	return 0;
}

Now to run it:

root# pwd
/private/tmp/111[ ... ]111/222[ ... ]222/333[ ... ]333/444[ ... ]444
root# gcc -o /tmp/fixit /tmp/fixit.c 
root# /tmp/fixit 
Unlink returned -1
Error was: No space left on device

ENOSPC? Guess which error you can’t find mentioned in the OS X unlink(2) man page?

It’s complicated:

  • If a regular user creates this tree, they can easily remove it with rm -rf
  • If a regular user creates the tree, root cannot remove it. Weird!
  • If root creates the tree, root cannot remove it
  • If root creates the tree, typical permissions prevent regular users from removing it
  • If root creates the tree, chown/chmod can change the protection of the containing directories so that a regular user can remove the entire tree
  • If root creates the tree, chmod -h and chown -h on the link return ENOSPC
  • If root creates the tree, this works: mkdir -p some/containing/paths; mv 1111* some/containing/paths/ BUT afterward this doesn’t work: rm -rf some (it returns “directory not empty” and ENOSPC errors)

Workaround

Somehow the path is short enough to create a symlink but too long to remove it. The workaround is just to shorten the path.

root# pwd
/private/tmp/111[ ... ]111/222[ ... ]222/333[ ... ]333/444[ ... ]444
root# ls
L
root# mv /private/tmp/1* /private/tmp/one
root# pwd
/private/tmp/one/222[ ... ]222/333[ ... ]333/444[ ... ]444
root# rm L
root# ls
root# rm -rf /tmp/one
root# 

Is The Workaround Enough?

So yeah, there’s a manual workaround, but here are some questions:

  • Is your antivirus program smart enough employ it if malware is stored in this manner? AV programs already have to deal with chflags(2) issues. Imagine a file stored in such a path combined with hostile file names and various applications of chflags.
  • Do you have a replacement tool for rm -rf if a malicious program or individual starts to fill your filesystem with these things?