From fc874e6f69c9bb05ffc836ce64bcfbd208662afd Mon Sep 17 00:00:00 2001 From: Baji Shaik Date: Fri, 3 Jul 2026 10:47:14 -0500 Subject: [PATCH v4 2/2] Reject out-of-range timestamps in uuidv7(interval) uuidv7() with a large negative or positive interval silently produced UUIDs whose timestamp was outside the range representable by UUID version 7's 48-bit millisecond field. Fix by pre-computing the valid timestamp window in PostgreSQL-epoch units (UUIDV7_MIN_TIMESTAMP and UUIDV7_MAX_TIMESTAMP) and validating the shifted TimestampTz before converting to Unix epoch. This avoids overflow concerns entirely and consolidates the lower-bound and upper-bound checks into a single condition. Also document the valid timestamp range for the shift parameter. Author: Baji Shaik Discussion: --- doc/src/sgml/func/func-uuid.sgml | 5 +++++ src/backend/utils/adt/uuid.c | 34 ++++++++++++++++++++++++++++-- src/test/regress/expected/uuid.out | 12 +++++++++++ src/test/regress/sql/uuid.sql | 9 ++++++++ 4 files changed, 58 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/func/func-uuid.sgml b/doc/src/sgml/func/func-uuid.sgml index cfd42433a95..89fd5781bcc 100644 --- a/doc/src/sgml/func/func-uuid.sgml +++ b/doc/src/sgml/func/func-uuid.sgml @@ -83,6 +83,11 @@ parameter shift will shift the computed timestamp by the given interval. Infinite interval values are not accepted. + The shifted timestamp must fall within the range supported by + UUID version 7's 48-bit millisecond timestamp field: from + 1970-01-01 00:00:00 UTC to approximately year 10889. + An error is raised if the resulting timestamp is outside this + range. uuidv7() diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c index ba3c30b8ebc..26b8bfeecf6 100644 --- a/src/backend/utils/adt/uuid.c +++ b/src/backend/utils/adt/uuid.c @@ -33,6 +33,25 @@ #define NS_PER_US INT64CONST(1000) #define US_PER_MS INT64CONST(1000) +/* + * The offset between the PostgreSQL epoch (2000-01-01) and the Unix epoch + * (1970-01-01) in microseconds. + */ +#define UUIDV7_EPOCH_OFFSET \ + ((int64) (POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * SECS_PER_DAY * USECS_PER_SEC) + +/* + * Valid timestamp range for UUID version 7, expressed in PostgreSQL-epoch + * microseconds. UUID v7 uses a 48-bit unsigned millisecond field relative + * to the Unix epoch, so the representable window is [1970-01-01, ~10889]. + * + * By pre-computing these bounds in PostgreSQL-epoch units we can validate + * the shifted TimestampTz directly, avoiding overflow during conversion. + */ +#define UUIDV7_MIN_TIMESTAMP (-UUIDV7_EPOCH_OFFSET) +#define UUIDV7_MAX_TIMESTAMP \ + (((INT64CONST(1) << 48) - 1) * US_PER_MS - UUIDV7_EPOCH_OFFSET) + /* * UUID version 7 uses 12 bits in "rand_a" to store 1/4096 (or 2^12) fractions of * sub-millisecond. While most Unix-like platforms provide nanosecond-precision @@ -712,8 +731,19 @@ uuidv7_interval(PG_FUNCTION_ARGS) TimestampTzGetDatum(ts), IntervalPGetDatum(shift))); - /* Convert a TimestampTz value back to an UNIX epoch timestamp */ - us = ts + (POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * SECS_PER_DAY * USECS_PER_SEC; + /* + * Reject timestamps outside the range representable by UUID version 7's + * 48-bit millisecond field. We compare in PostgreSQL-epoch units so that + * the subsequent conversion to Unix-epoch microseconds cannot overflow. + */ + if (ts < UUIDV7_MIN_TIMESTAMP || ts > UUIDV7_MAX_TIMESTAMP) + ereport(ERROR, + (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), + errmsg("timestamp out of range for UUID version 7"), + errdetail("UUID version 7 supports timestamps from 1970-01-01 to approximately year 10889."))); + + /* Convert the TimestampTz value to a Unix-epoch timestamp in usec */ + us = ts + UUIDV7_EPOCH_OFFSET; /* Generate an UUIDv7 */ uuid = generate_uuidv7(us / US_PER_MS, (us % US_PER_MS) * NS_PER_US + ns % NS_PER_US); diff --git a/src/test/regress/expected/uuid.out b/src/test/regress/expected/uuid.out index 0935651f1eb..286507101d8 100644 --- a/src/test/regress/expected/uuid.out +++ b/src/test/regress/expected/uuid.out @@ -275,6 +275,18 @@ DETAIL: UUID version 7 does not support infinite intervals. SELECT uuidv7('-infinity'::interval); ERROR: interval out of range for UUID version 7 DETAIL: UUID version 7 does not support infinite intervals. +-- uuidv7: timestamps before Unix epoch are rejected +SELECT uuidv7('-1000 years'::interval); +ERROR: timestamp out of range for UUID version 7 +DETAIL: UUID version 7 supports timestamps from 1970-01-01 to approximately year 10889. +-- uuidv7: timestamps beyond 48-bit ms field (~year 10889) are rejected +SELECT uuidv7('9000 years'::interval); +ERROR: timestamp out of range for UUID version 7 +DETAIL: UUID version 7 supports timestamps from 1970-01-01 to approximately year 10889. +-- uuidv7: large future intervals that overflow epoch conversion are rejected +SELECT uuidv7('292230 years'::interval); +ERROR: timestamp out of range for UUID version 7 +DETAIL: UUID version 7 supports timestamps from 1970-01-01 to approximately year 10889. -- extract functions -- version SELECT uuid_extract_version('11111111-1111-5111-8111-111111111111'); -- 5 diff --git a/src/test/regress/sql/uuid.sql b/src/test/regress/sql/uuid.sql index 9ee64a5fa9c..525ecb1a946 100644 --- a/src/test/regress/sql/uuid.sql +++ b/src/test/regress/sql/uuid.sql @@ -147,6 +147,15 @@ SELECT y, ts, prev_ts FROM uuidts WHERE ts < prev_ts; SELECT uuidv7('infinity'::interval); SELECT uuidv7('-infinity'::interval); +-- uuidv7: timestamps before Unix epoch are rejected +SELECT uuidv7('-1000 years'::interval); + +-- uuidv7: timestamps beyond 48-bit ms field (~year 10889) are rejected +SELECT uuidv7('9000 years'::interval); + +-- uuidv7: large future intervals that overflow epoch conversion are rejected +SELECT uuidv7('292230 years'::interval); + -- extract functions -- version -- 2.50.1 (Apple Git-155)