Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1vwxIa-006Pc5-0V for pgsql-hackers@arkaria.postgresql.org; Mon, 02 Mar 2026 07:02:36 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1vwxIZ-00FnHA-0L for pgsql-hackers@arkaria.postgresql.org; Mon, 02 Mar 2026 07:02:35 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1vwxIY-00FnH2-01 for pgsql-hackers@lists.postgresql.org; Mon, 02 Mar 2026 07:02:34 +0000 Received: from fhigh-b4-smtp.messagingengine.com ([202.12.124.155]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.98.2) (envelope-from ) id 1vwxIT-000000027ba-2ksH for pgsql-hackers@lists.postgresql.org; Mon, 02 Mar 2026 07:02:33 +0000 Received: from phl-compute-08.internal (phl-compute-08.internal [10.202.2.48]) by mailfhigh.stl.internal (Postfix) with ESMTP id B09007A0064 for ; Mon, 2 Mar 2026 02:02:29 -0500 (EST) Received: from phl-frontend-04 ([10.202.2.163]) by phl-compute-08.internal (MEProxy); Mon, 02 Mar 2026 02:02:29 -0500 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=paquier.xyz; h= cc:content-type:content-type:date:date:from:from:in-reply-to :in-reply-to:message-id:mime-version:references:reply-to:subject :subject:to:to; s=fm1; t=1772434949; x=1772521349; bh=kM0YI6emds clgsek0NhepGVuzwCLbHQbVxBFCqwfQjk=; b=MYKlHjKV4N4m55R+hEq2eCwl+7 Uk67g9EkXSey+UDvSTpIrmnalVXr1pj+a3z8ltqNq/NXQIKefV7kIq4GWCk6xMzm kA7oJpQahwq0Hjy0LX8BqSMYL7svEVzBc9GZpSJ/2qqTscq/iBuoUpI6FPLfUN79 RMGnmArG4O/SP4egZtAlnhkX8ABBpbhaZdWLplZnbwlhlviQ2hU99CBmDD/7obZv GEa54yzmoUfPHJmGga1Tb+c48nrB19O8/k/XS/SEMWVlQTiJTP2Yy8gfYMg4ITC7 WFRUcreQnfprwiPL5H3QVp9DqyCvw5+1rQdTg+jc/LILajRBi4yxZSRkkgHw== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:content-type:content-type:date:date :feedback-id:feedback-id:from:from:in-reply-to:in-reply-to :message-id:mime-version:references:reply-to:subject:subject:to :to:x-me-proxy:x-me-sender:x-me-sender:x-sasl-enc; s=fm1; t= 1772434949; x=1772521349; bh=kM0YI6emdsclgsek0NhepGVuzwCLbHQbVxB FCqwfQjk=; b=zKSZ6VMfPQbNHQUTR5b9ffRziWa9+0XKzyHw7mLA2ZtWdAITRCw qE46qcvFky1Q7FD+6wToKiqYlZIA2aA8m0d9BighoNcQRey4nwKdk/fbq863ChXu ZLWZFqSxOyOnFBh66d4wNTiGQxWgb47uOOIwPtBa8q8cRzMIc60qSdtGLxBYqvDA IyXkBaVL7JXu2v5GjP9CvO81gaCHrezwf7ksDliGZdqmkJjM58xmm62ETVq0oGCa 1ND7OkmHksVPgUry8kqSf83FuXFKvFN9HkQSEkYKzsDNvbSbaJ/uv+kd2kYcgxDF 9MIF5BVhezYlhnhV20RHi6ZyrHLm6yztNYQ== X-ME-Sender: X-ME-Received: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgeefgedrtddtgddvheejtddvucetufdoteggodetrf dotffvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfurfetoffkrfgpnffqhgenuceu rghilhhouhhtmecufedttdenucesvcftvggtihhpihgvnhhtshculddquddttddmnegfrh hlucfvnfffucdljedtmdenucfjughrpeffhffvuffkfhggtggujgesghdtreertddtvden ucfhrhhomhepofhitghhrggvlhcurfgrqhhuihgvrhcuoehmihgthhgrvghlsehprghquh hivghrrdighiiiqeenucggtffrrghtthgvrhhnpedvgeduuefhtdeuleettdevjeehheei veeuieegleetgeeljeelieeuieehgeevhfenucevlhhushhtvghrufhiiigvpedtnecurf grrhgrmhepmhgrihhlfhhrohhmpehmihgthhgrvghlsehprghquhhivghrrdighiiipdhn sggprhgtphhtthhopedupdhmohguvgepshhmthhpohhuthdprhgtphhtthhopehpghhsqh hlqdhhrggtkhgvrhhssehlihhsthhsrdhpohhsthhgrhgvshhqlhdrohhrgh X-ME-Proxy: Feedback-ID: i0fe9450f:Fastmail Received: by mail.messagingengine.com (Postfix) with ESMTPA for ; Mon, 2 Mar 2026 02:02:28 -0500 (EST) Date: Mon, 2 Mar 2026 16:02:24 +0900 From: Michael Paquier To: Postgres hackers Subject: Re: Non-compliant SASLprep implementation for ASCII characters Message-ID: References: MIME-Version: 1.0 Content-Type: multipart/signed; micalg=pgp-sha512; protocol="application/pgp-signature"; boundary="O3pDOfuo+GwKMiFD" Content-Disposition: inline In-Reply-To: List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --O3pDOfuo+GwKMiFD Content-Type: multipart/mixed; boundary="Jg2Ue2lcaa8Mswn8" Content-Disposition: inline --Jg2Ue2lcaa8Mswn8 Content-Type: text/plain; charset=us-ascii Content-Disposition: inline On Fri, Feb 27, 2026 at 12:05:28PM +0900, Michael Paquier wrote: > - 0001 is a test suite that I have been relying on for some time, > introduced as the test module test_saslprep. One artifact that Heikki > has mentioned to me offline while discussing this tool is that we > could also have a check for the entire range of valid UTF8 codepoints > to make sure that we never return an empty password for all these > codepoints. This check is slightly expensive (3s on my laptop, which > is not bad still a bit expensive), so I have implemented that as a TAP > test controlled by a PG_TEST_EXTRA. The only exception for the empty > password case is the nul character, that we disallow in CREATE/ALTER > ROLE. This test suite also adds a test to cover 390b3cbbb2af with an > incomplete UTF8 sequence, as a nice bonus. While thinking more about this one, I have come up with a smarter query based on set_byte() to build a full range of byteas for the ASCII characters to check, leading to this simpler pattern: SELECT set_byte('\x00'::bytea, 0, a) FROM generate_series(0, 127); A second thing that I have adjusted is the output for non-printable characters, using a CASE/WHEN shortcut. Attached is an updated version of the patch set with these adjustments. -- Michael --Jg2Ue2lcaa8Mswn8 Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0001-test_saslprep-Add-test-module-to-stress-SASLprep.patch Content-Transfer-Encoding: quoted-printable =46rom 95b2aa062f88a2da953cf68e30dcd0d01d387455 Mon Sep 17 00:00:00 2001 =46rom: Michael Paquier Date: Mon, 2 Mar 2026 15:55:11 +0900 Subject: [PATCH v2 1/2] test_saslprep: Add test module to stress SASLprep This includes two functions: - test_saslprep(), that performs pg_saslprep on a bytea. - test_saslprep_ranges(), able to check for all valid ranges of UTF-8 endpoints how pg_saslprep() works. --- src/test/modules/Makefile | 1 + src/test/modules/meson.build | 1 + src/test/modules/test_saslprep/.gitignore | 4 + src/test/modules/test_saslprep/Makefile | 25 ++ src/test/modules/test_saslprep/README | 25 ++ .../test_saslprep/expected/test_saslprep.out | 152 ++++++++++ src/test/modules/test_saslprep/meson.build | 38 +++ .../test_saslprep/sql/test_saslprep.sql | 19 ++ .../test_saslprep/t/001_saslprep_ranges.pl | 38 +++ .../test_saslprep/test_saslprep--1.0.sql | 30 ++ .../modules/test_saslprep/test_saslprep.c | 277 ++++++++++++++++++ .../test_saslprep/test_saslprep.control | 5 + doc/src/sgml/regress.sgml | 10 + 13 files changed, 625 insertions(+) create mode 100644 src/test/modules/test_saslprep/.gitignore create mode 100644 src/test/modules/test_saslprep/Makefile create mode 100644 src/test/modules/test_saslprep/README create mode 100644 src/test/modules/test_saslprep/expected/test_saslprep.o= ut create mode 100644 src/test/modules/test_saslprep/meson.build create mode 100644 src/test/modules/test_saslprep/sql/test_saslprep.sql create mode 100644 src/test/modules/test_saslprep/t/001_saslprep_ranges.pl create mode 100644 src/test/modules/test_saslprep/test_saslprep--1.0.sql create mode 100644 src/test/modules/test_saslprep/test_saslprep.c create mode 100644 src/test/modules/test_saslprep/test_saslprep.control diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index 4ac5c84db439..ddce25cfd90f 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -45,6 +45,7 @@ SUBDIRS =3D \ test_regex \ test_resowner \ test_rls_hooks \ + test_saslprep \ test_shm_mq \ test_slru \ test_tidstore \ diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build index e2b3eef41368..645f09a2260c 100644 --- a/src/test/modules/meson.build +++ b/src/test/modules/meson.build @@ -46,6 +46,7 @@ subdir('test_rbtree') subdir('test_regex') subdir('test_resowner') subdir('test_rls_hooks') +subdir('test_saslprep') subdir('test_shm_mq') subdir('test_slru') subdir('test_tidstore') diff --git a/src/test/modules/test_saslprep/.gitignore b/src/test/modules/t= est_saslprep/.gitignore new file mode 100644 index 000000000000..5dcb3ff97235 --- /dev/null +++ b/src/test/modules/test_saslprep/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_saslprep/Makefile b/src/test/modules/tes= t_saslprep/Makefile new file mode 100644 index 000000000000..f74375ee4ab4 --- /dev/null +++ b/src/test/modules/test_saslprep/Makefile @@ -0,0 +1,25 @@ +# src/test/modules/test_saslprep/Makefile + +MODULE_big =3D test_saslprep +OBJS =3D \ + $(WIN32RES) \ + test_saslprep.o +PGFILEDESC =3D "test_saslprep - test SASLprep implementation" + +EXTENSION =3D test_saslprep +DATA =3D test_saslprep--1.0.sql + +REGRESS =3D test_saslprep + +TAP_TESTS =3D 1 + +ifdef USE_PGXS +PG_CONFIG =3D pg_config +PGXS :=3D $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir =3D src/test/modules/test_saslprep +top_builddir =3D ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_saslprep/README b/src/test/modules/test_= saslprep/README new file mode 100644 index 000000000000..37e32fdc5669 --- /dev/null +++ b/src/test/modules/test_saslprep/README @@ -0,0 +1,25 @@ +src/test/modules/test_saslprep + +Tests for SASLprep +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +This repository contains a test suite for stressing the SASLprep +implementation internal to PostgreSQL. + +It provides a set of functions able to check the validity of a SASLprep +operation for a single byte as well as a range of these, acting as thin +wrappers standing on top of pg_saslprep(). + +Running the tests +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +NOTE: A portion of the tests requires --enable-tap-tests, with +PG_TEST_EXTRA=3Dsaslprep set to run the TAP test suite. + +Run + make check PG_TEST_EXTRA=3Dsaslprep +or + make installcheck PG_TEST_EXTRA=3Dsaslprep + +The SQL test suite can run with or without PG_TEST_EXTRA=3Dsaslprep +set. diff --git a/src/test/modules/test_saslprep/expected/test_saslprep.out b/sr= c/test/modules/test_saslprep/expected/test_saslprep.out new file mode 100644 index 000000000000..f72dbffa0a11 --- /dev/null +++ b/src/test/modules/test_saslprep/expected/test_saslprep.out @@ -0,0 +1,152 @@ +-- Tests for SASLprep +CREATE EXTENSION test_saslprep; +-- Incomplete UTF-8 sequence. +SELECT test_saslprep('\xef'); + test_saslprep =20 +----------------- + (,INVALID_UTF8) +(1 row) + +-- Range of ASCII characters. +SELECT + CASE + WHEN a =3D 0 THEN '' + WHEN a < 32 THEN '' + WHEN a =3D 127 THEN '' + ELSE chr(a) END AS dat, + set_byte('\x00'::bytea, 0, a) AS byt, + test_saslprep(set_byte('\x00'::bytea, 0, a)) AS saslprep + FROM generate_series(0,127) AS a; + dat | byt | saslprep =20 +----------+------+------------------- + | \x00 | ("\\x",SUCCESS) + | \x01 | ("\\x01",SUCCESS) + | \x02 | ("\\x02",SUCCESS) + | \x03 | ("\\x03",SUCCESS) + | \x04 | ("\\x04",SUCCESS) + | \x05 | ("\\x05",SUCCESS) + | \x06 | ("\\x06",SUCCESS) + | \x07 | ("\\x07",SUCCESS) + | \x08 | ("\\x08",SUCCESS) + | \x09 | ("\\x09",SUCCESS) + | \x0a | ("\\x0a",SUCCESS) + | \x0b | ("\\x0b",SUCCESS) + | \x0c | ("\\x0c",SUCCESS) + | \x0d | ("\\x0d",SUCCESS) + | \x0e | ("\\x0e",SUCCESS) + | \x0f | ("\\x0f",SUCCESS) + | \x10 | ("\\x10",SUCCESS) + | \x11 | ("\\x11",SUCCESS) + | \x12 | ("\\x12",SUCCESS) + | \x13 | ("\\x13",SUCCESS) + | \x14 | ("\\x14",SUCCESS) + | \x15 | ("\\x15",SUCCESS) + | \x16 | ("\\x16",SUCCESS) + | \x17 | ("\\x17",SUCCESS) + | \x18 | ("\\x18",SUCCESS) + | \x19 | ("\\x19",SUCCESS) + | \x1a | ("\\x1a",SUCCESS) + | \x1b | ("\\x1b",SUCCESS) + | \x1c | ("\\x1c",SUCCESS) + | \x1d | ("\\x1d",SUCCESS) + | \x1e | ("\\x1e",SUCCESS) + | \x1f | ("\\x1f",SUCCESS) + | \x20 | ("\\x20",SUCCESS) + ! | \x21 | ("\\x21",SUCCESS) + " | \x22 | ("\\x22",SUCCESS) + # | \x23 | ("\\x23",SUCCESS) + $ | \x24 | ("\\x24",SUCCESS) + % | \x25 | ("\\x25",SUCCESS) + & | \x26 | ("\\x26",SUCCESS) + ' | \x27 | ("\\x27",SUCCESS) + ( | \x28 | ("\\x28",SUCCESS) + ) | \x29 | ("\\x29",SUCCESS) + * | \x2a | ("\\x2a",SUCCESS) + + | \x2b | ("\\x2b",SUCCESS) + , | \x2c | ("\\x2c",SUCCESS) + - | \x2d | ("\\x2d",SUCCESS) + . | \x2e | ("\\x2e",SUCCESS) + / | \x2f | ("\\x2f",SUCCESS) + 0 | \x30 | ("\\x30",SUCCESS) + 1 | \x31 | ("\\x31",SUCCESS) + 2 | \x32 | ("\\x32",SUCCESS) + 3 | \x33 | ("\\x33",SUCCESS) + 4 | \x34 | ("\\x34",SUCCESS) + 5 | \x35 | ("\\x35",SUCCESS) + 6 | \x36 | ("\\x36",SUCCESS) + 7 | \x37 | ("\\x37",SUCCESS) + 8 | \x38 | ("\\x38",SUCCESS) + 9 | \x39 | ("\\x39",SUCCESS) + : | \x3a | ("\\x3a",SUCCESS) + ; | \x3b | ("\\x3b",SUCCESS) + < | \x3c | ("\\x3c",SUCCESS) + =3D | \x3d | ("\\x3d",SUCCESS) + > | \x3e | ("\\x3e",SUCCESS) + ? | \x3f | ("\\x3f",SUCCESS) + @ | \x40 | ("\\x40",SUCCESS) + A | \x41 | ("\\x41",SUCCESS) + B | \x42 | ("\\x42",SUCCESS) + C | \x43 | ("\\x43",SUCCESS) + D | \x44 | ("\\x44",SUCCESS) + E | \x45 | ("\\x45",SUCCESS) + F | \x46 | ("\\x46",SUCCESS) + G | \x47 | ("\\x47",SUCCESS) + H | \x48 | ("\\x48",SUCCESS) + I | \x49 | ("\\x49",SUCCESS) + J | \x4a | ("\\x4a",SUCCESS) + K | \x4b | ("\\x4b",SUCCESS) + L | \x4c | ("\\x4c",SUCCESS) + M | \x4d | ("\\x4d",SUCCESS) + N | \x4e | ("\\x4e",SUCCESS) + O | \x4f | ("\\x4f",SUCCESS) + P | \x50 | ("\\x50",SUCCESS) + Q | \x51 | ("\\x51",SUCCESS) + R | \x52 | ("\\x52",SUCCESS) + S | \x53 | ("\\x53",SUCCESS) + T | \x54 | ("\\x54",SUCCESS) + U | \x55 | ("\\x55",SUCCESS) + V | \x56 | ("\\x56",SUCCESS) + W | \x57 | ("\\x57",SUCCESS) + X | \x58 | ("\\x58",SUCCESS) + Y | \x59 | ("\\x59",SUCCESS) + Z | \x5a | ("\\x5a",SUCCESS) + [ | \x5b | ("\\x5b",SUCCESS) + \ | \x5c | ("\\x5c",SUCCESS) + ] | \x5d | ("\\x5d",SUCCESS) + ^ | \x5e | ("\\x5e",SUCCESS) + _ | \x5f | ("\\x5f",SUCCESS) + ` | \x60 | ("\\x60",SUCCESS) + a | \x61 | ("\\x61",SUCCESS) + b | \x62 | ("\\x62",SUCCESS) + c | \x63 | ("\\x63",SUCCESS) + d | \x64 | ("\\x64",SUCCESS) + e | \x65 | ("\\x65",SUCCESS) + f | \x66 | ("\\x66",SUCCESS) + g | \x67 | ("\\x67",SUCCESS) + h | \x68 | ("\\x68",SUCCESS) + i | \x69 | ("\\x69",SUCCESS) + j | \x6a | ("\\x6a",SUCCESS) + k | \x6b | ("\\x6b",SUCCESS) + l | \x6c | ("\\x6c",SUCCESS) + m | \x6d | ("\\x6d",SUCCESS) + n | \x6e | ("\\x6e",SUCCESS) + o | \x6f | ("\\x6f",SUCCESS) + p | \x70 | ("\\x70",SUCCESS) + q | \x71 | ("\\x71",SUCCESS) + r | \x72 | ("\\x72",SUCCESS) + s | \x73 | ("\\x73",SUCCESS) + t | \x74 | ("\\x74",SUCCESS) + u | \x75 | ("\\x75",SUCCESS) + v | \x76 | ("\\x76",SUCCESS) + w | \x77 | ("\\x77",SUCCESS) + x | \x78 | ("\\x78",SUCCESS) + y | \x79 | ("\\x79",SUCCESS) + z | \x7a | ("\\x7a",SUCCESS) + { | \x7b | ("\\x7b",SUCCESS) + | | \x7c | ("\\x7c",SUCCESS) + } | \x7d | ("\\x7d",SUCCESS) + ~ | \x7e | ("\\x7e",SUCCESS) + | \x7f | ("\\x7f",SUCCESS) +(128 rows) + +DROP EXTENSION test_saslprep; diff --git a/src/test/modules/test_saslprep/meson.build b/src/test/modules/= test_saslprep/meson.build new file mode 100644 index 000000000000..2fcc403ca072 --- /dev/null +++ b/src/test/modules/test_saslprep/meson.build @@ -0,0 +1,38 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +test_saslprep_sources =3D files( + 'test_saslprep.c', +) + +if host_system =3D=3D 'windows' + test_saslprep_sources +=3D rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_saslprep', + '--FILEDESC', 'test_saslprep - test SASLprep implementation',]) +endif + +test_saslprep =3D shared_module('test_saslprep', + test_saslprep_sources, + kwargs: pg_test_mod_args, +) +test_install_libs +=3D test_saslprep + +test_install_data +=3D files( + 'test_saslprep.control', + 'test_saslprep--1.0.sql', +) + +tests +=3D { + 'name': 'test_saslprep', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'regress': { + 'sql': [ + 'test_saslprep', + ], + }, + 'tap': { + 'tests': [ + 't/001_saslprep_ranges.pl', + ], + }, +} diff --git a/src/test/modules/test_saslprep/sql/test_saslprep.sql b/src/tes= t/modules/test_saslprep/sql/test_saslprep.sql new file mode 100644 index 000000000000..00bad48eca70 --- /dev/null +++ b/src/test/modules/test_saslprep/sql/test_saslprep.sql @@ -0,0 +1,19 @@ +-- Tests for SASLprep + +CREATE EXTENSION test_saslprep; + +-- Incomplete UTF-8 sequence. +SELECT test_saslprep('\xef'); + +-- Range of ASCII characters. +SELECT + CASE + WHEN a =3D 0 THEN '' + WHEN a < 32 THEN '' + WHEN a =3D 127 THEN '' + ELSE chr(a) END AS dat, + set_byte('\x00'::bytea, 0, a) AS byt, + test_saslprep(set_byte('\x00'::bytea, 0, a)) AS saslprep + FROM generate_series(0,127) AS a; + +DROP EXTENSION test_saslprep; diff --git a/src/test/modules/test_saslprep/t/001_saslprep_ranges.pl b/src/= test/modules/test_saslprep/t/001_saslprep_ranges.pl new file mode 100644 index 000000000000..b2b40e9108b6 --- /dev/null +++ b/src/test/modules/test_saslprep/t/001_saslprep_ranges.pl @@ -0,0 +1,38 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test all ranges of valid UTF-8 codepoints under SASLprep. +# +# This test is expensive and enabled with PG_TEST_EXTRA, which is +# why it exists as a TAP test. + +use strict; +use warnings FATAL =3D> 'all'; +use PostgreSQL::Test::Cluster; +use Test::More; + +if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bsaslprep\b/) +{ + plan skip_all =3D> "test saslprep not enabled in PG_TEST_EXTRA"; +} + +# Initialize node +my $node =3D PostgreSQL::Test::Cluster->new('main'); + +$node->init; +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION test_saslprep;'); + +# Among all the valid UTF-8 codepoint ranges, our implementation of +# SASLprep should never return an empty password if the operation is +# considered a success. +# The only exception is the nul character, prohibited in input of +# CREATE/ALTER ROLE. +my $result =3D $node->safe_psql( + 'postgres', qq[SELECT * FROM test_saslprep_ranges() + WHERE status =3D 'SUCCESS' AND res IN (NULL, '') +]); + +is($result, 'U+0000|SUCCESS|\x00|\x', "Only nul authorized for all valid U= TF8 codepoints"); + +$node->stop; +done_testing(); diff --git a/src/test/modules/test_saslprep/test_saslprep--1.0.sql b/src/te= st/modules/test_saslprep/test_saslprep--1.0.sql new file mode 100644 index 000000000000..01e5244809e7 --- /dev/null +++ b/src/test/modules/test_saslprep/test_saslprep--1.0.sql @@ -0,0 +1,30 @@ +/* src/test/modules/test_saslprep/test_saslprep--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_saslprep" to load this file. \quit + +-- +-- test_saslprep(bytea) +-- +-- Tests single byte sequence in SASLprep. +-- +CREATE FUNCTION test_saslprep(IN src bytea, + OUT res bytea, + OUT status text) +RETURNS record +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +-- +-- test_saslprep_ranges +-- +-- Tests all possible ranges of byte sequences in SASLprep. +-- +CREATE FUNCTION test_saslprep_ranges( + OUT codepoint text, + OUT status text, + OUT src bytea, + OUT res bytea) +RETURNS SETOF record +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; diff --git a/src/test/modules/test_saslprep/test_saslprep.c b/src/test/modu= les/test_saslprep/test_saslprep.c new file mode 100644 index 000000000000..c57627cc53f8 --- /dev/null +++ b/src/test/modules/test_saslprep/test_saslprep.c @@ -0,0 +1,277 @@ +/*------------------------------------------------------------------------= -- + * + * test_saslprep.c + * Test harness for the SASLprep implementation. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_saslprep/test_saslprep.c + * + * -----------------------------------------------------------------------= -- + */ + +#include "postgres.h" + +#include "access/htup_details.h" +#include "common/saslprep.h" +#include "fmgr.h" +#include "funcapi.h" +#include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "utils/builtins.h" + +PG_MODULE_MAGIC; + +static const char * +saslprep_status_to_text(pg_saslprep_rc rc) +{ + const char *status =3D "???"; + + switch (rc) + { + case SASLPREP_OOM: + status =3D "OOM"; + break; + case SASLPREP_SUCCESS: + status =3D "SUCCESS"; + break; + case SASLPREP_INVALID_UTF8: + status =3D "INVALID_UTF8"; + break; + case SASLPREP_PROHIBITED: + status =3D "PROHIBITED"; + break; + } + + return status; +} + +/* + * Simple function to test SASLprep with arbitrary bytes as input. + * + * This takes a bytea in input, returning in output the generating data as + * bytea with the status returned by pg_saslprep(). + */ +PG_FUNCTION_INFO_V1(test_saslprep); +Datum +test_saslprep(PG_FUNCTION_ARGS) +{ + bytea *string =3D PG_GETARG_BYTEA_PP(0); + char *src; + Size src_len; + char *input_data; + char *result; + Size result_len; + bytea *result_bytea =3D NULL; + const char *status =3D NULL; + Datum *values; + bool *nulls; + TupleDesc tupdesc; + pg_saslprep_rc rc; + + /* determine result type */ + if (get_call_result_type(fcinfo, NULL, &tupdesc) !=3D TYPEFUNC_COMPOSITE) + elog(ERROR, "return type must be a row type"); + + values =3D palloc0_array(Datum, tupdesc->natts); + nulls =3D palloc0_array(bool, tupdesc->natts); + + src_len =3D VARSIZE_ANY_EXHDR(string); + src =3D VARDATA_ANY(string); + + /* + * Copy the input given, to make SASLprep() act on a sanitized string. + */ + input_data =3D palloc0(src_len + 1); + strlcpy(input_data, src, src_len + 1); + + rc =3D pg_saslprep(input_data, &result); + status =3D saslprep_status_to_text(rc); + + if (result) + { + result_len =3D strlen(result); + result_bytea =3D palloc(result_len + VARHDRSZ); + SET_VARSIZE(result_bytea, result_len + VARHDRSZ); + memcpy(VARDATA(result_bytea), result, result_len); + values[0] =3D PointerGetDatum(result_bytea); + } + else + nulls[0] =3D true; + + values[1] =3D CStringGetTextDatum(status); + + PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)= )); +} + +/* Context structure for set-returning function with ranges */ +typedef struct +{ + int current_range; + char32_t current_codepoint; +} pg_saslprep_test_context; + +/* + * UTF-8 code point ranges. + */ +typedef struct +{ + char32_t start_codepoint; + char32_t end_codepoint; +} pg_utf8_codepoint_range; + +static const pg_utf8_codepoint_range pg_utf8_test_ranges[] =3D { + /* 1, 2, 3 bytes */ + {0x0000, 0xD7FF}, /* Basic Multilingual Plane, before surrogates */ + {0xE000, 0xFFFF}, /* Basic Multilingual Plane, after surrogates */ + /* 4 bytes */ + {0x10000, 0x1FFFF}, /* Supplementary Multilingual Plane */ + {0x20000, 0x2FFFF}, /* Supplementary Ideographic Plane */ + {0x30000, 0x3FFFF}, /* Tertiary Ideographic Plane */ + {0x40000, 0xDFFFF}, /* Unassigned planes */ + {0xE0000, 0xEFFFF}, /* Supplementary Special-purpose Plane */ + {0xF0000, 0xFFFFF}, /* Private Use Area A */ + {0x100000, 0x10FFFF}, /* Private Use Area B */ +}; + +#define PG_UTF8_TEST_RANGES_LEN \ + (sizeof(pg_utf8_test_ranges) / sizeof(pg_utf8_test_ranges[0])) + + +/* + * test_saslprep_ranges + * + * Test SASLprep across various UTF-8 ranges. + */ +PG_FUNCTION_INFO_V1(test_saslprep_ranges); +Datum +test_saslprep_ranges(PG_FUNCTION_ARGS) +{ + FuncCallContext *funcctx; + pg_saslprep_test_context *ctx; + HeapTuple tuple; + Datum result; + + /* First call setup */ + if (SRF_IS_FIRSTCALL()) + { + MemoryContext oldcontext; + TupleDesc tupdesc; + + funcctx =3D SRF_FIRSTCALL_INIT(); + oldcontext =3D MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); + + if (get_call_result_type(fcinfo, NULL, &tupdesc) !=3D TYPEFUNC_COMPOSITE) + elog(ERROR, "return type must be a row type"); + funcctx->tuple_desc =3D tupdesc; + + /* Allocate context with range setup */ + ctx =3D (pg_saslprep_test_context *) palloc(sizeof(pg_saslprep_test_cont= ext)); + ctx->current_range =3D 0; + ctx->current_codepoint =3D pg_utf8_test_ranges[0].start_codepoint; + funcctx->user_fctx =3D ctx; + + MemoryContextSwitchTo(oldcontext); + } + + funcctx =3D SRF_PERCALL_SETUP(); + ctx =3D (pg_saslprep_test_context *) funcctx->user_fctx; + + while (ctx->current_range < PG_UTF8_TEST_RANGES_LEN) + { + char32_t codepoint =3D ctx->current_codepoint; + unsigned char utf8_buf[5]; + char input_str[6]; + char *output =3D NULL; + pg_saslprep_rc rc; + int utf8_len; + const char *status; + bytea *input_bytea; + bytea *output_bytea; + char codepoint_str[16]; + Datum values[4] =3D {0}; + bool nulls[4] =3D {0}; + const pg_utf8_codepoint_range *range =3D + &pg_utf8_test_ranges[ctx->current_range]; + + CHECK_FOR_INTERRUPTS(); + + /* Switch to next range if finished with the previous one */ + if (ctx->current_codepoint > range->end_codepoint) + { + ctx->current_range++; + if (ctx->current_range < PG_UTF8_TEST_RANGES_LEN) + ctx->current_codepoint =3D + pg_utf8_test_ranges[ctx->current_range].start_codepoint; + continue; + } + + codepoint =3D ctx->current_codepoint; + + /* Convert code point to UTF-8 */ + utf8_len =3D unicode_utf8len(codepoint); + if (unlikely(utf8_len =3D=3D 0)) + { + ctx->current_codepoint++; + continue; + } + unicode_to_utf8(codepoint, utf8_buf); + + /* Create null-terminated string */ + memcpy(input_str, utf8_buf, utf8_len); + input_str[utf8_len] =3D '\0'; + + /* Test with pg_saslprep */ + rc =3D pg_saslprep(input_str, &output); + + /* Prepare output values */ + MemSet(nulls, false, sizeof(nulls)); + + /* codepoint as text U+XXXX format */ + if (codepoint <=3D 0xFFFF) + snprintf(codepoint_str, sizeof(codepoint_str), "U+%04X", codepoint); + else + snprintf(codepoint_str, sizeof(codepoint_str), "U+%06X", codepoint); + values[0] =3D CStringGetTextDatum(codepoint_str); + + /* status */ + status =3D saslprep_status_to_text(rc); + values[1] =3D CStringGetTextDatum(status); + + /* input_bytes */ + input_bytea =3D (bytea *) palloc(VARHDRSZ + utf8_len); + SET_VARSIZE(input_bytea, VARHDRSZ + utf8_len); + memcpy(VARDATA(input_bytea), utf8_buf, utf8_len); + values[2] =3D PointerGetDatum(input_bytea); + + /* output_bytes */ + if (output !=3D NULL) + { + int output_len =3D strlen(output); + + output_bytea =3D (bytea *) palloc(VARHDRSZ + output_len); + SET_VARSIZE(output_bytea, VARHDRSZ + output_len); + memcpy(VARDATA(output_bytea), output, output_len); + values[3] =3D PointerGetDatum(output_bytea); + pfree(output); + } + else + { + nulls[3] =3D true; + values[3] =3D (Datum) 0; + } + + /* Build and return tuple */ + tuple =3D heap_form_tuple(funcctx->tuple_desc, values, nulls); + result =3D HeapTupleGetDatum(tuple); + + /* Move to next code point */ + ctx->current_codepoint++; + + SRF_RETURN_NEXT(funcctx, result); + } + + /* All done */ + SRF_RETURN_DONE(funcctx); +} diff --git a/src/test/modules/test_saslprep/test_saslprep.control b/src/tes= t/modules/test_saslprep/test_saslprep.control new file mode 100644 index 000000000000..13015c43880f --- /dev/null +++ b/src/test/modules/test_saslprep/test_saslprep.control @@ -0,0 +1,5 @@ +# test_saslprep extension +comment =3D 'Test SASLprep implementation' +default_version =3D '1.0' +module_pathname =3D '$libdir/test_saslprep' +relocatable =3D true diff --git a/doc/src/sgml/regress.sgml b/doc/src/sgml/regress.sgml index d80dd46c5fdb..285d06195336 100644 --- a/doc/src/sgml/regress.sgml +++ b/doc/src/sgml/regress.sgml @@ -342,6 +342,16 @@ make check-world PG_TEST_EXTRA=3D'kerberos ldap ssl lo= ad_balance libpq_encryption' =20 + + saslprep + + + Runs the TAP test suite under src/test/modules/test_saslp= rep. + Not enabled by default because it is resource-intensive. + + + + sepgsql --=20 2.53.0 --Jg2Ue2lcaa8Mswn8 Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-Make-implementation-of-SASLprep-compliant-for-ASC.patch Content-Transfer-Encoding: quoted-printable =46rom 49a1d26e006cf45389db82521b62a26bcdf0487e Mon Sep 17 00:00:00 2001 =46rom: Michael Paquier Date: Fri, 27 Feb 2026 11:42:50 +0900 Subject: [PATCH v2 2/2] Make implementation of SASLprep compliant for ASCII characters --- src/common/saslprep.c | 12 ---- .../test_saslprep/expected/test_saslprep.out | 66 +++++++++---------- .../test_saslprep/t/001_saslprep_ranges.pl | 4 +- 3 files changed, 34 insertions(+), 48 deletions(-) diff --git a/src/common/saslprep.c b/src/common/saslprep.c index 2ad2cefc14fb..38d50dd823c4 100644 --- a/src/common/saslprep.c +++ b/src/common/saslprep.c @@ -1061,18 +1061,6 @@ pg_saslprep(const char *input, char **output) /* Ensure we return *output as NULL on failure */ *output =3D NULL; =20 - /* - * Quick check if the input is pure ASCII. An ASCII string requires no - * further processing. - */ - if (pg_is_ascii(input)) - { - *output =3D STRDUP(input); - if (!(*output)) - goto oom; - return SASLPREP_SUCCESS; - } - /* * Convert the input from UTF-8 to an array of Unicode codepoints. * diff --git a/src/test/modules/test_saslprep/expected/test_saslprep.out b/sr= c/test/modules/test_saslprep/expected/test_saslprep.out index f72dbffa0a11..92f93365343e 100644 --- a/src/test/modules/test_saslprep/expected/test_saslprep.out +++ b/src/test/modules/test_saslprep/expected/test_saslprep.out @@ -19,38 +19,38 @@ SELECT FROM generate_series(0,127) AS a; dat | byt | saslprep =20 ----------+------+------------------- - | \x00 | ("\\x",SUCCESS) - | \x01 | ("\\x01",SUCCESS) - | \x02 | ("\\x02",SUCCESS) - | \x03 | ("\\x03",SUCCESS) - | \x04 | ("\\x04",SUCCESS) - | \x05 | ("\\x05",SUCCESS) - | \x06 | ("\\x06",SUCCESS) - | \x07 | ("\\x07",SUCCESS) - | \x08 | ("\\x08",SUCCESS) - | \x09 | ("\\x09",SUCCESS) - | \x0a | ("\\x0a",SUCCESS) - | \x0b | ("\\x0b",SUCCESS) - | \x0c | ("\\x0c",SUCCESS) - | \x0d | ("\\x0d",SUCCESS) - | \x0e | ("\\x0e",SUCCESS) - | \x0f | ("\\x0f",SUCCESS) - | \x10 | ("\\x10",SUCCESS) - | \x11 | ("\\x11",SUCCESS) - | \x12 | ("\\x12",SUCCESS) - | \x13 | ("\\x13",SUCCESS) - | \x14 | ("\\x14",SUCCESS) - | \x15 | ("\\x15",SUCCESS) - | \x16 | ("\\x16",SUCCESS) - | \x17 | ("\\x17",SUCCESS) - | \x18 | ("\\x18",SUCCESS) - | \x19 | ("\\x19",SUCCESS) - | \x1a | ("\\x1a",SUCCESS) - | \x1b | ("\\x1b",SUCCESS) - | \x1c | ("\\x1c",SUCCESS) - | \x1d | ("\\x1d",SUCCESS) - | \x1e | ("\\x1e",SUCCESS) - | \x1f | ("\\x1f",SUCCESS) + | \x00 | (,PROHIBITED) + | \x01 | (,PROHIBITED) + | \x02 | (,PROHIBITED) + | \x03 | (,PROHIBITED) + | \x04 | (,PROHIBITED) + | \x05 | (,PROHIBITED) + | \x06 | (,PROHIBITED) + | \x07 | (,PROHIBITED) + | \x08 | (,PROHIBITED) + | \x09 | (,PROHIBITED) + | \x0a | (,PROHIBITED) + | \x0b | (,PROHIBITED) + | \x0c | (,PROHIBITED) + | \x0d | (,PROHIBITED) + | \x0e | (,PROHIBITED) + | \x0f | (,PROHIBITED) + | \x10 | (,PROHIBITED) + | \x11 | (,PROHIBITED) + | \x12 | (,PROHIBITED) + | \x13 | (,PROHIBITED) + | \x14 | (,PROHIBITED) + | \x15 | (,PROHIBITED) + | \x16 | (,PROHIBITED) + | \x17 | (,PROHIBITED) + | \x18 | (,PROHIBITED) + | \x19 | (,PROHIBITED) + | \x1a | (,PROHIBITED) + | \x1b | (,PROHIBITED) + | \x1c | (,PROHIBITED) + | \x1d | (,PROHIBITED) + | \x1e | (,PROHIBITED) + | \x1f | (,PROHIBITED) | \x20 | ("\\x20",SUCCESS) ! | \x21 | ("\\x21",SUCCESS) " | \x22 | ("\\x22",SUCCESS) @@ -146,7 +146,7 @@ SELECT | | \x7c | ("\\x7c",SUCCESS) } | \x7d | ("\\x7d",SUCCESS) ~ | \x7e | ("\\x7e",SUCCESS) - | \x7f | ("\\x7f",SUCCESS) + | \x7f | (,PROHIBITED) (128 rows) =20 DROP EXTENSION test_saslprep; diff --git a/src/test/modules/test_saslprep/t/001_saslprep_ranges.pl b/src/= test/modules/test_saslprep/t/001_saslprep_ranges.pl index b2b40e9108b6..cf455571dd2b 100644 --- a/src/test/modules/test_saslprep/t/001_saslprep_ranges.pl +++ b/src/test/modules/test_saslprep/t/001_saslprep_ranges.pl @@ -25,14 +25,12 @@ $node->safe_psql('postgres', 'CREATE EXTENSION test_sas= lprep;'); # Among all the valid UTF-8 codepoint ranges, our implementation of # SASLprep should never return an empty password if the operation is # considered a success. -# The only exception is the nul character, prohibited in input of -# CREATE/ALTER ROLE. my $result =3D $node->safe_psql( 'postgres', qq[SELECT * FROM test_saslprep_ranges() WHERE status =3D 'SUCCESS' AND res IN (NULL, '') ]); =20 -is($result, 'U+0000|SUCCESS|\x00|\x', "Only nul authorized for all valid U= TF8 codepoints"); +is($result, '', "No empty or NULL values for all valid UTF8 codepoints"); =20 $node->stop; done_testing(); --=20 2.53.0 --Jg2Ue2lcaa8Mswn8-- --O3pDOfuo+GwKMiFD Content-Type: application/pgp-signature; name=signature.asc -----BEGIN PGP SIGNATURE----- iQIzBAEBCgAdFiEEG72nH6vTowiyblFKnvQgOdbyQH0FAmmlNgAACgkQnvQgOdby QH3qHA//cZB3Z7VpfBW2vxHxtoi/dxzHKNOl7rRHlZq6hFAI/l/aKiy1tdg/e1wA U/g8OyoTIISSBSzgdoWalrsTzeonrA0tzaL4OObm2CLY6pR4eqPv1LAUVrn6zRho RWONE0iXKifJgI2F0KydmbBu8VyUb1oMQ1OrEZMVR4pS2SwYOCkMHHbQAyA6YZRL XN9WYXVFqBFXB712ciPKPkEU2wevquVeZh6gaSHDUnj8ipUkeb+0zuxtIyL/hHUI lNGEd+Fp4tzO+CdxEmSw36wlVGdkVyoGlut8uvOOv5rzGc1gcKL06z5/5pPyMLM/ zQAWlfVIUpHKSEENXArdFVbMQUOQu5YYAFbVETIXzaSJHVDnWASlbU4eaBJgJtX9 ZMyBVR3PPtTnqI7UJMHSof47Nn6hGGySjZ3/OGa751rDojoa1ri50jrhlglV4DSS POAA416bNZ3sLk5J7bLgVaMB2H9xvayE50fxwKURp3Oo+MBlsWoMqE6dKxwebKHD MkLnZK3KlRMl1Xrpv6UEgQqRZN0KNu6h23I4JMbb5wPhvBBKS+OgEaZI40Q3nkfd siF2pPYSiLVYJtN04g7ivp78oxiN1mv0O3mxvBY5ytUa7lXxeKiwGZaEFG0Zx71s Ljq7DN52JlZeHGk4AjCVbYuZEr2kzTR/EStUznWaIxwWpVM4GsI= =hWNh -----END PGP SIGNATURE----- --O3pDOfuo+GwKMiFD--