You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This finding was identified during an agentic unsafe Rust code review performed by Gemini AI, followed by human review and verification.
The Issue
The safe public trait method FixedInt::decode_fixed performs an out-of-bounds memory read when provided with a byte slice shorter than the target integer width. While the trait documentation specifies that the input slice must be exactly REQUIRED_SPACE bytes, decode_fixed is marked safe and fails to assert or validate the slice length at runtime. On little-endian platforms:
When called by safe code with a slice shorter than size_of::<Self>() (such as u64::decode_fixed(&[])), read_unaligned reads past the end of the input buffer into adjacent unallocated or uninitialized memory. Reading out of bounds of an allocated object is immediate UB.
Minimal Reproduction (Miri)
use integer_encoding::FixedInt;fnmain(){let empty:&[u8] = &[];// Calling safe public method decode_fixed on an empty slice triggers an out-of-bounds readlet _val = u64::decode_fixed(empty);}
error: Undefined Behavior: memory access failed: attempting to access 8 bytes, but got alloc2 which is at or beyond the end of the allocation of size 0 bytes
--> /usr/local/google/home/manishearth/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/integer-encoding-3.0.4/src/fixed.rs:71:26
|
71 | unsafe { (src.as_ptr() as *const $t).read_unaligned() }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
...
91 | impl_fixedint!(u64);
| ------------------- in this macro invocation
|
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
= note: stack backtrace:
0: <u64 as integer_encoding::FixedInt>::decode_fixed
at /usr/local/google/home/manishearth/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/integer-encoding-3.0.4/src/fixed.rs:71:26: 71:70
1: main
at src/bin/repro1.rs:6:16: 6:40
= note: this error originates in the macro `impl_fixedint` (in Nightly builds, run with -Z macro-backtrace for more info)
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
error: aborting due to 1 previous error
Suggested Fix
Add a runtime length assertion at the start of decode_fixed across all target configurations, matching the check already enforced in encode_fixed:
The full audit report below also contains additional minor findings (such as missing safety comments or undocumented FFI assumptions) that are probably worth fixing as well but not the primary goal of this issue. The audit report has not been human-reviewed, it may contain misleading claims.
Full Gemini Codebase Audit Report Appendix
Unsafe Rust Review: integer_encoding (v3)
Overall Safety Assessment
integer_encoding (v3) provides fast encoding and decoding of primitive integer types to and from binary representations, supporting fixed-width (FixedInt) and variable-width (VarInt, zigzag protobuf style) integer encodings.
The crate has a low overall volume of unsafe code (confined exclusively to src/fixed.rs within the impl_fixedint! macro generated for primitive integers), but exhibits poor safety practices and contains a critical soundness vulnerability. The crate exposes a public safe trait FixedInt with methods encode_fixed, decode_fixed, and encode_fixed_light. While encode_fixed and encode_fixed_light are sound, decode_fixed violates Rust safety guidelines by performing unchecked out-of-bounds pointer reads (read_unaligned) when passed slices shorter than the target integer width.
Additionally, the unsafe blocks in src/fixed.rs lack any // SAFETY: proof comments, and the type punning in encode_fixed_light relies on dubious transmute patterns involving references to references (&self) that depend implicitly on compiler deref coercion.
Critical Findings
1. Out-of-bounds Read in public safe function FixedInt::decode_fixed 🔴 🚨
Priority: 🔴 High
Threat Vector: 🚨 Untrusted Input
Bug Type: Out-of-bounds Read
src/fixed.rs:71 and src/fixed.rs:77: Out-of-bounds Read in public safe function FixedInt::decode_fixed.
Vulnerability: Out-of-bounds Read (buffer over-read) resulting in Undefined Behavior and potential process crash (SIGSEGV).
Description: FixedInt::decode_fixed(src: &[u8]) -> Self is a safe public method exported on the public safe trait FixedInt (implemented for u8, u16, u32, u64, usize, i8, i16, i32, i64, isize). The documentation states: "src must be exactly REQUIRED_SPACE bytes." However, the implementation does not assert or dynamically check src.len(). On little-endian platforms (#[cfg(target_endian = "little")], line 71), and on big-endian platforms for 1-byte types (line 77), decode_fixed executes:
If a safe caller passes a slice shorter than size_of::<$t>() (e.g., u64::decode_fixed(&[]) or u32::decode_fixed(&[0x01])), read_unaligned reads size_of::<$t>() bytes starting from src.as_ptr(). This reads memory out of bounds of the slice buffer into unallocated or unrelated memory, violating memory safety. In adversarial or unlucky memory layouts (such as src located at the boundary of an unmapped memory page), this triggers a hardware segmentation fault. If it does not crash, it returns primitive integers constructed from uninitialized or unrelated stack/heap bytes.
Remediation: Add assert_eq!(src.len(), Self::REQUIRED_SPACE); (or assert!(src.len() >= Self::REQUIRED_SPACE);) at the start of decode_fixed for all target configurations. Alternatively, use standard library safe conversion APIs (try_into() followed by from_le_bytes).
Fishy Findings
1. Fragile transmute on reference-to-reference (&self) 🟠 ⚠️
Priority: 🟠 Medium
Threat Vector: ⚠️ Accidental Misuse
Bug Type: Fragile Type Punning
src/fixed.rs:44 (encode_fixed_light): Fragile transmute on reference-to-reference (&self).
Description: The method encode_fixed_light<'a>(&'a self) -> &'a [u8] invokes transmute::<&$t, *const u8>(&self). Because self is passed by reference (&'a $t), the expression &self has type &&'a $t (reference to reference). Passing &&$t to an intrinsic expecting &$t relies on implicit argument deref coercion (&&$t -> &$t). While explicit turbofish type parameters <&$t, *const u8> prevent the compiler from transmuting &&$t directly (which would return a pointer to the local stack parameter self and dangle immediately upon return), writing &self in a transmute is hazardous. If refactored without explicit turbofish parameters (e.g., transmute(&self)), it silently compiles into a catastrophic use-after-free vulnerability.
Remediation: Replace transmute::<&$t, *const u8>(&self) with std::slice::from_raw_parts(self as *const $t as *const u8, Self::REQUIRED_SPACE) or safe slice conversion.
Description: encode_fixed_light returns a byte slice viewing the direct in-memory representation of $t. On big-endian architectures, this produces big-endian wire format, whereas encode_fixed and decode_fixed explicitly reverse bytes on big-endian targets to maintain a little-endian wire format. This inconsistency is a logic hazard for callers on big-endian platforms.
Missing Safety Comments
1. Missing // SAFETY: comment in encode_fixed_light 🟡
src/fixed.rs:42: Missing // SAFETY: comment before the unsafe block in encode_fixed_light justifying transmute and std::slice::from_raw_parts.
Proposed proof comment:
// SAFETY:// Operation: `transmute::<&$t, *const u8>(&self)` and `std::slice::from_raw_parts(ptr, Self::REQUIRED_SPACE)`.// Required contract (transmute): Source type `&$t` and destination type `*const u8` must have the same size.// Required contract (from_raw_parts): `ptr` must be non-null, properly aligned for `u8`, valid for reads of `Self::REQUIRED_SPACE` bytes, lie within a single allocated object, and the memory must not be mutated for lifetime `'a` except through `UnsafeCell`.// Evidence:// - Precondition: A reference `&$t` and raw pointer `*const u8` both occupy 1 machine word (`size_of::<&$t>() == size_of::<*const u8>()`).// - Type Invariant: `&self` (of type `&&$t`) undergoes argument deref coercion to `&$t`, pointing to the valid initialized integer `self`.// - Precondition: Primitive integer types (`$t`) have alignment >= 1 (satisfying `u8` alignment 1) and occupy exactly `Self::REQUIRED_SPACE` (`size_of::<$t>()`) initialized bytes with no uninitialized padding bytes.// - Local Invariant: `self` is immutably borrowed for lifetime `'a`; since primitive integers contain no `UnsafeCell`, borrow checking guarantees no intervening mutation for `'a`.// Therefore all obligations for `transmute` and `from_raw_parts` are discharged.
src/fixed.rs:55: Missing // SAFETY: comment before the unsafe block in encode_fixed justifying raw pointer dereference to &[u8; Self::REQUIRED_SPACE].
Proposed proof comment:
// SAFETY:// Operation: Dereferencing raw pointer `*const [u8; Self::REQUIRED_SPACE]` to form `&[u8; Self::REQUIRED_SPACE]`.// Required contract: The pointer must be non-null, properly aligned for `[u8; Self::REQUIRED_SPACE]`, valid for reads of `Self::REQUIRED_SPACE` bytes, and point to initialized memory. The memory must not be mutated for the duration of the created reference.// Evidence:// - Local Invariant: `self` is a live pass-by-value local stack variable of type `$t`. `&self` forms a non-null valid pointer to this stack memory.// - Precondition: `[u8; N]` requires alignment 1, which is satisfied by any address valid for `$t`.// - Precondition: Primitive integers `$t` occupy exactly `Self::REQUIRED_SPACE` bytes and contain no uninitialized padding bytes.// - Local Invariant: `encoded` borrows `self` within `encode_fixed`; `self` is not mutated or moved after line 55.// Therefore dereferencing to `&[u8; Self::REQUIRED_SPACE]` is sound.
3. Missing // SAFETY: comment in decode_fixed (little-endian) 🟡
src/fixed.rs:71: Missing // SAFETY: comment before the unsafe block in decode_fixed (little-endian branch) justifying read_unaligned.
Note: This proof assumes the critical out-of-bounds read vulnerability is fixed by adding assert_eq!(src.len(), Self::REQUIRED_SPACE);.
Proposed proof comment:
// SAFETY:// Operation: `read_unaligned()` on `*const $t`.// Required contract: The pointer must be valid for reads of `size_of::<$t>()` (`Self::REQUIRED_SPACE`) bytes and point to properly initialized memory.// Evidence:// - Local Invariant: `assert_eq!(src.len(), Self::REQUIRED_SPACE)` ensures `src` contains at least `Self::REQUIRED_SPACE` bytes.// - Type Invariant: `src` is a valid `&[u8]` slice, ensuring all `src.len()` bytes lie within a single allocated object and are valid initialized `u8` values.// - Precondition: All byte bit patterns are valid representations for primitive integer types `$t`.// - Precondition: `read_unaligned` has no alignment precondition.// Therefore reading a `$t` from `src.as_ptr()` is sound.
4. Missing // SAFETY: comment in decode_fixed (big-endian 1-byte) 🟡
src/fixed.rs:77: Missing // SAFETY: comment before the unsafe block in decode_fixed (big-endian 1-byte branch) justifying read_unaligned.
Note: This proof assumes assert_eq!(src.len(), Self::REQUIRED_SPACE); is added.
Proposed proof comment:
// SAFETY:// Operation: `read_unaligned()` on `*const $t` where `Self::REQUIRED_SPACE == 1`.// Required contract: The pointer must be valid for reads of 1 byte and point to initialized memory.// Evidence:// - Local Invariant: `assert_eq!(src.len(), Self::REQUIRED_SPACE)` guarantees `src` contains at least 1 initialized byte.// - Precondition: Any 1-byte bit pattern is valid for 1-byte integer types (`u8`, `i8`).// Therefore `read_unaligned` is sound.
5. Missing // SAFETY: comment in decode_fixed (big-endian multi-byte) 🟡
src/fixed.rs:82: Missing // SAFETY: comment before the unsafe block in decode_fixed (big-endian multi-byte branch) justifying read_unaligned.
Proposed proof comment:
// SAFETY:// Operation: `read_unaligned()` on `*const $t` derived from `src_fin.as_ptr()`.// Required contract: The pointer must be valid for reads of `size_of::<$t>()` (`Self::REQUIRED_SPACE`) bytes and point to properly initialized memory.// Evidence:// - Local Invariant: `src_fin` is an initialized local stack array of exactly `Self::REQUIRED_SPACE` bytes.// - DEPENDENCY LEMMA: `src_fin.copy_from_slice(src)` panics if `src.len() != Self::REQUIRED_SPACE`, ensuring `src` had the exact length and initializing `src_fin` with valid bytes.// - Precondition: Primitive integers `$t` accept arbitrary byte patterns as valid values, and `read_unaligned` does not require alignment.// Therefore reading from `src_fin` is sound.
Note
This finding was identified during an agentic unsafe Rust code review performed by Gemini AI, followed by human review and verification.
The Issue
The safe public trait method
FixedInt::decode_fixedperforms an out-of-bounds memory read when provided with a byte slice shorter than the target integer width. While the trait documentation specifies that the input slice must be exactlyREQUIRED_SPACEbytes,decode_fixedis marked safe and fails to assert or validate the slice length at runtime. On little-endian platforms:integer-encoding-rs/src/fixed.rs
Lines 69 to 72 in ab48af6
And on big-endian platforms for 1-byte integers:
integer-encoding-rs/src/fixed.rs
Line 77 in ab48af6
When called by safe code with a slice shorter than
size_of::<Self>()(such asu64::decode_fixed(&[])),read_unalignedreads past the end of the input buffer into adjacent unallocated or uninitialized memory. Reading out of bounds of an allocated object is immediate UB.Minimal Reproduction (Miri)
Suggested Fix
Add a runtime length assertion at the start of
decode_fixedacross all target configurations, matching the check already enforced inencode_fixed:Note
The full audit report below also contains additional minor findings (such as missing safety comments or undocumented FFI assumptions) that are probably worth fixing as well but not the primary goal of this issue. The audit report has not been human-reviewed, it may contain misleading claims.
Full Gemini Codebase Audit Report Appendix
Unsafe Rust Review:
integer_encoding(v3)Overall Safety Assessment
integer_encoding(v3) provides fast encoding and decoding of primitive integer types to and from binary representations, supporting fixed-width (FixedInt) and variable-width (VarInt, zigzag protobuf style) integer encodings.The crate has a low overall volume of unsafe code (confined exclusively to
src/fixed.rswithin theimpl_fixedint!macro generated for primitive integers), but exhibits poor safety practices and contains a critical soundness vulnerability. The crate exposes a public safe traitFixedIntwith methodsencode_fixed,decode_fixed, andencode_fixed_light. Whileencode_fixedandencode_fixed_lightare sound,decode_fixedviolates Rust safety guidelines by performing unchecked out-of-bounds pointer reads (read_unaligned) when passed slices shorter than the target integer width.Additionally, the unsafe blocks in
src/fixed.rslack any// SAFETY:proof comments, and the type punning inencode_fixed_lightrelies on dubioustransmutepatterns involving references to references (&self) that depend implicitly on compiler deref coercion.Critical Findings
1. Out-of-bounds Read in public safe function
FixedInt::decode_fixed🔴 🚨Priority: 🔴 High
Threat Vector: 🚨 Untrusted Input
Bug Type:
Out-of-bounds Readsrc/fixed.rs:71andsrc/fixed.rs:77: Out-of-bounds Read in public safe functionFixedInt::decode_fixed.FixedInt::decode_fixed(src: &[u8]) -> Selfis a safe public method exported on the public safe traitFixedInt(implemented foru8,u16,u32,u64,usize,i8,i16,i32,i64,isize). The documentation states: "src must be exactly REQUIRED_SPACE bytes." However, the implementation does not assert or dynamically checksrc.len(). On little-endian platforms (#[cfg(target_endian = "little")], line 71), and on big-endian platforms for 1-byte types (line 77),decode_fixedexecutes:If a safe caller passes a slice shorter than
size_of::<$t>()(e.g.,u64::decode_fixed(&[])oru32::decode_fixed(&[0x01])),read_unalignedreadssize_of::<$t>()bytes starting fromsrc.as_ptr(). This reads memory out of bounds of the slice buffer into unallocated or unrelated memory, violating memory safety. In adversarial or unlucky memory layouts (such assrclocated at the boundary of an unmapped memory page), this triggers a hardware segmentation fault. If it does not crash, it returns primitive integers constructed from uninitialized or unrelated stack/heap bytes.assert_eq!(src.len(), Self::REQUIRED_SPACE);(orassert!(src.len() >= Self::REQUIRED_SPACE);) at the start ofdecode_fixedfor all target configurations. Alternatively, use standard library safe conversion APIs (try_into()followed byfrom_le_bytes).Fishy Findings
1. Fragile⚠️
transmuteon reference-to-reference (&self) 🟠Priority: 🟠 Medium
Threat Vector:⚠️ Accidental Misuse
Bug Type:
Fragile Type Punningsrc/fixed.rs:44(encode_fixed_light): Fragiletransmuteon reference-to-reference (&self).encode_fixed_light<'a>(&'a self) -> &'a [u8]invokestransmute::<&$t, *const u8>(&self). Becauseselfis passed by reference (&'a $t), the expression&selfhas type&&'a $t(reference to reference). Passing&&$tto an intrinsic expecting&$trelies on implicit argument deref coercion (&&$t -> &$t). While explicit turbofish type parameters<&$t, *const u8>prevent the compiler from transmuting&&$tdirectly (which would return a pointer to the local stack parameterselfand dangle immediately upon return), writing&selfin atransmuteis hazardous. If refactored without explicit turbofish parameters (e.g.,transmute(&self)), it silently compiles into a catastrophic use-after-free vulnerability.transmute::<&$t, *const u8>(&self)withstd::slice::from_raw_parts(self as *const $t as *const u8, Self::REQUIRED_SPACE)or safe slice conversion.2. Inconsistent endianness wire representation 🟡⚠️
Priority: 🟡 Low
Threat Vector:⚠️ Accidental Misuse
Bug Type:
Logic Inconsistencysrc/fixed.rs:41(encode_fixed_light): Inconsistent endianness wire representation.encode_fixed_lightreturns a byte slice viewing the direct in-memory representation of$t. On big-endian architectures, this produces big-endian wire format, whereasencode_fixedanddecode_fixedexplicitly reverse bytes on big-endian targets to maintain a little-endian wire format. This inconsistency is a logic hazard for callers on big-endian platforms.Missing Safety Comments
1. Missing
// SAFETY:comment inencode_fixed_light🟡-Priority: 🟡 Low
-Bug Type:
Missing Safety Commentsrc/fixed.rs:42: Missing// SAFETY:comment before theunsafeblock inencode_fixed_lightjustifyingtransmuteandstd::slice::from_raw_parts.Proposed proof comment:
2. Missing
// SAFETY:comment inencode_fixed🟡-Priority: 🟡 Low
-Bug Type:
Missing Safety Commentsrc/fixed.rs:55: Missing// SAFETY:comment before theunsafeblock inencode_fixedjustifying raw pointer dereference to&[u8; Self::REQUIRED_SPACE].Proposed proof comment:
3. Missing
// SAFETY:comment indecode_fixed(little-endian) 🟡-Priority: 🟡 Low
-Bug Type:
Missing Safety Commentsrc/fixed.rs:71: Missing// SAFETY:comment before theunsafeblock indecode_fixed(little-endian branch) justifyingread_unaligned.Note: This proof assumes the critical out-of-bounds read vulnerability is fixed by adding
assert_eq!(src.len(), Self::REQUIRED_SPACE);.Proposed proof comment:
4. Missing
// SAFETY:comment indecode_fixed(big-endian 1-byte) 🟡-Priority: 🟡 Low
-Bug Type:
Missing Safety Commentsrc/fixed.rs:77: Missing// SAFETY:comment before theunsafeblock indecode_fixed(big-endian 1-byte branch) justifyingread_unaligned.Note: This proof assumes
assert_eq!(src.len(), Self::REQUIRED_SPACE);is added.Proposed proof comment:
5. Missing
// SAFETY:comment indecode_fixed(big-endian multi-byte) 🟡-Priority: 🟡 Low
-Bug Type:
Missing Safety Commentsrc/fixed.rs:82: Missing// SAFETY:comment before theunsafeblock indecode_fixed(big-endian multi-byte branch) justifyingread_unaligned.Proposed proof comment: