Summary
In the TrustedRandomAccess specialization of Zip::next_back
(library/core/src/iter/adapters/zip.rs, impl<A, B> ZipImpl<A, B> for Zip<A, B> where A: TrustedRandomAccessNoCoerce + Iterator, B: TrustedRandomAccessNoCoerce + Iterator), the trim step runs when
A::MAY_HAVE_SIDE_EFFECT || B::MAY_HAVE_SIDE_EFFECT and the sizes differ:
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());
}
The trim loops only run for a side with MAY_HAVE_SIDE_EFFECT == true. When exactly one side has side effects and that side is the shorter one, the longer plain side is (correctly) left untrimmed, so the debug_assert_eq! compares unequal sizes and panics under debug_assertions.
Reproduction (std built with debug assertions)
let a = [1u8, 2, 3];
let b = [1u8, 2, 3, 4, 5];
// `Map<slice::Iter<u8>, _>` is TrustedRandomAccessNoCoerce with MAY_HAVE_SIDE_EFFECT = true,
// `slice::Iter<u8>` is TrustedRandomAccessNoCoerce with MAY_HAVE_SIDE_EFFECT = false.
let mut it = a.iter().map(|x| *x).zip(b.iter());
let _ = it.next_back(); // debug_assert_eq!(3, 5) fires
Trace: Zip::new sets len = min(3, 5) = 3. next_back: old_len = 3, sz_a = 3, sz_b = 5; sz_a != sz_b && old_len == sz_a is true; sz_a > old_len is false so a is not trimmed; B::MAY_HAVE_SIDE_EFFECT is false so b is not trimmed; the assertion compares 3 with 5.
The symmetric case (plain longer a, side-effect shorter b) fails the same way. When both sides have side effects the longer side is trimmed and the assertion holds; when neither has, the block is skipped.
Behavior in release builds is correct: the plain side needs no trimming because __iterator_get_unchecked(i) on it is position independent for i < len.
Found by
Kani harnesses check_zip_next_back_side_effect_a / _b (verify-rust-std challenge 16, PR model-checking#602): Zip::new(slice_a.iter().map(bump), slice_b.iter()) over symbolic slices up to length 5, then next_back(). Kani reports the failure at panicking::assert_failed_inner; the both variant passes.
Existing coretests (test_zip_next_back_side_effects, test_issue_82291) use side effects on both sides or equal lengths, so they do not reach this case.
Suggested fix
Either assert only what the code needs (self.len <= self.a.size() && self.len <= self.b.size()), or restrict the equality assertion to the case A::MAY_HAVE_SIDE_EFFECT && B::MAY_HAVE_SIDE_EFFECT.
Upstream master (checked 2026-08-17): identical code at zip.rs lines 376-387.
@rustbot label +C-bug +T-libs +A-iterators
Summary
In the
TrustedRandomAccessspecialization ofZip::next_back(
library/core/src/iter/adapters/zip.rs,impl<A, B> ZipImpl<A, B> for Zip<A, B> where A: TrustedRandomAccessNoCoerce + Iterator, B: TrustedRandomAccessNoCoerce + Iterator), the trim step runs whenA::MAY_HAVE_SIDE_EFFECT || B::MAY_HAVE_SIDE_EFFECTand the sizes differ:The trim loops only run for a side with
MAY_HAVE_SIDE_EFFECT == true. When exactly one side has side effects and that side is the shorter one, the longer plain side is (correctly) left untrimmed, so thedebug_assert_eq!compares unequal sizes and panics underdebug_assertions.Reproduction (std built with debug assertions)
Trace:
Zip::newsetslen = min(3, 5) = 3.next_back:old_len = 3,sz_a = 3,sz_b = 5;sz_a != sz_b && old_len == sz_ais true;sz_a > old_lenis false soais not trimmed;B::MAY_HAVE_SIDE_EFFECTis false sobis not trimmed; the assertion compares 3 with 5.The symmetric case (plain longer
a, side-effect shorterb) fails the same way. When both sides have side effects the longer side is trimmed and the assertion holds; when neither has, the block is skipped.Behavior in release builds is correct: the plain side needs no trimming because
__iterator_get_unchecked(i)on it is position independent fori < len.Found by
Kani harnesses
check_zip_next_back_side_effect_a/_b(verify-rust-std challenge 16, PR model-checking#602):Zip::new(slice_a.iter().map(bump), slice_b.iter())over symbolic slices up to length 5, thennext_back(). Kani reports the failure atpanicking::assert_failed_inner; thebothvariant passes.Existing coretests (
test_zip_next_back_side_effects,test_issue_82291) use side effects on both sides or equal lengths, so they do not reach this case.Suggested fix
Either assert only what the code needs (
self.len <= self.a.size() && self.len <= self.b.size()), or restrict the equality assertion to the caseA::MAY_HAVE_SIDE_EFFECT && B::MAY_HAVE_SIDE_EFFECT.Upstream master (checked 2026-08-17): identical code at zip.rs lines 376-387.
@rustbot label +C-bug +T-libs +A-iterators