Skip to content

fix(core): clamp uuidv7 RNG output to a valid uint32 so a nonconformant Math.random cannot crash event capture - #4616

Open
shahidrogers wants to merge 1 commit into
PostHog:mainfrom
shahidrogers:fix/uuidv7-clamp-nextuint32
Open

fix(core): clamp uuidv7 RNG output to a valid uint32 so a nonconformant Math.random cannot crash event capture#4616
shahidrogers wants to merge 1 commit into
PostHog:mainfrom
shahidrogers:fix/uuidv7-clamp-nextuint32

Conversation

@shahidrogers

Copy link
Copy Markdown

Problem

The vendored UUIDv7 generator in @posthog/core builds its random fields from a Math.random()-based nextUint32() (Web Crypto is deliberately disabled for React Native compatibility). If the environment's Math.random() ever returns a nonconformant value — ≥ 1.0 or NaNnextUint32() produces a value outside [0, 2^32), and UUID.fromFieldsV7 throws RangeError: invalid field value.

This is not hypothetical. On React Native Android, Hermes implements Math.random with C++ std::uniform_real_distribution (facebook/hermes#1169), which is documented to occasionally return its upper bound (1.0) in most standard-library implementations. In our production fintech app (posthog-react-native 4.61.4 / @posthog/core 1.46.7) affected devices hit the throw inside PromiseQueue.add → uuidv7() during the SDK's internal event-queue flush — a code path the host app cannot wrap in a try/catch — producing a fatal startup crash loop ~1.5 s after every cold start (Crashlytics: repeated RangeError: invalid field value with the fromFieldsV7 → generateOrAbortCore → generateOrResetCore → generate → uuidv7 → add → addPendingPromise → flushInternal → flush JS stack). Reinstalling the app does not help; those users are hard-locked out. Related older report: #710.

Changes

  • packages/core/src/vendor/uuidv7.ts: clamp getDefaultRandom().nextUint32() with >>> 0 (ToUint32). A misbehaving Math.random now degrades entropy for that one id (NaN → 0, ≥ 2^32 wraps) instead of throwing. The timestamp bits are untouched and every downstream field (randA ≤ 0xfff, randBHi < 2^30, randBLo ≤ 0xffffffff, counter ≤ MAX_COUNTER) is mathematically in range, so generated ids remain spec-valid UUIDv7. Conformant engines are unaffected: for in-range inputs >>> 0 is an identity.
  • packages/core/src/vendor/uuidv7.spec.ts: new spec asserting V7Generator.generate() returns a well-formed v7 UUID when Math.random() is stubbed to 1.0, 1.5, and NaN (all three throw today), plus a sanity check with the real Math.random.
  • Changeset included (@posthog/core: patch).

Release info Sub-libraries affected

Libraries affected

The changed package is @posthog/core, so every SDK consuming it picks the fix up via the changesets updateInternalDependencies flow. The consumer where the crash was observed:

  • All of them
  • posthog-js (web)
  • posthog-js-lite (web lite)
  • posthog-node
  • posthog-react-native
  • @posthog/react-native-plugin
  • @posthog/react
  • @posthog/ai
  • @posthog/convex
  • @posthog/next
  • @posthog/nextjs-config
  • @posthog/nuxt
  • @posthog/openfeature-node-provider
  • @posthog/openfeature-web-provider
  • @posthog/rollup-plugin
  • @posthog/webpack-plugin
  • @posthog/types
  • @posthog/browser-common

Checklist

  • Tests for new code
  • Accounted for the impact of any changes across different platforms (identity for conformant Math.random; only nonconforming values change behavior, from throw to wrap)
  • Accounted for backwards compatibility of any changes (no breaking changes!)
  • Took care not to unnecessarily increase the bundle size (one >>> 0)

If releasing new changes

  • Changeset file added (.changeset/fix-uuidv7-clamp-nextuint32.md — authored manually, same format as pnpm changeset output)

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

  • Authored with Claude Code during a production Crashlytics investigation directed by @shahidrogers (who previously attempted the broader fix(core): keep UUIDv7 valid by sanitizing generator inputs #4064, closed by the stale bot).
  • Decisions: an earlier approach sanitized the timestamp/inputs across the generator (fix(core): keep UUIDv7 valid by sanitizing generator inputs #4064); this PR is deliberately narrower — since 1.46.x already validates unixTsMs in generateOrAbortCore with a distinct error message, production stacks prove the failing fields are RNG-derived, so only nextUint32 needs guarding.
  • Verified by running the patched generator (tsx) with Math.random stubbed to 1.0 / 1.5 / NaN — all previously threw, all now produce regex-valid v7 UUIDs. Full monorepo test suite not run locally; relying on CI.

A nonconformant Math.random() that returns >= 1 or NaN (e.g. Hermes on
Android, whose Math.random is built on C++ std::uniform_real_distribution
— documented to occasionally return its upper bound) pushes the uuidv7
random fields out of range, making fromFieldsV7 throw
'RangeError: invalid field value' on every capture and crash-looping RN
apps during the internal event-queue flush. Clamp nextUint32() with
'>>> 0' so a bad random value degrades entropy for that id instead of
throwing; timestamp bits untouched, ids remain spec-valid UUIDv7.
@shahidrogers
shahidrogers requested a review from a team as a code owner August 24, 2026 04:05
@shahidrogers
shahidrogers marked this pull request as draft August 24, 2026 04:06
@marandaneto
marandaneto marked this pull request as ready for review August 24, 2026 07:58
@marandaneto

Copy link
Copy Markdown
Member

thanks @shahidrogers and good catch

@@ -0,0 +1,5 @@
---
'@posthog/core': patch

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: This changeset only releases @posthog/core, so consumers can publish without requiring the fixed core version. Please also add patch entries for every package whose runtime directly uses this UUIDv7 generator, inherits the core capture path, or ships the affected posthog-node: @posthog/ai, @posthog/convex, @posthog/mcp, @posthog/next, @posthog/nuxt, posthog-js-lite, posthog-node, and posthog-react-native.

@marandaneto

Copy link
Copy Markdown
Member

this lib is vendored so LiosK/uuidv7#17

@marandaneto

Copy link
Copy Markdown
Member

@shahidrogers

Commits must have verified signatures.

you need to sign your commits and force push so we can merge your pr, thanks again

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.

2 participants