Change:
I discovered a memory safety vulnerability in the standard library's . Before the rest of the issue, I just want to say thanks in advance to everyone involved for helping to keep rust safe :) In , the weak count is incremented using , and then it's checked whether the result passed . As is documented in 's code, this is because if the counter gets too close to overflowing, the overflow might happen before the call to completes, breaking Arc's invariants and probably causing UB. Checking if the counter passed means that at least 2^15 (in 16-bit platforms) increments to the counter before completes are needed for an overflow. However, in , the code increments the weak count, without checking that the counter doesn't pass . This allows the counter to get all the way up to . Then, using , we can get a much more favorable race than intended. Therefore, the attack is as follows: We use to increment the weak count all the way to , then run three times in parallel. The first two will call , but the third will result in a weak count of 1, indicating that there are no remaining s (while there actually are). Then we can abuse this broken invariant to cause UB, for example through , as long as we manage to do so before either of the two calls complete. Note than to run this exploit yourself, if you're running a 64-bit architecture, the exploit won't run in a reasonable length of time. So has to be modified to lower the counter to 32-bits, to demonstrate the vulnerability. <details<summaryThe exploit in code </summary<p</p</details<details<summaryRunning on 64-bit architectures</summary<pThis is how I tested the exploit myself. I modified the so that the counter would only be a , and ran the exploit as a "test", with this command: This is also present . </p</detailsThe fix is very simple. In the compare-exchange loop, add a check that if , and it's not , panic. Add a comment explaining that not straying too much above is a global invariant, and must be enforced in all increment operations. I'm not sure how severe it is, since it depends on a race with , and I don't know how to assess winning this race in real code on different platforms. It should also be noted that in order for this to be exploited, a large amount of s need to be leaked (depending on size), without also leaking heap memory (otherwise the program would run out of memory first). <!-- TRIAGEBOTSTART --<!-- TRIAGEBOTASSIGNSTART --<!-- TRIAGEBOTASSIGNDATASTARTTRIAGEBOTASSIGNDATAEND --<!-- TRIAGEBOTASSIGNEND --<!-- TRIAGEBOT_END --
I've opened a pull request
assign
Error: Parsing assign command in failed: ...'noamtashma' ' '... Please file an issue on GitHub at if there's a problem with this bot, or reach out on on Zulip.
I must stress that our 16-bit targets do not support "atomics" , so the hypothetical ability to exploit this on them is not actually present.
Error: Parsing assign command in failed: ...'noamtashma' ' '... Please file an issue on GitHub at if there's a problem with this bot, or reach out on on Zulip.
(I changed the title because this issue is about an overflow - I don't think there's an underflow issue as well)
There was some discussion about whether should be to this. I have personally declined to add because as it was explained to me, while this does allow a use-after-free, this can only be done during the microseconds the program is being concurrently terminated. While we should still fix this, the broken invariants will be closed over by process death, one way or another. :crab: :hocho:
I wanted to add I-unsound, since I think an RCE during that time window can still be a serious vulnerability. (Or, I wonder if it might be possible that, after gaining control of a thread, a hacker could also gain control of the other thread and prevent the abort from actually happening?) Especially since rust doesn't have much control over the system's duration, or any detail for that matter. IMO the main thing stopping this from being severe is that, to be vulnerable your code must leak a lot of values, without leaking actual heap space. Or that the attacker wouldn't be able to have a lot of close calls to functions in order to win the race. But since being unsound means that "adversarial" safe code can be unsafe, I don't think that's a relevant reason not to tag this as unsoundness.
From the security perspective, I just want to emphasize that "adversarial" should be considered non-literal -- source code is generally considered within the trust boundary, and if that's ever not the case then you need an entire sandboxing solution for both compilation and execution.
I do think it's accurate to classify this as an unsoundness bug, even though it's very low risk. Let's mark it as I-unsound with P-low for now.

Discussion:
/// /// Going above this limit will abort your program (although not /// necessarily) at _exactly_ `MAX_REFCOUNT + 1` references. <ins> /// Trying to go above it might call a `panic` (if not actually going above it). /// /// This is a global invariant, and also applies when using a compare-exchange loop. /// /// See comment in `Arc::clone`. </ins> const MAX_REFCOUNT: usize = (isize::MAX) as usize; <ins> /// The error in case either counter reaches above `MAX_REFCOUNT`, and we can `panic` safely. const INTERNAL_OVERFLOW_ERROR: &str = "Arc counter overflow"; </ins> #[cfg(not(sanitize = "thread"))] macro_rules! acquire { ($x:expr) => {

continue; } <ins> // We can't allow the refcount to increase much past `MAX_REFCOUNT`. assert!(cur <= MAX_REFCOUNT, "{}", INTERNAL_OVERFLOW_ERROR); </ins> // NOTE: this code currently ignores the possibility of overflow // into usize::MAX; in general both Rc and Arc need to be adjusted // to deal with overflow.

// the worst already happened and we actually do overflow the `usize` counter. However, that // requires the counter to grow from `isize::MAX` to `usize::MAX` between the increment // above and the `abort` below, which seems exceedingly unlikely. <ins> // // This is a global invariant, and also applies when using a compare-exchange loop to increment // counters in other methods. // Otherwise, the counter could be brought to an almost-overflow using a compare-exchange loop, // and then overflow using a few `fetch_add`s. </ins> if old_size > MAX_REFCOUNT { abort(); }

return None; } // See comments in `Arc::clone` for why we do this (for `mem::forget`). <del> if n > MAX_REFCOUNT { abort(); } </del> <ins> assert!(n <= MAX_REFCOUNT, "{}", INTERNAL_OVERFLOW_ERROR); </ins> Some(n + 1) }) .ok()