flink (AT_EMPTY_PATH / AT_SYMLINK_FOLLOW) security considerations

From: Andy Lutomirski
Date: Tue Aug 13 2013 - 13:22:06 EST


Linux 3.10 (and many earlier kernels) allow flink using an incantation
like linkat(AT_FDCWD, "/proc/self/fd/N", destdirfd, newname,
AT_SYMLINK_FOLLOW); It's possible to do much the same thing using
linkat(oldfd, "", destdirfd, newname, AT_EMPTY_PATH) if you're
privileged on 3.10, and the requirement for privilege is dropped in
3.11-rc5.

The immediate motivation for dropping the privilege requirement is the
O_TMPFILE changes: you can create a temporary file with O_TMPFILE,
write to it, and then give it a name with linkat(..., AT_EMPTY_PATH).
You can prevent this behavior by using O_TMPFILE | O_EXCL.

Apparently there's some kind of new security issue here [1], but I
don't know what it is. So I'd like to get other people's thoughts.

Some notes:

All linkat variations do this:

/* Make sure we don't allow creating hardlink to an unlinked file */
if (inode->i_nlink == 0 && !(inode->i_state & I_LINKABLE))
error = -ENOENT;

That means that deleted files (except for O_TMPFILE, which sets
I_LINKABLE) can't be flinked.

Both flink variants work on O_PATH fds.

I've attached some test code if you want to play with this stuff.

Possible changes include inspecting f_cred before flink, requiring
I_ILINKABLE if unprivileged, and reverting the 3.11 change.

[1] https://lwn.net/Articles/562488/ -- see the comments
#include <stdio.h>
#include <err.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

#define __O_TMPFILE 020000000
#define O_TMPFILE (__O_TMPFILE | O_DIRECTORY)
#define AT_EMPTY_PATH 0x1000

int main(int argc, char **argv)
{
char buf[128];

if (argc != 4)
errx(1, "Usage: flinktest TMPDIR PATH linkat|proc");

int fd = open(argv[1], O_TMPFILE | O_RDWR, 010600);
if (fd == -1)
err(1, "O_TMPFILE");
write(fd, "test", 4);

if (!strcmp(argv[3], "linkat")) {
if (linkat(fd, "", AT_FDCWD, argv[2], AT_EMPTY_PATH) != 0)
err(1, "linkat");
} else if (!strcmp(argv[3], "proc")) {
sprintf(buf, "/proc/self/fd/%d", fd);
if (linkat(AT_FDCWD, buf, AT_FDCWD, argv[2], AT_SYMLINK_FOLLOW) != 0)
err(1, "linkat");
} else {
errx(1, "invalid mode");
}
return 0;
}
#include <stdio.h>
#include <err.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

#define AT_EMPTY_PATH 0x1000
#ifndef O_PATH
#define O_PATH 010000000
#endif

int main(int argc, char **argv)
{
char buf[128];

if (argc != 5)
errx(1, "Usage: flinktest OLDPATH NEWPATH <normal|O_PATH> AT_EMPTY_PATH|proc");

int flag;
if (!strcmp(argv[3], "normal"))
flag = O_RDONLY;
else if (!strcmp(argv[3], "O_PATH"))
flag = O_PATH;
else
errx(1, "bad open mode");

int fd = open(argv[1], flag);
if (fd == -1)
err(1, "open");

if (!strcmp(argv[4], "AT_EMPTY_PATH")) {
if (linkat(fd, "", AT_FDCWD, argv[2], AT_EMPTY_PATH) != 0)
err(1, "linkat");
} else if (!strcmp(argv[4], "proc")) {
sprintf(buf, "/proc/self/fd/%d", fd);
if (linkat(AT_FDCWD, buf, AT_FDCWD, argv[2], AT_SYMLINK_FOLLOW) != 0)
err(1, "linkat");
} else {
errx(1, "invalid mode");
}
return 0;
}