42 comments

[ 3.4 ms ] story [ 128 ms ] thread
It's admiring how quickly they fix these. Kudos to the Rust team. But I worry about older software compiled with an older version of Rust which does not have this fixed.
> It's admiring how quickly they fix these

This is a weird comment, especially on this fix. The bug was reported back in 2018. From the linked blog post:

   We also want to thank Florian Weimer for reviewing the UNIX-like fix and for 
   reporting the same issue back in 2018, even though the Security Response WG 
   didn't realize the severity of the issue at the time.
See: https://github.com/rust-lang/rust/issues/48504
It wasn't known to be a security vulnerability as time.
Failing to recognize it as such is not a hugely different kind of failure. Symlink attacks have been known about for a long time. I'm not castigating the devs/teams here (I don't even think the underlying security risk is that high) but any credit for speedy response should carry caaqil's significant caveat.
> But I worry about older software compiled with an older version of Rust which does not have this fixed

That's true for everything, right? Anyone running older versions of Java, Python, Ruby, Go, C++ (gcc/llvm), etc. that don't update all are exposed to unpatched security vulnerabilities, in both their 3P library dependencies, stdlib, and compilers/interpreters.

I don't see how Rust can force people to update any more than any other language.

Tbf in python or java you can update the runtime without having to touch the program.
Maybe by releasing often (every 6 weeks or out of band for bug fixes like this), they make it easier to use a current version of the compiler, in comparison to the old Java release cycle of multiple years.

Also testing the compiler against the whole ecosystem on crates.io before releasing a new version helps building trust in the backwards compatibility of new compiler versions.

So they are doing something to establish a culture where updating your compiler regularly is encouraged. I don't have actual numbers and I don't know how enterprise Rust shops handle their compiler versions, but they are doing things to encourage (not force) users to keep their compilers up to date.

Virtually every other language has a stable ABI and dynamically links the standard library, so if you have a vulnerability in the standard library, update it and you're good to go. With Rust, you need to also recompile every program and other library on the system that uses std, which is likely all of them.
Golang, which is most used system language in the last few years, also bundles stdlib.

Additionally, quite a lot of software seems to be distributed in self-contained format; for example many Java apps (even those intended to be installed on a server) come bundled with JRE, and Linux GUI apps and games are usually distributed in Flatpak/Snap/AppImage.

Of course let's not forget about distributing software as Docker images, which is probably the most common way software is installed in many server environments.

In general, programs should never operate on the same file name more than once. If there's more than one thing you need to do with a file, open it and then operate on the FD instead.
This issue seems like it would be common across languages, or at least those languages that have filesystem APIs in the stdlib. My understanding is that it is fixed in Python 3.3, while in C++ concurrent file system access is considered UB and therefore this isn’t a bug.

Do people know the state in other languages?

UB?
It stands for "Undefined Behaviour"

It's commonly used in C/C++ communities (elsewhere too, but not as often).

It appears the race was on for commenters to jump in and define undefined behavior. Your comment is similar to my own.

I feel like this sort of thing can be utilized as some sort of dev honeypot.

It's easy enough to delete your comment if you see older ones after you've replied. With the amount of discussion around distributed system design on here, it's pretty egregious that we can't get the simplest transactional behavior right.
It’s not so simple. I get internet points for my comment, even if it is nearly the same as someone else. Furthermore, the “first” comment chronologically is not necessarily sorted first. Indeed, older comments are sorted below. It’s actually later comments which are more near the top when others read threads, thus they get more exposure and upvotes and stay up top.

In any case, I see no harm in any of this. Bits are cheap.

As I said, we are not doing it right.
"Undefined Behavior", a.k.a. "don't do that". It's everything the language allows you to do but the spec (you didn't read) says you should not, otherwise anything can happen. For example you should not access an array out of bounds, nor dereference a null pointer; so the compiler is free to assume this will never happen, and if it does the generated code may do very unexpected things, like not a straight segfault but continue running with unintended behavior.

There are many UB cases in C/C++ many programmers don't suspect (more subtle than dereferencing null), and this regularly introduces very hard-to-debug bugs and securities issues. Rust does (mostly) get rid of them so you can be sure that your code has no unexpected side-effects.

