v2-0002-Reject-pre-epoch-timestamps-in-uuidv7-interval.patch

application/octet-stream

Filename: v2-0002-Reject-pre-epoch-timestamps-in-uuidv7-interval.patch
Type: application/octet-stream
Part: 2
Message: Re: uuidv7 improperly accepts dates before 1970-01-01

Patch

Same data as JSON: GET /api/v1/attachments/:id/patch the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes. API reference →
Format: format-patch
Series: patch v2-0002
Subject: Reject pre-epoch timestamps in uuidv7(interval)
File+
doc/src/sgml/func/func-uuid.sgml 6 0
src/backend/utils/adt/uuid.c 20 2
src/test/regress/expected/uuid.out 8 0
src/test/regress/sql/uuid.sql 6 0
From c6c5bf80a4988af23caaf4e8f620cb65c119b3b5 Mon Sep 17 00:00:00 2001
From: Baji Shaik <baji.pgdev@gmail.com>
Date: Wed, 24 Jun 2026 18:07:03 -0500
Subject: [PATCH v2 2/3] Reject pre-epoch timestamps in uuidv7(interval)

uuidv7() with a large negative interval silently produced UUIDs whose
timestamp wrapped around to the far future, since UUID version 7 uses
an unsigned 48-bit millisecond field that cannot represent dates before
the Unix epoch.

Fix by using pg_add_s64_overflow() for the epoch conversion (so that
very large intervals that overflow int64 produce a clear error instead
of wrapping to the wrong sign), then rejecting negative results.

Also document the valid timestamp range for the shift parameter.

Author: Baji Shaik <baji.pgdev@gmail.com>
Discussion: https://postgr.es/m/18F007D6-1A33-48C8-BA51-E7A858DE0C89%40thebuild.com
---
 doc/src/sgml/func/func-uuid.sgml   |  6 ++++++
 src/backend/utils/adt/uuid.c       | 22 ++++++++++++++++++++--
 src/test/regress/expected/uuid.out |  8 ++++++++
 src/test/regress/sql/uuid.sql      |  6 ++++++
 4 files changed, 40 insertions(+), 2 deletions(-)

diff --git a/doc/src/sgml/func/func-uuid.sgml b/doc/src/sgml/func/func-uuid.sgml
index cfd42433a95..41fc5739c31 100644
--- a/doc/src/sgml/func/func-uuid.sgml
+++ b/doc/src/sgml/func/func-uuid.sgml
@@ -83,6 +83,12 @@
         parameter <parameter>shift</parameter> will shift the computed
         timestamp by the given <type>interval</type>.
         Infinite interval values are not accepted.
+        The shifted timestamp must fall between the Unix epoch
+        (1970-01-01 00:00:00 UTC) and approximately
+        10889-08-02 05:31:50.655 UTC, which is the range representable
+        by UUID version 7's 48-bit millisecond timestamp field.
+        An error is raised if the resulting timestamp is outside this
+        range.
        </para>
        <para>
         <literal>uuidv7()</literal>
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 1197cb6c027..ca5dbca8ac3 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -17,6 +17,7 @@
 #include <time.h>				/* for clock_gettime() */
 
 #include "common/hashfn.h"
+#include "common/int.h"
 #include "lib/hyperloglog.h"
 #include "libpq/pqformat.h"
 #include "port/pg_bswap.h"
@@ -694,8 +695,25 @@ 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;
+	/*
+	 * Convert a TimestampTz value back to a UNIX epoch timestamp.  Use
+	 * overflow-safe addition since large intervals can exceed int64 range.
+	 */
+	if (pg_add_s64_overflow(ts,
+							(int64) (POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) *
+							SECS_PER_DAY * USECS_PER_SEC,
+							&us))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
+				 errmsg("timestamp out of range for UUID version 7"),
+				 errdetail("The shifted timestamp overflows the representable range.")));
+
+	/* UUID v7 uses an unsigned 48-bit millisecond field; reject pre-epoch */
+	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 does not support timestamps before the Unix epoch (1970-01-01 00:00:00 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 28706d77abc..72730825cda 100644
--- a/src/test/regress/expected/uuid.out
+++ b/src/test/regress/expected/uuid.out
@@ -268,6 +268,14 @@ 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 does not support timestamps before the Unix epoch (1970-01-01 00:00:00 UTC).
+-- 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:  The shifted timestamp overflows the representable range.
 -- 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 01bbc8558fc..eb1a12949c5 100644
--- a/src/test/regress/sql/uuid.sql
+++ b/src/test/regress/sql/uuid.sql
@@ -144,6 +144,12 @@ 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: large future intervals that overflow epoch conversion are rejected
+SELECT uuidv7('292230 years'::interval);
+
 -- extract functions
 
 -- version
-- 
2.50.1 (Apple Git-155)