-
Notifications
You must be signed in to change notification settings - Fork 13.6k
fix Zip unsoundness (again) #141076
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix Zip unsoundness (again) #141076
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,7 +18,6 @@ pub struct Zip<A, B> { | |
// index, len and a_len are only used by the specialized version of zip | ||
index: usize, | ||
len: usize, | ||
a_len: usize, | ||
} | ||
impl<A: Iterator, B: Iterator> Zip<A, B> { | ||
pub(in crate::iter) fn new(a: A, b: B) -> Zip<A, B> { | ||
|
@@ -158,7 +157,6 @@ macro_rules! zip_impl_general_defaults { | |
b, | ||
index: 0, // unused | ||
len: 0, // unused | ||
a_len: 0, // unused | ||
} | ||
} | ||
|
||
|
@@ -299,9 +297,8 @@ where | |
B: TrustedRandomAccess + Iterator, | ||
{ | ||
fn new(a: A, b: B) -> Self { | ||
let a_len = a.size(); | ||
let len = cmp::min(a_len, b.size()); | ||
Zip { a, b, index: 0, len, a_len } | ||
let len = cmp::min(a.size(), b.size()); | ||
Zip { a, b, index: 0, len } | ||
} | ||
|
||
#[inline] | ||
|
@@ -315,17 +312,6 @@ where | |
unsafe { | ||
Some((self.a.__iterator_get_unchecked(i), self.b.__iterator_get_unchecked(i))) | ||
} | ||
} else if A::MAY_HAVE_SIDE_EFFECT && self.index < self.a_len { | ||
let i = self.index; | ||
// as above, increment before executing code that may panic | ||
self.index += 1; | ||
self.len += 1; | ||
// match the base implementation's potential side effects | ||
// SAFETY: we just checked that `i` < `self.a.len()` | ||
unsafe { | ||
self.a.__iterator_get_unchecked(i); | ||
} | ||
None | ||
} else { | ||
None | ||
} | ||
|
@@ -371,36 +357,42 @@ where | |
A: DoubleEndedIterator + ExactSizeIterator, | ||
B: DoubleEndedIterator + ExactSizeIterator, | ||
{ | ||
if A::MAY_HAVE_SIDE_EFFECT || B::MAY_HAVE_SIDE_EFFECT { | ||
let sz_a = self.a.size(); | ||
let sz_b = self.b.size(); | ||
// Adjust a, b to equal length, make sure that only the first call | ||
// of `next_back` does this, otherwise we will break the restriction | ||
// on calls to `self.next_back()` after calling `get_unchecked()`. | ||
if sz_a != sz_b { | ||
// No effects when the iterator is exhausted, to reduce the number of | ||
// cases the unsafe code has to handle. | ||
// See #137255 for a case where where too many epicycles lead to unsoundness. | ||
if self.index < self.len { | ||
let old_len = self.len; | ||
|
||
// since get_unchecked and the side-effecting code can execute user code | ||
// which can panic we decrement the counter beforehand | ||
// so that the same index won't be accessed twice, as required by TrustedRandomAccess. | ||
// Additionally this will ensure that the side-effects code won't run a second time. | ||
self.len -= 1; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So now, if we somehow arrive in this code again, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yep |
||
|
||
// Adjust a, b to equal length if we're iterating backwards. | ||
if A::MAY_HAVE_SIDE_EFFECT || B::MAY_HAVE_SIDE_EFFECT { | ||
// note if some forward-iteration already happened then these aren't the real | ||
// remaining lengths of the inner iterators, so we have to relate them to | ||
// Zip's internal length-tracking. | ||
let sz_a = self.a.size(); | ||
if A::MAY_HAVE_SIDE_EFFECT && sz_a > self.len { | ||
for _ in 0..sz_a - self.len { | ||
// since next_back() may panic we increment the counters beforehand | ||
// to keep Zip's state in sync with the underlying iterator source | ||
self.a_len -= 1; | ||
self.a.next_back(); | ||
} | ||
debug_assert_eq!(self.a_len, self.len); | ||
} | ||
let sz_b = self.b.size(); | ||
if B::MAY_HAVE_SIDE_EFFECT && sz_b > self.len { | ||
for _ in 0..sz_b - self.len { | ||
self.b.next_back(); | ||
// This condition can and must only be true on the first `next_back` call, | ||
// otherwise we will break the restriction on calls to `self.next_back()` | ||
// after calling `get_unchecked()`. | ||
if sz_a != sz_b && (old_len == sz_a || old_len == sz_b) { | ||
if A::MAY_HAVE_SIDE_EFFECT && sz_a > old_len { | ||
for _ in 0..sz_a - old_len { | ||
self.a.next_back(); | ||
} | ||
} | ||
if B::MAY_HAVE_SIDE_EFFECT && sz_b > old_len { | ||
for _ in 0..sz_b - old_len { | ||
Comment on lines
+382
to
+389
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The subtlety here that we are catching is that the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's hopefully less surprising if one starts with the regular implementation and only then looks at the specialized one. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you mean the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes |
||
self.b.next_back(); | ||
} | ||
} | ||
debug_assert_eq!(self.a.size(), self.b.size()); | ||
Comment on lines
+379
to
+393
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So we're calling It feels kinda weird that we potentially There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The regular, non- It's the side-effects of the trimming that we try to emulate here because otherwise it'd look like we're jumping into the middle of the iterator. Personally I'd prefer if we just did the jumping, but some people like their cute liddle closure side-effects and are sad if they don't get to run. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm. It's not obvious to me that this sort of behavior is guaranteed or even expected according to the contract of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's just no stable way skip in the middle of an iterator and elide its side-effects. So people might try to reason from its public interface as-if things were implemented by |
||
} | ||
} | ||
} | ||
if self.index < self.len { | ||
// since get_unchecked executes code which can panic we increment the counters beforehand | ||
// so that the same index won't be accessed twice, as required by TrustedRandomAccess | ||
self.len -= 1; | ||
self.a_len -= 1; | ||
let i = self.len; | ||
// SAFETY: `i` is smaller than the previous value of `self.len`, | ||
// which is also smaller than or equal to `self.a.len()` and `self.b.len()` | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We record the original length we are working with...