Hacker News new | ask | show | jobs
by jerf 1798 days ago
One of my minor disappointments with Go, considering the time it came out and the UNIX heritage that it descended from, was that it didn't prioritize the *at() functions. It's difficult, if not virtually impossible, to write secure code with the "traditional" path-based system because every time you do one thing, then some other thing to a path that has some sort of security implication, you've written a TOCTOU problem if somebody can wedge between those two things to change some critical aspect of the file.

It's hard for me to blame programmers for not using these functions more when hardly any language properly exposes them. But since nobody exposes them, nobody's aware they should use them.... chicken & egg strike again.

3 comments

But openat, for example, is still path-based; it just changes the directory that the path is relative to. If you give it an absolute path, it will open it, and I didn't see any reason in the man page why you couldn't just pass in a bunch of ../../ as the usual exploits do. Maybe you're referring to another category of bugs?
Sorry, I was unclear. Too much context in my head from the times I've jousted with this and I forgot to contextualize properly. (Which is ironic since part of my complaint is precisely that too few people know this stuff.) That family of functions allows you to open things based on handles more easily. So you can open a directory, and while holding on to the handle for that directory, know that you are still in that directory, even potentially open files in that directory and then, once you do that, know that you have a file in that directory (or, atomically, don't).

It's the difference between

     dirHandle = open("some path");
     fileInDir = openat(dirHandle, "some file");
versus

     dir = open("some path")
     // examine the directory, then
     fileInDir = open("some path/some file");
In the second case, between those two lines, you can have something else jump in and modify or remove or repermission or whatever the "some file". It has never been the largest security issue, but it's been a running undercurrent of securit issues for decades.

In the first case, you have atomically-safe operations; you either get the directory or don't, then either get the file handle or don't, etc, and once you have the handle nobody else can take it from you, even if they rename the file under you, etc. It means that if you are writing logic like "if the file is setuid, do this", there's no way for an external process to wedge in between the two things.

In other words, you ought to be able to not just read from a file handle, but also open relative to the handle directly, and do all those other things. Any API that operates in terms of paths is pretty much intrinsically open to TOCTOU, because any time you "check" a path vs. "use" the path, which is fairly common, you have a window of opportunity for lossage. I'm not sure I've yet seen a non-C way of doing this built into a standard library.

Also... before you jump in with some "what ifs", no, these functions don't magically make your code more secure. You still have to use them correctly and it's still pretty easy to mistakenly let path-based logic slip in accidentally even so. It doesn't make insecure code secure; it makes guaranteed insecure (in security-sensitive contexts, obviously a lot of time this isn't a security issue) code possible to write securely.

Makes sense, but I think that you only gain safety when you are checking attributes of the directories leading to the file, but not when you are checking the file itself. For example, you said

> In the second case, between those two lines, you can have something else jump in and modify or remove or repermission or whatever the "some file".

Modifying/removing/repermissioning "some file" is still possible even with openat() if you do it between the time you open("some path") and openat("some file"). There is still a race condition there in either case if you are examining the contents of the directory (e.g. "stat"ing the file and then calling openat). You can also modify/repermission "some path" as well. The only thing openat() protects you from is removing/replacing "some path" (not "some file") and I agree that that is valuable for security purposes.

'Modifying/removing/repermissioning "some file" is still possible even with openat() if you do it between the time you open("some path") and openat("some file").'

This is part of what I was trying to head off with my parenthetical. You still have to use it correctly to do secure things. But at least it's possible. This kind of security is basically impossible with pure path-based APIs. Plus, as mentioned elsewhere, there are some additional flags you can use for even more security that you can't get out of an API that is "open(filename)", simply because that API is mathematically incapable of carrying such flags (assuming you don't start trying to encode them in the filename itself, but that way lies madness).

It's doable when you need it, something like:

    filefd, err := syscall.Openat(int(dir.Fd()), filename, os.O_RDONLY, 0)
    file := os.NewFile(uintptr(filefd), filename) // for use with library functions
For what it's worth, Linux 5.6 introduced openat2 [1] which accepts some additional flags controlling path resolution.

For example, RESOLVE_IN_ROOT "is as though the calling process had used chroot(2) to (temporarily) modify its root directory (to the directory referred to by dirfd)".

[1] https://man7.org/linux/man-pages/man2/openat2.2.html

He was - TOCTOU has its own wiki page [1]. These can be nastier, because they don't require the attacker to be able to submit strings or file names.

[1] https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use

I guess I'm not sure how you would use open() that would expose a TOCTOU bug that openat () wouldn't. Can you give an example?
I was unclear. See my other cousin reply; you can't use it yourself to have a directory handle and securely open files in that directory. You can only open things by path.
I’m confused. How would using *at() APIs prevent race conditions?