BUG #19743: pgcrypto crypt(): sha-crypt rounds= silently wraps modulo 2^32

From: PG Bug reporting form <noreply(at)postgresql(dot)org>
To: pgsql-bugs(at)lists(dot)postgresql(dot)org
Cc: kehan5800(at)gmail(dot)com
Subject: BUG #19743: pgcrypto crypt(): sha-crypt rounds= silently wraps modulo 2^32
Date: 2026-10-04 05:10:34
Message-ID: 19743-c48782d257b89819@postgresql.org
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-bugs

The following bug has been logged on the website:

Bug reference: 19743
Logged by: Ke
Email address: kehan5800(at)gmail(dot)com
PostgreSQL version: 18.6
Operating system: ubuntu 22
Description:

px_crypt_shacrypt() (contrib/pgcrypto/crypt-sha.c:189 on master) parses the
rounds= option of a caller-supplied sha256crypt/sha512crypt salt with

int srounds = strtoint(num, &endp, 10);

if (*endp != '$')
ereport(ERROR, ... "could not parse salt options");

strtoint() (src/common/string.c) signals overflow only by setting errno to
ERANGE and returns the value cast to int anyway. This caller never looks at
errno, so srounds is the request after a signed 32-bit truncation, and the
clamp-with-NOTICE block that follows (crypt-sha.c:209-226) operates on that
truncated number. The comment above that block says "We don't do this
silently and print a NOTICE in such a case", but for inputs >= 2^32 the
"exceeds maximum" branch can never fire.

Reproduction (any build with --with-openssl):

CREATE EXTENSION pgcrypto;

-- 2^32 + 1000: computed with 1000 rounds, no NOTICE at all
SELECT crypt('password', '$5$rounds=4294968296$saltsalt$');
crypt
---------------------------------------------------------------------
$5$rounds=1000$saltsalt$azOwbpkvuuBKkE82dQPwTsQE8JyT9Fflpr9aKid3aT9

SELECT crypt('password', '$5$rounds=1000$saltsalt$')
= crypt('password', '$5$rounds=4294968296$saltsalt$') AS same_hash;
same_hash
-----------
t

-- 2^32 - 1: the NOTICE names a number the caller never wrote
SELECT crypt('password', '$5$rounds=4294967295$saltsalt$');
NOTICE: rounds=-1 is below supported value (1000), using 1000 instead

-- control: a value that fits in int is clamped and warned about
SET statement_timeout = '2s';
SELECT crypt('password', '$5$rounds=1000000000$saltsalt$');
NOTICE: rounds=1000000000 exceeds maximum supported value (999999999),
using 999999999 instead

The effective count is read back from the returned hash string, which
records the rounds actually used. Walking the 2^32 boundary on master:

requested NOTICE effective
4294966296 "rounds=-1000 is below supported value" 1000
4294967295 "rounds=-1 is below supported value" 1000
4294967296 "rounds=0 is below supported value" 1000
4294967297 "rounds=1 is below supported value" 1000
4294968295 "rounds=999 is below supported value" 1000
4294968296 (none) 1000
4294968300 (none) 1004
8589936592 (none) 2000

Expected: a rounds= value that does not fit in int is treated as out of
range, i.e. clamped to PX_SHACRYPT_ROUNDS_MAX (999999999) with the existing
"exceeds maximum supported value" NOTICE (or rejected), and the NOTICEs
report the value the caller supplied.

Actual: the value is reduced modulo 2^32; when the remainder lands inside
[1000, 999999999] the hash is computed at that work factor with no
diagnostic at all, so rounds=4294968296 yields a 1000-round hash, the
minimum.

gen_salt('sha256crypt', n) cannot produce such a salt (its argument is
int4), so the path is a hand-written or imported salt string, i.e. the path
used to verify or re-hash a hash produced elsewhere. Drepper's reference
implementation, which the comment says is being kept compatible, parses
rounds with strtoul() and clamps; libxcrypt (Ubuntu 22.04's crypt(3))
refuses this setting string and returns the failure token "*0". Neither
reduces it modulo 2^32.

Suggested fix: check errno around the call, as other strtoint() callers do:

errno = 0;
srounds = strtoint(num, &endp, 10);
if (*endp != '$')
ereport(ERROR, ...);
if (errno == ERANGE)
srounds = (num[0] == '-') ? PX_SHACRYPT_ROUNDS_MIN
: PX_SHACRYPT_ROUNDS_MAX;

(or parse into a wider type) and print the supplied string rather than the
truncated int in the two NOTICEs.

Related: the integer options of pgp_sym_encrypt() (s2k-count, s2k-mode,
compress-level, ...) are parsed with atoi() and wrap the same way; that is
already being discussed under BUG #19714, so it is only mentioned here.

Browse pgsql-bugs by date

  From Date Subject
Next Message PG Bug reporting form 2026-10-04 05:16:30 BUG #19744: contrib/seg output truncates to 6 significant digits, breaking dump/restore
Previous Message Tom Lane 2026-10-04 01:00:09 Re: BUG #19742: `INTERSECT` under a `UNION ALL` with an empty arm fails with "could not find pathkey item t"