Skip to content

Commit 7d7b22d

Browse files
committed
Auto merge of #81169 - dylni:fix-soundness-issue-for-replace-range, r=KodrAus
Fix soundness issue for `replace_range` and `range` Fixes #81138 by only calling `start_bound` and `end_bound` once. I also fixed the same issue for [`BTreeMap::range`](https://doc.rust-lang.org/std/collections/struct.BTreeMap.html#method.range) and [`BTreeSet::range`](https://doc.rust-lang.org/std/collections/struct.BTreeSet.html#method.range).
2 parents 94e6ea9 + b96063c commit 7d7b22d

File tree

3 files changed

+70
-6
lines changed

3 files changed

+70
-6
lines changed

Diff for: library/alloc/src/collections/btree/navigate.rs

+10-3
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@ where
2525
K: Borrow<Q>,
2626
R: RangeBounds<Q>,
2727
{
28-
match (range.start_bound(), range.end_bound()) {
28+
// WARNING: Inlining these variables would be unsound (#81138)
29+
// We assume the bounds reported by `range` remain the same, but
30+
// an adversarial implementation could change between calls
31+
let start = range.start_bound();
32+
let end = range.end_bound();
33+
match (start, end) {
2934
(Excluded(s), Excluded(e)) if s == e => {
3035
panic!("range start and end are equal and excluded in BTreeMap")
3136
}
@@ -41,7 +46,8 @@ where
4146
let mut max_found = false;
4247

4348
loop {
44-
let front = match (min_found, range.start_bound()) {
49+
// Using `range` again would be unsound (#81138)
50+
let front = match (min_found, start) {
4551
(false, Included(key)) => match min_node.search_node(key) {
4652
SearchResult::Found(kv) => {
4753
min_found = true;
@@ -61,7 +67,8 @@ where
6167
(_, Unbounded) => min_node.first_edge(),
6268
};
6369

64-
let back = match (max_found, range.end_bound()) {
70+
// Using `range` again would be unsound (#81138)
71+
let back = match (max_found, end) {
6572
(false, Included(key)) => match max_node.search_node(key) {
6673
SearchResult::Found(kv) => {
6774
max_found = true;

Diff for: library/alloc/src/string.rs

+10-3
Original file line numberDiff line numberDiff line change
@@ -1553,18 +1553,25 @@ impl String {
15531553
// Replace_range does not have the memory safety issues of a vector Splice.
15541554
// of the vector version. The data is just plain bytes.
15551555

1556-
match range.start_bound() {
1556+
// WARNING: Inlining this variable would be unsound (#81138)
1557+
let start = range.start_bound();
1558+
match start {
15571559
Included(&n) => assert!(self.is_char_boundary(n)),
15581560
Excluded(&n) => assert!(self.is_char_boundary(n + 1)),
15591561
Unbounded => {}
15601562
};
1561-
match range.end_bound() {
1563+
// WARNING: Inlining this variable would be unsound (#81138)
1564+
let end = range.end_bound();
1565+
match end {
15621566
Included(&n) => assert!(self.is_char_boundary(n + 1)),
15631567
Excluded(&n) => assert!(self.is_char_boundary(n)),
15641568
Unbounded => {}
15651569
};
15661570

1567-
unsafe { self.as_mut_vec() }.splice(range, replace_with.bytes());
1571+
// Using `range` again would be unsound (#81138)
1572+
// We assume the bounds reported by `range` remain the same, but
1573+
// an adversarial implementation could change between calls
1574+
unsafe { self.as_mut_vec() }.splice((start, end), replace_with.bytes());
15681575
}
15691576

15701577
/// Converts this `String` into a [`Box`]`<`[`str`]`>`.

Diff for: library/alloc/tests/string.rs

+50
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
use std::borrow::Cow;
2+
use std::cell::Cell;
23
use std::collections::TryReserveError::*;
4+
use std::ops::Bound;
35
use std::ops::Bound::*;
6+
use std::ops::RangeBounds;
47
use std::panic;
8+
use std::str;
59

610
pub trait IntoCow<'a, B: ?Sized>
711
where
@@ -561,6 +565,52 @@ fn test_replace_range_unbounded() {
561565
assert_eq!(s, "");
562566
}
563567

568+
#[test]
569+
fn test_replace_range_evil_start_bound() {
570+
struct EvilRange(Cell<bool>);
571+
572+
impl RangeBounds<usize> for EvilRange {
573+
fn start_bound(&self) -> Bound<&usize> {
574+
Bound::Included(if self.0.get() {
575+
&1
576+
} else {
577+
self.0.set(true);
578+
&0
579+
})
580+
}
581+
fn end_bound(&self) -> Bound<&usize> {
582+
Bound::Unbounded
583+
}
584+
}
585+
586+
let mut s = String::from("🦀");
587+
s.replace_range(EvilRange(Cell::new(false)), "");
588+
assert_eq!(Ok(""), str::from_utf8(s.as_bytes()));
589+
}
590+
591+
#[test]
592+
fn test_replace_range_evil_end_bound() {
593+
struct EvilRange(Cell<bool>);
594+
595+
impl RangeBounds<usize> for EvilRange {
596+
fn start_bound(&self) -> Bound<&usize> {
597+
Bound::Included(&0)
598+
}
599+
fn end_bound(&self) -> Bound<&usize> {
600+
Bound::Excluded(if self.0.get() {
601+
&3
602+
} else {
603+
self.0.set(true);
604+
&4
605+
})
606+
}
607+
}
608+
609+
let mut s = String::from("🦀");
610+
s.replace_range(EvilRange(Cell::new(false)), "");
611+
assert_eq!(Ok(""), str::from_utf8(s.as_bytes()));
612+
}
613+
564614
#[test]
565615
fn test_extend_ref() {
566616
let mut a = "foo".to_string();

0 commit comments

Comments
 (0)