VarIntReader can leave a complete varint partially consumed
Description
Reading an out-of-range value can stop in the middle of a complete varint. A subsequent read then interprets the remaining bytes as another value, losing stream framing.
Steps to Reproduce
Cargo.toml:
[package]
name = "integer-encoding-repro"
version = "0.1.0"
edition = "2021"
[dependencies]
integer-encoding = "=4.1.0"
src/main.rs:
use integer_encoding::VarIntReader;
use std::io::{Cursor, ErrorKind};
fn main() {
let mut input = Cursor::new([0x80, 0x80, 0x80, 0x80, 0x01, 0x2a]);
let err = input.read_varint::<u8>().unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidData);
assert_eq!(input.position(), 3);
assert_eq!(input.read_varint::<u8>().unwrap(), 128);
assert_eq!(input.read_varint::<u8>().unwrap(), 42);
}
Run cargo run.
Expected Behavior
The reader documentation says it generally reads a whole varint. The first five bytes encode 2^28, followed by the independent value 42; even if the first read reports an error, consuming the complete first value would leave 42 next.
Actual Behavior
The assertions pass: the first read returns InvalidData after consuming three bytes. The next read consumes 80 01 as 128; only the following read reaches 42.
The processor limits stored bytes by the target type. The reader loop reads another byte before push rejects it. This is a synchronous reproduction; the async source has a similar loop but was not executed in the recorded investigation. The reproducer passed in debug and release with Rust 1.91.1 on macOS arm64 and default features. Later versions have not been checked.
Change history
- v1 · created · 2026-10-28 13:59 UTC
{ "title": "VarIntReader can leave a complete varint partially consumed", "body": "## Description\n\nReading an out-of-range value can stop in the middle of a complete varint. A subsequent read then interprets the remaining bytes as another value, losing stream framing.\n\n## Steps to Reproduce\n\n`Cargo.toml`:\n\n```toml\n[package]\nname = \"integer-encoding-repro\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\ninteger-encoding = \"=4.1.0\"\n```\n\n`src/main.rs`:\n\n```rust\nuse integer_encoding::VarIntReader;\nuse std::io::{Cursor, ErrorKind};\n\nfn main() {\n let mut input = Cursor::new([0x80, 0x80, 0x80, 0x80, 0x01, 0x2a]);\n let err = input.read_varint::<u8>().unwrap_err();\n assert_eq!(err.kind(), ErrorKind::InvalidData);\n assert_eq!(input.position(), 3);\n assert_eq!(input.read_varint::<u8>().unwrap(), 128);\n assert_eq!(input.read_varint::<u8>().unwrap(), 42);\n}\n```\n\nRun `cargo run`.\n\n## Expected Behavior\n\nThe [reader documentation](https://github.com/dermesser/integer-encoding-rs/blob/e0a10d01e86a38d0df177676c62f9a10c845ab53/src/reader.rs#L14-L23) says it generally reads a whole varint. The first five bytes encode 2^28, followed by the independent value 42; even if the first read reports an error, consuming the complete first value would leave 42 next.\n\n## Actual Behavior\n\nThe assertions pass: the first read returns `InvalidData` after consuming three bytes. The next read consumes `80 01` as 128; only the following read reaches 42.\n\nThe [processor](https://github.com/dermesser/integer-encoding-rs/blob/e0a10d01e86a38d0df177676c62f9a10c845ab53/src/reader.rs#L40-L63) limits stored bytes by the target type. The [reader loop](https://github.com/dermesser/integer-encoding-rs/blob/e0a10d01e86a38d0df177676c62f9a10c845ab53/src/reader.rs#L94-L114) reads another byte before `push` rejects it. This is a synchronous reproduction; the async source has a similar loop but was not executed in the recorded investigation. The reproducer passed in debug and release with Rust 1.91.1 on macOS arm64 and default features. Later versions have not been checked.", "validity": "reported", "upstream_status": "not_reported" }