Generic FixedInt I/O panics for a valid sixteen-byte implementation
Description
The public synchronous FixedIntReader and FixedIntWriter adapters panic before I/O when given a safe sixteen-byte FixedInt implementation. Their public signatures do not state an eight-byte size limit.
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::{FixedInt, FixedIntReader, FixedIntWriter};
use std::io::Cursor;
use std::panic::{catch_unwind, AssertUnwindSafe};
#[derive(Clone, Copy, Debug, PartialEq)]
struct Fixed128(u128);
impl FixedInt for Fixed128 {
type Bytes = [u8; 16];
fn encode_fixed(self, dst: &mut [u8]) -> Option<()> {
if dst.len() != 16 { return None; }
dst.copy_from_slice(&self.0.to_le_bytes());
Some(())
}
fn encode_fixed_light(self) -> Self::Bytes { self.0.to_le_bytes() }
fn decode_fixed(src: &[u8]) -> Option<Self> {
let bytes: [u8; 16] = src.try_into().ok()?;
Some(Self(u128::from_le_bytes(bytes)))
}
fn switch_endianness(self) -> Self { Self(self.0.swap_bytes()) }
}
fn main() {
let value = Fixed128(1);
let mut bytes = [0; 16];
assert_eq!(value.encode_fixed(&mut bytes), Some(()));
assert_eq!(Fixed128::decode_fixed(&bytes), Some(value));
let mut reader = Cursor::new(bytes);
let read_panics = catch_unwind(AssertUnwindSafe(|| reader.read_fixedint::<Fixed128>())).is_err();
let mut writer = Vec::new();
let write_panics = catch_unwind(AssertUnwindSafe(|| writer.write_fixedint(value))).is_err();
assert!(read_panics && write_panics);
assert_eq!(reader.position(), 0);
assert!(writer.is_empty());
}
Run cargo run.
Expected Behavior
The generic adapters can process this complete, sixteen-byte little-endian representation, or their supported size restriction is made explicit.
Actual Behavior
Both calls panic while slicing their buffers, before reading or writing any bytes. The program catches the panics and its assertions pass (the default panic hook may still print messages).
The reader and writer allocate eight-byte arrays and slice them using size_of::<FI>(). The public trait does not state that FI must fit in eight bytes. The built-in types fit on the tested host; this finding concerns custom implementations. Async adapters show the same source pattern, but were not executed in the recorded investigation.
The reproducer caught both panics 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": "Generic FixedInt I/O panics for a valid sixteen-byte implementation", "body": "## Description\n\nThe public synchronous `FixedIntReader` and `FixedIntWriter` adapters panic before I/O when given a safe sixteen-byte `FixedInt` implementation. Their public signatures do not state an eight-byte size limit.\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::{FixedInt, FixedIntReader, FixedIntWriter};\nuse std::io::Cursor;\nuse std::panic::{catch_unwind, AssertUnwindSafe};\n\n#[derive(Clone, Copy, Debug, PartialEq)]\nstruct Fixed128(u128);\n\nimpl FixedInt for Fixed128 {\n type Bytes = [u8; 16];\n fn encode_fixed(self, dst: &mut [u8]) -> Option<()> {\n if dst.len() != 16 { return None; }\n dst.copy_from_slice(&self.0.to_le_bytes());\n Some(())\n }\n fn encode_fixed_light(self) -> Self::Bytes { self.0.to_le_bytes() }\n fn decode_fixed(src: &[u8]) -> Option<Self> {\n let bytes: [u8; 16] = src.try_into().ok()?;\n Some(Self(u128::from_le_bytes(bytes)))\n }\n fn switch_endianness(self) -> Self { Self(self.0.swap_bytes()) }\n}\n\nfn main() {\n let value = Fixed128(1);\n let mut bytes = [0; 16];\n assert_eq!(value.encode_fixed(&mut bytes), Some(()));\n assert_eq!(Fixed128::decode_fixed(&bytes), Some(value));\n let mut reader = Cursor::new(bytes);\n let read_panics = catch_unwind(AssertUnwindSafe(|| reader.read_fixedint::<Fixed128>())).is_err();\n let mut writer = Vec::new();\n let write_panics = catch_unwind(AssertUnwindSafe(|| writer.write_fixedint(value))).is_err();\n assert!(read_panics && write_panics);\n assert_eq!(reader.position(), 0);\n assert!(writer.is_empty());\n}\n```\n\nRun `cargo run`.\n\n## Expected Behavior\n\nThe generic adapters can process this complete, sixteen-byte little-endian representation, or their supported size restriction is made explicit.\n\n## Actual Behavior\n\nBoth calls panic while slicing their buffers, before reading or writing any bytes. The program catches the panics and its assertions pass (the default panic hook may still print messages).\n\nThe [reader](https://github.com/dermesser/integer-encoding-rs/blob/e0a10d01e86a38d0df177676c62f9a10c845ab53/src/reader.rs#L143-L149) and [writer](https://github.com/dermesser/integer-encoding-rs/blob/e0a10d01e86a38d0df177676c62f9a10c845ab53/src/writer.rs#L69-L76) allocate eight-byte arrays and slice them using `size_of::<FI>()`. The [public trait](https://github.com/dermesser/integer-encoding-rs/blob/e0a10d01e86a38d0df177676c62f9a10c845ab53/src/fixed.rs#L9-L35) does not state that `FI` must fit in eight bytes. The built-in types fit on the tested host; this finding concerns custom implementations. Async adapters show the same source pattern, but were not executed in the recorded investigation.\n\nThe reproducer caught both panics 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" }