-
Notifications
You must be signed in to change notification settings - Fork 12.9k
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
Fix backtraces on Windows/GNU #37359
Conversation
(rust_highfive has picked a reviewer for you, use r? to override) |
It looks like this patch to libbacktrace hasn't made its way upstream yet, perhaps we could continue to just pass it manually? |
be encoded in the current locale, so we use `QueryFullProcessImageNameA`. | ||
As a result paths not representable in the current locale are not supported. */ | ||
DWORD buf_size = MAX_PATH; | ||
HANDLE handle = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, GetCurrentProcessId()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't you just call GetCurrentProcess
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I tried it first actually and it didn't work. AFAIU, GetCurrentProcess()
returns a pseudo handle -1
and QueryFullProcessImageName
needs a real non--1
process handle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah okay. Yay for inconsistent process functions!
static char buf[MAX_PATH]; | ||
/* The returned name is later passed to `open`, so it needs to | ||
be encoded in the current locale, so we use `QueryFullProcessImageNameA`. | ||
As a result paths not representable in the current locale are not supported. */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is super unfortunate.
Yet another reason to avoid MinGW and friends on Windows.
I don't plan to submit this patch upstream (it's not in general libbacktrace style and probably needs to be completely reworked before upstreaming), so it'll need to be reapplied on every libbacktrace update (not a big deal). |
@petrochenkov hm I little confused. We didn't need a patch before, so how come we need a patch now? Why not just specify the binary via the standard means we did before? |
We specified the binary via the standard means and didn't need this patch before May 14, but then #33554 was merged to address security concerns described in #21889 and backtraces were broken. |
I think I'm still not following. #21889 was an observation that if you hand arbitrary input to libbacktrace it's got a high chance of segfaulting/crashing. This means that if you can control the input passed to libbacktrace you may be able to take control of arbitrary executables. (emphasis on may) This, when combined with us passing So, my takeaway from that is:
So given all that, why are we modifying libbacktrace? Why are we not just changing the behavior so only on Windows we do the same thing here but externally? I'm harping on this because we're inevitably going to update libbacktrace in the future, which will then inevitably overwrite these changes if they're not upstream. |
Without modifying libbacktrace we have a method of giving the file name at moment of time
Windows is indeed affected by this hypothetical vulnerability - (Personally, I'm not especially concerned by this "vulnerability" and initially proposed to simply revert #33554 on Windows, but there was some opposition.) |
How does modifying libbacktrace fix this problem?
This... sounds bad? |
By making executable filename retrieval happen immediately before reading the file. |
I.. don't think that fixes the bug? The bug at least as I am aware of it on Unix is:
Regardless if where in the process we read the path name, you're vulnerable no matter what if the path being looked at is in any way overwritable. |
@alexcrichton On Windows files being executed are locked and can't be modified, so the situation is better. |
From what I'm led to believe If Windows files can't be modified while they're being executed then this sounds plausible, but also means that there's no race, which means there's no need to modify libbacktrace. |
They can't be modified, but they can be renamed :) To clarify, my goal is to fix backtraces, not to build an impenetrable defense. |
Ok, so taking a step back here, I feel like we're talking past each other. Here's how I see the current state of play:
So, given that, it is impossible to not expose this problem on Windows. There is no path on Windows which represents the current executable impervious to these race conditions. And finally, given that, the current stance of the libs team is that if we pass file names to libbacktrace we're opening up the standard library to a possible arbitrary code execution exploit on Windows. In short, my current understanding leads me to the conclusion that this fix is not possible to do while preventing these theoretical problems with libbacktrace. So that brings us to the problem of backtraces. Backtraces are currently broken on MinGW (#33985) presumably because libbacktrace can't find all the dwarf debug info. This PR is fixing that. And finally, putting all that together, my conclusion would be that the MinGW backtrace issue is WONTFIX if this is the only solution. Other possible vectors include:
The first two options are hard, the latter would require more than me from the libs team to weigh in. I would personally be against it. |
This is wrong. There's no race condition on Windows if the file id (name, descriptor, handle) is retrieved simultaneously with reading from that file. EDIT: Moved discussion of alternatives to #33985 to avoid derailing the thread. |
To add another perspective, if the attacker has access to filesystem, then what prevents him from writing specially crafted DWARF right into our executable (before or after it's launched)? |
There is still totally a race condition, it is just a much narrower time window, which makes exploitation harder. I am of the opinion that if the attacker has control over the the executable file, then you've already lost. I can't think of any scenarios where an attacker would only have control over the executable file after it was executed. |
For example, if the executable is run from some "cache" dir, and there is some process that renames files after they are run. That would be a vulnerability in all Rust applications, so exploitation opportunities abound. |
This is exactly what I wrote in the next paragraph. I think this is good enough, but the race can be completely removed if we circumvent file names and work only with handles (I haven't investigated how to do this yet). |
I'm not aware of any functions that allow retrieving a handle to the process file directly (I can't even find the handle in the PEB!). As far as I can tell, the only option to enable libbacktrace to actually work without allowing this vulnerability is to fix all the bugs in libbacktrace so that it doesn't have any vulnerabilities which could be exploited by this vulnerability. @arielb1 Would it be unrealistic to expect everyone to respect a big scary warning that says to not do that? |
As @retep998, a "small race condition" is still just that, a race condition. Can't work around that fact.
Spot on! This would indeed solve the problem, and is essentially how the solution on Linux works.
I don't think it's really too productive to talk about attacks here. The current stance of the libs team is that this race is unacceptable. Reversing that decision shouldn't happen as part of this thread and should likely happen with discussion on an issue. In light of that the purpose of the PR here would be to re-enable nice backtraces on MinGW while also preserving the same guarantees we had before, that is never giving possibly arbitrary information to libbacktrace. The PR as-is does not do this (it still has the same race, as I mentioned before). |
The point of that message is that we are giving arbitrary information to libbacktrace right now on all platforms, if this arbitrary information is written into debuginfo section of the original executable file. So, removal of the race (which I'm still going to pursue) still doesn't give any guarantees, only makes it a bit harder to come up with an exploit. |
The case I am worried about is:
In that case, an attacker can cause a trusted Rust program to be run, rename it, download an untrusted file with the same name that exploits libbacktrace, and cause the trusted program to panic. This is a bit obscure, but it is a potential vulnerability every time you run a Rust program - the "daemon" does not even have to be written in Rust. That's not something we want. |
@petrochenkov the exploit on linux was this:
This may or may not directly translate to Linux, but the idea is that you do not need write access to a file to exploit this problem. That is, it's not black and white in that sense. |
ReactOS sources is a good reference for Windows internals. Here, for example we can see that file identifier is kept inside of |
Nice find! Can we find any official documentation though for |
@alexcrichton |
@petrochenkov yeah we'll be fine if the symbol disappears, but if the symbol changes ABI (e.g. different function signature) that'll cause segfaults, likely exploitable ones too. @brson, @vadimcn, thoughts on using the private API of |
I rate the chance of this API just changing its signature as very low. Microsoft has been pretty conservative with even undocumented APIs, especially ones that's been there for a long time (I found a reference to I think this PR is a fine short/medium term solution. In the long term we are going to re-write libbacktrace in Rust, right? :) There may be other ways though: AFAIK, opening a file without FILE_SHARE_DELETE sharing mode will prevent anyone else from moving it. So perhaps we could query process image file name, open the file, then query again and compare the paths? |
@vadimcn I think that'd still be susceptible to the same race. Once you witness the filename it could get changed, and then wouldn't querying the current path still return that original file? |
I thought @petrochenkov has determined that |
So, the sequence is: Looks like an alternative. |
This may require more invasive changes in libbacktrace though. |
@petrochenkov I'm not sure I quite follow your explanation but if it handles the race that sounds good to me. Presumably we could implement that entirely outside libbacktrace, right? That is, we do all the querying business to ensure that (I like the idea of no changes to libbacktrace!) |
So the proposed action sequence is:
There are two things I dislike here:
|
Once the file is opened we can release the lock, right? And yes non-ascii paths wouldn't be supported but that seems to me like an acceptable tradeoff to not having to patch libbacktrace (and having this silently regress or prevent upgrades in the future) |
Yep, it looks like libbacktrace successfully opens the file only once, so it can probably be unlocked after the first successfully obtained backtrace.
The ASCII issue affects home directories of all people with username in native non-ASCII language, while patched libbacktrace affects only a person updating our copy of libbacktrace (likely me), 1-2 times a year max. |
The libs team discussed this a bit during triage yesterday, and the general sentiment was that we should avoid modifying libbacktrace and instead stick to Rust code where possible. It seems like the only downside of not modifying libbacktrace is that it doesn't support non-ascii filenames, which mostly comes up with home directories. In that sense it seems like we could perhaps use a relative path to the current directory which likely won't include the username most of the time and cover almost all cases for debugging. |
I'll close this for now then since implementation external to libbacktrace won't share code with this implementation. |
This is done by adding a function that can return a filename to pass to backtrace_create_state. The filename is obtained in a safe way by first getting the filename, locking the file so it can't be moved, and then getting the filename again and making sure it's the same. See: rust-lang#37359 (comment) Issue: rust-lang#33985
…trochenkov Make backtraces work on Windows GNU targets again. This is done by adding a function that can return a filename to pass to backtrace_create_state. The filename is obtained in a safe way by first getting the filename, locking the file so it can't be moved, and then getting the filename again and making sure it's the same. See: #37359 (comment) Issue: #33985 Note though that this isn't that pretty... I had to implement a `WideCharToMultiByte` wrapper function to convert to the ANSI code page. This will work better than only allowing ASCII provided that the ANSI code page is set to the user's local language, which is often the case. Also, please make sure that I didn't break the Unix build.
Query name of the current executable each time we are going to read debuginfo from it.
See #33985 (comment), other messages in #33985, and #21889 for more details.
Fixes #33985
r? @alexcrichton