ClockSequence::usable_bits default returns bytes instead of bits, truncating custom v7 counters #866

Closed
opened 2026-03-15 09:56:02 -05:00 by meng-xu-cs · 1 comment
meng-xu-cs commented 2026-03-15 09:56:02 -05:00 (Migrated from github.com)

Summary

The default ClockSequence::usable_bits() implementation currently returns core::mem::size_of::<Self::Output>(), which is a byte count, but the method is documented and consumed as a bit count.

For custom ClockSequence implementations that rely on the default, Timestamp::{now,from_unix} stores that value in usable_counter_bits, and Uuid::new_v7 then uses it as the number of counter bits to preserve in the UUID.

That means the default path silently truncates custom counters:

  • u16 defaults to 2 instead of 16
  • u32 defaults to 4 instead of 32
  • u64 defaults to 8 instead of 64
  • u128 defaults to 16 instead of 128

Reproduction

use uuid::{ClockSequence, Timestamp};

struct TwoByteCounter;

impl ClockSequence for TwoByteCounter {
    type Output = u16;

    fn generate_sequence(&self, _seconds: u64, _subsec_nanos: u32) -> Self::Output {
        u16::MAX
    }

    // use the default `usable_bits()`
}

fn main() {
    // Expected: 16
    // Actual: 2
    assert_eq!(TwoByteCounter.usable_bits(), 16);

    let ts = Timestamp::from_unix(TwoByteCounter, 1_700_000_000, 0);

    // `ts` now internally carries `usable_counter_bits = 2`, so only 2 bits
    // of the custom counter are treated as significant when `Uuid::new_v7(ts)`
    // encodes the UUID.
    let _ = ts;
}

Impact

This appears to affect third-party/custom ClockSequence implementations that rely on the default usable_bits().

The built-in contexts override usable_bits(), so Context, ContextV7, and NoContext do not appear to be affected.

For affected custom v7 generators, the effective counter space is much smaller than the API contract suggests:

  • u16: 16 bits collapse to 2 bits (4 states)
  • u32: 32 bits collapse to 4 bits (16 states)
  • u64: 64 bits collapse to 8 bits (256 states)
  • u128: 128 bits collapse to 16 bits (65,536 states)

This can silently reduce monotonic sequencing/uniqueness entropy for high-rate custom v7 UUID generation.

Suggested fix

Change the default implementation to count bits instead of bytes:

fn usable_bits(&self) -> usize
where
    Self::Output: Sized,
{
    cmp::min(128, core::mem::size_of::<Self::Output>() * 8)
}
### Summary The default `ClockSequence::usable_bits()` implementation currently returns `core::mem::size_of::<Self::Output>()`, which is a **byte** count, but the method is documented and consumed as a **bit** count. For custom `ClockSequence` implementations that rely on the default, `Timestamp::{now,from_unix}` stores that value in `usable_counter_bits`, and `Uuid::new_v7` then uses it as the number of counter **bits** to preserve in the UUID. That means the default path silently truncates custom counters: - `u16` defaults to `2` instead of `16` - `u32` defaults to `4` instead of `32` - `u64` defaults to `8` instead of `64` - `u128` defaults to `16` instead of `128` ### Reproduction ```rust use uuid::{ClockSequence, Timestamp}; struct TwoByteCounter; impl ClockSequence for TwoByteCounter { type Output = u16; fn generate_sequence(&self, _seconds: u64, _subsec_nanos: u32) -> Self::Output { u16::MAX } // use the default `usable_bits()` } fn main() { // Expected: 16 // Actual: 2 assert_eq!(TwoByteCounter.usable_bits(), 16); let ts = Timestamp::from_unix(TwoByteCounter, 1_700_000_000, 0); // `ts` now internally carries `usable_counter_bits = 2`, so only 2 bits // of the custom counter are treated as significant when `Uuid::new_v7(ts)` // encodes the UUID. let _ = ts; } ``` ### Impact This appears to affect third-party/custom `ClockSequence` implementations that rely on the default `usable_bits()`. The built-in contexts override `usable_bits()`, so `Context`, `ContextV7`, and `NoContext` do not appear to be affected. For affected custom v7 generators, the effective counter space is much smaller than the API contract suggests: - `u16`: 16 bits collapse to 2 bits (`4` states) - `u32`: 32 bits collapse to 4 bits (`16` states) - `u64`: 64 bits collapse to 8 bits (`256` states) - `u128`: 128 bits collapse to 16 bits (`65,536` states) This can silently reduce monotonic sequencing/uniqueness entropy for high-rate custom v7 UUID generation. ### Suggested fix Change the default implementation to count bits instead of bytes: ```rust fn usable_bits(&self) -> usize where Self::Output: Sized, { cmp::min(128, core::mem::size_of::<Self::Output>() * 8) } ```
KodrAus commented 2026-03-16 22:34:37 -05:00 (Migrated from github.com)

Thanks @meng-xu-cs! This is a straight up bug

Thanks @meng-xu-cs! This is a straight up bug
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
gh-uuid-rs/uuid#866
No description provided.