rust-lang / rust-lang/rust

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

Open
#161,266 2 comments 0 reactions 1 assignee View on GitHub

@im-lunex is already working on this.

Since Aug 18, 2026.

A-iterators C-bug requires-nightly T-libs
Dominant language
Rust
Stars
119k
Forks
16.1k
PR merge metrics
PR metrics pending

Description

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/verify-rust-std#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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.