Skip to content

fix: randomize UUIDv7 rand_a on millisecond-resolution clocks#217

Closed
mimifuwacc wants to merge 1 commit into
google:masterfrom
mimifuwacc:fix/v7-submilli-fallback
Closed

fix: randomize UUIDv7 rand_a on millisecond-resolution clocks#217
mimifuwacc wants to merge 1 commit into
google:masterfrom
mimifuwacc:fix/v7-submilli-fallback

Conversation

@mimifuwacc

@mimifuwacc mimifuwacc commented Jun 21, 2026

Copy link
Copy Markdown

Summary

UUIDv7 fills the 12-bit rand_a field with a sub-millisecond fraction of the timestamp ((nano % 1ms) >> 8). On platforms whose wall clock has no sub-millisecond resolution this fraction is always zero, so rand_a carries no entropy and is effectively a constant.

This adds a fallback: when the platform is not guaranteed to have a sub-millisecond clock, rand_a is filled with random bits instead, as permitted by RFC 9562. Ordering within a millisecond is still guaranteed by the existing lastV7time + 1 counter, so monotonicity is unaffected.

Note: This aligns with the approach that appears likely to be adopted by the standard library's uuid implementation in Go 1.27, which has the same implementation distinction — filling rand_a with random bits where the wall clock has no guaranteed sub-millisecond resolution. See golang/go#80084.

Details

  • getV7Time now branches on a compile-time constant hasSubMilliClock:
    • true → existing behavior (sub-millisecond timestamp fraction).
    • false → random 12-bit rand_a via the package's rander (so SetRand / the rand pool are still honored).
  • hasSubMilliClock is defined per-platform via build constraints:
    • version7_wasm.go (js || wasip1) → false
    • version7_other.go (!js && !wasip1) → true
  • The constraint is js || wasip1 rather than just js: on wasm targets the clock resolution is host-dependent and sub-millisecond precision is not guaranteed, so narrowing to js would miss WASI hosts (e.g. Node's WASI) that only expose millisecond precision. The only trade-off of including wasip1 is that on a wasip1 host that does have sub-ms resolution we use random bits instead of the fractional-timestamp ordering — but intra-millisecond ordering is still preserved by the counter, so this is acceptable.

Tests

No new test is included. The new branch is selected by a compile-time constant that is only false on wasm targets (js, wasip1), and CI runs go test ./... on native (amd64) only, where the constant is true. A test would therefore exercise the existing path, not the new one, unless CI is extended to run under a wasm runtime. Behavior was instead verified manually:

Target Runner Result
native (amd64) go test ./... pass
js/wasm node full suite pass (exercises the random rand_a branch)
wasip1/wasm wasmtime go test -run Test ./... pass (random branch; the Fuzz* seed-corpus read fails only due to the WASI filesystem sandbox, unrelated to this change)

Happy to add a wasm CI job (or refactor the rand_a computation into a unit-testable helper) if preferred.

@mimifuwacc mimifuwacc requested a review from a team as a code owner June 21, 2026 18:21
@mimifuwacc mimifuwacc marked this pull request as draft June 22, 2026 16:14
@jdebp

jdebp commented Jun 22, 2026

Copy link
Copy Markdown

This fix is not correct. In the GOOS == "js" case it correctly makes the rand_a field into 12 bits of random data. But then the timestamp comparison logic includes those random data in the timestamp. The timestamp change test logic must also be different in the GOOS == "js" case.

Although on the other hand this implementation is not using the optional counter, just the timestamp and either 62 or 74 bits of random data, depending from timestamp precision available. So all of the timestamp comparison logic, which applies when one is using an infixed counter between the timestamp and the random data per RFC 9562 § 6.2, is inapplicable.

On the gripping hand, this implementation is not doing the RFC 9562 § 6.2 monotonicity check properly. The whole UUID, timestamp plus random data, must monotonically increment.

@hajimehoshi

Copy link
Copy Markdown

I think it's ok to fill rand_a with random values, and monotonic increment is just an option:

https://datatracker.ietf.org/doc/html/rfc9562#section-5.7-2

UUIDv7 values are created by allocating a Unix timestamp in milliseconds in the most significant 48 bits and filling the remaining 74 bits, excluding the required version and variant bits, with random bits for each new UUIDv7 generated to provide uniqueness as per Section 6.9. Alternatively, implementations MAY fill the 74 bits, jointly, with a combination of the following subfields, in this order from the most significant bits to the least, to guarantee additional monotonicity within a millisecond:

  1. An OPTIONAL sub-millisecond timestamp fraction (12 bits at maximum) as per Section 6.2 (Method 3).
  2. An OPTIONAL carefully seeded counter as per Section 6.2 (Method 1 or 2).
  3. Random data for each new UUIDv7 generated for any remaining space.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants