Skip to content

buf: assert Buf contract invariants in default methods - #847

Open
puretechteam wants to merge 1 commit into
tokio-rs:masterfrom
puretechteam:fix/buf-adapter-invariant
Open

puretechteam wants to merge 1 commit into
tokio-rs:masterfrom
puretechteam:fix/buf-adapter-invariant

Conversation

@puretechteam

Copy link
Copy Markdown

Summary

Custom Buf implementations that violate the documented contract currently
produce opaque index out of bounds panics from inside the default method
bodies and the IntoIter / Take / Chain adapters. This patch adds
debug_assert!s at each affected site so a violating impl surfaces a panic
naming the violated contract, and tightens the rustdoc to spell out the
invariants explicitly.

Closes #833.

Invariant being defended

A custom Buf implementation must satisfy:

  • chunk() == &[] iff remaining() == 0
  • chunk().len() <= remaining()
  • chunks_vectored returns n <= dst.len()

When a custom impl breaks any of these, the consumer previously panicked with
the raw index out of bounds: the len is 0 but the index is 0 form, making
the contract violation hard to diagnose.

What the assertions protect

debug_assert!s added at:

  • Buf::get_u8 and Buf::get_i8 (default method bodies in
    src/buf/buf_impl.rs) — assert !chunk.is_empty() after the
    remaining() >= 1 guard.
  • Buf::try_get_u8 and Buf::try_get_i8 (default method bodies in
    src/buf/buf_impl.rs) — same shape.
  • IntoIter::next (src/buf/iter.rs) — same shape after the
    has_remaining() guard.
  • Take::chunks_vectored (src/buf/take.rs) — assert
    cnt <= dst.len() immediately after the inner chunks_vectored
    returns, before the dst[..cnt] slice and before the two
    unsafe { transmute } lifetime extensions.
  • Chain::chunks_vectored (src/buf/chain.rs) — two assertions:
    one after a.chunks_vectored(dst) and one after the cumulative
    a + b write.

Release-mode behavior is unchanged. debug_assert! is compiled out in
release; the original safe slice / index operations continue to bounds-check
before any unsafe block is reached. Per the maintainer's stated position
in #833 (comment 4776265881),
panics from contract violations are acceptable; UB is not.

get_u8 and get_i8 are included as obvious parallels of try_get_u8
and try_get_i8 even though they are not explicitly listed in the
issue body — flagging here so this can be discussed.

Documentation clarified

Two sentences added to the Buf trait rustdoc:

  • On Buf::chunk (after the existing iff clause): "chunk().len() must
    satisfy chunk().len() <= remaining()."
  • On Buf::chunks_vectored (in the Implementer notes section): "The
    return value must satisfy n <= dst.len()."

These are non-tautological additions; the existing iff clause on chunk
did not preclude an impl that returns a chunk longer than remaining().

Regression tests

Added in tests/test_buf.rs and tests/test_iter.rs. Each uses a custom
Buf impl that violates the contract and asserts that the new
debug_assert! fires via #[should_panic(expected = "Buf contract")].
The matching string locks in the panic message — a regression to the
opaque index out of bounds panic would fail the test.

  • InconsistentChunkremaining() = 1, chunk() = &[].
    Drives try_get_u8, try_get_i8, get_u8, get_i8,
    IntoIter::next.
  • OverreportingChunkschunks_vectored returns 2 from a
    1-element dst.
    Drives Take::chunks_vectored and Chain::chunks_vectored.

Each test is deterministic, isolated, and gated with #[cfg(feature = "std")]
where it depends on IoSlice.

How tests and validation were run

cargo fmt --all -- --check          # clean
cargo test --quiet                  # 1,309 passed, 0 failed
cargo test --quiet --test test_buf \
    --test test_iter --test test_take --test test_chain
                                    # 895 passed, 0 failed

Pre-existing cargo clippy --all-features -- -D warnings errors in the
upstream tree (items_after_test_module, etc.) are unrelated to this
patch; clippy is not enforced in CI for this repo.

Scope

This change is scoped to issue #833. No public API surface changes; no
behavior change in release builds for well-behaved Buf impls.

Note: this patch hardens the consumer side (default method bodies and
adapters), which is complementary to — and not in conflict with — the
implementer-side flexibility preserved by
#754 (which weakened the
chunks_vectored doc claim rather than adding runtime defense at
implementer call sites).

Refs: #833

Custom Buf implementations that violate the documented contract
(remaining() > 0 with chunk() == &[], or chunks_vectored returning
n > dst.len()) cause opaque index out of bounds panics in the
default method bodies and the IntoIter / Take / Chain adapters.

Add debug_assert!s at the affected sites so violating impls surface a
named panic message naming the violated contract. Release behavior is
unchanged for well-behaved impls; the safe slice / index ops continue
to bounds-check before any unsafe, preserving the maintainer's
position that panics are acceptable but UB is not.

Also tighten the rustdoc on Buf::chunk and Buf::chunks_vectored to
spell out the chunk().len() <= remaining() and n <= dst.len()
invariants explicitly.

Add regression tests covering each of the five sites from tokio-rs#833
plus the get_u8 / get_i8 parallels, using a custom Buf impl
(InconsistentChunk) and a chunks_vectored-overreporting impl
(OverreportingChunks).

Refs: tokio-rs#833
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.

Buf adapters can panic when custom Buf implementations break invariants

1 participant