Skip to content

Commit 1f7a9f5

Browse files
committed
fix Zip unsoundness (again)
Some history: The Zip TrustedRandomAccess specialization has tried to emulate the side-effects of the naive implementation for a long time, including backwards iteration. #82292 tried to fix unsoundness (#82291) in that side-effect-preservation code, but this introduced some panic-safety unsoundness (#86443), but the fix #86452 didn't fix it for nested Zip iterators (#137255). Rather than piling yet another fix ontop of this heap of fixes this PR reduces the number of cases in which side-effects will be preserved; the necessary API guarantee change was approved in #83791 but we haven't made use of that so far.
1 parent 16c1c54 commit 1f7a9f5

File tree

3 files changed

+100
-75
lines changed

3 files changed

+100
-75
lines changed

library/core/src/iter/adapters/zip.rs

+33-41
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ pub struct Zip<A, B> {
1818
// index, len and a_len are only used by the specialized version of zip
1919
index: usize,
2020
len: usize,
21-
a_len: usize,
2221
}
2322
impl<A: Iterator, B: Iterator> Zip<A, B> {
2423
pub(in crate::iter) fn new(a: A, b: B) -> Zip<A, B> {
@@ -158,7 +157,6 @@ macro_rules! zip_impl_general_defaults {
158157
b,
159158
index: 0, // unused
160159
len: 0, // unused
161-
a_len: 0, // unused
162160
}
163161
}
164162

@@ -299,9 +297,8 @@ where
299297
B: TrustedRandomAccess + Iterator,
300298
{
301299
fn new(a: A, b: B) -> Self {
302-
let a_len = a.size();
303-
let len = cmp::min(a_len, b.size());
304-
Zip { a, b, index: 0, len, a_len }
300+
let len = cmp::min(a.size(), b.size());
301+
Zip { a, b, index: 0, len }
305302
}
306303

307304
#[inline]
@@ -315,17 +312,6 @@ where
315312
unsafe {
316313
Some((self.a.__iterator_get_unchecked(i), self.b.__iterator_get_unchecked(i)))
317314
}
318-
} else if A::MAY_HAVE_SIDE_EFFECT && self.index < self.a_len {
319-
let i = self.index;
320-
// as above, increment before executing code that may panic
321-
self.index += 1;
322-
self.len += 1;
323-
// match the base implementation's potential side effects
324-
// SAFETY: we just checked that `i` < `self.a.len()`
325-
unsafe {
326-
self.a.__iterator_get_unchecked(i);
327-
}
328-
None
329315
} else {
330316
None
331317
}
@@ -371,36 +357,42 @@ where
371357
A: DoubleEndedIterator + ExactSizeIterator,
372358
B: DoubleEndedIterator + ExactSizeIterator,
373359
{
374-
if A::MAY_HAVE_SIDE_EFFECT || B::MAY_HAVE_SIDE_EFFECT {
375-
let sz_a = self.a.size();
376-
let sz_b = self.b.size();
377-
// Adjust a, b to equal length, make sure that only the first call
378-
// of `next_back` does this, otherwise we will break the restriction
379-
// on calls to `self.next_back()` after calling `get_unchecked()`.
380-
if sz_a != sz_b {
360+
// No effects when the iterator is exhausted, to reduce the number of
361+
// cases the unsafe code has to handle.
362+
// See #137255 for a case where where too many epicycles lead to unsoundness.
363+
if self.index < self.len {
364+
let old_len = self.len;
365+
366+
// since get_unchecked and the side-effecting code can execute user code
367+
// which can panic we decrement the counter beforehand
368+
// so that the same index won't be accessed twice, as required by TrustedRandomAccess.
369+
// Additionally this will ensure that the side-effects code won't run a second time.
370+
self.len -= 1;
371+
372+
// Adjust a, b to equal length if we're iterating backwards.
373+
if A::MAY_HAVE_SIDE_EFFECT || B::MAY_HAVE_SIDE_EFFECT {
374+
// note if some forward-iteration already happened then these aren't the real
375+
// remaining lengths of the inner iterators, so we have to relate them to
376+
// Zip's internal length-tracking.
381377
let sz_a = self.a.size();
382-
if A::MAY_HAVE_SIDE_EFFECT && sz_a > self.len {
383-
for _ in 0..sz_a - self.len {
384-
// since next_back() may panic we increment the counters beforehand
385-
// to keep Zip's state in sync with the underlying iterator source
386-
self.a_len -= 1;
387-
self.a.next_back();
388-
}
389-
debug_assert_eq!(self.a_len, self.len);
390-
}
391378
let sz_b = self.b.size();
392-
if B::MAY_HAVE_SIDE_EFFECT && sz_b > self.len {
393-
for _ in 0..sz_b - self.len {
394-
self.b.next_back();
379+
// This condition can and must only be true on the first `next_back` call,
380+
// otherwise we will break the restriction on calls to `self.next_back()`
381+
// after calling `get_unchecked()`.
382+
if sz_a != sz_b && (old_len == sz_a || old_len == sz_b) {
383+
if A::MAY_HAVE_SIDE_EFFECT && sz_a > old_len {
384+
for _ in 0..sz_a - old_len {
385+
self.a.next_back();
386+
}
395387
}
388+
if B::MAY_HAVE_SIDE_EFFECT && sz_b > old_len {
389+
for _ in 0..sz_b - old_len {
390+
self.b.next_back();
391+
}
392+
}
393+
debug_assert_eq!(self.a.size(), self.b.size());
396394
}
397395
}
398-
}
399-
if self.index < self.len {
400-
// since get_unchecked executes code which can panic we increment the counters beforehand
401-
// so that the same index won't be accessed twice, as required by TrustedRandomAccess
402-
self.len -= 1;
403-
self.a_len -= 1;
404396
let i = self.len;
405397
// SAFETY: `i` is smaller than the previous value of `self.len`,
406398
// which is also smaller than or equal to `self.a.len()` and `self.b.len()`

library/coretests/tests/iter/adapters/cloned.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ fn test_cloned_side_effects() {
3131
.zip(&[1]);
3232
for _ in iter {}
3333
}
34-
assert_eq!(count, 2);
34+
// Zip documentation provides some leeway about side-effects
35+
assert!([1, 2].iter().any(|v| *v == count));
3536
}
3637

3738
#[test]

library/coretests/tests/iter/adapters/zip.rs

+65-33
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,19 @@ fn test_zip_next_back_side_effects_exhausted() {
107107
iter.next();
108108
iter.next();
109109
iter.next();
110-
iter.next();
110+
assert_eq!(iter.next(), None);
111111
assert_eq!(iter.next_back(), None);
112-
assert_eq!(a, vec![1, 2, 3, 4, 6, 5]);
112+
113+
assert!(a.starts_with(&[1, 2, 3]));
114+
let a_len = a.len();
115+
// Tail-side-effects of forward-iteration are "at most one" per next().
116+
// And for reverse iteration we don't guarantee much either.
117+
// But we can put some bounds on the possible behaviors.
118+
assert!(a_len <= 6);
119+
assert!(a_len >= 3);
120+
a.sort();
121+
assert_eq!(a, &[1, 2, 3, 4, 5, 6][..a.len()]);
122+
113123
assert_eq!(b, vec![200, 300, 400]);
114124
}
115125

@@ -120,7 +130,8 @@ fn test_zip_cloned_sideffectful() {
120130

121131
for _ in xs.iter().cloned().zip(ys.iter().cloned()) {}
122132

123-
assert_eq!(&xs, &[1, 1, 1, 0][..]);
133+
// Zip documentation permits either case.
134+
assert!([&[1, 1, 1, 0], &[1, 1, 0, 0]].iter().any(|v| &xs == *v));
124135
assert_eq!(&ys, &[1, 1][..]);
125136

126137
let xs = [CountClone::new(), CountClone::new()];
@@ -139,7 +150,8 @@ fn test_zip_map_sideffectful() {
139150

140151
for _ in xs.iter_mut().map(|x| *x += 1).zip(ys.iter_mut().map(|y| *y += 1)) {}
141152

142-
assert_eq!(&xs, &[1, 1, 1, 1, 1, 0]);
153+
// Zip documentation permits either case.
154+
assert!([&[1, 1, 1, 1, 1, 0], &[1, 1, 1, 1, 0, 0]].iter().any(|v| &xs == *v));
143155
assert_eq!(&ys, &[1, 1, 1, 1]);
144156

145157
let mut xs = [0; 4];
@@ -168,7 +180,8 @@ fn test_zip_map_rev_sideffectful() {
168180

169181
{
170182
let mut it = xs.iter_mut().map(|x| *x += 1).zip(ys.iter_mut().map(|y| *y += 1));
171-
(&mut it).take(5).count();
183+
// the current impl only trims the tails if the iterator isn't exhausted
184+
(&mut it).take(3).count();
172185
it.next_back();
173186
}
174187
assert_eq!(&xs, &[1, 1, 1, 1, 1, 1]);
@@ -211,9 +224,18 @@ fn test_zip_nth_back_side_effects_exhausted() {
211224
iter.next();
212225
iter.next();
213226
iter.next();
214-
iter.next();
227+
assert_eq!(iter.next(), None);
215228
assert_eq!(iter.nth_back(0), None);
216-
assert_eq!(a, vec![1, 2, 3, 4, 6, 5]);
229+
assert!(a.starts_with(&[1, 2, 3]));
230+
let a_len = a.len();
231+
// Tail-side-effects of forward-iteration are "at most one" per next().
232+
// And for reverse iteration we don't guarantee much either.
233+
// But we can put some bounds on the possible behaviors.
234+
assert!(a_len <= 6);
235+
assert!(a_len >= 3);
236+
a.sort();
237+
assert_eq!(a, &[1, 2, 3, 4, 5, 6][..a.len()]);
238+
217239
assert_eq!(b, vec![200, 300, 400]);
218240
}
219241

@@ -237,32 +259,6 @@ fn test_zip_trusted_random_access_composition() {
237259
assert_eq!(z2.next().unwrap(), ((1, 1), 1));
238260
}
239261

240-
#[test]
241-
#[cfg(panic = "unwind")]
242-
fn test_zip_trusted_random_access_next_back_drop() {
243-
use std::panic::{AssertUnwindSafe, catch_unwind};
244-
245-
let mut counter = 0;
246-
247-
let it = [42].iter().map(|e| {
248-
let c = counter;
249-
counter += 1;
250-
if c == 0 {
251-
panic!("bomb");
252-
}
253-
254-
e
255-
});
256-
let it2 = [(); 0].iter();
257-
let mut zip = it.zip(it2);
258-
catch_unwind(AssertUnwindSafe(|| {
259-
zip.next_back();
260-
}))
261-
.unwrap_err();
262-
assert!(zip.next().is_none());
263-
assert_eq!(counter, 1);
264-
}
265-
266262
#[test]
267263
fn test_double_ended_zip() {
268264
let xs = [1, 2, 3, 4, 5, 6];
@@ -275,6 +271,42 @@ fn test_double_ended_zip() {
275271
assert_eq!(it.next(), None);
276272
}
277273

274+
#[test]
275+
#[cfg(panic = "unwind")]
276+
fn test_nested_zip_panic_safety() {
277+
use std::panic::{resume_unwind, catch_unwind};
278+
use std::panic::AssertUnwindSafe;
279+
use std::sync::atomic::{AtomicUsize, Ordering};
280+
281+
let mut panic = true;
282+
let witness = [8, 9, 10, 11, 12].map(|i| (i, AtomicUsize::new(0)));
283+
let a = witness.as_slice().iter().map(|e| {
284+
e.1.fetch_add(1, Ordering::Relaxed);
285+
if panic {
286+
panic = false;
287+
resume_unwind(Box::new(()))
288+
}
289+
e.0
290+
});
291+
let b = [1, 2, 3, 4].as_slice().iter().copied();
292+
let c = [5, 6, 7].as_slice().iter().copied();
293+
let ab = zip(a, b);
294+
295+
let mut abc = zip(ab, c);
296+
297+
assert_eq!(abc.len(), 3);
298+
catch_unwind(AssertUnwindSafe(|| abc.next_back())).ok();
299+
assert_eq!(abc.len(), 2);
300+
assert_eq!(abc.next(), Some(((8, 1), 5)));
301+
assert_eq!(abc.next_back(), Some(((9, 2), 6)));
302+
for (i, (_, w)) in witness.iter().enumerate() {
303+
let v = w.load(Ordering::Relaxed);
304+
assert!(v <= 1, "expected idx {i} to be visited at most once, actual: {v}");
305+
}
306+
// backwards trimming panicked and should only run once, so this one won't be visited.
307+
assert_eq!(witness[3].1.load(Ordering::Relaxed), 0);
308+
}
309+
278310
#[test]
279311
fn test_issue_82282() {
280312
fn overflowed_zip(arr: &[i32]) -> impl Iterator<Item = (i32, &())> {

0 commit comments

Comments
 (0)