Hi all,
While reviewing some of the SCRAM code, I have been reminded about the
following bit of code in saslprep.c:
/*
* Quick check if the input is pure ASCII. An ASCII string requires no
* further processing.
*/
if (pg_is_ascii(input))
{
*output = STRDUP(input);
if (!(*output))
goto oom;
return SASLPREP_SUCCESS;
}And after cross-checking that with RFCs 3454 (Stringprep) and 4013 (SASLprep), I got reminded of the fact that this implementation artifact is wrong because not all ASCII characters are allowed: - 0x00~0x1F (0~31), control characters, are prohibited. - 0x7F (127, DEL) is prohibited. The rest of the ASCII character range is OK. Another question one may ask is: does making our SCRAM implementation compliant impact our SCRAM implementation at all? The answer to this question is no. If we are dealing with an ASCII-only password with prohibited characters, our calls of pg_saslprep() deal with the SCRAM verifiers generated by CREATE/ALTER role and the SASLprep() calls done during an exchange the same way: even if we have prohibited ASCII characters, the bytes are fed as-is to the scram build code. All our callers of pg_saslprep() make sure that the same thing happens. In short, I see no downside in just making our implementation compliant, which should be actually beneficial for future callers of this routine, should we have any. One point can be made for the efficiency of checking ASCII-only passwords, but the default count of 4096 used for the computation of the SCRAM verifiers outweights that point by far IMO: the SCRAM computation is more expensive than this ASCII-only shortcut anyway. Attached are two patches, that I'd like to propose for this commit fest: - 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. - 0002 is the change to make the implementation compliant, impacting the tests. This removes nul from the list of valid cases, and the SQL tests show the compliant behavior. Even if we don't do 0002, 0001 shows benefits of its own. I am adding that to the upcoming CF. Thanks, -- Michael
From 6b106f52ad9e1933b727e05539261e51f9209075 Mon Sep 17 00:00:00 2001 From: Michael Paquier <[email protected]> Date: Fri, 27 Feb 2026 11:40:53 +0900 Subject: [PATCH v1 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 | 150 ++++++++++ src/test/modules/test_saslprep/meson.build | 38 +++ .../test_saslprep/sql/test_saslprep.sql | 14 + .../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, 618 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.out 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 44c7163c1cd5..0bb5dc7e2088 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -44,6 +44,7 @@ SUBDIRS = \ 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 2634a519935a..d20a97e8af32 100644 --- a/src/test/modules/meson.build +++ b/src/test/modules/meson.build @@ -45,6 +45,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/test_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/test_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 = test_saslprep +OBJS = \ + $(WIN32RES) \ + test_saslprep.o +PGFILEDESC = "test_saslprep - test SASLprep implementation" + +EXTENSION = test_saslprep +DATA = test_saslprep--1.0.sql + +REGRESS = test_saslprep + +TAP_TESTS = 1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_saslprep +top_builddir = ../../../.. +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 +================== + +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 +================= + +NOTE: A portion of the tests requires --enable-tap-tests, with +PG_TEST_EXTRA=saslprep set to run the TAP test suite. + +Run + make check PG_TEST_EXTRA=saslprep +or + make installcheck PG_TEST_EXTRA=saslprep + +The SQL test suite can run with or without PG_TEST_EXTRA=saslprep +set. diff --git a/src/test/modules/test_saslprep/expected/test_saslprep.out b/src/test/modules/test_saslprep/expected/test_saslprep.out new file mode 100644 index 000000000000..5a0ded7b4214 --- /dev/null +++ b/src/test/modules/test_saslprep/expected/test_saslprep.out @@ -0,0 +1,150 @@ +-- Tests for SASLprep +CREATE EXTENSION test_saslprep; +-- Incomplete UTF-8 sequence. +SELECT test_saslprep('\xef'); + test_saslprep +----------------- + (,INVALID_UTF8) +(1 row) + +-- Range of ASCII characters, skip nul (0) and '\' (92) as invalid bytea. +SELECT chr(a) AS dat, chr(a)::bytea AS byt, test_saslprep(chr(a)::bytea) + FROM generate_series(1,91) as a; + dat | byt | test_saslprep +----------+------+------------------- + \x01 | \x01 | ("\\x01",SUCCESS) + \x02 | \x02 | ("\\x02",SUCCESS) + \x03 | \x03 | ("\\x03",SUCCESS) + \x04 | \x04 | ("\\x04",SUCCESS) + \x05 | \x05 | ("\\x05",SUCCESS) + \x06 | \x06 | ("\\x06",SUCCESS) + \x07 | \x07 | ("\\x07",SUCCESS) + \x08 | \x08 | ("\\x08",SUCCESS) + | \x09 | ("\\x09",SUCCESS) + +| \x0a | ("\\x0a",SUCCESS) + | | + \x0B | \x0b | ("\\x0b",SUCCESS) + \x0C | \x0c | ("\\x0c",SUCCESS) + \r | \x0d | ("\\x0d",SUCCESS) + \x0E | \x0e | ("\\x0e",SUCCESS) + \x0F | \x0f | ("\\x0f",SUCCESS) + \x10 | \x10 | ("\\x10",SUCCESS) + \x11 | \x11 | ("\\x11",SUCCESS) + \x12 | \x12 | ("\\x12",SUCCESS) + \x13 | \x13 | ("\\x13",SUCCESS) + \x14 | \x14 | ("\\x14",SUCCESS) + \x15 | \x15 | ("\\x15",SUCCESS) + \x16 | \x16 | ("\\x16",SUCCESS) + \x17 | \x17 | ("\\x17",SUCCESS) + \x18 | \x18 | ("\\x18",SUCCESS) + \x19 | \x19 | ("\\x19",SUCCESS) + \x1A | \x1a | ("\\x1a",SUCCESS) + \x1B | \x1b | ("\\x1b",SUCCESS) + \x1C | \x1c | ("\\x1c",SUCCESS) + \x1D | \x1d | ("\\x1d",SUCCESS) + \x1E | \x1e | ("\\x1e",SUCCESS) + \x1F | \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) + = | \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) +(91 rows) + +SELECT chr(a) AS dat, chr(a)::bytea AS byt, test_saslprep(chr(a)::bytea) + FROM generate_series(93,127) as a; + dat | byt | test_saslprep +------+------+------------------- + ] | \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 | ("\\x7f",SUCCESS) +(35 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 = files( + 'test_saslprep.c', +) + +if host_system == 'windows' + test_saslprep_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_saslprep', + '--FILEDESC', 'test_saslprep - test SASLprep implementation',]) +endif + +test_saslprep = shared_module('test_saslprep', + test_saslprep_sources, + kwargs: pg_test_mod_args, +) +test_install_libs += test_saslprep + +test_install_data += files( + 'test_saslprep.control', + 'test_saslprep--1.0.sql', +) + +tests += { + '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/test/modules/test_saslprep/sql/test_saslprep.sql new file mode 100644 index 000000000000..0b6c3d3a8e61 --- /dev/null +++ b/src/test/modules/test_saslprep/sql/test_saslprep.sql @@ -0,0 +1,14 @@ +-- Tests for SASLprep + +CREATE EXTENSION test_saslprep; + +-- Incomplete UTF-8 sequence. +SELECT test_saslprep('\xef'); + +-- Range of ASCII characters, skip nul (0) and '\' (92) as invalid bytea. +SELECT chr(a) AS dat, chr(a)::bytea AS byt, test_saslprep(chr(a)::bytea) + FROM generate_series(1,91) as a; +SELECT chr(a) AS dat, chr(a)::bytea AS byt, test_saslprep(chr(a)::bytea) + FROM generate_series(93,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 => 'all'; +use PostgreSQL::Test::Cluster; +use Test::More; + +if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bsaslprep\b/) +{ + plan skip_all => "test saslprep not enabled in PG_TEST_EXTRA"; +} + +# Initialize node +my $node = 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 = $node->safe_psql( + 'postgres', qq[SELECT * FROM test_saslprep_ranges() + WHERE status = 'SUCCESS' AND res IN (NULL, '') +]); + +is($result, 'U+0000|SUCCESS|\x00|\x', "Only nul authorized for all valid UTF8 codepoints"); + +$node->stop; +done_testing(); diff --git a/src/test/modules/test_saslprep/test_saslprep--1.0.sql b/src/test/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/modules/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 = "???"; + + switch (rc) + { + case SASLPREP_OOM: + status = "OOM"; + break; + case SASLPREP_SUCCESS: + status = "SUCCESS"; + break; + case SASLPREP_INVALID_UTF8: + status = "INVALID_UTF8"; + break; + case SASLPREP_PROHIBITED: + status = "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 = PG_GETARG_BYTEA_PP(0); + char *src; + Size src_len; + char *input_data; + char *result; + Size result_len; + bytea *result_bytea = NULL; + const char *status = NULL; + Datum *values; + bool *nulls; + TupleDesc tupdesc; + pg_saslprep_rc rc; + + /* determine result type */ + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "return type must be a row type"); + + values = palloc0_array(Datum, tupdesc->natts); + nulls = palloc0_array(bool, tupdesc->natts); + + src_len = VARSIZE_ANY_EXHDR(string); + src = VARDATA_ANY(string); + + /* + * Copy the input given, to make SASLprep() act on a sanitized string. + */ + input_data = palloc0(src_len + 1); + strlcpy(input_data, src, src_len + 1); + + rc = pg_saslprep(input_data, &result); + status = saslprep_status_to_text(rc); + + if (result) + { + result_len = strlen(result); + result_bytea = palloc(result_len + VARHDRSZ); + SET_VARSIZE(result_bytea, result_len + VARHDRSZ); + memcpy(VARDATA(result_bytea), result, result_len); + values[0] = PointerGetDatum(result_bytea); + } + else + nulls[0] = true; + + values[1] = 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[] = { + /* 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 = SRF_FIRSTCALL_INIT(); + oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); + + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "return type must be a row type"); + funcctx->tuple_desc = tupdesc; + + /* Allocate context with range setup */ + ctx = (pg_saslprep_test_context *) palloc(sizeof(pg_saslprep_test_context)); + ctx->current_range = 0; + ctx->current_codepoint = pg_utf8_test_ranges[0].start_codepoint; + funcctx->user_fctx = ctx; + + MemoryContextSwitchTo(oldcontext); + } + + funcctx = SRF_PERCALL_SETUP(); + ctx = (pg_saslprep_test_context *) funcctx->user_fctx; + + while (ctx->current_range < PG_UTF8_TEST_RANGES_LEN) + { + char32_t codepoint = ctx->current_codepoint; + unsigned char utf8_buf[5]; + char input_str[6]; + char *output = NULL; + pg_saslprep_rc rc; + int utf8_len; + const char *status; + bytea *input_bytea; + bytea *output_bytea; + char codepoint_str[16]; + Datum values[4] = {0}; + bool nulls[4] = {0}; + const pg_utf8_codepoint_range *range = + &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 = + pg_utf8_test_ranges[ctx->current_range].start_codepoint; + continue; + } + + codepoint = ctx->current_codepoint; + + /* Convert code point to UTF-8 */ + utf8_len = unicode_utf8len(codepoint); + if (unlikely(utf8_len == 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] = '\0'; + + /* Test with pg_saslprep */ + rc = pg_saslprep(input_str, &output); + + /* Prepare output values */ + MemSet(nulls, false, sizeof(nulls)); + + /* codepoint as text U+XXXX format */ + if (codepoint <= 0xFFFF) + snprintf(codepoint_str, sizeof(codepoint_str), "U+%04X", codepoint); + else + snprintf(codepoint_str, sizeof(codepoint_str), "U+%06X", codepoint); + values[0] = CStringGetTextDatum(codepoint_str); + + /* status */ + status = saslprep_status_to_text(rc); + values[1] = CStringGetTextDatum(status); + + /* input_bytes */ + input_bytea = (bytea *) palloc(VARHDRSZ + utf8_len); + SET_VARSIZE(input_bytea, VARHDRSZ + utf8_len); + memcpy(VARDATA(input_bytea), utf8_buf, utf8_len); + values[2] = PointerGetDatum(input_bytea); + + /* output_bytes */ + if (output != NULL) + { + int output_len = strlen(output); + + output_bytea = (bytea *) palloc(VARHDRSZ + output_len); + SET_VARSIZE(output_bytea, VARHDRSZ + output_len); + memcpy(VARDATA(output_bytea), output, output_len); + values[3] = PointerGetDatum(output_bytea); + pfree(output); + } + else + { + nulls[3] = true; + values[3] = (Datum) 0; + } + + /* Build and return tuple */ + tuple = heap_form_tuple(funcctx->tuple_desc, values, nulls); + result = 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/test/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 = 'Test SASLprep implementation' +default_version = '1.0' +module_pathname = '$libdir/test_saslprep' +relocatable = 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='kerberos ldap ssl load_balance libpq_encryption' </listitem> </varlistentry> + <varlistentry> + <term><literal>saslprep</literal></term> + <listitem> + <para> + Runs the TAP test suite under <filename>src/test/modules/test_saslprep</filename>. + Not enabled by default because it is resource-intensive. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><literal>sepgsql</literal></term> <listitem> -- 2.53.0
From 639d385eaee847ed09e575b3664a80783de795e8 Mon Sep 17 00:00:00 2001 From: Michael Paquier <[email protected]> Date: Fri, 27 Feb 2026 11:42:50 +0900 Subject: [PATCH v1 2/2] Make implementation of SASLprep compliant for ASCII characters --- src/common/saslprep.c | 12 ---- .../test_saslprep/expected/test_saslprep.out | 64 +++++++++---------- .../test_saslprep/t/001_saslprep_ranges.pl | 4 +- 3 files changed, 33 insertions(+), 47 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 = NULL; - /* - * Quick check if the input is pure ASCII. An ASCII string requires no - * further processing. - */ - if (pg_is_ascii(input)) - { - *output = 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/src/test/modules/test_saslprep/expected/test_saslprep.out index 5a0ded7b4214..deeab303fa52 100644 --- a/src/test/modules/test_saslprep/expected/test_saslprep.out +++ b/src/test/modules/test_saslprep/expected/test_saslprep.out @@ -12,38 +12,38 @@ SELECT chr(a) AS dat, chr(a)::bytea AS byt, test_saslprep(chr(a)::bytea) FROM generate_series(1,91) as a; dat | byt | test_saslprep ----------+------+------------------- - \x01 | \x01 | ("\\x01",SUCCESS) - \x02 | \x02 | ("\\x02",SUCCESS) - \x03 | \x03 | ("\\x03",SUCCESS) - \x04 | \x04 | ("\\x04",SUCCESS) - \x05 | \x05 | ("\\x05",SUCCESS) - \x06 | \x06 | ("\\x06",SUCCESS) - \x07 | \x07 | ("\\x07",SUCCESS) - \x08 | \x08 | ("\\x08",SUCCESS) - | \x09 | ("\\x09",SUCCESS) - +| \x0a | ("\\x0a",SUCCESS) + \x01 | \x01 | (,PROHIBITED) + \x02 | \x02 | (,PROHIBITED) + \x03 | \x03 | (,PROHIBITED) + \x04 | \x04 | (,PROHIBITED) + \x05 | \x05 | (,PROHIBITED) + \x06 | \x06 | (,PROHIBITED) + \x07 | \x07 | (,PROHIBITED) + \x08 | \x08 | (,PROHIBITED) + | \x09 | (,PROHIBITED) + +| \x0a | (,PROHIBITED) | | - \x0B | \x0b | ("\\x0b",SUCCESS) - \x0C | \x0c | ("\\x0c",SUCCESS) - \r | \x0d | ("\\x0d",SUCCESS) - \x0E | \x0e | ("\\x0e",SUCCESS) - \x0F | \x0f | ("\\x0f",SUCCESS) - \x10 | \x10 | ("\\x10",SUCCESS) - \x11 | \x11 | ("\\x11",SUCCESS) - \x12 | \x12 | ("\\x12",SUCCESS) - \x13 | \x13 | ("\\x13",SUCCESS) - \x14 | \x14 | ("\\x14",SUCCESS) - \x15 | \x15 | ("\\x15",SUCCESS) - \x16 | \x16 | ("\\x16",SUCCESS) - \x17 | \x17 | ("\\x17",SUCCESS) - \x18 | \x18 | ("\\x18",SUCCESS) - \x19 | \x19 | ("\\x19",SUCCESS) - \x1A | \x1a | ("\\x1a",SUCCESS) - \x1B | \x1b | ("\\x1b",SUCCESS) - \x1C | \x1c | ("\\x1c",SUCCESS) - \x1D | \x1d | ("\\x1d",SUCCESS) - \x1E | \x1e | ("\\x1e",SUCCESS) - \x1F | \x1f | ("\\x1f",SUCCESS) + \x0B | \x0b | (,PROHIBITED) + \x0C | \x0c | (,PROHIBITED) + \r | \x0d | (,PROHIBITED) + \x0E | \x0e | (,PROHIBITED) + \x0F | \x0f | (,PROHIBITED) + \x10 | \x10 | (,PROHIBITED) + \x11 | \x11 | (,PROHIBITED) + \x12 | \x12 | (,PROHIBITED) + \x13 | \x13 | (,PROHIBITED) + \x14 | \x14 | (,PROHIBITED) + \x15 | \x15 | (,PROHIBITED) + \x16 | \x16 | (,PROHIBITED) + \x17 | \x17 | (,PROHIBITED) + \x18 | \x18 | (,PROHIBITED) + \x19 | \x19 | (,PROHIBITED) + \x1A | \x1a | (,PROHIBITED) + \x1B | \x1b | (,PROHIBITED) + \x1C | \x1c | (,PROHIBITED) + \x1D | \x1d | (,PROHIBITED) + \x1E | \x1e | (,PROHIBITED) + \x1F | \x1f | (,PROHIBITED) | \x20 | ("\\x20",SUCCESS) ! | \x21 | ("\\x21",SUCCESS) " | \x22 | ("\\x22",SUCCESS) @@ -144,7 +144,7 @@ SELECT chr(a) AS dat, chr(a)::bytea AS byt, test_saslprep(chr(a)::bytea) | | \x7c | ("\\x7c",SUCCESS) } | \x7d | ("\\x7d",SUCCESS) ~ | \x7e | ("\\x7e",SUCCESS) - \x7F | \x7f | ("\\x7f",SUCCESS) + \x7F | \x7f | (,PROHIBITED) (35 rows) 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_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 = $node->safe_psql( 'postgres', qq[SELECT * FROM test_saslprep_ranges() WHERE status = 'SUCCESS' AND res IN (NULL, '') ]); -is($result, 'U+0000|SUCCESS|\x00|\x', "Only nul authorized for all valid UTF8 codepoints"); +is($result, '', "No empty or NULL values for all valid UTF8 codepoints"); $node->stop; done_testing(); -- 2.53.0
signature.asc
Description: PGP signature