> the spec (you didn't read)

This, to me, reads like it implies that reading the spec shouldn't be necessary to use a language correctly. The issue with that is that the spec is the only document you need to read, understand and follow in order to write safe, cross-implementation-compatible code.

When you work with low-level languages and refuse to read the spec carefully, you are to blame more than the language or the spec.

> this regularly introduces very hard-to-debug bugs and securities issues

Yeah, that's true. The reality is that a lot of developers come from a C-syntax language (like C#, C, Java, ...) and assume that it's all the same because it looks the same. Mostly they follow video tutorials on youtube and blog-y websites instead of standard-adhering documentation (like cppreference.com). I've seen C++ developers that, after building 10k+ line of code programs, never knew why to use `delete`, what RAII even is, and have not a clue about what a mutex does.

These are fundamental education problems. Rust has the benefit of being different enough from common "first languages" that one is inclined to look at the documentation.

I'm not the only one who is endlessly tired of being told that the design of C++, with its undefined behavior and "need to read the spec", is somehow at fault for developers being too stubborn, entitled or stupid to take 3 minutes to read and understand the documentation.

When a Rust developer makes a security mistake, causes a crash or other unexpected behavior, the developer is likely at fault. With C++, it's always "oh c++ is just like that". It's a complete mystery to me how people cannot understand that sometimes a language with lots of power can have pitfalls that need to be avoided by the programmer.

The audience for such a spec is mostly compiler authors and similar categories. A lot of people using a language don't need to internalize chapter and verse.
> When you work with low-level languages and refuse to read the spec carefully, you are to blame more than the language or the spec.

This is true to some extent but I think there is a reasonable argument that the language specs are generally geared towards people building compilers or similar tools, and it's not reasonable to expect everyone who works with a language to be intimately familiar with the spec at that level. In the case of C++, the language complexity seems to be at the point where even people who are highly-skilled and work on tools like Clang are still hesitant to say they fully understand the rules around undefined behaviour.

The difference is that the C++ spec is ~1700+ pages of very dense and oftentimes subtle language that no one fully understands. Moreover, even the committee can't tell you all the UB that exists. There's a proposal to simply enumerate the explicit UB [1] they do know about, but even that doesn't exist yet.

When the situation with the language is such that no human or program yet invented can confidently assert the defined-ness of any particular bit of code, it's 100% the language's fault. Who else could be at fault for it being impossible to write safely?

An amusing anecdote is that there's a conference for UB detectors called sv-comp. They have a suite of example programs with completely defined behavior that are used to detect false-positives. People keep finding UB in the C examples [2].

[1] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p170...

[2] https://runtimeverification.com/blog/mare-than-14-of-sv-comp...

> developers being too stubborn, entitled or stupid to take 3 minutes to read and understand the documentation.

Understanding C++ from first principles to the level that you never write UB, by reading the spec, is much closer to a 3 decades task than a 3 minute task.

In C++, any concurrent filesystem access is undefined behavior (which seems pretty crazy to me)

https://en.cppreference.com/w/cpp/filesystem

> The behavior is undefined if the calls to functions in this library introduce a file system race, that is, when multiple threads, processes, or computers interleave access and modification to the same object in a file system.

---

Golang also seems vulnerable to the same issue

https://github.com/golang/go/blob/d15481b8c7f5f73a8b987a0c1d...

Line 78 checks that the path isn't a symlink (time-of-check). Then line 97 calls openFdAt which on line 174 opens the path by name, without NOFOLLOW (time-of-use).

I bet this is a pretty common vulnerability.

(comment deleted)
This relates to the fact that there's some talk going on in the Rust community to extend lifetime checks to domains outside of memory safety (like filesystems).

Here's an RFC that tries to extend lifetimes to file handles: https://github.com/rust-lang/rfcs/blob/master/text/3128-io-s...

Interesting RFC. I'm interested in how something like `OwnedFd` can work. In what sense is it "owned"? What's stopping the OS from deleting/changing the file underneath it? Do OS's provide some API to this end, or will the Rust std library do some checks before allowing operations on it?
File Descriptors are not files. They are unique per process so the rust compiler should be able to completely track their ownership.
No they're not. Other processes can pass you an fd at startup, you can send/receive fds across a Unix domain socket, and there are even syscalls for copying another process's fds.
But another process wouldn't be passing an OwnedFd in that case, and sending an OwnedFd away would pass the underlying FD and drop the wrapping OwnedFd. The process receiving the FD would have to take care (as today in any C program) to determine whether they are responsible dispose of the FD or not.
The receiving process gets a copy of the file descriptor. Both processes must close(2) it.
If process A opens a file and passes a file descriptor to process B, then process B's descriptor will continue to refer to the same object regardless of what process A does.

Process A can unlink the descriptor, swap the directory entry out for some other object or whatever, and Process B will still have a handle to the same initial object.

Nothing stops A from mutating the filesystem object in other ways. For example, A's write to the middle of the file or truncation of the file will be visible to B. But merely unlinking the file doesn't alter B's view of the object.

This doesn’t go with my mental model of unix filesystem permissions, can someone explain it to me?

The vulnerability mentions a userspace compromise aimed at a system directory, not another sensitive user directory.

In my mind, a user trying to delete say /usr/bin on a POSIX system is going to be slapped down immediately unless they are root, or have the proper group access, and this is not the responsibility of any standard library, it’s the responsibility of the fs layer and kernel.

So, what am I misunderstanding? Opening up a link to /usr/bin doesn’t ever give me permissions on /usr/bin over what I have.

To quote the post:

> If std::fs::remove_dir_all followed symbolic links, they could find a privileged program that removes a directory they have access to (called temp/), create a symlink from temp/foo to sensitive/, and wait for the privileged program to delete foo/

This is a classic confused deputy exploit - the attacker doesn't need access to the system directory, they just need to be able to point the deputy at that directory. The race condition on checking if the directory is a link means it's possible to trick the privileged program into misusing its privileges.

If I remember correctly, rust compiles to static binaries by default, meaning they include a copy of all used parts of both libraries and standard library in the executable.

Does that mean that any Rust program compiled with any affected version of the standard library is affected? How would I even find those?

Statically linked binary make deployments really easy, but this one scenario where they have real downsides.

> Does that mean that any Rust program compiled with any affected version of the standard library is affected?

Most programs don't use `remove_dir_all`, and a smaller fraction of those use it on a directory that untrusted actors can write to.

But yes, tracking down those that do is a challenge.

The bug exists in all versions of libstd before today's, so in this case finding possibly affected Rust programs is easy: all of them.

As for other situations: when Rust programs are built with Cargo, it creates a Cargo.lock file, which is a machine-readable record of all dependencies and their checksums used in the build. Distros could archive it and later use it track all affected dependencies. Unfortunately, libstd is a magical compiler built-in, not a regular dependency, so it doesn't get recorded that way.

How do I find out if an executable was built with rust? (Assuming it's not my code...)
> Note that the following targets don't have usable APIs to properly mitigate the attack, and are thus still vulnerable even with a patched toolchain:

> - macOS before version 10.10 (Yosemite)

> - REDOX

It's funny to me that they chose to include RedoxOS in this security advisory. For those of you serving your customers over RedoxOS in production, beware.