Skip to content

event::Iter::size_hint and count report the total, not the remaining #1990

Description

@3x3xX3N0N

Summary

Iter::size_hint returns the total number of events in the container rather than the number
of events remaining, so after k calls to next() it over-reports by k. Iter::count has
the same shape: it ignores the current position and returns the total.

Iterator::size_hint is specified as "the bounds on the remaining length of the iterator", and
Iterator::count as "the number of iterations", i.e. the count of items the iterator will
still yield.

No unsoundness — see Impact.

The code

src/event/events.rs:203-224 (1.2.1):

impl<'a> Iterator for Iter<'a> {
    type Item = &'a Event;

    fn next(&mut self) -> Option<Self::Item> {
        let ret = self
            .inner
            .inner
            .get(self.pos)
            .map(Event::from_sys_event_ref);
        self.pos += 1;
        ret
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        let size = self.inner.inner.len();
        (size, Some(size))
    }

    fn count(self) -> usize {
        self.inner.inner.len()
    }
}

self.pos is the read cursor, advanced by next(). Neither size_hint nor count consults it.

Reproduction

let mut it = events.iter();          // suppose 4 events are present
let _ = it.next();
let _ = it.next();
assert_eq!(it.size_hint(), (2, Some(2)));  // fails: reports (4, Some(4))
assert_eq!(it.count(), 2);                 // fails: returns 4

Impact

Low, and bounded by a trait that is deliberately absent.

  • No unsoundness. Iter implements neither ExactSizeIterator nor TrustedLen, so no
    caller is entitled to treat the hint as authoritative for unchecked capacity or unchecked
    indexing. Adapters use it only as an allocation hint.
  • The observable effects are an over-allocation in collect() / extend() when the iterator
    has been partially consumed, and a wrong answer from count() in the same situation.
  • Callers who iterate a fresh Iter to exhaustion — which is the documented and overwhelmingly
    common pattern, for event in &events — see correct behaviour, because at position zero the
    total and the remaining count coincide.

Suggested fix

    fn size_hint(&self) -> (usize, Option<usize>) {
        let size = self.inner.inner.len().saturating_sub(self.pos);
        (size, Some(size))
    }

    fn count(self) -> usize {
        self.inner.inner.len().saturating_sub(self.pos)
    }

saturating_sub rather than - is required, not stylistic: next() increments self.pos
even on the call that returns None (events.rs:212), so pos can exceed len() once the
iterator is exhausted and a plain subtraction would underflow.

Since the corrected size_hint would then be exact, impl ExactSizeIterator for Iter<'_> becomes
available if upstream wants it — but that is a separate, additive decision and is not required
to fix the contract violation.

How this was found

Line-by-line read of mio 1.2.1 during a cargo vet certification of warden's dependency graph,
reading src/event/events.rs against the Iterator trait contract.

Not verified by execution: the reproduction above is derived from the source and has not been
run against a live Poll.

Upstream context

Checked against the default branch before filing: src/event/events.rs:216-222 is unchanged --
size_hint returns self.inner.inner.len() and count returns the same, neither consulting
self.pos.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions