Skip to content

fix: honor the ExactSizeIterator contract in the IPv4 address iterators - #94

Merged
3Hren merged 1 commit into
mainfrom
fix/exact-size-iterator-32bit
Jul 12, 2026
Merged

fix: honor the ExactSizeIterator contract in the IPv4 address iterators#94
3Hren merged 1 commit into
mainfrom
fix/exact-size-iterator-32bit

Conversation

@3Hren

@3Hren 3Hren commented Jul 12, 2026

Copy link
Copy Markdown
Member

The bug

An IPv4 /0 network holds exactly 2^32 addresses -- one more than u32::MAX, and one more than a 32-bit usize can represent. Both IPv4 address iterators (Ipv4NetworkAddrs and ContiguousIpv4NetworkAddrs) implement ExactSizeIterator and both got this wrong, in two separate ways.

On 64-bit, today, in released code: len() returns 2^32 while size_hint() returns (usize::MAX, None). ExactSizeIterator requires size_hint() to be exactly (len(), Some(len())), so the contract is violated for /0. Measured on x86-64 before this change:

len()       = 4294967296
size_hint() = (18446744073709551615, None)

On 32-bit: Ipv4NetworkAddrs::len() (self.remaining as usize) silently truncates 2^32 to 0, and ContiguousIpv4NetworkAddrs::len() (1usize << 32) does not even compile:

error: this arithmetic operation will overflow
  --> src/net/contiguous.rs:412:23
   | if rem == 0 { 1usize << 32 } else { rem as usize }
   |               ^^^^^^^^^^^^ attempt to shift left by `32_i32`, which would overflow

The fix

size_hint is exact whenever the count fits, and saturates honestly when it does not -- adopting the usize::try_from idiom the IPv6 iterators in this crate already use, so both families now share one scheme instead of two:

match usize::try_from(self.remaining()) {
    Ok(n) => (n, Some(n)),
    Err(..) => (usize::MAX, None),
}

ContiguousIpv4NetworkAddrs gains a private remaining() -> u64 as the single source of truth for the count. It replaces the u32 wrapping-sentinel arithmetic that size_hint, count and len each re-derived independently.

ExactSizeIterator is implemented on 64-bit targets only. On a 32-bit target no exact length exists for /0, so the contract is unsatisfiable and the trait must not be implemented. The standard library draws the same line: RangeInclusive<u32> is not an ExactSizeIterator while Range<u32> is. count() no longer routes through len(), so it keeps working on both pointer widths.

This is not a breaking change for 64-bit targets: the trait, and len(), behave exactly as before for every network except /0, where size_hint() is now correct.

Verification

cargo build --lib on main fails on i686-unknown-linux-gnu with the overflow above. On this branch it builds clean, as does wasm32-unknown-unknown -- both 32-bit-usize targets.

Run under a real 32-bit usize (wasm32, executed under node): /0 reports an honest saturated lower bound, everything that fits stays exact, count() works without ExactSizeIterator, and .len() correctly fails to resolve.

size_hint also got shorter: 24 instructions with a branch to a /0 special case, down to 22 straight-line instructions -- LLVM proves usize::try_from(u64) infallible on 64-bit and folds the error arm away.

Tests

Unit tests for size_hint() == (len(), Some(len())) at /0, across a full drain, and on non-contiguous masks, for both IPv4 iterators, plus the IPv6 honesty counterpart. Property tests (proptest) assert the contract holds in every drain state for IPv4 across the full prefix range, and for IPv6 that the hint's lower bound never overstates what is actually yielded. The pin-test that previously asserted (usize::MAX, None) now asserts the corrected value and stands as the regression guard. Benchmarks cover the changed path.

An IPv4 /0 network holds 2^32 addresses, one more than `u32::MAX` and one
more than a 32-bit `usize` can hold. Both IPv4 address iterators claimed
`ExactSizeIterator` regardless, and neither could keep its promise:

- `len()` and `size_hint()` disagreed for /0 on every target. `len()`
  reported the true 2^32 while `size_hint()` saturated to
  `(usize::MAX, None)`, breaking the trait's requirement that `size_hint`
  return exactly `(len(), Some(len()))`.
- On a 32-bit target `Ipv4NetworkAddrs::len()` silently truncated /0 to 0,
  and `ContiguousIpv4NetworkAddrs::len()` failed to compile outright:
  `1usize << 32` is a deny-by-default `arithmetic_overflow` error.

Make `size_hint` exact whenever the count fits a `usize` and saturate
honestly when it does not, adopting the `usize::try_from` idiom the IPv6
iterators already use. On 64-bit /0 now reports `(2^32, Some(2^32))`, so
the contradiction with `len()` disappears; on 32-bit it reports
`(usize::MAX, None)`, an honest lower bound.

Gate `ExactSizeIterator` itself to 64-bit targets, where every IPv4 count
fits. On 32-bit the trait simply is not implemented, matching the standard
library's decision not to implement it for `RangeInclusive<u32>` while
implementing it for `Range<u32>`. `count()` no longer routes through
`len()`, so it stays available and correct on both pointer widths.

The rewritten `size_hint` is also leaner on 64-bit: the /0 special case
folds away entirely, turning 24 instructions with a branch into 22
straight-line ones.
@3Hren
3Hren merged commit 95f316f into main Jul 12, 2026
4 checks passed
@3Hren
3Hren deleted the fix/exact-size-iterator-32bit branch July 12, 2026 10:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant