From d34590d2ba9c4ae6d7d8c6cbd4828dc9539a6520 Mon Sep 17 00:00:00 2001 From: Paul Menage Date: Wed, 29 Jul 2026 15:52:32 +0100 Subject: [PATCH] Add bytecheck support Add a `bytecheck` feature to the crate, which derives `bytecheck::CheckBytes` and implements `bytecheck::Verify` to allows for run-time verification of arbitrary int values received from untrusted sources. --- .github/workflows/test-bytecheck.yml | 15 ++++++++++++ CHANGELOG.md | 6 +++++ Cargo.toml | 4 ++++ src/lib.rs | 2 ++ src/signed.rs | 20 ++++++++++++++++ src/unsigned.rs | 20 ++++++++++++++++ tests/tests.rs | 34 ++++++++++++++++++++++++++++ 7 files changed, 101 insertions(+) create mode 100644 .github/workflows/test-bytecheck.yml diff --git a/.github/workflows/test-bytecheck.yml b/.github/workflows/test-bytecheck.yml new file mode 100644 index 0000000..4723a25 --- /dev/null +++ b/.github/workflows/test-bytecheck.yml @@ -0,0 +1,15 @@ +name: test bytecheck +run-name: ${{ github.actor }}'s patch +on: [push, pull_request] +jobs: + build-and-test: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + cache: true + toolchain: nightly + - run: | + cargo test --no-default-features --features=bytecheck + cargo test --no-default-features --features=bytecheck,std diff --git a/CHANGELOG.md b/CHANGELOG.md index 832a637..2966952 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Added + +- Added optional `bytecheck` support. Enable using the `bytecheck` feature. + ## 2.1.1 ### Added diff --git a/Cargo.toml b/Cargo.toml index 59deb3d..e24a115 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,9 @@ borsh = ["dep:borsh"] schemars = ["dep:schemars", "std"] +# Derive/implement traits for bytecheck +bytecheck = ["dep:bytecheck"] + # Implement traits for bytemuck bytemuck = ["dep:bytemuck"] @@ -52,6 +55,7 @@ borsh = { version = "1.5.1", optional = true, features = ["unstable__schema"], d schemars = { version = "0.8.21", optional = true, features = ["derive"], default-features = false } bytemuck = { version = "1", optional = true, default-features = false } bin-proto = { version = "0.12.2", optional = true, default-features = false } +bytecheck = { version = "0.8.2", optional = true, default-features = false } arbitrary = { version = "1", optional = true, default-features = false } quickcheck = { version = "1", optional = true, default-features = false } diff --git a/src/lib.rs b/src/lib.rs index 8685fc2..24cde27 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -136,3 +136,5 @@ impl fmt::Display for TryNewError { write!(f, "Value too large to fit within this integer type") } } + +impl core::error::Error for TryNewError {} diff --git a/src/signed.rs b/src/signed.rs index 89ef51c..ec7484c 100644 --- a/src/signed.rs +++ b/src/signed.rs @@ -137,6 +137,8 @@ impl_signed_integer_native!((i8, u8), (i16, u16), (i32, u32), (i64, u64), (i128, /// [`bytemuck::NoUninit`]: https://docs.rs/bytemuck/1/bytemuck/trait.NoUninit.html /// [`bytemuck::Contiguous`]: https://docs.rs/bytemuck/1/bytemuck/trait.Contiguous.html #[derive(Copy, Clone, Eq, PartialEq, Default, Ord, PartialOrd, Hash)] +#[cfg_attr(feature = "bytecheck", derive(bytecheck::CheckBytes))] +#[cfg_attr(feature = "bytecheck", bytecheck(verify))] #[repr(transparent)] pub struct Int { value: T, @@ -1548,6 +1550,24 @@ macro_rules! int_impl { }; } +#[cfg(feature = "bytecheck")] +unsafe impl< + T: SignedInteger + BuiltinInteger + Copy, + const BITS: usize, + C: bytecheck::rancor::Fallible + ?Sized, + > bytecheck::Verify for Int +where + C::Error: bytecheck::rancor::Source, + Self: Integer, +{ + fn verify(&self, _context: &mut C) -> Result<(), C::Error> { + if self.value > Self::MAX.value || self.value < Self::MIN.value { + bytecheck::rancor::fail!(TryNewError); + } + Ok(()) + } +} + // Because the methods within this macro are effectively copy-pasted for each underlying integer type, // each documentation test gets executed five times (once for each underlying type), even though the // tests themselves aren't specific to said underlying type. This severely slows down `cargo test`, diff --git a/src/unsigned.rs b/src/unsigned.rs index 2005837..64579e4 100644 --- a/src/unsigned.rs +++ b/src/unsigned.rs @@ -125,6 +125,8 @@ impl_integer_native!((u8, i8), (u16, i16), (u32, i32), (u64, i64), (u128, i128)) /// /// When `cfg(feature = "bytemuck")` is set, the appropriate bytemuck traits will be implemented. #[derive(Copy, Clone, Eq, PartialEq, Default, Ord, PartialOrd, Hash)] +#[cfg_attr(feature = "bytecheck", derive(bytecheck::CheckBytes))] +#[cfg_attr(feature = "bytecheck", bytecheck(verify))] #[repr(transparent)] pub struct UInt { value: T, @@ -161,6 +163,24 @@ where pub const MASK: T = Self::MAX.value; } +#[cfg(feature = "bytecheck")] +unsafe impl< + T: UnsignedInteger + BuiltinInteger + Copy, + const BITS: usize, + C: bytecheck::rancor::Fallible + ?Sized, + > bytecheck::Verify for UInt +where + C::Error: bytecheck::rancor::Source, + Self: Integer, +{ + fn verify(&self, _context: &mut C) -> Result<(), C::Error> { + if self.value > Self::MAX.value { + bytecheck::rancor::fail!(TryNewError); + } + Ok(()) + } +} + // Next are specific implementations for u8, u16, u32, u64 and u128. A couple notes: // - The existence of MAX also serves as a neat bounds-check for BITS: If BITS is too large, // the subtraction overflows which will fail to compile. This simplifies things a lot. diff --git a/tests/tests.rs b/tests/tests.rs index ea1cf9b..65d1315 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -4408,6 +4408,40 @@ mod bin_proto_tests { } } +#[cfg(feature = "bytecheck")] +mod bytecheck_tests { + use super::*; + use bytecheck::{check_bytes, rancor}; + + #[test] + fn test_validity_unsigned() { + let good_u6_val = 0x3fu8; + unsafe { check_bytes::((&raw const good_u6_val).cast()).unwrap() }; + + let bad_u6_val = 0x40u8; + unsafe { check_bytes::((&raw const bad_u6_val).cast()).unwrap_err() }; + } + + #[test] + fn test_validity_signed() { + let good_i31_val = i31::MAX.value() as i32; + unsafe { check_bytes::((&raw const good_i31_val).cast()).unwrap() }; + + let good_i32_val2 = i31::MIN.value() as i32; + unsafe { check_bytes::((&raw const good_i32_val2).cast()).unwrap() }; + + let bad_i31_val = (i31::MAX.value() as i32) + 1; + unsafe { + check_bytes::((&raw const bad_i31_val).cast()).unwrap_err() + }; + + let bad_i31_val2 = (i31::MIN.value() as i32) - 1; + unsafe { + check_bytes::((&raw const bad_i31_val2).cast()).unwrap_err() + }; + } +} + #[test] fn new_and_as_specific_types() { let a = u6::new(42);