The trouble with symbolic links
At the 2022 sambaXP conference, Jeremy Allison gave a talk titled "The UNIX Filesystem API is profoundly broken: What to do about it?". LWN regulars may recall hints of these talks in a recent comment thread. He started his talk with the problems that symbolic links ("symlinks") cause for application developers, then discussed how the solutions to the problems posed by symlinks led to substantial increases in the complexity of the APIs involved in working with pathnames.
Allison explained that hard links were the first "interesting addition" to the original Unix filesystem API; unlike symlinks, though, they are not dangerous, and are, in fact, easy to use. A hard link is simply the connection between a directory entry and the inode for the file (or directory) to which that entry refers. Unix systems allow multiple links to any file, but require that the inode and directory entries all reside on the same filesystem.
By contrast, symlinks contain another path as data, and the kernel transparently operates on the file at that path when system calls like open() or chown() are called on the symlink. This seemingly innocuous feature led to the addition of incredible amounts of complexity in the effort to fulfill the needs of programs that need to be aware of whether a pathname contains a symlink or not. Such programs include archival programs like tar, file synchronization and transfer programs such as rsync, network filesystem servers like Samba, and many more that suffer security problems as a result of not giving sufficient attention to symlinks in pathnames.
The variety of security problems resulting from symlinks can be seen in a search of CVE entries, which gave Allison 1,361 results when he ran it. These include vulnerabilities that facilitate information disclosure, privilege escalation, and arbitrary file manipulation including deletion, among other attacks. Without discussing any specific CVE in detail, he gave an example of the kind of security problem that can result from symlink-related vulnerabilities.
An application running as root may try to check that /data/mydir is a regular directory (not a symlink) before opening the file /data/mydir/passwd. In between the time the program does the directory check and the file open, an attacker could replace the mydir directory with a symlink to /etc, and now the file opened is, unexpectedly, /etc/passwd. This is a kind of race condition known as a time-of-check-to-time-of-use (TOCTOU) race.
Symlinks and complexity
Symlinks were created, Allison theorized, because hard links are restricted to linking within the same filesystem, so only symlinks (which lack that restriction) could be used if an administrator wanted to add new storage media without changing the paths to users' data. He quoted an advertisement for 4.2BSD, which touted, "This feature frees the user of the constraints of the strict hierarchy that a tree structure imposes. This flexibility is essential for good namespace management."
The addition of symlinks led to the lstat() system call, which provided the means to identify whether the last component in a pathname is a symlink. This was, unfortunately, insufficient for handling symlinks pointing to directories earlier in the path, he explained. An application could attempt to check each component of the path individually, but not atomically — another application could make a change to one of the components during this process, leading to security vulnerabilities.
An option to the open() system call, O_NOFOLLOW, exhibits the same problem as lstat(). O_NOFOLLOW instructs the system call to fail with ELOOP if the last component in the pathname is a symbolic link, but it only checks the last component. The realpath() C library function follows symlinks in a path and produces an absolute, canonical pathname that the application can then compare with the original. Allison described this as an appealing but incorrect solution to the problem. Another process could make a change in between the time realpath() is called and another function is used to manipulate the file in some fashion. In other words, the same TOCTOU race applies here.
Allison said that the openat() system call was designed as a solution to this problem; it introduces the idea of referring to files with respect to a directory that's indicated by an already-open file descriptor. The only reliable way to identify a file's path is to walk the hierarchy using multiple calls to openat(). Everything else would be vulnerable to race conditions.
But Allison also pointed out the flaw in this technique. "You cannot create a new directory with open(), you cannot remove a file, unlink a file, or delete a directory with an open() call." So, more functions following the pattern of openat() had to be created: mkdirat(), linkat(), unlinkat(), renameat(), and more. Some are still missing, like getxattrat() and setxattrat(). Some, like fchownat() and faccessat(), don't follow the pattern cleanly.
Allison didn't mince words: "So our original clean and beautiful POSIX filesystem API doesn’t look very clean and beautiful anymore...pathnames as a concept are now utterly broken in POSIX." One could reasonably attribute, in part at least, any perceived bitterness to Allison's struggles with the long road to a fix for CVE-2021-20316 in Samba.
Because of the talk's focus on the role of symlinks in complicating the Unix pathname API, Allison did not directly raise the point that race conditions involving pathnames can occur even without symlinks. It seems a major source of complexity is the lack of a mechanism for atomically batching together operations that involve walking directories and symlinks to eventually perform some operation on a file.
Workarounds
Allison then explained the use of the O_PATH flag to open(), which will return a file descriptor that is only useful for passing to the *at() system calls as the dirfd argument. Unfortunately for Samba, file descriptors opened with O_PATH cannot be used for reading or writing extended attributes. He found a workaround, demonstrated by a snippet of code that he described as "one of the most beautiful hacks I’ve ever seen, it’s so ugly it makes you want to vomit, but it’s amazing."
int fd = openat(dirfd, "file", O_PATH|O_NOFOLLOW); sprintf(buf, "/proc/self/fd/%d", fd); getxattr(buf, ea_name, value, size);
The contents of /proc/self/fd are symlinks that represent every file descriptor the process has open. Allison explained the code: "If you print into a buffer '/proc/self/fd/' and then the number of the descriptor you just got back from O_PATH, you can then pass it to getxattr() or setxattr() as a path, and it can’t be symlink-raced." He wasn't sure whether to attribute this code to Android or Red Hat developers, but a similar use of /proc/self/fd/ can be found in the open() man page.
Allison reiterated the main point of his talk: "The concept of pathnames is unusable on POSIX, completely. For a non-trivial application, for a regular person writing code on POSIX, you will have symlink races in your code."
Examples of (since fixed) CVEs were then provided, including one in the Rust standard library, which was discussed extensively here. In the last few minutes of the talk, Allison noted several proposed solutions offered by LWN readers, including a special prctl() call and restrictions on when non-root symlinks are followed. He said that the MOUNT_NOSYMFOLLOW mount option, which simply forbids following symlinks within a filesystem, is his preferred solution: "It’s perfect. It does exactly what we need." Allison's talk concluded on that point.
While it certainly seems desirable to forbid symlinks in the name of cleaning up the POSIX API, they are a frequently used system-administration tool. Several popular "symlink managers" exist. Gnu Stow, for example, provides a way for administrators to install programs into a new directory hierarchy, such as /usr/local/stow/packagename-version/, and then create forests of symlinks from /usr/local/bin/example to /usr/local/stow/packagename-version/bin/example, using the minimum number of symlinks necessary. This makes it possible to "uninstall" a package simply by removing the symlinks with the help of stow -D.
The /etc/alternatives system created by Debian allows administrators to switch between substitutable packages in a similar manner without forcing the uninstallation or reinstallation of either package. In a similar vein, the Nix and Guix distributions make heavy use of symlinks — a Guix profile consists of a tree of symlinks to packages installed within /gnu/store/, making it easy to switch between grouped combinations of specific versions of packages.
Banning symlinks entirely would break these use cases, but restricting their creation to the root user would most likely suffice. Users may still have other legitimate needs for symlinks, though, and substantially restricting them would likely be an unpopular change.
SambaXP has made the talk's video and slides available.
Index entries for this article | |
---|---|
Kernel | Symbolic links |
GuestArticles | Riddoch, Chris |
Posted Jul 7, 2022 14:54 UTC (Thu)
by kees (subscriber, #27264)
[Link] (4 responses)
Posted Jul 7, 2022 17:02 UTC (Thu)
by adobriyan (subscriber, #30858)
[Link]
Posted Jul 8, 2022 1:10 UTC (Fri)
by himi (subscriber, #340)
[Link] (1 responses)
Posted Jul 8, 2022 14:54 UTC (Fri)
by harisphnx (subscriber, #139363)
[Link]
Posted Jul 11, 2022 13:07 UTC (Mon)
by scientes (guest, #83068)
[Link]
Posted Jul 7, 2022 15:32 UTC (Thu)
by Sesse (subscriber, #53779)
[Link] (13 responses)
Posted Jul 7, 2022 16:07 UTC (Thu)
by khim (subscriber, #9252)
[Link] (11 responses)
It's like saying that car without wheels and an engine is not broken. I mean: you can still pull it with enough horses attached thus move people and cargo, right? So it's still usable, kinda. Similarly here: the only difference between pathname and filename is the fact that pathnames have hierarchy. Sure, apps which don't need pathnames but are just happy to use them as opaque file identifiers in a flat namespace are not broken with symlinks. But everything else is broken is a sense that Joe Average Developer would write incorrect code 10 times out of 10. How you can call this “not utterly broken” is beyond me. On the contrary: practically any app which deals with file hierarchies (and just with individual files) is broken. Not all such programs lead to security vulnerability because sometimes you have situation “Joe can break Joe's program which s/he can crash directly anyway” which is considered acceptable. But they are broken, nonetheless.
Posted Jul 7, 2022 16:09 UTC (Thu)
by Sesse (subscriber, #53779)
[Link] (3 responses)
Posted Jul 8, 2022 8:57 UTC (Fri)
by gerdesj (subscriber, #5446)
[Link] (2 responses)
That's quite a large notable exception!
Posted Jul 9, 2022 0:47 UTC (Sat)
by willy (subscriber, #9762)
[Link]
Posted Jul 23, 2022 0:17 UTC (Sat)
by DimeCadmium (subscriber, #157243)
[Link]
Posted Jul 7, 2022 17:01 UTC (Thu)
by adobriyan (subscriber, #30858)
[Link] (4 responses)
What are the requirements for, say, non-broken text editor wrt symlinks?
Posted Jul 8, 2022 13:00 UTC (Fri)
by ilammy (subscriber, #145312)
[Link] (3 responses)
If a symlink in the open path has changed while a file is being edited, users might or might not want to be notified about that instead of overwriting some other file.
If there is a symlink loop, search by all files in the project should not hang or print out infinite results.
Posted Jul 8, 2022 13:05 UTC (Fri)
by Sesse (subscriber, #53779)
[Link]
Posted Jul 8, 2022 20:24 UTC (Fri)
by nybble41 (subscriber, #55106)
[Link]
Posted Jul 8, 2022 22:26 UTC (Fri)
by k8to (guest, #15413)
[Link]
Posted Jul 7, 2022 17:54 UTC (Thu)
by jthill (subscriber, #56558)
[Link] (1 responses)
Besides, open and fstat the path's final directory, if getuid() owns it you don't even have to vet whatever real path you'd need to get you there, then either you don't follow final symlinks or you iteratively re-vet what readlinkat() for them gets you; and if when you later decide to rewrite your hosts file the path it's going to isn't a previously-vetted device and inode you'd would be well within reason to just refuse to continue.
Instead of blackholing symlinks a quick little library to implement the operations in terms of those safety checks seems reasonable; things like ssh and samba for which those aren't a enough are distinctly not Joe Average Coder projects. `std::filesystem` should probably operate by default only with getuid()-owned files anyway.
Posted Jul 7, 2022 22:39 UTC (Thu)
by khim (subscriber, #9252)
[Link]
The concept of injecting user input into a string is utterly broken, yes. That's why we have entirely different API which solves that issue once and for all. We don't have anything similar for pathnames. Have you actually read the article? Even finding the path's final directory is quite non-trivial. Make one. Give it to Jeremy and we can make a bet about how many ways it's broken.
Posted Jul 8, 2022 2:32 UTC (Fri)
by k8to (guest, #15413)
[Link]
99% of the time, you can just ignore them entirely as a developer and everything works.
It's true that once developers start trying to care about symbolic links explicitly they often make many errors. Usually it's enough to just use libc's realpath and consider the final answer. Trying to care about symbolic links in more detail is usually a path to madness.
For some problems, there are reasonable patterns, like writing code to "monitor" a directory and its contents, you usually don't want to follow links at all to avoid infinite loops etc, with limited user-provided exceptions.
For other problems, there may not be a good solution. I haven't worked on any problems like that in my long programming career, however.
Posted Jul 7, 2022 15:48 UTC (Thu)
by iabervon (subscriber, #722)
[Link] (6 responses)
I could imagine a C library change that would set the prctl and handle symlinks in user space with realpath() for the traditionally-named path-based functions, and include variants that you use instead if you're going to validate the path at all.
Posted Jul 7, 2022 16:11 UTC (Thu)
by khim (subscriber, #9252)
[Link]
This would have worked great if we had a time machine and had the ability to go back to 1991 or 1992 to change Linux API. I don't think we have this option today.
Posted Jul 7, 2022 16:23 UTC (Thu)
by nix (subscriber, #2304)
[Link] (3 responses)
How many of them do you imagine will get *that* right, given how many bugs and races there have been in, say, rm -rf, which you'd think is heavily tested? Oh look, a new set of security holes, probably worse than the last lot.
-- N., very heavy user of symlinks, thank god for Nix (the distro, no relation) making sure that not too many break when entire /usr trees are symlink farms
Posted Jul 7, 2022 17:38 UTC (Thu)
by pbonzini (subscriber, #60935)
[Link] (1 responses)
Posted Sep 3, 2022 11:56 UTC (Sat)
by nix (subscriber, #2304)
[Link]
Posted Jul 7, 2022 19:33 UTC (Thu)
by iabervon (subscriber, #722)
[Link]
The only change I can see to outcomes of path-based operations from having separate resolve and operate syscalls would be that, if you rename a symlink over a regular file, a poorly-timed open() could get ELOOP instead of either getting the original file or the target of the symlink. Replacing one symlink with another would be atomic, and any other operation isn't atomic today (that is, you can't replace a directory with a symlink atomically, and you can't replace both a symlink and another file atomically; I guess you might get ELOOP when you could only have gotten ENOENT).
As far as changes to userspace program code, the only one would be that, if you've explicitly called realpath() yourself and validated the result in some way, you'd call realopen() on the string you validated rather than calling open() and getting another round of symlink resolution that might be different.
Posted Jul 7, 2022 18:56 UTC (Thu)
by bartoc (guest, #124262)
[Link]
Posted Jul 7, 2022 17:40 UTC (Thu)
by sbaugh (guest, #103291)
[Link] (13 responses)
Posted Jul 7, 2022 20:09 UTC (Thu)
by wahern (subscriber, #37304)
[Link] (12 responses)
Posted Jul 7, 2022 21:33 UTC (Thu)
by neilbrown (subscriber, #359)
[Link] (11 responses)
But it isn't just bind mounts, it is bind mounts in a private namespace. I don't think they are insecure.
The insecurity of symlinks is that if I create a symlink you have to follow it. When I create a bind mount in a private namespace you can't even see it let alone follow it
Posted Jul 7, 2022 22:15 UTC (Thu)
by willy (subscriber, #9762)
[Link] (10 responses)
Posted Jul 7, 2022 22:43 UTC (Thu)
by khim (subscriber, #9252)
[Link] (1 responses)
It all comes down to the original sin: the ability of less-privileged programs to dictate the worldview of more privileged programs. This is fixable (Android, macOS, Windows, some Linux distributions are trying to fix that) but I wonder how much time would it take. Years? Decades?
Posted Aug 5, 2022 16:11 UTC (Fri)
by immibis (subscriber, #105511)
[Link]
Posted Jul 7, 2022 22:58 UTC (Thu)
by Hello71 (subscriber, #103412)
[Link] (6 responses)
Posted Jul 8, 2022 5:13 UTC (Fri)
by matthias (subscriber, #94967)
[Link]
Are you suggesting that if someone calls a suid binary inside a container, the binary should not use the mount namespace of the container but the root mount namespace instead?
Posted Jul 8, 2022 6:35 UTC (Fri)
by neilbrown (subscriber, #359)
[Link]
That's exactly the point I was going to make. bind mounts in private namespaces are perfectly safe. setuid programs as profoundly broken, whether you use namespaces or not.
Posted Jul 8, 2022 10:16 UTC (Fri)
by tialaramex (subscriber, #21167)
[Link] (2 responses)
One of the things I appreciate in Rust is that where there are special cases somebody takes the time to think about whether in fact there's some more general principle of which the special case was just the easy to observe part, the tip of the iceberg so to say, and to provide for the general principle, rather than just the special case.
Example: If you're a Rust beginner you learn that Option's None and Result's Error can be passed back up to your caller where appropriate with the '?' try operator and this seems like a special case, you can't go around treating an integer, or a file handle, or some other kind of thing this way. Or can you? In fact the general principle is the (nightly gated) Try trait, and Option and Result merely implement Try (they're allowed to do this even in stable Rust because Rust internally isn't subject to its own stability rules or it'd be recursively impossible to develop anything)
Another example: In languages with ad hoc polymorphism ("Function overloading") it's obvious that "LWN.net".contains("LWN") and "LWN.net".contains('.') are both reasonable things to write so you just overload the function to do either. Rust doesn't have ad hoc polymorphism, so it must decide what type that parameter is such that both "LWN" and '.' match or else provide two functions with different names. The resulting trait Pattern once again represents a more general principle - any pattern which could match parts of a string, and so in Rust you can also write "LWN.net".contains(char::is_alphabetic) because hey, char::is_alphabetic is a function which matches parts of a string so why not.
I think SUID just isn't one of those things, it's a profoundly special case, and so we should immediately be prejudiced against it as a design feature of the operating system. Rather than everything just falling out naturally, as we'd like for a general solution, SUID means everywhere we need to handle this differently. If you've just written a bunch of low level code there's a good chance that if I remind you that SUID exists you'll need to go back and add a bunch of conditional branches to handle that...
Posted Jul 8, 2022 11:25 UTC (Fri)
by farnz (subscriber, #17727)
[Link] (1 responses)
Another reason to be biased against SUID is that it's a quick solution to the problem of users not having root access to the entire machine.
There are three different ways to run something as root on a Linux-like system (assuming that you've got an ordinary user account):
The first is a non-starter if you're not trusted with root yourself - the coordination to keep someone with root access around is a pain.
The second involves more code. You have to have something running that can make the policy decisions based on a string sent to it, and then execute the process for you. There's also complexity around transferring "enough" environment state to the privileged process, and transferring results back.
The third is easy. Most of your SUID process's state is the same as it would have if it wasn't SUID, except for a few "minor" bits here and there (and this list of things is growing as we find security holes that cannot be fixed except by resetting state).
Ideally, everyone would bother to do the second option for things that need to be privileged - but that's a lot of work, and SUID is a "neat hack" that means you don't need to do it.
Posted Jul 9, 2022 20:19 UTC (Sat)
by NYKevin (subscriber, #129325)
[Link]
Posted Jul 8, 2022 12:32 UTC (Fri)
by hallyn (subscriber, #22558)
[Link]
User namespaces are designed such that this is supposed to not be necessary: you can only mount things if you create a new user namespace, and if you are fully unprivileged, you can only map ns-root to your own or a delegated uid, so you cannot trick host root with bind mounts and a setuid-root binary. The problem with user namespaces comes because user namespaces also want to grant ns-root privilege over the resources owned by the creator. And they do that pretty well, it's just that that expands the amount of kernel code which an unprivileged user can exercise, and exploit.
> privilege escalation should occur via communication with a system daemon using a well-defined protocol
Several people have tried to the plumbing needed for plan-9 factotum functionality into the kernel. My last attempt (expanding on Ashwin Ganti's original code) was 10 years ago - https://lore.kernel.org/all/20100427164139.GA7359@us.ibm.... , and there was a more recent independent patch posted at https://lore.kernel.org/all/20180210165845.18852-1-metux@... .
Posted Jul 12, 2022 8:10 UTC (Tue)
by koverstreet (✭ supporter ✭, #4296)
[Link]
Which is pretty huge.
Posted Jul 7, 2022 20:27 UTC (Thu)
by developer122 (guest, #152928)
[Link] (4 responses)
(I suppose this would require a filesystem to be checked and it's symlinks validated/invalidated during mounting)
Posted Jul 7, 2022 22:55 UTC (Thu)
by Hello71 (subscriber, #103412)
[Link] (3 responses)
Posted Jul 8, 2022 9:01 UTC (Fri)
by taladar (subscriber, #68407)
[Link]
Posted Jul 8, 2022 21:12 UTC (Fri)
by developer122 (guest, #152928)
[Link] (1 responses)
If you're giving someone permission to make a simlink in a folder, then why care about any of it's parents? How is it different than regular wread/write access to the folder? (you'd be able to make/change files there anyway) Wouldn't altering/removing a parent break the simlink during lookup?
Posted Jul 8, 2022 23:12 UTC (Fri)
by developer122 (guest, #152928)
[Link]
I still don't see what the issue is with altering parent directories of the folder in question though, or what special capability symlinks add to that scenario.
Posted Jul 7, 2022 22:51 UTC (Thu)
by zaitseff (subscriber, #851)
[Link] (7 responses)
As Chris points out, the problem is not so much symlinks as Time Of Check to Time of Use (TOCTOU). Take, for example, a problem I've faced writing a simple C program: The naïve way would be to code up something like the following pseudo-C for determining which directory to use: And there you have it: a TOCTOU error (check for /home/john/.trader before creating the final target path using mkdir() later). No symlinks need be involved. Now you can merrily post your solution to the above problem! :-)
Posted Jul 7, 2022 23:04 UTC (Thu)
by Paf (subscriber, #91811)
[Link]
Now for something else which was security critical …………….. eek
Posted Jul 7, 2022 23:06 UTC (Thu)
by zaitseff (subscriber, #851)
[Link] (4 responses)
Posted Jul 7, 2022 23:57 UTC (Thu)
by mpr22 (subscriber, #60784)
[Link] (3 responses)
Posted Jul 8, 2022 0:46 UTC (Fri)
by zaitseff (subscriber, #851)
[Link] (2 responses)
Indeed. However, I can't quite see how to use these: something I didn't mention is that mkdir() is only run if writing a data file: if no data file is saved (a user-initiated operation), no directory is created. So I'm not sure how to avoid the TOCTOU problem in such a situation.
Posted Jul 8, 2022 1:20 UTC (Fri)
by mpr22 (subscriber, #60784)
[Link] (1 responses)
Safely creating the directory if it doesn't exist is still kind of a pain in the neck, because you can't combine the operations of "make a directory" and "open a file descriptor pointing to the directory" into a single indivisible syscall.
Posted Jul 8, 2022 4:16 UTC (Fri)
by magfr (subscriber, #16052)
[Link]
Posted Jul 8, 2022 2:39 UTC (Fri)
by k8to (guest, #15413)
[Link]
But if you want to have a problem have a stable dir it settles on and uses consistently, you've kind of designed a TOCTOU problem into the concept of your program or library. At least with classic system calls.
Of course *at calls allow one to do precisely what I implied. Open the dir, and keep it around for use. With pipe magic you can even eliminate TOCTOU across multiple processes.
Posted Jul 8, 2022 2:21 UTC (Fri)
by felixfix (subscriber, #242)
[Link] (3 responses)
Hard links are also restricted to regular files, or at least to not being a directory. I checked just now, and almost all my symlinks are to directories.
Posted Jul 8, 2022 8:36 UTC (Fri)
by nim-nim (subscriber, #34454)
[Link] (1 responses)
Windows uses magic registry entries for the same need, it works about as ugly.
Posted Jul 8, 2022 10:28 UTC (Fri)
by grawity (subscriber, #80596)
[Link]
Even Windows (after the Vista migration to space-less directories) now ships with symlinks like
Posted Jul 22, 2022 7:40 UTC (Fri)
by callegar (guest, #16148)
[Link]
Posted Jul 8, 2022 7:51 UTC (Fri)
by rgb (subscriber, #57129)
[Link]
Posted Jul 9, 2022 0:27 UTC (Sat)
by pj (subscriber, #4506)
[Link]
Posted Jul 9, 2022 3:31 UTC (Sat)
by Subsentient (guest, #142918)
[Link] (1 responses)
I personally don't *want* most userspace programs to be able to tell if something is a link. I've much more often had issues with a program that saw a symlink and refused to dereference it, than I ever have with a program that used them implicitly.
Note to other userspace developers: Please use stat() instead of lstat() whenever possible. You don't know if there's a good reason it's a symlink, for example, a .config folder could be pointing into an sshfs network drive so my settings are synced between machines. I put symlinks in weird places if I think they're useful, and the only times it hurts me are when some application is using lstat() and/or looking for symlinks some other way.
Posted Jul 9, 2022 11:55 UTC (Sat)
by mathstuf (subscriber, #69389)
[Link]
One place you can't is when asking "does this path exist?" because `stat` will return "no" for broken (or looped) symlinks while `lstat` will say "yes".
Posted Jul 11, 2022 20:00 UTC (Mon)
by ma4ris5 (guest, #151140)
[Link] (2 responses)
Here is one conceptual way to _open up a file for read_ in a safe way
It is possible to validate a path with file descriptors,
"_from the directory_" can be checked this way:
"_from the directory_" physical path can be discovered in a way "pwd -P" does it:
There are some other details that must be taken care too (signal handling, unrelated file descriptors),
For opening file for write can be done with the above elements too: grab a validated folder,
Posted Jul 12, 2022 3:26 UTC (Tue)
by neilbrown (subscriber, #359)
[Link] (1 responses)
Or you could use realpath() to expand all the symlinks, validate the path in whatever way you care about, and then use openat2() with RESOLVE_NO_SYMLINKS.
(requires Linux 5.6 or later, glibc doesn't have a wrapper)
Posted Jul 15, 2022 6:10 UTC (Fri)
by ma4ris5 (guest, #151140)
[Link]
With all uncertainty, the approach that I mentioned, is to first hold file descriptor to something
After gaining trust for a folder file descriptor, use it in safe ways (for example create a file with symbolic links turned off).
So from Samba developer, /proc/self approach works for solving the real path for validation:
fd = openat(-1, "/bin", O_DIRECTORY); /* fd = 3 */
"/usr/bin" is safe, so fd 3 can be used for following operations.
Posted Jul 12, 2022 8:33 UTC (Tue)
by koverstreet (✭ supporter ✭, #4296)
[Link] (1 responses)
Are there any attacks that would miss?
Posted Jul 12, 2022 12:30 UTC (Tue)
by jengelh (guest, #33263)
[Link]
Posted Jul 12, 2022 20:50 UTC (Tue)
by civodul (guest, #58311)
[Link] (1 responses)
In some cases, such as the Stow/Guix/Nix examples, symlinks could be replaced by a unionfs. Maybe a way forward?
Posted Sep 8, 2022 12:32 UTC (Thu)
by nix (subscriber, #2304)
[Link]
Or did you mean to suggest some other, cleverer trick that I hadn't thought of? (Quite likely, because the naive approach is obviously a disaster.)
Posted Jul 15, 2022 4:51 UTC (Fri)
by flussence (guest, #85566)
[Link]
Posted Jul 15, 2022 6:54 UTC (Fri)
by sven_wagner (guest, #114232)
[Link] (3 responses)
The problem starts when higher privileged accounts use user data to do tasks with higher privileges. I.e. when an admin is archiving (or restoring) user homes, syncing them from one Server to another (i.e. rsync).
A few possible solutions:
In nearly all cases (i believe) either the root job/cronjob should have been run under lesser privileges or should have been splitted into two phases like:
Then the root task would not need symlink checking of user input as the restore task runs with user permissions only.
The build process for example should not be able to write it's own config/scripts, so that a toctou attack could only alter the data actually used for the build which could be checked after a recursive copy into the build-process user space.
Another way to prevent TOCTOU when reading (as root) user provided data (that resides in symlinks) would be to use immutable flag on the directories in the tree during the root task. The user himself cannot set the flag, so the admin already knows when this flag is set somewhere (and needs to stay) or otherwise can set it blindly and just remove it afterwards. Symlinks cannot be set immutable, but setting the directory immutable removes the users ability to remove or alter the symlinks or directories there, so after setting immutable flags to directories in user tree, the admin can once recheck, that all directories have the flag and then has no TOCTOU problems with directory/symlink changes until he removes the flags again after finishing the task. Maybe the user could still use fuse to overmount directories in is space, but the thread is about symlinks. (To prevent collusion with user scripts or cronjobs, flock could help)
Using chroot could also be used to solve the "overwriting /etc/passwd" risk.
Yet another way:
Symlinks are not a problem here, administration with only(!) multi-decades old security mechanisms, actually is. There is a reason for SELinux to exist and there are hundreds (or more) of reasons to use it. =)
Posted Jul 15, 2022 10:57 UTC (Fri)
by ma4ris5 (guest, #151140)
[Link] (1 responses)
I remember when I started studying at "Helsinki University of Technology (HUT)" in 1992.
There were VT computer terminals with UART serial ports, attached to Unix desktops for shared
IT administrators told, that Unix vendors had fixed the TOCTTOU soft link race conditions:
Fortunately symbolic link races were history: students were safe, and we learned to prevent the race conditions in the future.
Posted Jul 15, 2022 22:19 UTC (Fri)
by ma4ris5 (guest, #151140)
[Link]
If somebody would try to search for the remaining race conditions,
IT departments were in control again.
The world would look quite different in Linux side, if universities would have demanded
Something like this might happen in next 10 years:
Security scanners
Other attack mitigations
Converting all existing code bases into safety should be as easy as possible (the 10 year project).
Posted Jul 16, 2022 8:08 UTC (Sat)
by sven_wagner (guest, #114232)
[Link]
Before starting the job, move the complete user space folder into another one where the user cannot reach it, check for still open file handles of the user, then do your job without fear of toctou by evil users and at the end move the folder back in place where the user can work on it.
Another opportunity would be to temporarily remove login of the user, sigstop all of his processes and disable his cron/at jobs, let the privileged task be done and afterwards sigcont the processes again.
Similar for shares, disable the user, check no currect connection is alive, do the job and enable the user again. Or just completely shutdown samba while the higher privileged job runs.
Those who wants the user to be able to work while the higher privileged job is ongoing, would just add more insecurities that reside within the programs that work on the files but don't expect the file to be changed while reading or maybe even mmapping it. At least whatever the higher privileged task does with the data, cannot be assumed to be consistent.
If you let the user change anything inside his userspace while higher privileged tasks are running, the user might just add data to the end of a file currently read by root and punch the data at the beginning so that he does not exceed his quota. The process reading the file (into / partition?) could end uptrying to read like 16TiB before the user has to try using collapse instead of punch to see if the root cronjob continues to read even more of the file.
Posted Jul 18, 2022 10:17 UTC (Mon)
by ras (subscriber, #33059)
[Link] (4 responses)
Parallelism is the hardest thing to get right in the programming world. POSIX offers no help whatsoever. It doesn't have transactions, and it doesn't have mandatory locks. Even the simplest of things like doing an atomic set of writes to a single file so hard we have the SQLite designer tell us to use it like fwrite(). Using an entire SQL library just to do reliable writes should be an absurd recommendation, but as things stand: he's right.
Because it's near impossible to get right, there are many bugs, some have security implications, and thus POSIX's total non-support of transactions has the cause of CVE after CVE for literally decades. It's not just symlinks. We seen streams of CVE's over the downright trivial operation of creating a temporary file, and surprise surprise hard links have their share of CVE's too. Renames, copies, moves, deletes are all the same. If you can't be 100% sure of what the file system looks like when you execute them (and POSIX ensures you can never be sure if there is more than one thread), then the end result is likely to be a gamble.
Or to put it another way, if POSIX did support transactions, most of the symlink CVE's he mentions, and all the need for all these xxxxat() operations would just vanish. Ergo, symlink's aren't the problem, and getting rid of them won't fix it.
Posted Jul 19, 2022 13:35 UTC (Tue)
by mathstuf (subscriber, #69389)
[Link] (3 responses)
Posted Jul 19, 2022 14:59 UTC (Tue)
by ras (subscriber, #33059)
[Link] (1 responses)
It's not clear if that still true, but it blows me away anyway.
Posted Jul 19, 2022 16:40 UTC (Tue)
by mathstuf (subscriber, #69389)
[Link]
Posted Jul 19, 2022 16:24 UTC (Tue)
by kleptog (subscriber, #1183)
[Link]
But this also shows the difficulty, given the amount of work database systems have to do to make transactions work while giving everyone a consistent view and making it perform. Then again, the problem space of filesystems is much simpler.
Even if we supported only transactional metadata changes (rename, move, create, unlink) would be a step up. Certainly being able the work on a fixed snapshot of the filesystem would be good enough for quite a lot of purposes (I think btrfs has this).
Posted Jul 21, 2022 17:50 UTC (Thu)
by kdbotts (guest, #159822)
[Link]
Symlinks have immense utility. They are essential and extremely valuable features of Linux and Unix, and have been for 40 years. Not just every distribution, but every deployment and pretty much every host intimately depend on symlinks. Breaking them is simply not an option. I find it hard to imagine a worse violation of "WE DO NOT BREAK USER SPACE!"
Posted Jul 22, 2022 10:44 UTC (Fri)
by jwilk (subscriber, #63328)
[Link]
The linked mail from 2019 proposed removal of this feature, but AFAICS it still works, so I guess it's not going to be removed after all?
Posted Jul 22, 2022 13:53 UTC (Fri)
by jdisnard (subscriber, #90248)
[Link]
Where do bind mounts fit into the picture here?
Posted Jul 23, 2022 0:41 UTC (Sat)
by DimeCadmium (subscriber, #157243)
[Link]
I think this overstates the amount of applications for which it actually matters. All you actually need to prevent symlink attacks is... other/untrusted users can't write to the directories you deal with or their parents. Which is the case for most applications: the directory they're operating in is owned by the user they're running as, and all of its parent directories are owned by the same user (or root).
The most common case that brings symlink attacks into possibility, I think, is an application which runs as root and does operations in (for example) /tmp or another world-writeable directory. Other than that it's really only multi-user servers that matter. (For example, Apache was mentioned in the comments: Apache only has to care about symlinks because it's not running as the same user who owns the website-files, and could therefore read files that the user can't.)
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
> I believe it's a bit of a stretch to say that “pathnames as a concept are now utterly broken in POSIX” just because userspace cannot verify that a path name is contained within some arbitrary part of the file system.
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
Is the concept of user input utterly broken because little bobby tables is such a hellraiser? I don't think so, I don't see how pointing to all the CVEs for failure to scrub user input is any different than pointing to all the CVEs for failure to scrub paths.
The trouble with symbolic links
> Is the concept of user input utterly broken because little bobby tables is such a hellraiser?
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
If we, collectively, were serious about security, we would be mounting all filesystems with nosuid.
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
const char *traditional_path = "/home/john/.PROGRAM"; /* Just for testing */
const char *xdg_default_path = "/home/john/.local/share/PROGRAM";
const char *xdg_data_home = getenv("XDG_DATA_HOME");
char *xdg_path;
if (stat(traditional_path, &statbuf) == 0 && S_ISDIR(statbuf.st_mode)) {
return traditional_path;
} else if (xdg_data_home != NULL && *xdg_data_home != '\0' && *xdg_data_home == '/') {
/* Use xdg_data_home + "/PROGRAM"; */
xdg_path = strcat_malloc(xdg_data_home, "/PROGRAM");
return xdg_path;
} else {
return xdg_default_path;
}
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
Probably for that exact reason but I have wished for it at times.
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
~\Application Data
to ~\AppData\Roaming
, because programs can't be bothered to check environment and just hardcode paths...
Hard links are also way more "symmetric" and the inherent "asymmetry" in symbolic links has value and usage.
The trouble with symbolic links
another level of indirection
“All problems in computer science can be solved by another level of indirection." And each level of indirection creates another level of pain.
The alternative would be to still run on the bare metal writing assembly I guess.
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
using openat() family of functions, to open folders starting from root into the actual file,
and then _from the directory_, that contains the file, back to root folder with ".." traversal.
The inode numbers should match this validation.
Somehow discover the folder that contains the non-soft linked file.
Both directory and file must be fd-opened, and directory must contain that non-soft link file.
Fork, forked program must set working directory as the directory fd.
Then forked program can read /proc/self/cwd and print it for the caller process.
to make fork work robustly, but that is out of scope for now.
open non-soft linked file there.
The trouble with symbolic links
If any symlinks have been inserted into the path, the open will fail.
The trouble with symbolic links
that is nearest of the goal operation.
File descriptor is stable (Kernel guarantees that), so holding it prevents many kinds of race conditions.
Then validate the opened file descriptor based on trusted sources for security reasons.
resolved_path = realpath("/proc/self/fd/3", NULL); /* resolved_path="/usr/bin" */
The trouble with symbolic links
- not owned by the current user
- not owned by the root user
- that point to a different uid/gid as the symlink
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
Or when a build-process (in one user account) uses another users data tree to act on it.
Then TOCTOU becomes interesting.
- archive/restore only the user home folders (/home/*) but NOT their contents as at that directory level root privileges are needed to create the folders with their owners/permissions and
- use separated archives per home folder and archive/restore them from within user space (sudo -u $user tar -xf $archive)
SElinux provides a way to allow a script to have root privileges (as in setting the owner of arbritary files) while by design it needs a "whitelist" which group of files /dirs (sockets .. etc) it is allowed to modify (and how exactly), before it can do so. A process that can write to home_t or samba_share_t (which i assume often needs chown privileges) would not be able to write to etc_t. Instead of getting tricked by the user with changing symlinks to write to /etc/passwd it would create an audit record of this attack vector for beeing used (and prevented).
On the other hand, the cronjob that runs as root would also not need to read shadow_t and (as it is not whitelisted) could also not leak data on beeing tricked by TOCTOU attacks.
Other than by preventing symbolic links from beeing used to trick some other process by entirely removing the whole symlink mechanism, selinux prevents all not whitelisted accesses on a syscall level AND also reports the already prevented attack.
The trouble with symbolic links
It is named now as "Aalto University", this is different from "University of Helsinki", where Linus studied.
use with private home folders on NFS. I remember learning Gopher, and also Mosaic browser:
Maybe it was an early version of Mosaic - according to Wikipedia first release was at 1993.
Some students in universities were using soft links in /tmp/ folders to gain root access before the fixes.
Unix administrators in HUT forbid to try such attacks on the computers: attempts would be noticed and there were severe consequences.
The trouble with symbolic links
TOCTTOU races were (partially) fixed, for the remaining ones,
there was logging and software level booby traps.
alerts would be raised for one of the first attempts,
and user would be thrown out and blocked.
for Unixes to remove soft link TOCTTOU races at file system level before 1992.
- open(), chmod() chcon() stat() etc. TOCTTOU hazard libc functions will be marked as insecure at source code level.
- All applications that use those functions, will need to be altered (just like Samba multi year project to fix one CVE),
to avoid unsafe calls, otherwise those projects will become obsolete.
- All commands, that have in source code such calls (like "setfacl -R" command which was mentioned in the Samba video )
are marked as vulnerable because of the possibility of existing TOCTTOU races.
- Security aware application container deployments must use latest OS, because older OS versions don't get the (massive multi year) fixes.
- Container (non-root-path) mounts that use data disks that follow soft links by default will be marked as insecure.
- Of course, SELinux might come into containers too as mandatory, when OS is present at file system level.
- Single binary containers take more ground (Examples: Rust, Go).
There is only host kernel, read only binary, configuration and root disk, data partition with no
soft link support, no vulnerable OS binaries at file system (only minimal set like "/work/", "/dev/", "/proc" and "/sys")).
No suid binaries. Container runs as non-root user.
Creating new safe applications should be the easy (default) way. Creating vulnerable code is fine to be a bit harder to do.
The trouble with symbolic links
(Fortunately the user cannot directly see the offset of the root processes filehandle, so he must rather guess where the other process currently is, to do this type of attack)
Is this attack vector now caused by PUNCH or COLLAPSE? Do we have to remove them just for beeing able to run root commands on userspace data while the user is able to work on it as is suggested here with symlinks?
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
PostgreSQL has support for transactional large objects. To call it a filesystem is a bit of a stretch since objects only have numeric identifiers not names. However, they have permissions, and support open/close/seek/read/write and are all within a transaction. You could certainly build a complete transactional filesystem on top of it.
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links
The trouble with symbolic links