Using the readlink function to avoid symbolic link race conditions when opening a file path
Asked Answered
I

1

6

I'm reading this paper. On the page 11 the paper says:

Unix applications can obtain access to files without encountering symlink races. This is important for normal application programmers who, for example, might want to write an ftp server that securely checks file system accesses against a security policy. The programmer can accomplish this by leveraging the fact that the current working directory of a process is private state and will not change between the time that it performs a check on a file relative to this directory, and the time that the call completes. The programmer can leverage this to perform a race free open by recursively expanding (via readlink) and following a path one component at a time until they have reached a file, or until they have found that the path violates policy.

The paper is describing a technique for avoiding time-of-check to time-of-use issues when checking and opening a file path. In other words, we have a file path that we want to make sure that it doesn't violate some security policy before opening it. But we should avoid time-of-check to time-of-use issues. The paper is describing a method for doing so using the readlink function. I can't understand the technique that it is describing. Can someone elaborate this technique?

Incredulity answered 27/1, 2023 at 22:45 Comment(12)
This is a pure programming question. I suggest to move it to SO.Albertype
@Albertype How should I move it to Stackoverflow?Incredulity
Not you. One of moderators can do that.Albertype
Be careful drawing conclusions from that paper. For example: "This type of race can only occur in the presence of shared memory. Examples of mechanisms supporting this in Linux include: the SYSV shared memory facilities, mmap, and memory shared among multi-threads created via the clone call. Debugging interfaces such as ptrace that allow processes to modify the memory of other processes must also be taken into account." That's not true. Right of the top of my head I could use alarm() or some variation of it to interrupt my process with a signal and run a signal handler asynchronously.Negate
And then there's this: "We have had promising results with preliminary experiments in facilitating proxy-based file system access with shared library replacement. It is interesting to note that this approach also solves the problem of argument races." That's almost funny. "Shared library replacement" means they're trying to make an untrusted process "safe" by putting the security checks in the address space of the untrusted process. If that's part of the technique in question, don't waste any more brain cycles on it.Negate
Yes and no, @AndrewHenle. What the paper is describing there appears to be a mechanism for "coercing applications into always accessing the file system using an access pattern that we can easily verify is safe". Yes, the application could circumvent that, but the monitor is denying use of access patterns that it cannot verify, so the result of the kind of circumvention you describe would be that the application is prevented from some behaviors that would otherwise be permitted to it, not gaining capabilities it would not otherwise have.Indisposed
@JohnBollinger The approach used here - a monitor that approves each syscall before it's allowed to proceed - is fundamentally unsound and a TOCTOU bug just waiting to be exploited. Even AppArmor, which provides additional security similar to this but does it all in the kernel context of one syscall itself - has had several CVEs for race conditions. Fundamentally, to be secure the only safe approach is to copy the data into kernel space then verify the data. AppArmor failed because it copied, checked, then allowed the actual system call to proceed - which would do another copy.Negate
@AndrewHenle, I'm no special pleader for Janus, but copying system call data from user space into kernel space before verifying it is indeed among the features the paper describes. The shared-library replacement provides a compatibility shim, not the policy monitor itself, which revolves around a kernel module. But in any case, such copying is no particular protection against the kind of attack that this question is focused on.Indisposed
@JohnBollinger The entire readlink() race condition vulnerability disappears if you only pass the value(s) into the kernel once and evaluate them there and the value acted is always the value that was evaluated. Whatever file the link(s) that were copied into kernel space and evaluated refer to will be the file checked for access and then opened. Or not. There's then no way to evaluate a link to /tmp/foo, allow further access, and then have the actual system call act on an unallowed link to /etc/shadow because something modified the data.Negate
Acknowledged, @AndrewHenle. But I see no viable way to implement that short of integrating the policy testing directly into the system call implementations. Which I guess Linux already has, in the form of SELinux. I acknowledge more broadly that from a security perspective, policy enforcement via system call interposition is inferior to security policy enforcement built into the system calls themselves. But that's a far cry from where you started: Janus does not put security checks in the address space of the untrusted process.Indisposed
@Albertype This is 100% a security question, not a programming one. Perhaps you misunderstood the question? The basic technique directly asked about in the question is trivial; the indicated context for using it, and the security implications of using that technique in that context - and why it does (or doesn't) improve security - is the interesting part. See the accepted answer, which you'll note contains no source code.Endogen
@CBHacking: Look at the article: "leveraging the fact that the current working directory of a process is private state and will not change between..." This is a pure programming topic. Also the assumptions that a process has a single thread is a pure programming question. This is my view. To me it is fine that you have different opinion. You can ask moderators to move it back to the Security SE.Albertype
I
5

I can't understand the technique that it is describing. Can someone elaborate this technique?

Part of the context for this is:

Our approaches work by coercing applications into always accessing the file system using an access pattern that we can easily verify is safe [...], and then simply disallowing all access that does not conform to this access pattern

The text in question is a description of one kind of access pattern that the application being monitored will be allowed to exercise. This particular pattern avoids a symlink race, which occurs when the target of a symbolic link is changed contemporaneously with checking access to a file via a path that passes through that symlink. In such a case, the check might be based on a different symlink target than the actual access, which may allow security policy to be circumvented.

The pattern described is that the program will actually walk the filesystem tree, one directory at a time, to the directory of the targeted file, expanding symbolic links as it goes. In more detail:

  1. if the path is absolute then chdir() to the root directory and convert the path to a relative one

  2. take the first component of the target path (e.g. foo in foo/bar/baz)

  3. call readlink() on the one-component relative path (e.g. foo alone), and

    • if readlink succeeds then recurse on the resolved path, starting at (1);
    • if readlink fails other than with EINVAL then the overall procedure fails with that error number;
  4. If this point is reached then we have successfully resolved the current path component to a non-symlink entry in the current working directory (and the cwd may have changed during the process; see next). In that case,

    • if this is the last component in the path then it is the path to open
    • otherwise, chdir() to that path, make the next path component the current one, and loop back to (3)

An application that consistently employed that approach when opening files would be compatible with a security policy featuring simple rules such as that attempts to open() files are denied if the path is absolute, if it contains more than one component, or if that component is a symbolic link.

The point being made in the paper is that the remaining cases and each step in the above procedure are easy for a policy monitor running in user space, such as Janus's, to check correctly, whereas many other accesses are more difficult. The paper is not trying to say that that procedure is inherently more secure than just opening the file directly by whatever path is given.

Indisposed answered 29/1, 2023 at 18:31 Comment(6)
It's worth noting that the procedure described is grossly unsafe for a multithreaded process. The version of Janus discussed in the paper supports only single-threaded processes, and although the paper doesn't seem to carry a date, it somewhat dates itself by claiming (section 5.1.3) that "Relatively few Linux applications use multi-threading." Although that may be true in a broad sense, it's not a claim I would try to make today about the kinds of processes I might wanted monitor via a tool such as Janus.Indisposed
Wonderful algorithm. But I still have a question. The paper says: "The programmer can leverage this to perform a race free open by recursively expanding (via readlink) and following a path one component at a time until they have reached a file, or until they have found that the path violates policy." So I believe it is describing a secure race-free general method for checking the file path before opening it. This is why I didn't mentioned anything about the sandbox in my question. Continued in the next comment.Incredulity
The paper also considers this general method (without its policy check) as the "good behavior" that should be enforced by the sandbox. Your excellent answer gives the algorithm for the "good behavior". I believe by 1) adding a 2.a step that checks the current path component for adhering to the policy and 2) modifying the second case of step 4 to use open() with O_NOFOLLOW flag and fchdir() and also to loop back to 2.a and 3) adding a step 5 doing the actual open() with the O_NOFOLLOW flag we will have a symbolic-race-free open with the policy checked correctly. Am I right? Thanks.Incredulity
@Plastic, yes, of course the application is free to perform its own internal policy enforcement as it traverses the tree. However, although I agree open() with O_NOFOLLOW combined with fchdir() hardens the above against some kinds of filesystem-based attacks, I don't think the authors had that in mind. For if they were thinking in terms of file descriptors for directories, then I would expect them to look toward openat() instead of relying on the process's current working directory for anything.Indisposed
Overall, I'm not convinced the the authors thought as much about application behavior without an external policy monitor as they did about behavior in conjunction with a monitor.Indisposed
Yes probably it wasn't the intention of the author (or at least it was a side discussion). The main point of the paper is the one your answer explains in detail. Thanks again. I almost lost my hope for receiving an answer with an algorithm.Incredulity

© 2022 - 2025 — McMap. All rights reserved.