BUG #19590: to_date/to_timestamp "Y,YYY" accepts out-of-range values
Hackorum builds and tests every patch posted to the lists, not only commitfest submissions. This is Hackorum's own CI rather than the PostgreSQL project's, and it is still under testing - please report anything that looks wrong.
You can run a PostgreSQL built from this patch straight from Docker, with no checkout and no build:
docker run --rm -p 5432:5432 ghcr.io/hackorum-dev/postgres-patch:t253265psql -h localhost -U postgresBuilt from patchset v2 (message #2), September 20, 2026 at 09:45 AM.
Every patchset is also pushed to a branch of our PostgreSQL fork, so you can check out the same tree CI built. Without a PostgreSQL checkout:
git clone --branch t253265_2 https://github.com/hackorum-dev/postgres.gitIn a checkout you already have, add the fork once:
git remote add hackorum https://github.com/hackorum-dev/postgres.gitthen, for this patchset and every later one:
git fetch hackorum t253265_2 && git checkout t253265_2Patchset v2 (message #2) is on t253265_2
The following bug has been logged on the website:
Bug reference: 19590
Logged by: Michael Malis
Email address: malis@pgrust.com
PostgreSQL version: 18.4
Operating system: Debian 18.4-1.pgdg13+1, aarch64
Description:
The Y,YYY template field parses its millennia component with a bare
sscanf(..., "%d", ...), which silently truncates values too large for int
instead of rejecting them, so out-of-range input yields a wrong year:
SELECT to_date('4294969320,024','Y,YYY'); -- 2024024-01-01 (expected:
error)
SELECT to_date('-4294965272,024','Y,YYY'); -- 2024024-01-01 (expected:
error)
4294969320 is 2^32 + 2024, so it truncates to 2024 and is read as 2024
millennia; any multiple of 2^32 works, and %d accepts a sign, so wrapped
negatives too. to_timestamp() shares the code path. Every other numeric
field rejects this:
SELECT to_date('4294969320','YYYY');
-- ERROR: value for "YYYY" in source string is out of range
Cause: DCH_Y_YYY is the only numeric field using raw sscanf; the others go
through from_char_parse_int_len(), which range-checks with strtol/ERANGE.
The existing pg_mul_s32_overflow guard in DCH_Y_YYY runs too late. %d has
already discarded the magnitude.
Suggested fix: after the sscanf, re-scan the millennia field with strtol and
reject ERANGE or out-of-int-range values, matching
from_char_parse_int_len():
errno = 0;
lval = strtol(s, &endptr, 10);
if (errno == ERANGE || lval < INT_MIN || lval > INT_MAX)
ereturn(escontext,,
(errcode(ERRCODE_DATETIME_FIELD_OVERFLOW),
errmsg("value for \"%s\" in source string is out of range",
"Y,YYY"),
errdetail("Value must be in the range %d to %d.", INT_MIN,
INT_MAX)));
strtol skips leading whitespace and stops at the comma exactly as %d does,
so this only adds a rejection path; the ERANGE test covers 32-bit-long
platforms where strtol saturates.
Hi, Michael!
Thanks for report.
DCH_Y_YYY used to parse millennia with sscanf("%d"). That silently
truncates values outside int range, so
```
SELECT to_date('4294969320,024', 'Y,YYY'); -- 2^32+2024
SELECT to_date('-4294965272,024', 'Y,YYY');
```
used to return 2024024-01-01 instead of erroring.
Other numeric fields already reject this via
from_char_parse_int_len(); the pg_mul_s32_overflow() check in
DCH_Y_YYY runs too late, after %d has discarded the magnitude.
This patch replaces the millennia %d with a single strtol(), using
the same ERANGE / INT_MIN / INT_MAX checks as from_char_parse_int_len().
The years part remains sscanf("%03d"): the field width limits the
conversion to three characters, so the value always fits in int. The
existing mul/add overflow checks are unchanged (they still catch
cases like 1000000000,999).
Y,YYY has two pieces: variable-width millennia up to a comma, then
three year digits. We parse and validate each separately.
Patch with tests, attached.
пт, 31 июл. 2026 г. в 14:47, PG Bug reporting form <noreply@postgresql.org>:
The following bug has been logged on the website:
Bug reference: 19590
Logged by: Michael Malis
Email address: malis@pgrust.com
PostgreSQL version: 18.4
Operating system: Debian 18.4-1.pgdg13+1, aarch64
Description:The Y,YYY template field parses its millennia component with a bare
sscanf(..., "%d", ...), which silently truncates values too large for int
instead of rejecting them, so out-of-range input yields a wrong year:SELECT to_date('4294969320,024','Y,YYY'); -- 2024024-01-01 (expected:
error)
SELECT to_date('-4294965272,024','Y,YYY'); -- 2024024-01-01 (expected:
error)4294969320 is 2^32 + 2024, so it truncates to 2024 and is read as 2024
millennia; any multiple of 2^32 works, and %d accepts a sign, so wrapped
negatives too. to_timestamp() shares the code path. Every other numeric
field rejects this:SELECT to_date('4294969320','YYYY');
-- ERROR: value for "YYYY" in source string is out of rangeCause: DCH_Y_YYY is the only numeric field using raw sscanf; the others go
through from_char_parse_int_len(), which range-checks with strtol/ERANGE.
The existing pg_mul_s32_overflow guard in DCH_Y_YYY runs too late. %d has
already discarded the magnitude.Suggested fix: after the sscanf, re-scan the millennia field with strtol
and
reject ERANGE or out-of-int-range values, matching
from_char_parse_int_len():errno = 0;
lval = strtol(s, &endptr, 10);
if (errno == ERANGE || lval < INT_MIN || lval > INT_MAX)
ereturn(escontext,,
(errcode(ERRCODE_DATETIME_FIELD_OVERFLOW),
errmsg("value for \"%s\" in source string is out of range",
"Y,YYY"),
errdetail("Value must be in the range %d to %d.", INT_MIN,
INT_MAX)));strtol skips leading whitespace and stops at the comma exactly as %d does,
so this only adds a rejection path; the ERANGE test covers 32-bit-long
platforms where strtol saturates.
--
Regards,
Rachitskiy Andrey