Skip to content

Zip::next_back debug assertion fires when only one side has side effects and it is the shorter side #161266

Description

@MavenRain

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

Metadata

Metadata

Assignees

Labels

A-iteratorsArea: IteratorsC-bugCategory: This is a bug.T-libsRelevant to the library team, which will review and decide on the PR/issue.requires-nightlyThis issue requires a nightly compiler in some way. When possible, use a F-* label instead.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions