-
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
Conversation
rustbot has assigned @workingjubilee. Use |
CC @steffahn |
This comment has been minimized.
This comment has been minimized.
1f7a9f5
to
41aca57
Compare
exciting! |
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.
Could you either commit these comments or, if they're wrong (and that seems reasonably likely), correct them? The description of the issue so far is... a bit in-the-weeds. Tests are a valuable opportunity to spend time describing the actual issues we are trying to avoid.
}); | ||
let b = [1, 2, 3, 4].as_slice().iter().copied(); | ||
let c = [5, 6, 7].as_slice().iter().copied(); | ||
let ab = zip(a, b); |
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.
let ab = zip(a, b); | |
// The actual case we are testing: this Zip gets nested in the next, and | |
// `a` forces a premature unwind from Zip's iterator-driving code. | |
let ab = zip(a, b); |
41aca57
to
ec51514
Compare
This comment has been minimized.
This comment has been minimized.
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. ¹ see merge commit for linkfied issues.
ec51514
to
61c2c12
Compare
@rustbot ready The issue is complicated, so I don't think those few suggested comments would a good job, so I tried to add some more instead. |
@workingjubilee will you take another look at it or should I reroll? |
// 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; |
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...
// 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 comment
The reason will be displayed to describe this comment to others. Learn more.
So now, if we somehow arrive in this code again, old_len
will be the lower self.len
value, preventing the rerun...
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.
yep
// 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 { | ||
self.b.next_back(); | ||
} | ||
} | ||
debug_assert_eq!(self.a.size(), self.b.size()); |
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.
So we're calling next_back
a bunch of times just to run side-effects of the closure, which becomes observable. Right?
It feels kinda weird that we potentially next_back
a bunch of times here. This allows using next_back
to force running side effects that would not be encountered if we only drove using next
, right?
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.
The regular, non-TrustedRandomAccess
-specialized next_back
naturally performs this tail-trimming because it needs to bring the two inner iterators down to the same length (known through ExactSizeIterator
) before it can pop off a tail element at the same index.
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 comment
The 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 core::iter::zip
. Nonetheless, I suppose this is fine for now, since this is the current behavior.
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.
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 next()
.
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 { |
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.
The subtlety here that we are catching is that the len
of the zip-iter (zipter?) is the lesser of the two sizes.
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.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
you mean the default fn
implementation?
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.
yes
Alright, sorry for taking so long to review this. It's quite painful to reason about. I'm satisfied with it. @bors r+ |
Rollup of 10 pull requests Successful merges: - #141076 (fix Zip unsoundness (again)) - #142444 (adding run-make test to autodiff) - #143704 (Be a bit more careful around exotic cycles in in the inliner) - #144073 (Don't test panic=unwind in panic_main.rs on Fuchsia) - #144083 (miri sleep tests: increase slack) - #144092 (bootstrap: Detect musl hosts) - #144098 (Do not lint private-in-public for RPITIT) - #144103 (Rename `emit_unless` to `emit_unless_delay`) - #144108 (Ignore tests/run-make/link-eh-frame-terminator/rmake.rs when cross-compiling) - #144115 (fix outdated comment) r? `@ghost` `@rustbot` modify labels: rollup
Rollup merge of #141076 - the8472:fix-zip-panic-safety2, r=workingjubilee 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. fixes #137255
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.
fixes #137255