-
Notifications
You must be signed in to change notification settings - Fork 432
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use-after-free when using a ThreadRng from a std::thread_local destructor #968
Comments
@nathdobson thanks for pointing this out. We can't use After looking into the alternatives, I don't think we have a whole lot of options here. We either:
|
We obviously drop
We reduce |
We've use-after-free for either the |
I wasn't aware that this was possible. Can you provide more context? (Is it not a bug of the platform implementation?) As @josephlr says, it sounds like we don't have a good option here. |
This was my initial reaction as well, and I don't think it is. If a thread has multiple thread-local objects, on thread termination you could either (a) run the destructor for every object then free all of the thread-local memory at once or (b) run the destructor for one object, free its memory, then destory+free the next object, and so on. Most platforms do (a), but this bug is easier to spot with (b). Either is fine, as Rust just says that the destuctor runs then memory gets freed. With either (a) or (b) we could still hit this bug, as the thread-local RNG could get destroyed while other thread-local variables still have a handle to it. The One idea could be to just use thread_local!(
static THREAD_RNG_KEY: ReseedingRng<Core, OsRng> = { ... }
);
// ThreadRng is zero-sized, but we have a marker to make it !Send + !Sync
pub struct ThreadRng(UnsafeCell<()>);
pub fn thread_rng() -> ThreadRng {
ThreadRng(UnsafeCell::new(()))
}
impl RngCore for ThreadRng {
fn fill_bytes(&mut self, dest: &mut [u8]) {
THREAD_RNG_KEY.with(|rng| rng.fill_bytes(dest))
}
} This is really simple, but might be less performant, as you have to do thread-local lookup each time, instead of only once at ThreadRng creation time. We would need to look at benchmarks. |
LLVM may be able to optimize the TLS use by merging multiple accesses together. When Rust was still using green threads, TLS access was problematic, as LLVM would sometimes keep a pointer alive across yield points that could potentially make the current function get executed on a different native thread. |
Thanks for the explanation @josephlr.
Is there a reliable way to do this? If so, then simply disallowing usage in destructors with a check in debug code might be enough, otherwise I guess we need to use the Use-after-free is potentially a security issue. Is this exploitable? |
|
It's exploitable: Any transactional update jazz, or maybe some complex handshake code, could involve RAII guards that do ECDSA signatures in a destructor, so attackers would compromises secret keys if the PRNG lacks enough entropy. Avoid ECDSA etc. but yes exploitable. |
Wrong issue? Edit: I now get what you meant. |
I think
All their |
TL;DR; Using I ran the (1):
|
Given the above results, my proposal would be:
Does that sound reasonable to people? |
Option (4) might be a good choice, too. The performance impact is moderate, and anyone needing more performance can (and probably should) use |
Agreed. And neither of the differences between (4) and (5) don't appear very significant. Interesting point about inner-vs-outer access (2 vs 3). Assuming thread boundaries are respected, I think the only way code could be called between the rng accessor and
There is a risk such an issue would go undetected until it is hard to fix. On the other hand, it seems (6) may cause a panic in the originally-reported behaviour only on some platforms, thus being a portability issue, so neither is perfect. Are there any other reasons we should/should not allow use of |
I'd kinda favor (6) partially because we could almost achieve (6) using
If this worked, we'd require unsafe code only in I think this code fails only because We give up all autotraits except I'm unsure why |
A possible issue with (6) is that panicking in a thread local destructor will abort on many targets (or at least OSX). |
We must choose between panic (1,2,3,6) or leak (4,5) under this scenario, right? I think panic provides a safer more observable default. If they happen, async destructors (poll_drop) should avoid both panics and leaks, btw. |
You mean via an impl like (6) but which does not return |
This is a minimal fix for rust-random#968.
I created a branch in which to play with the pure We can eliminate the first unsafe by using a |
Could you explain in a bit more detail the failure case with (3), i.e. this:
This sounds like a data race, shouldn't that be impossible with thread-local variables? |
I’m also curious. As the sample implementation is written now, the example will just panic if Unless the intent was to say that users can misuse the In my opinion the possibility of a panic (from |
It is not clear to me if In particular I think something along the lines of the following snippet can be fairly plausible: let thread_rng = Box::new(thread_rng());
let ptr = Box::into_raw(thread_rng);
// something that could maybe panic, if panic occurs you end up with a leaked reference
// eventually on thread termination the TLS destructor may run and may abort if the thread
// termination is due to this or a different panic.
let thread_rng = Box::from_raw(ptr);
// carry on... |
@RalfJung I thought running dtors & freeing TLS at the same time was impossible, but apparently not (see the top of this issue). But I expect you know better than I do on this topic. @nagisa given how thread_local works, I agree, there wouldn't appear to be a difference between (2) and (3). Good point that we shouldn't assume a I guess this means that the best option is still (3) for a 0.7 patch release and (5) for 0.8. |
I am not very knowledgeable on TLS things, I just looked a bit at the APIs when they came up for Miri.^^ I was just curious what kind of code flow you are imagining. If I understand correctly, everything discussed here is actually layered on top of the Right now, the docs don't give any indication for when it is legal to "leak" the TLS pointer out of |
Lets evaluate any safety issues again. Options (1) and (4) do not use All other options use The current code and options (3) and (5) all leak a reference of an object referred by
As @RalfJung says we could try to clarify this with the libs team. Alternatively we may as well simply choose (2) over (3) and a similar variation on (5). Option (6) has a serious issue. I think that's everything? In that case, I believe our options are:
I'm inclined to take the paths of least resistance: (2) for 0.7 and either (4) or a variant of (5) for 0.8. I believe we can create PRs now? |
Fixed in the master branch. Now, the question is do we fix for 0.7 too? I'm inclined not to (since the perf hit may be more significant than the issue itself and 0.8 should not be too long now). |
@dhardy Can we close this now that 0.8 is out? Should we file a RustSec advisory? |
Yes, we should close it. I don't think there is any exploit to report. |
A ThreadRng is just a raw pointer to the std::thread_local variable THREAD_RNG_KEY. If a client reuses a ThreadRng after the THREAD_RNG_KEY is dropped (e.g. inside another std::thread_local destructor), this is undefined behavior.
On targets with thread local support in the linker, the memory is typically freed after all destructors are run, so the use-after-destroy is hard to detect. However, other targets destroy and free at the same time, causing a use-after-free. This test can reproduce the use-after-free on OSX.
On targets that support it, THREAD_RNG_KEY could use the unstable #[thread_local] instead of std::thread_local. Other targets would probably have to use an Rc.
I'm happy to write a PR once we confirm the intended direction.
The text was updated successfully, but these errors were encountered: