public inbox for [email protected]  
help / color / mirror / Atom feed
uuidv7 improperly accepts dates before 1970-01-01
2+ messages / 2 participants
[nested] [flat]

* uuidv7 improperly accepts dates before 1970-01-01
@ 2026-04-25 00:19  Christophe Pettus <[email protected]>
  0 siblings, 1 reply; 2+ messages in thread

From: Christophe Pettus @ 2026-04-25 00:19 UTC (permalink / raw)
  To: [email protected]

Hii,

When playing around with UUIDv7s, I discovered that it accepts this:

xof=# SELECT uuidv7(INTERVAL '-1000 years');
                uuidv7                
--------------------------------------
 e4ea52a0-bda1-7121-8f1f-3d9bb3d9a76e
(1 row)

But RFC 9562 defines the time field as an unsigned number of milliseconds since Unix epoch, so timestamps earlier than that should be rejected.  "Don't do that" is one answer, but for good hygiene, here's a patch that adds a < 0 check and a regression test.  Applies cleanly to HEAD, make check passes.



Attachments:

  [application/octet-stream] 0001-uuidv7-fix-negative-shift.diff (2.5K, 2-0001-uuidv7-fix-negative-shift.diff)
  download | inline diff:
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 6ee3752ac78..42ea29aafe2 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -690,6 +690,19 @@ uuidv7_interval(PG_FUNCTION_ARGS)
 	/* Convert a TimestampTz value back to an UNIX epoch timestamp */
 	us = ts + (POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * SECS_PER_DAY * USECS_PER_SEC;
 
+	/*
+	 * Per RFC 9562 the UUIDv7 timestamp is an unsigned count of milliseconds
+	 * since 1970-01-01 UTC; a shift that lands before the Unix epoch has no
+	 * valid encoding.  Reject it explicitly -- otherwise the negative int64
+	 * would be silently converted to a huge uint64 at the uint64 parameter
+	 * of generate_uuidv7() and packed into the timestamp bytes as garbage.
+	 */
+	if (us < 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("timestamp out of range for UUID version 7"),
+				 errdetail("UUID version 7 timestamps must not be before 1970-01-01 UTC.")));
+
 	/* 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 9c5dda9e9ab..a29634ba5e6 100644
--- a/src/test/regress/expected/uuid.out
+++ b/src/test/regress/expected/uuid.out
@@ -261,6 +261,17 @@ SELECT y, ts, prev_ts FROM uuidts WHERE ts < prev_ts;
 ---+----+---------
 (0 rows)
 
+-- A negative shift that stays on or after 1970-01-01 must still succeed.
+SELECT uuid_extract_version(uuidv7(INTERVAL '-1 day'));  -- 7
+ uuid_extract_version 
+----------------------
+                    7
+(1 row)
+
+-- A shift before 1970-01-01 has no RFC 9562 encoding and must be rejected.
+SELECT uuidv7(INTERVAL '-1000 years');
+ERROR:  timestamp out of range for UUID version 7
+DETAIL:  UUID version 7 timestamps must not be before 1970-01-01 UTC.
 -- 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 8cc2ad40614..0720a910ece 100644
--- a/src/test/regress/sql/uuid.sql
+++ b/src/test/regress/sql/uuid.sql
@@ -140,6 +140,12 @@ WITH uuidts AS (
 )
 SELECT y, ts, prev_ts FROM uuidts WHERE ts < prev_ts;
 
+-- A negative shift that stays on or after 1970-01-01 must still succeed.
+SELECT uuid_extract_version(uuidv7(INTERVAL '-1 day'));  -- 7
+
+-- A shift before 1970-01-01 has no RFC 9562 encoding and must be rejected.
+SELECT uuidv7(INTERVAL '-1000 years');
+
 -- extract functions
 
 -- version


^ permalink  raw  reply  [nested|flat] 2+ messages in thread

* Re: uuidv7 improperly accepts dates before 1970-01-01
@ 2026-04-25 12:26  Andrey Borodin <[email protected]>
  parent: Christophe Pettus <[email protected]>
  0 siblings, 0 replies; 2+ messages in thread

From: Andrey Borodin @ 2026-04-25 12:26 UTC (permalink / raw)
  To: Christophe Pettus <[email protected]>; +Cc: PostgreSQL mailing lists <[email protected]>



> On 25 Apr 2026, at 05:19, Christophe Pettus <[email protected]> wrote:
> 
> "Don't do that" is one answer, but for good hygiene, here's a patch that adds a < 0 check and a regression test.

Hi Christophe!

We intentionally left ability to overflow unix_ts_ms bits. In some cases one might want to
intentionally break time locality by using construction like SELECT uuidv7(INTERVAL '1000 years' * shard_id);
This will give time locality for UUIDs generated on each shard. We consulted with RFC authors
about this feature, and they confirmed that shifting time is compliant with RFC wording.
We wrote the specific test that ensures vast space for shift, but not unlimited.

Time shifting would become a footgun if we throw an exception when overflown.
If you use SELECT uuidv7(INTERVAL '-1000 years'); for generating identifiers, they will still be unique and
time-local, and more over - they will be ascending for a single backend. So no documented guarantees
are broken.

Thank you!


Best regards, Andrey Borodin.





^ permalink  raw  reply  [nested|flat] 2+ messages in thread


end of thread, other threads:[~2026-04-25 12:26 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-04-25 00:19 uuidv7 improperly accepts dates before 1970-01-01 Christophe Pettus <[email protected]>
2026-04-25 12:26 ` Andrey Borodin <[email protected]>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox