Skip to content

Commit 338ce9b

Browse files
authored
Unrolled build for #155708
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.
2 parents ca9a134 + da545d0 commit 338ce9b

2 files changed

Lines changed: 46 additions & 5 deletions

File tree

library/alloc/src/str.rs

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,26 @@ macro_rules! copy_slice_and_advance {
126126
// the bounds for String-join are S: Borrow<str> and for Vec-join Borrow<[T]>
127127
// [T] and str both impl AsRef<[T]> for some T
128128
// => s.borrow().as_ref() and we always have slices
129+
//
130+
// # Safety notes
131+
//
132+
// `Borrow` is a safe trait, and implementations are not required
133+
// to be deterministic. An inconsistent `Borrow` implementation could return slices
134+
// of different lengths on consecutive calls (e.g. by using interior mutability).
135+
//
136+
// This implementation calls `borrow()` multiple times:
137+
// 1. To calculate `reserved_len`, all elements are borrowed once.
138+
// 2. The first element is borrowed again when copied via `extend_from_slice`.
139+
// 3. Subsequent elements are borrowed a second time when building the mapped iterator.
140+
//
141+
// Risks and Mitigations:
142+
// - If the first element GROWS on the second borrow, the length subtraction underflows.
143+
// We mitigate this by doing a `checked_sub` to panic rather than allowing an underflow
144+
// that fabricates a huge destination slice.
145+
// - If elements 2..N GROW on their second borrow, the target slice bounds set by `checked_sub`
146+
// means that `split_at_mut` inside `copy_slice_and_advance!` will correctly panic.
147+
// - If elements SHRINK on their second borrow, the spare space is never written, and the final
148+
// length set via `set_len` masks trailing uninitialized bytes.
129149
#[cfg(not(no_global_oom_handling))]
130150
fn join_generic_copy<B, T, S>(slice: &[S], sep: &[T]) -> Vec<T>
131151
where
@@ -161,19 +181,21 @@ where
161181

162182
unsafe {
163183
let pos = result.len();
164-
let target = result.spare_capacity_mut().get_unchecked_mut(..reserved_len - pos);
184+
let target_len = reserved_len.checked_sub(pos).expect("inconsistent Borrow implementation");
185+
let target = result.spare_capacity_mut().get_unchecked_mut(..target_len);
165186

166187
// Convert the separator and slices to slices of MaybeUninit
167-
// to simplify implementation in specialize_for_lengths
188+
// to simplify implementation in specialize_for_lengths.
168189
let sep_uninit = core::slice::from_raw_parts(sep.as_ptr().cast(), sep.len());
169190
let iter_uninit = iter.map(|it| {
170191
let it = it.borrow().as_ref();
171192
core::slice::from_raw_parts(it.as_ptr().cast(), it.len())
172193
});
173194

174-
// copy separator and slices over without bounds checks
175-
// generate loops with hardcoded offsets for small separators
176-
// massive improvements possible (~ x2)
195+
// copy separator and slices over without bounds checks.
196+
// `specialize_for_lengths!` internally calls `s.borrow()`, but because it uses
197+
// the bounds-checked `split_at_mut` any misbehaving implementation
198+
// will not write out of bounds.
177199
let remain = specialize_for_lengths!(sep_uninit, target, iter_uninit; 0, 1, 2, 3, 4);
178200

179201
// A weird borrow implementation may return different

library/alloctests/tests/str.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,25 @@ fn test_join_issue_80335() {
194194
test_join!("0-0-0", arr, "-");
195195
}
196196

197+
#[test]
198+
#[should_panic(expected = "inconsistent Borrow implementation")]
199+
fn test_join_inconsistent_borrow() {
200+
use std::borrow::Borrow;
201+
use std::cell::Cell;
202+
203+
struct E(Cell<u32>);
204+
205+
impl Borrow<str> for E {
206+
fn borrow(&self) -> &str {
207+
let count = self.0.get();
208+
self.0.set(count + 1);
209+
if count == 0 { "" } else { "longer string" }
210+
}
211+
}
212+
213+
let _s = [E(Cell::new(0)), E(Cell::new(0))].join("");
214+
}
215+
197216
#[test]
198217
#[cfg_attr(miri, ignore)] // Miri is too slow
199218
fn test_unsafe_slice() {

0 commit comments

Comments
 (0)