From: "Theo de Raadt" Subject: Re: openat(2) is mostly useless, sadly Cc: tech@cvs.openbsd.org Date: Fri, 30 May 2025 09:51:39 -0600 Theo de Raadt wrote: > The family of system calls related to openat(2) are mostly useless in > practice, rarely used. When they are used it is often ineffectively or > even with performance-reducing results. Below, find part of the code for a regression test. It builds a tree of files, directories, and symlinks which looks like this, and then it compares then access patterns between normal use, O_BELOW, F_BELOW (which should be the same), and finally also unveil() which is subtly different. ./: -rw------- 1 root wobj 18 May 30 09:43 upper testdir: drwx------ 3 root wobj 512 May 30 09:46 ./ drwxrwx--- 3 build wobj 512 May 30 07:58 ../ lrwxr-xr-x 1 root wobj 15 May 30 07:41 crazy-local-symlink@ -> subdir/../local lrwxr-xr-x 1 root wobj 26 May 30 07:41 crazy-local-symlink2@ -> subdir/../../testdir/local -rw------- 1 root wobj 18 May 30 09:43 local drwx------ 2 root wobj 512 May 30 07:28 subdir/ lrwxr-xr-x 1 root wobj 2 May 30 07:41 symlink-to-..@ -> .. lrwxr-xr-x 1 root wobj 5 May 30 07:41 symlink-to-home@ -> /home lrwxr-xr-x 1 root wobj 5 May 30 07:41 symlink-to-local@ -> local lrwxr-xr-x 1 root wobj 1 May 30 07:41 symlink-to-root@ -> / lrwxr-xr-x 1 root wobj 8 May 30 07:41 symlink-to-upper@ -> ../upper lrwxr-xr-x 1 root wobj 16 May 30 07:41 symlink2-to-local@ -> symlink-to-local lrwxr-xr-x 1 root wobj 15 May 30 07:41 symlink2-to-root@ -> symlink-to-root lrwxr-xr-x 1 root wobj 16 May 30 07:41 symlink2-to-upper@ -> symlink-to-upper testdir/subdir: total 8 drwx------ 2 root wobj 512 May 30 07:28 ./ drwx------ 3 root wobj 512 May 30 09:46 ../ without F_BELOW or O_BELOW 0 local 1 .. 0 . 1 / 1 /home 1 /etc/services 1 ../upper 0 symlink-to-local 0 symlink2-to-local 1 symlink-to-.. 1 symlink-to-root 1 symlink2-to-root 1 symlink-to-upper 1 symlink2-to-upper 1 symlink-to-home 0 crazy-local-symlink 1 crazy-local-symlink2 with fcntl F_BELOW 0 local 1 ..: No such file or directory 0 . 1 /: No such file or directory 1 /home: No such file or directory 1 /etc/services: No such file or directory 1 ../upper: No such file or directory 0 symlink-to-local 0 symlink2-to-local 1 symlink-to-..: No such file or directory 1 symlink-to-root: No such file or directory 1 symlink2-to-root: No such file or directory 1 symlink-to-upper: No such file or directory 1 symlink2-to-upper: No such file or directory 1 symlink-to-home: No such file or directory 0 crazy-local-symlink 1 crazy-local-symlink2: No such file or directory with open O_BELOW 0 local 1 ..: No such file or directory 0 . 1 /: No such file or directory 1 /home: No such file or directory 1 /etc/services: No such file or directory 1 ../upper: No such file or directory 0 symlink-to-local 0 symlink2-to-local 1 symlink-to-..: No such file or directory 1 symlink-to-root: No such file or directory 1 symlink2-to-root: No such file or directory 1 symlink-to-upper: No such file or directory 1 symlink2-to-upper: No such file or directory 1 symlink-to-home: No such file or directory 0 crazy-local-symlink 1 crazy-local-symlink2: No such file or directory with unveil 0 local 1 ..: No such file or directory 0 . 1 /: No such file or directory 1 /home: No such file or directory 1 /etc/services: No such file or directory 1 ../upper: No such file or directory 0 symlink-to-local 0 symlink2-to-local 1 symlink-to-..: No such file or directory 1 symlink-to-root: No such file or directory 1 symlink2-to-root: No such file or directory 1 symlink-to-upper: No such file or directory 1 symlink2-to-upper: No such file or directory 1 symlink-to-home: No such file or directory 0 crazy-local-symlink 1 crazy-local-symlink2 WORKED, BUT SHOULD HAVE FAILED I may have missed some cases which need to be added. This shows that with O_BELOW we are unable to look above '.', even using symlinks. We also cannot traverse above, to get below. In particular, symlink traversals are contained. That is a strong part of this concept. Many programs contain code to be careful with symbolic links escaping their intended place, and this is very tricky. O_BELOW will be kernel-assist containment of that policy. (I'm suggesting that noone should delete their application specific containment code...). With unveil(), we can traverse above the spot, to get below. That's because of how unveil works. unveil can do paths starting with '/', or all sorts of '..' or symlink traversals, as long as in-the-end it lands on an unveil-remembered-file-vnode or below an unveil-remembered-directory-vnode. #include #include #include #include #include #include #include #include #include void setup(void); int test(char *label, int dirfd, int test); struct paths { char *path; int block; }; struct paths paths[] = { { "local", 0 }, { "..", 1 }, { ".", 0 }, { "/", 1 }, { "/home", 1 }, { "/etc/services", 1 }, { "../upper", 1 }, { "symlink-to-local", 0 }, { "symlink2-to-local", 0 }, // symlink to symlink to inside { "symlink-to-..", 1 }, { "symlink-to-root", 1 }, { "symlink2-to-root", 1 }, // symlink to symlink to root { "symlink-to-upper", 1 }, { "symlink2-to-upper", 1 }, // symlink to symlink to upper { "symlink-to-home", 1 }, { "crazy-local-symlink", 0 }, { "crazy-local-symlink2", 1 }, }; /* * Create a heirarchy we will test against */ void setup(void) { int fd; fd = open("upper", O_CREAT|O_RDWR, 0600); if (fd > 0) { dprintf(fd, "contents of upper\n"); close(fd); } mkdir("testdir", 0700); mkdir("testdir/subdir", 0700); chdir("testdir"); fd = open("local", O_CREAT|O_RDWR, 0600); if (fd > 0) { dprintf(fd, "contents of local\n"); close(fd); } symlink("local", "symlink-to-local"); symlink("symlink-to-local", "symlink2-to-local"); symlink("..", "symlink-to-.."); symlink("/", "symlink-to-root"); symlink("symlink-to-root", "symlink2-to-root"); symlink("../upper", "symlink-to-upper"); symlink("symlink-to-upper", "symlink2-to-upper"); symlink("/home", "symlink-to-home"); symlink("subdir/../local", "crazy-local-symlink"); symlink("subdir/../../testdir/local", "crazy-local-symlink2"); } int test(char *label, int dirfd, int test) { unsigned int i; int fd, err = 0; printf("%s\n", label); for (i = 0; i < sizeof (paths)/sizeof(paths[0]); i++) { fd = openat(dirfd, paths[i].path, O_RDONLY); if (fd == -1) { printf("\t%d\t%s: %s\n", paths[i].block, paths[i].path, strerror(errno)); if (test && paths[i].block == 0) { printf("\t\tFAILED, BUT SHOULD HAVE WORKED\n"); err = 1; } } else { printf("\t%d\t%s\n", paths[i].block, paths[i].path); close(fd); if (test && paths[i].block == 1) { printf("\t\tWORKED, BUT SHOULD HAVE FAILED\n"); err = 1; } } } if (err) printf("AT LEAST ONE ERROR\n"); close(dirfd); return err; } int main(int argc, char *argv[]) { int fd, err = 0; setup(); fd = open(".", O_DIRECTORY|O_RDONLY); err += test("without F_BELOW or O_BELOW", fd, 0); close(fd); fd = open(".", O_DIRECTORY|O_RDONLY); fcntl(fd, F_BELOW, 0); err += test("with fcntl F_BELOW", fd, 1); close(fd); fd = open(".", O_DIRECTORY|O_RDONLY|O_BELOW); err += test("with open O_BELOW", fd, 1); close(fd); unveil(".", "rw"); unveil(NULL, NULL); fd = open(".", O_DIRECTORY|O_RDONLY); err += test("with unveil", fd, 2); close(fd); // unveil() has corrupted the test environment, do not add // new tests after this if (err) exit(1); }