]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Add built-in fuzzing harnesses for security testing.
authorAndrew Dunstan <andrew@dunslane.net>
Thu, 9 Apr 2026 18:20:40 +0000 (14:20 -0400)
committerAndrew Dunstan <andrew@dunslane.net>
Fri, 10 Apr 2026 11:13:08 +0000 (07:13 -0400)
Add 12 libFuzzer-compatible fuzzing harnesses behind a new -Dfuzzing=true
meson option.  Each harness implements LLVMFuzzerTestOneInput() and can
also be built in standalone mode (reading from files) when no fuzzer
engine is detected.

Frontend targets (no backend dependencies):
  fuzz_json            - non-incremental JSON parser (pg_parse_json)
  fuzz_json_incremental - incremental/chunked JSON parser
  fuzz_conninfo        - libpq connection string parser (PQconninfoParse)
  fuzz_pglz            - PGLZ decompressor (pglz_decompress)
  fuzz_unescapebytea   - libpq bytea unescape (PQunescapeBytea)
  fuzz_b64decode       - base64 decoder (pg_b64_decode)
  fuzz_saslprep        - SASLprep normalization (pg_saslprep)
  fuzz_parsepgarray    - array literal parser (parsePGArray)
  fuzz_pgbench_expr    - pgbench expression parser (via Bison/Flex)

Backend targets (link against postgres_lib):
  fuzz_rawparser       - SQL raw parser (raw_parser)
  fuzz_regex           - regex engine (pg_regcomp/pg_regexec)
  fuzz_typeinput       - type input functions (numeric/date/timestamp/interval)

15 files changed:
meson_options.txt
src/test/fuzzing/fuzz_b64decode.c [new file with mode: 0644]
src/test/fuzzing/fuzz_conninfo.c [new file with mode: 0644]
src/test/fuzzing/fuzz_json.c [new file with mode: 0644]
src/test/fuzzing/fuzz_json_incremental.c [new file with mode: 0644]
src/test/fuzzing/fuzz_parsepgarray.c [new file with mode: 0644]
src/test/fuzzing/fuzz_pgbench_expr.c [new file with mode: 0644]
src/test/fuzzing/fuzz_pglz.c [new file with mode: 0644]
src/test/fuzzing/fuzz_rawparser.c [new file with mode: 0644]
src/test/fuzzing/fuzz_regex.c [new file with mode: 0644]
src/test/fuzzing/fuzz_saslprep.c [new file with mode: 0644]
src/test/fuzzing/fuzz_typeinput.c [new file with mode: 0644]
src/test/fuzzing/fuzz_unescapebytea.c [new file with mode: 0644]
src/test/fuzzing/meson.build [new file with mode: 0644]
src/test/meson.build

index 6a793f3e4794374a7c4d8e12a8c46f5b5c55ef5c..4f60abccdc3a0047c44a69f6e37483512b050b2b 100644 (file)
@@ -43,6 +43,9 @@ option('cassert', type: 'boolean', value: false,
 option('tap_tests', type: 'feature', value: 'auto',
   description: 'Enable TAP tests')
 
+option('fuzzing', type: 'boolean', value: false,
+  description: 'Build fuzz testing targets')
+
 option('injection_points', type: 'boolean', value: false,
   description: 'Enable injection points')
 
diff --git a/src/test/fuzzing/fuzz_b64decode.c b/src/test/fuzzing/fuzz_b64decode.c
new file mode 100644 (file)
index 0000000..f388aaf
--- /dev/null
@@ -0,0 +1,98 @@
+/*-------------------------------------------------------------------------
+ *
+ * fuzz_b64decode.c
+ *    Fuzzing harness for pg_b64_decode()
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *    src/test/fuzzing/fuzz_b64decode.c
+ *
+ * This harness feeds arbitrary byte sequences to pg_b64_decode(),
+ * which decodes base64-encoded data per RFC 4648.  The function is
+ * a pure computation with no global state.
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <stdio.h>
+
+#include "common/base64.h"
+
+int                    LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
+
+int
+LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+{
+       int                     dstlen;
+       uint8      *dst;
+
+       if (size == 0)
+               return 0;
+
+       /* Allocate a buffer large enough for any valid decoding */
+       dstlen = pg_b64_dec_len((int) size);
+       dst = malloc(dstlen);
+       if (!dst)
+               return 0;
+
+       (void) pg_b64_decode((const char *) data, (int) size, dst, dstlen);
+
+       free(dst);
+       return 0;
+}
+
+#ifdef STANDALONE_FUZZ_TARGET
+int
+main(int argc, char **argv)
+{
+       int                     i;
+       int                     ret = 0;
+
+       for (i = 1; i < argc; i++)
+       {
+               FILE       *f = fopen(argv[i], "rb");
+               long            len;
+               uint8_t    *buf;
+               size_t          n_read;
+
+               if (!f)
+               {
+                       fprintf(stderr, "%s: could not open %s: %m\n", argv[0], argv[i]);
+                       ret = 1;
+                       continue;
+               }
+
+               fseek(f, 0, SEEK_END);
+               len = ftell(f);
+               fseek(f, 0, SEEK_SET);
+
+               if (len < 0)
+               {
+                       fprintf(stderr, "%s: could not determine size of %s\n",
+                                       argv[0], argv[i]);
+                       fclose(f);
+                       ret = 1;
+                       continue;
+               }
+
+               buf = malloc(len);
+               if (!buf)
+               {
+                       fprintf(stderr, "%s: out of memory\n", argv[0]);
+                       fclose(f);
+                       return 1;
+               }
+
+               n_read = fread(buf, 1, len, f);
+               fclose(f);
+
+               LLVMFuzzerTestOneInput(buf, n_read);
+               free(buf);
+       }
+
+       return ret;
+}
+#endif                                                 /* STANDALONE_FUZZ_TARGET */
diff --git a/src/test/fuzzing/fuzz_conninfo.c b/src/test/fuzzing/fuzz_conninfo.c
new file mode 100644 (file)
index 0000000..6199370
--- /dev/null
@@ -0,0 +1,105 @@
+/*-------------------------------------------------------------------------
+ *
+ * fuzz_conninfo.c
+ *    Fuzzing harness for libpq connection string parsing
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *    src/test/fuzzing/fuzz_conninfo.c
+ *
+ * This harness feeds arbitrary byte sequences to PQconninfoParse(),
+ * which parses both key=value connection strings and PostgreSQL URIs
+ * (postgresql://...).  The function is completely standalone and
+ * requires no database connection or other initialization.
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <stdio.h>
+
+#include "libpq-fe.h"
+
+int                    LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
+
+int
+LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+{
+       char       *str;
+       char       *errmsg = NULL;
+       PQconninfoOption *opts;
+
+       if (size == 0)
+               return 0;
+
+       /* PQconninfoParse expects a NUL-terminated string */
+       str = malloc(size + 1);
+       if (!str)
+               return 0;
+       memcpy(str, data, size);
+       str[size] = '\0';
+
+       opts = PQconninfoParse(str, &errmsg);
+       if (opts)
+               PQconninfoFree(opts);
+       if (errmsg)
+               PQfreemem(errmsg);
+
+       free(str);
+       return 0;
+}
+
+#ifdef STANDALONE_FUZZ_TARGET
+int
+main(int argc, char **argv)
+{
+       int                     i;
+       int                     ret = 0;
+
+       for (i = 1; i < argc; i++)
+       {
+               FILE       *f = fopen(argv[i], "rb");
+               long            len;
+               uint8_t    *buf;
+               size_t          n_read;
+
+               if (!f)
+               {
+                       fprintf(stderr, "%s: could not open %s: %m\n", argv[0], argv[i]);
+                       ret = 1;
+                       continue;
+               }
+
+               fseek(f, 0, SEEK_END);
+               len = ftell(f);
+               fseek(f, 0, SEEK_SET);
+
+               if (len < 0)
+               {
+                       fprintf(stderr, "%s: could not determine size of %s\n",
+                                       argv[0], argv[i]);
+                       fclose(f);
+                       ret = 1;
+                       continue;
+               }
+
+               buf = malloc(len);
+               if (!buf)
+               {
+                       fprintf(stderr, "%s: out of memory\n", argv[0]);
+                       fclose(f);
+                       return 1;
+               }
+
+               n_read = fread(buf, 1, len, f);
+               fclose(f);
+
+               LLVMFuzzerTestOneInput(buf, n_read);
+               free(buf);
+       }
+
+       return ret;
+}
+#endif                                                 /* STANDALONE_FUZZ_TARGET */
diff --git a/src/test/fuzzing/fuzz_json.c b/src/test/fuzzing/fuzz_json.c
new file mode 100644 (file)
index 0000000..b265ddf
--- /dev/null
@@ -0,0 +1,104 @@
+/*-------------------------------------------------------------------------
+ *
+ * fuzz_json.c
+ *    Fuzzing harness for the non-incremental JSON parser
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *    src/test/fuzzing/fuzz_json.c
+ *
+ * This harness feeds arbitrary byte sequences to pg_parse_json() via
+ * makeJsonLexContextCstringLen().  It uses the null semantic action so
+ * that only lexing and structural validation are exercised.
+ *
+ * Build with a fuzzing engine (e.g. libFuzzer via -fsanitize=fuzzer)
+ * or in standalone mode, which reads files named on the command line.
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <stdio.h>
+
+#include "common/jsonapi.h"
+#include "mb/pg_wchar.h"
+
+/*
+ * Entry point for libFuzzer and other engines that call
+ * LLVMFuzzerTestOneInput().
+ */
+int                    LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
+
+int
+LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+{
+       JsonLexContext lex;
+
+       if (size == 0)
+               return 0;
+
+       makeJsonLexContextCstringLen(&lex, (const char *) data, size,
+                                                                PG_UTF8, true);
+       setJsonLexContextOwnsTokens(&lex, true);
+
+       (void) pg_parse_json(&lex, &nullSemAction);
+
+       freeJsonLexContext(&lex);
+
+       return 0;
+}
+
+#ifdef STANDALONE_FUZZ_TARGET
+int
+main(int argc, char **argv)
+{
+       int                     i;
+       int                     ret = 0;
+
+       for (i = 1; i < argc; i++)
+       {
+               FILE       *f = fopen(argv[i], "rb");
+               long            len;
+               uint8_t    *buf;
+               size_t          n_read;
+
+               if (!f)
+               {
+                       fprintf(stderr, "%s: could not open %s: %m\n", argv[0], argv[i]);
+                       ret = 1;
+                       continue;
+               }
+
+               fseek(f, 0, SEEK_END);
+               len = ftell(f);
+               fseek(f, 0, SEEK_SET);
+
+               if (len < 0)
+               {
+                       fprintf(stderr, "%s: could not determine size of %s\n",
+                                       argv[0], argv[i]);
+                       fclose(f);
+                       ret = 1;
+                       continue;
+               }
+
+               buf = malloc(len);
+               if (!buf)
+               {
+                       fprintf(stderr, "%s: out of memory\n", argv[0]);
+                       fclose(f);
+                       return 1;
+               }
+
+               n_read = fread(buf, 1, len, f);
+               fclose(f);
+
+               LLVMFuzzerTestOneInput(buf, n_read);
+               free(buf);
+       }
+
+       return ret;
+}
+#endif                                                 /* STANDALONE_FUZZ_TARGET */
diff --git a/src/test/fuzzing/fuzz_json_incremental.c b/src/test/fuzzing/fuzz_json_incremental.c
new file mode 100644 (file)
index 0000000..7692f5c
--- /dev/null
@@ -0,0 +1,127 @@
+/*-------------------------------------------------------------------------
+ *
+ * fuzz_json_incremental.c
+ *    Fuzzing harness for the incremental JSON parser
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *    src/test/fuzzing/fuzz_json_incremental.c
+ *
+ * This harness feeds arbitrary byte sequences to
+ * pg_parse_json_incremental() in small chunks, exercising the
+ * incremental lexer's boundary handling.  The first byte of the input
+ * is used to vary the chunk size so that the fuzzer can explore
+ * different splitting strategies.
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <stdio.h>
+
+#include "common/jsonapi.h"
+#include "mb/pg_wchar.h"
+
+int                    LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
+
+int
+LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+{
+       JsonLexContext lex;
+       size_t          chunk_size;
+       size_t          offset;
+
+       if (size < 2)
+               return 0;
+
+       /*
+        * Use the first byte to select a chunk size between 1 and 128.  This lets
+        * the fuzzer explore different ways of splitting the same input across
+        * incremental parse calls.
+        */
+       chunk_size = (data[0] % 128) + 1;
+       data++;
+       size--;
+
+       makeJsonLexContextIncremental(&lex, PG_UTF8, true);
+       setJsonLexContextOwnsTokens(&lex, true);
+
+       offset = 0;
+       while (offset < size)
+       {
+               size_t          remaining = size - offset;
+               size_t          to_feed = (remaining < chunk_size) ? remaining : chunk_size;
+               bool            is_last = (offset + to_feed >= size);
+               JsonParseErrorType result;
+
+               result = pg_parse_json_incremental(&lex, &nullSemAction,
+                                                                                  (const char *) data + offset,
+                                                                                  to_feed, is_last);
+
+               offset += to_feed;
+
+               if (result != JSON_SUCCESS && result != JSON_INCOMPLETE)
+                       break;
+               if (result == JSON_SUCCESS)
+                       break;
+       }
+
+       freeJsonLexContext(&lex);
+
+       return 0;
+}
+
+#ifdef STANDALONE_FUZZ_TARGET
+int
+main(int argc, char **argv)
+{
+       int                     i;
+       int                     ret = 0;
+
+       for (i = 1; i < argc; i++)
+       {
+               FILE       *f = fopen(argv[i], "rb");
+               long            len;
+               uint8_t    *buf;
+               size_t          n_read;
+
+               if (!f)
+               {
+                       fprintf(stderr, "%s: could not open %s: %m\n", argv[0], argv[i]);
+                       ret = 1;
+                       continue;
+               }
+
+               fseek(f, 0, SEEK_END);
+               len = ftell(f);
+               fseek(f, 0, SEEK_SET);
+
+               if (len < 0)
+               {
+                       fprintf(stderr, "%s: could not determine size of %s\n",
+                                       argv[0], argv[i]);
+                       fclose(f);
+                       ret = 1;
+                       continue;
+               }
+
+               buf = malloc(len);
+               if (!buf)
+               {
+                       fprintf(stderr, "%s: out of memory\n", argv[0]);
+                       fclose(f);
+                       return 1;
+               }
+
+               n_read = fread(buf, 1, len, f);
+               fclose(f);
+
+               LLVMFuzzerTestOneInput(buf, n_read);
+               free(buf);
+       }
+
+       return ret;
+}
+#endif                                                 /* STANDALONE_FUZZ_TARGET */
diff --git a/src/test/fuzzing/fuzz_parsepgarray.c b/src/test/fuzzing/fuzz_parsepgarray.c
new file mode 100644 (file)
index 0000000..38c67da
--- /dev/null
@@ -0,0 +1,102 @@
+/*-------------------------------------------------------------------------
+ *
+ * fuzz_parsepgarray.c
+ *    Fuzzing harness for parsePGArray()
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *    src/test/fuzzing/fuzz_parsepgarray.c
+ *
+ * This harness feeds arbitrary byte sequences to parsePGArray(),
+ * which parses PostgreSQL array literal syntax ({elem,"elem",...})
+ * including nested arrays, quoted elements, and backslash escaping.
+ * The function is standalone and requires no database connection.
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <stdio.h>
+
+#include "fe_utils/string_utils.h"
+
+int                    LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
+
+int
+LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+{
+       char       *str;
+       char      **items = NULL;
+       int                     nitems = 0;
+
+       if (size == 0)
+               return 0;
+
+       /* parsePGArray expects a NUL-terminated string */
+       str = malloc(size + 1);
+       if (!str)
+               return 0;
+       memcpy(str, data, size);
+       str[size] = '\0';
+
+       (void) parsePGArray(str, &items, &nitems);
+       free(items);
+
+       free(str);
+       return 0;
+}
+
+#ifdef STANDALONE_FUZZ_TARGET
+int
+main(int argc, char **argv)
+{
+       int                     i;
+       int                     ret = 0;
+
+       for (i = 1; i < argc; i++)
+       {
+               FILE       *f = fopen(argv[i], "rb");
+               long            len;
+               uint8_t    *buf;
+               size_t          n_read;
+
+               if (!f)
+               {
+                       fprintf(stderr, "%s: could not open %s: %m\n", argv[0], argv[i]);
+                       ret = 1;
+                       continue;
+               }
+
+               fseek(f, 0, SEEK_END);
+               len = ftell(f);
+               fseek(f, 0, SEEK_SET);
+
+               if (len < 0)
+               {
+                       fprintf(stderr, "%s: could not determine size of %s\n",
+                                       argv[0], argv[i]);
+                       fclose(f);
+                       ret = 1;
+                       continue;
+               }
+
+               buf = malloc(len);
+               if (!buf)
+               {
+                       fprintf(stderr, "%s: out of memory\n", argv[0]);
+                       fclose(f);
+                       return 1;
+               }
+
+               n_read = fread(buf, 1, len, f);
+               fclose(f);
+
+               LLVMFuzzerTestOneInput(buf, n_read);
+               free(buf);
+       }
+
+       return ret;
+}
+#endif                                                 /* STANDALONE_FUZZ_TARGET */
diff --git a/src/test/fuzzing/fuzz_pgbench_expr.c b/src/test/fuzzing/fuzz_pgbench_expr.c
new file mode 100644 (file)
index 0000000..a326fa7
--- /dev/null
@@ -0,0 +1,211 @@
+/*-------------------------------------------------------------------------
+ *
+ * fuzz_pgbench_expr.c
+ *    Fuzzing harness for the pgbench expression parser
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *    src/test/fuzzing/fuzz_pgbench_expr.c
+ *
+ * This harness feeds arbitrary byte sequences to the pgbench expression
+ * parser (expr_yyparse).  The parser exercises a Bison grammar and Flex
+ * lexer that handle arithmetic expressions, function calls, boolean
+ * operators, and CASE expressions.
+ *
+ * The pgbench expression parser normally calls syntax_error() on any
+ * parse error, which calls exit(1).  This harness provides replacement
+ * definitions of syntax_error(), strtoint64(), and strtodouble() so
+ * that the generated parser and lexer objects can link without pulling
+ * in pgbench.c.  Our syntax_error() uses longjmp to recover rather
+ * than exiting.
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <setjmp.h>
+#include <stdio.h>
+
+#include "pgbench.h"
+#include "fe_utils/psqlscan.h"
+
+static sigjmp_buf fuzz_jmp_buf;
+
+static void free_pgbench_expr(PgBenchExpr *expr);
+
+static const PsqlScanCallbacks fuzz_callbacks = {
+       NULL,                                           /* no variable lookup needed */
+};
+
+int                    LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
+
+/*
+ * Replacement for pgbench.c's syntax_error().  Instead of calling exit(),
+ * we longjmp back to the fuzzer's recovery point.
+ */
+void
+syntax_error(const char *source, int lineno, const char *line,
+                        const char *command, const char *msg,
+                        const char *more, int column)
+{
+       siglongjmp(fuzz_jmp_buf, 1);
+}
+
+/*
+ * Replacement for pgbench.c's strtoint64().
+ */
+bool
+strtoint64(const char *str, bool errorOK, int64 *result)
+{
+       char       *end;
+
+       errno = 0;
+       *result = strtoi64(str, &end, 10);
+
+       if (errno == ERANGE || errno != 0 || end == str || *end != '\0')
+               return false;
+       return true;
+}
+
+/*
+ * Replacement for pgbench.c's strtodouble().
+ */
+bool
+strtodouble(const char *str, bool errorOK, double *dv)
+{
+       char       *end;
+
+       errno = 0;
+       *dv = strtod(str, &end);
+
+       if (errno == ERANGE || errno != 0 || end == str || *end != '\0')
+               return false;
+       return true;
+}
+
+/*
+ * Recursively free a PgBenchExpr tree.
+ */
+static void
+free_pgbench_expr(PgBenchExpr *expr)
+{
+       PgBenchExprLink *link;
+       PgBenchExprLink *next;
+
+       if (expr == NULL)
+               return;
+
+       switch (expr->etype)
+       {
+               case ENODE_CONSTANT:
+                       break;
+               case ENODE_VARIABLE:
+                       pg_free(expr->u.variable.varname);
+                       break;
+               case ENODE_FUNCTION:
+                       for (link = expr->u.function.args; link != NULL; link = next)
+                       {
+                               next = link->next;
+                               free_pgbench_expr(link->expr);
+                               pg_free(link);
+                       }
+                       break;
+       }
+
+       pg_free(expr);
+}
+
+int
+LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+{
+       char       *str;
+       PsqlScanState sstate;
+       yyscan_t        yyscanner;
+       PgBenchExpr *result = NULL;
+
+       if (size == 0)
+               return 0;
+
+       /* expr_yyparse needs a NUL-terminated string */
+       str = malloc(size + 1);
+       if (!str)
+               return 0;
+       memcpy(str, data, size);
+       str[size] = '\0';
+
+       sstate = psql_scan_create(&fuzz_callbacks);
+       psql_scan_setup(sstate, str, (int) size, 0, true);
+
+       yyscanner = expr_scanner_init(sstate, "fuzz", 1, 0, "\\set");
+
+       if (sigsetjmp(fuzz_jmp_buf, 0) == 0)
+       {
+               (void) expr_yyparse(&result, yyscanner);
+       }
+
+       /* Clean up regardless of success or longjmp */
+       expr_scanner_finish(yyscanner);
+       psql_scan_finish(sstate);
+       psql_scan_destroy(sstate);
+
+       if (result)
+               free_pgbench_expr(result);
+
+       free(str);
+       return 0;
+}
+
+#ifdef STANDALONE_FUZZ_TARGET
+int
+main(int argc, char **argv)
+{
+       int                     i;
+       int                     ret = 0;
+
+       for (i = 1; i < argc; i++)
+       {
+               FILE       *f = fopen(argv[i], "rb");
+               long            len;
+               uint8_t    *buf;
+               size_t          n_read;
+
+               if (!f)
+               {
+                       fprintf(stderr, "%s: could not open %s: %m\n", argv[0], argv[i]);
+                       ret = 1;
+                       continue;
+               }
+
+               fseek(f, 0, SEEK_END);
+               len = ftell(f);
+               fseek(f, 0, SEEK_SET);
+
+               if (len < 0)
+               {
+                       fprintf(stderr, "%s: could not determine size of %s\n",
+                                       argv[0], argv[i]);
+                       fclose(f);
+                       ret = 1;
+                       continue;
+               }
+
+               buf = malloc(len);
+               if (!buf)
+               {
+                       fprintf(stderr, "%s: out of memory\n", argv[0]);
+                       fclose(f);
+                       return 1;
+               }
+
+               n_read = fread(buf, 1, len, f);
+               fclose(f);
+
+               LLVMFuzzerTestOneInput(buf, n_read);
+               free(buf);
+       }
+
+       return ret;
+}
+#endif                                                 /* STANDALONE_FUZZ_TARGET */
diff --git a/src/test/fuzzing/fuzz_pglz.c b/src/test/fuzzing/fuzz_pglz.c
new file mode 100644 (file)
index 0000000..b1912e3
--- /dev/null
@@ -0,0 +1,127 @@
+/*-------------------------------------------------------------------------
+ *
+ * fuzz_pglz.c
+ *    Fuzzing harness for the PostgreSQL LZ decompressor
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *    src/test/fuzzing/fuzz_pglz.c
+ *
+ * This harness feeds arbitrary byte sequences to pglz_decompress(),
+ * which decompresses PostgreSQL's native LZ-compressed data.  The
+ * decompressor is a pure function with no global state, making it
+ * ideal for fuzzing.
+ *
+ * The first 4 bytes of the fuzzer input are interpreted as the
+ * claimed raw (uncompressed) size in little-endian byte order,
+ * capped at 1 MB.  The remaining bytes are the compressed payload.
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <stdio.h>
+#include <string.h>
+
+#include "common/pg_lzcompress.h"
+
+#define MAX_RAW_SIZE (1024 * 1024)
+
+int                    LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
+
+int
+LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+{
+       int32           rawsize;
+       char       *dest;
+
+       /* Need at least 4 bytes for the raw size, plus some compressed data */
+       if (size < 5)
+               return 0;
+
+       /* Extract claimed raw size from first 4 bytes (little-endian) */
+       rawsize = (int32) data[0] |
+               ((int32) data[1] << 8) |
+               ((int32) data[2] << 16) |
+               ((int32) data[3] << 24);
+
+       /* Reject nonsensical sizes */
+       if (rawsize <= 0 || rawsize > MAX_RAW_SIZE)
+               return 0;
+
+       dest = malloc(rawsize);
+       if (!dest)
+               return 0;
+
+       /* Try decompression with completeness check */
+       (void) pglz_decompress((const char *) data + 4,
+                                                  (int32) (size - 4),
+                                                  dest,
+                                                  rawsize,
+                                                  true);
+
+       /* Also try without completeness check to exercise that path */
+       (void) pglz_decompress((const char *) data + 4,
+                                                  (int32) (size - 4),
+                                                  dest,
+                                                  rawsize,
+                                                  false);
+
+       free(dest);
+       return 0;
+}
+
+#ifdef STANDALONE_FUZZ_TARGET
+int
+main(int argc, char **argv)
+{
+       int                     i;
+       int                     ret = 0;
+
+       for (i = 1; i < argc; i++)
+       {
+               FILE       *f = fopen(argv[i], "rb");
+               long            len;
+               uint8_t    *buf;
+               size_t          n_read;
+
+               if (!f)
+               {
+                       fprintf(stderr, "%s: could not open %s: %m\n", argv[0], argv[i]);
+                       ret = 1;
+                       continue;
+               }
+
+               fseek(f, 0, SEEK_END);
+               len = ftell(f);
+               fseek(f, 0, SEEK_SET);
+
+               if (len < 0)
+               {
+                       fprintf(stderr, "%s: could not determine size of %s\n",
+                                       argv[0], argv[i]);
+                       fclose(f);
+                       ret = 1;
+                       continue;
+               }
+
+               buf = malloc(len);
+               if (!buf)
+               {
+                       fprintf(stderr, "%s: out of memory\n", argv[0]);
+                       fclose(f);
+                       return 1;
+               }
+
+               n_read = fread(buf, 1, len, f);
+               fclose(f);
+
+               LLVMFuzzerTestOneInput(buf, n_read);
+               free(buf);
+       }
+
+       return ret;
+}
+#endif                                                 /* STANDALONE_FUZZ_TARGET */
diff --git a/src/test/fuzzing/fuzz_rawparser.c b/src/test/fuzzing/fuzz_rawparser.c
new file mode 100644 (file)
index 0000000..ba5f70c
--- /dev/null
@@ -0,0 +1,162 @@
+/*-------------------------------------------------------------------------
+ *
+ * fuzz_rawparser.c
+ *    Fuzzing harness for the PostgreSQL raw SQL parser
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *    src/test/fuzzing/fuzz_rawparser.c
+ *
+ * This harness feeds arbitrary byte sequences to raw_parser(), which
+ * performs lexical and grammatical analysis of SQL statements.  It
+ * performs minimal backend initialization (just the memory-context
+ * subsystem) and catches all parser errors via PG_TRY/PG_CATCH.
+ *
+ * The harness links against postgres_lib using archive semantics.
+ * It provides stub definitions for symbols normally supplied by
+ * main/main.c (progname, parse_dispatch_option) so that the linker
+ * does not pull in main.o and conflict with the harness's own main().
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include <stdio.h>
+
+#include "mb/pg_wchar.h"
+#include "miscadmin.h"
+#include "parser/parser.h"
+#include "postmaster/postmaster.h"
+#include "utils/memutils.h"
+#include "utils/palloc.h"
+
+/*
+ * Stub definitions for symbols that main/main.c normally provides.
+ * By defining them here we prevent the archive linker from pulling in
+ * main.o (which defines its own main()).
+ */
+const char *progname = "fuzz_rawparser";
+
+DispatchOption
+parse_dispatch_option(const char *name)
+{
+       return DISPATCH_POSTMASTER;
+}
+
+static bool initialized = false;
+
+static void fuzz_initialize(void);
+
+int                    LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
+
+/*
+ * One-time initialization: set up memory contexts and encoding.
+ */
+static void
+fuzz_initialize(void)
+{
+       MemoryContextInit();
+       SetDatabaseEncoding(PG_UTF8);
+       SetMessageEncoding(PG_UTF8);
+}
+
+int
+LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+{
+       char       *str;
+       MemoryContext fuzz_context;
+       MemoryContext oldcontext;
+
+       if (!initialized)
+       {
+               fuzz_initialize();
+               initialized = true;
+       }
+
+       if (size == 0)
+               return 0;
+
+       /*
+        * Create a temporary memory context for each parse attempt so that all
+        * allocations made by the parser are freed afterwards.
+        */
+       fuzz_context = AllocSetContextCreate(TopMemoryContext,
+                                                                                "Fuzz Context",
+                                                                                ALLOCSET_DEFAULT_SIZES);
+       oldcontext = MemoryContextSwitchTo(fuzz_context);
+
+       /* raw_parser() expects a NUL-terminated string */
+       str = palloc(size + 1);
+       memcpy(str, data, size);
+       str[size] = '\0';
+
+       PG_TRY();
+       {
+               (void) raw_parser(str, RAW_PARSE_DEFAULT);
+       }
+       PG_CATCH();
+       {
+               FlushErrorState();
+       }
+       PG_END_TRY();
+
+       MemoryContextSwitchTo(oldcontext);
+       MemoryContextDelete(fuzz_context);
+
+       return 0;
+}
+
+#ifdef STANDALONE_FUZZ_TARGET
+int
+main(int argc, char **argv)
+{
+       int                     i;
+       int                     ret = 0;
+
+       for (i = 1; i < argc; i++)
+       {
+               FILE       *f = fopen(argv[i], "rb");
+               long            len;
+               uint8_t    *buf;
+               size_t          n_read;
+
+               if (!f)
+               {
+                       fprintf(stderr, "%s: could not open %s: %m\n", argv[0], argv[i]);
+                       ret = 1;
+                       continue;
+               }
+
+               fseek(f, 0, SEEK_END);
+               len = ftell(f);
+               fseek(f, 0, SEEK_SET);
+
+               if (len < 0)
+               {
+                       fprintf(stderr, "%s: could not determine size of %s\n",
+                                       argv[0], argv[i]);
+                       fclose(f);
+                       ret = 1;
+                       continue;
+               }
+
+               buf = malloc(len);
+               if (!buf)
+               {
+                       fprintf(stderr, "%s: out of memory\n", argv[0]);
+                       fclose(f);
+                       return 1;
+               }
+
+               n_read = fread(buf, 1, len, f);
+               fclose(f);
+
+               LLVMFuzzerTestOneInput(buf, n_read);
+               free(buf);
+       }
+
+       return ret;
+}
+#endif                                                 /* STANDALONE_FUZZ_TARGET */
diff --git a/src/test/fuzzing/fuzz_regex.c b/src/test/fuzzing/fuzz_regex.c
new file mode 100644 (file)
index 0000000..584ee32
--- /dev/null
@@ -0,0 +1,193 @@
+/*-------------------------------------------------------------------------
+ *
+ * fuzz_regex.c
+ *    Fuzzing harness for the PostgreSQL regular expression engine
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *    src/test/fuzzing/fuzz_regex.c
+ *
+ * This harness feeds arbitrary byte sequences to pg_regcomp() and
+ * pg_regexec(), exercising the full POSIX/ARE regex compiler and
+ * executor.  The first byte selects regex flags; the remaining bytes
+ * are split between the regex pattern and a test subject string.
+ *
+ * The harness links against postgres_lib using archive semantics.
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include <stdio.h>
+
+#include "catalog/pg_collation.h"
+#include "mb/pg_wchar.h"
+#include "miscadmin.h"
+#include "postmaster/postmaster.h"
+#include "regex/regex.h"
+#include "utils/memutils.h"
+#include "utils/palloc.h"
+
+/* Stubs for symbols from main/main.c */
+const char *progname = "fuzz_regex";
+
+DispatchOption
+parse_dispatch_option(const char *name)
+{
+       return DISPATCH_POSTMASTER;
+}
+
+static bool initialized = false;
+
+static void fuzz_initialize(void);
+
+int                    LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
+
+static void
+fuzz_initialize(void)
+{
+       MemoryContextInit();
+       SetDatabaseEncoding(PG_UTF8);
+       SetMessageEncoding(PG_UTF8);
+}
+
+int
+LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+{
+       uint8_t         flags_byte;
+       int                     re_flags;
+       size_t          pat_len;
+       size_t          subj_len;
+       const char *pat_start;
+       const char *subj_start;
+       pg_wchar   *pat_wchar;
+       pg_wchar   *subj_wchar;
+       int                     pat_wlen;
+       int                     subj_wlen;
+       regex_t         re;
+       regmatch_t      matches[10];
+       MemoryContext fuzz_context;
+       MemoryContext oldcontext;
+
+       if (!initialized)
+       {
+               fuzz_initialize();
+               initialized = true;
+       }
+
+       /* Need at least flags byte + 1 byte of pattern */
+       if (size < 2)
+               return 0;
+
+       /*
+        * First byte selects regex flags. We map bits to useful flag combinations
+        * to get good coverage of different regex modes.
+        */
+       flags_byte = data[0];
+       re_flags = REG_ADVANCED;
+       if (flags_byte & 0x01)
+               re_flags = REG_EXTENDED;        /* ERE instead of ARE */
+       if (flags_byte & 0x02)
+               re_flags |= REG_ICASE;
+       if (flags_byte & 0x04)
+               re_flags |= REG_NEWLINE;
+       if (flags_byte & 0x08)
+               re_flags |= REG_NOSUB;
+
+       data++;
+       size--;
+
+       /* Split remaining input: first half pattern, second half subject */
+       pat_len = size / 2;
+       if (pat_len == 0)
+               pat_len = 1;
+       subj_len = size - pat_len;
+
+       pat_start = (const char *) data;
+       subj_start = (const char *) data + pat_len;
+
+       fuzz_context = AllocSetContextCreate(TopMemoryContext,
+                                                                                "Fuzz Context",
+                                                                                ALLOCSET_DEFAULT_SIZES);
+       oldcontext = MemoryContextSwitchTo(fuzz_context);
+
+       /* Convert to pg_wchar for the regex API */
+       pat_wchar = palloc((pat_len + 1) * sizeof(pg_wchar));
+       pat_wlen = pg_mb2wchar_with_len(pat_start, pat_wchar, (int) pat_len);
+
+       if (pg_regcomp(&re, pat_wchar, pat_wlen, re_flags, C_COLLATION_OID) == 0)
+       {
+               /* Compile succeeded â€” try executing against the subject */
+               if (subj_len > 0)
+               {
+                       subj_wchar = palloc((subj_len + 1) * sizeof(pg_wchar));
+                       subj_wlen = pg_mb2wchar_with_len(subj_start, subj_wchar,
+                                                                                        (int) subj_len);
+
+                       (void) pg_regexec(&re, subj_wchar, subj_wlen, 0, NULL,
+                                                         lengthof(matches), matches, 0);
+               }
+
+               pg_regfree(&re);
+       }
+
+       MemoryContextSwitchTo(oldcontext);
+       MemoryContextDelete(fuzz_context);
+
+       return 0;
+}
+
+#ifdef STANDALONE_FUZZ_TARGET
+int
+main(int argc, char **argv)
+{
+       int                     i;
+       int                     ret = 0;
+
+       for (i = 1; i < argc; i++)
+       {
+               FILE       *f = fopen(argv[i], "rb");
+               long            len;
+               uint8_t    *buf;
+               size_t          n_read;
+
+               if (!f)
+               {
+                       fprintf(stderr, "%s: could not open %s: %m\n", argv[0], argv[i]);
+                       ret = 1;
+                       continue;
+               }
+
+               fseek(f, 0, SEEK_END);
+               len = ftell(f);
+               fseek(f, 0, SEEK_SET);
+
+               if (len < 0)
+               {
+                       fprintf(stderr, "%s: could not determine size of %s\n",
+                                       argv[0], argv[i]);
+                       fclose(f);
+                       ret = 1;
+                       continue;
+               }
+
+               buf = malloc(len);
+               if (!buf)
+               {
+                       fprintf(stderr, "%s: out of memory\n", argv[0]);
+                       fclose(f);
+                       return 1;
+               }
+
+               n_read = fread(buf, 1, len, f);
+               fclose(f);
+
+               LLVMFuzzerTestOneInput(buf, n_read);
+               free(buf);
+       }
+
+       return ret;
+}
+#endif                                                 /* STANDALONE_FUZZ_TARGET */
diff --git a/src/test/fuzzing/fuzz_saslprep.c b/src/test/fuzzing/fuzz_saslprep.c
new file mode 100644 (file)
index 0000000..7717076
--- /dev/null
@@ -0,0 +1,104 @@
+/*-------------------------------------------------------------------------
+ *
+ * fuzz_saslprep.c
+ *    Fuzzing harness for pg_saslprep()
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *    src/test/fuzzing/fuzz_saslprep.c
+ *
+ * This harness feeds arbitrary byte sequences to pg_saslprep(),
+ * which performs SASLprep normalization (RFC 4013) on UTF-8 strings.
+ * This involves Unicode NFKC normalization, character mapping, and
+ * prohibited character detection.  The function is standalone and
+ * requires no database connection.
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <stdio.h>
+
+#include "common/saslprep.h"
+
+int                    LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
+
+int
+LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+{
+       char       *str;
+       char       *output = NULL;
+
+       if (size == 0)
+               return 0;
+
+       /* pg_saslprep expects a NUL-terminated string */
+       str = malloc(size + 1);
+       if (!str)
+               return 0;
+       memcpy(str, data, size);
+       str[size] = '\0';
+
+       (void) pg_saslprep(str, &output);
+
+       if (output)
+               free(output);
+
+       free(str);
+       return 0;
+}
+
+#ifdef STANDALONE_FUZZ_TARGET
+int
+main(int argc, char **argv)
+{
+       int                     i;
+       int                     ret = 0;
+
+       for (i = 1; i < argc; i++)
+       {
+               FILE       *f = fopen(argv[i], "rb");
+               long            len;
+               uint8_t    *buf;
+               size_t          n_read;
+
+               if (!f)
+               {
+                       fprintf(stderr, "%s: could not open %s: %m\n", argv[0], argv[i]);
+                       ret = 1;
+                       continue;
+               }
+
+               fseek(f, 0, SEEK_END);
+               len = ftell(f);
+               fseek(f, 0, SEEK_SET);
+
+               if (len < 0)
+               {
+                       fprintf(stderr, "%s: could not determine size of %s\n",
+                                       argv[0], argv[i]);
+                       fclose(f);
+                       ret = 1;
+                       continue;
+               }
+
+               buf = malloc(len);
+               if (!buf)
+               {
+                       fprintf(stderr, "%s: out of memory\n", argv[0]);
+                       fclose(f);
+                       return 1;
+               }
+
+               n_read = fread(buf, 1, len, f);
+               fclose(f);
+
+               LLVMFuzzerTestOneInput(buf, n_read);
+               free(buf);
+       }
+
+       return ret;
+}
+#endif                                                 /* STANDALONE_FUZZ_TARGET */
diff --git a/src/test/fuzzing/fuzz_typeinput.c b/src/test/fuzzing/fuzz_typeinput.c
new file mode 100644 (file)
index 0000000..ee5e813
--- /dev/null
@@ -0,0 +1,218 @@
+/*-------------------------------------------------------------------------
+ *
+ * fuzz_typeinput.c
+ *    Fuzzing harness for PostgreSQL type input functions
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *    src/test/fuzzing/fuzz_typeinput.c
+ *
+ * This harness feeds arbitrary byte sequences to the backend's type
+ * input functions: numeric_in, date_in, timestamp_in, timestamptz_in,
+ * and interval_in.  These functions parse textual representations of
+ * data types and are a key part of PostgreSQL's input validation.
+ *
+ * The first byte of input selects which type parser to call; the
+ * remaining bytes are the type-input string.  All functions support
+ * soft error handling via ErrorSaveContext, so errors are caught
+ * without ereport/PG_TRY.  PG_TRY/PG_CATCH is used as a safety net
+ * for any unexpected hard errors.
+ *
+ * The harness links against postgres_lib using archive semantics.
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include <stdio.h>
+
+#include "fmgr.h"
+#include "mb/pg_wchar.h"
+#include "miscadmin.h"
+#include "nodes/miscnodes.h"
+#include "pgtime.h"
+#include "postmaster/postmaster.h"
+#include "utils/builtins.h"
+#include "utils/datetime.h"
+#include "utils/memutils.h"
+#include "utils/numeric.h"
+#include "utils/palloc.h"
+#include "utils/timestamp.h"
+
+/* Stubs for symbols from main/main.c */
+const char *progname = "fuzz_typeinput";
+
+DispatchOption
+parse_dispatch_option(const char *name)
+{
+       return DISPATCH_POSTMASTER;
+}
+
+/* Type selector values */
+#define FUZZ_NUMERIC   0
+#define FUZZ_DATE              1
+#define FUZZ_TIMESTAMP 2
+#define FUZZ_TIMESTAMPTZ 3
+#define FUZZ_INTERVAL  4
+#define FUZZ_NTYPES            5
+
+static bool initialized = false;
+
+static void fuzz_initialize(void);
+
+int                    LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
+
+static void
+fuzz_initialize(void)
+{
+       MemoryContextInit();
+       SetDatabaseEncoding(PG_UTF8);
+       SetMessageEncoding(PG_UTF8);
+
+       /*
+        * Initialize timezone subsystem.  Use "GMT" because it is resolved
+        * without filesystem access (the timezone data directory may not exist in
+        * a fuzzing build).
+        */
+       pg_timezone_initialize();
+}
+
+int
+LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+{
+       char       *str;
+       int                     type_sel;
+
+       LOCAL_FCINFO(fcinfo, 3);
+       ErrorSaveContext escontext;
+       MemoryContext fuzz_context;
+       MemoryContext oldcontext;
+
+       if (!initialized)
+       {
+               fuzz_initialize();
+               initialized = true;
+       }
+
+       /* Need at least type selector + 1 byte of input */
+       if (size < 2)
+               return 0;
+
+       type_sel = data[0] % FUZZ_NTYPES;
+       data++;
+       size--;
+
+       fuzz_context = AllocSetContextCreate(TopMemoryContext,
+                                                                                "Fuzz Context",
+                                                                                ALLOCSET_DEFAULT_SIZES);
+       oldcontext = MemoryContextSwitchTo(fuzz_context);
+
+       /* Build a NUL-terminated string from the input */
+       str = palloc(size + 1);
+       memcpy(str, data, size);
+       str[size] = '\0';
+
+       /* Set up ErrorSaveContext for soft error handling */
+       memset(&escontext, 0, sizeof(escontext));
+       escontext.type = T_ErrorSaveContext;
+       escontext.error_occurred = false;
+       escontext.details_wanted = false;
+
+       /* Set up FunctionCallInfo */
+       memset(fcinfo, 0, SizeForFunctionCallInfo(3));
+       fcinfo->nargs = 3;
+       fcinfo->args[0].value = CStringGetDatum(str);
+       fcinfo->args[0].isnull = false;
+       fcinfo->args[1].value = ObjectIdGetDatum(InvalidOid);   /* typelem */
+       fcinfo->args[1].isnull = false;
+       fcinfo->args[2].value = Int32GetDatum(-1);      /* typmod */
+       fcinfo->args[2].isnull = false;
+       fcinfo->context = (Node *) &escontext;
+
+       PG_TRY();
+       {
+               switch (type_sel)
+               {
+                       case FUZZ_NUMERIC:
+                               (void) numeric_in(fcinfo);
+                               break;
+                       case FUZZ_DATE:
+                               (void) date_in(fcinfo);
+                               break;
+                       case FUZZ_TIMESTAMP:
+                               (void) timestamp_in(fcinfo);
+                               break;
+                       case FUZZ_TIMESTAMPTZ:
+                               (void) timestamptz_in(fcinfo);
+                               break;
+                       case FUZZ_INTERVAL:
+                               (void) interval_in(fcinfo);
+                               break;
+               }
+       }
+       PG_CATCH();
+       {
+               FlushErrorState();
+       }
+       PG_END_TRY();
+
+       MemoryContextSwitchTo(oldcontext);
+       MemoryContextDelete(fuzz_context);
+
+       return 0;
+}
+
+#ifdef STANDALONE_FUZZ_TARGET
+int
+main(int argc, char **argv)
+{
+       int                     i;
+       int                     ret = 0;
+
+       for (i = 1; i < argc; i++)
+       {
+               FILE       *f = fopen(argv[i], "rb");
+               long            len;
+               uint8_t    *buf;
+               size_t          n_read;
+
+               if (!f)
+               {
+                       fprintf(stderr, "%s: could not open %s: %m\n", argv[0], argv[i]);
+                       ret = 1;
+                       continue;
+               }
+
+               fseek(f, 0, SEEK_END);
+               len = ftell(f);
+               fseek(f, 0, SEEK_SET);
+
+               if (len < 0)
+               {
+                       fprintf(stderr, "%s: could not determine size of %s\n",
+                                       argv[0], argv[i]);
+                       fclose(f);
+                       ret = 1;
+                       continue;
+               }
+
+               buf = malloc(len);
+               if (!buf)
+               {
+                       fprintf(stderr, "%s: out of memory\n", argv[0]);
+                       fclose(f);
+                       return 1;
+               }
+
+               n_read = fread(buf, 1, len, f);
+               fclose(f);
+
+               LLVMFuzzerTestOneInput(buf, n_read);
+               free(buf);
+       }
+
+       return ret;
+}
+#endif                                                 /* STANDALONE_FUZZ_TARGET */
diff --git a/src/test/fuzzing/fuzz_unescapebytea.c b/src/test/fuzzing/fuzz_unescapebytea.c
new file mode 100644 (file)
index 0000000..a900344
--- /dev/null
@@ -0,0 +1,103 @@
+/*-------------------------------------------------------------------------
+ *
+ * fuzz_unescapebytea.c
+ *    Fuzzing harness for PQunescapeBytea()
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *    src/test/fuzzing/fuzz_unescapebytea.c
+ *
+ * This harness feeds arbitrary byte sequences to PQunescapeBytea(),
+ * which decodes bytea escape formats: hex (\xDEAD...) and legacy
+ * backslash-octal (\352\273\276...).  The function is completely
+ * standalone and requires no database connection.
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <stdio.h>
+
+#include "libpq-fe.h"
+
+int                    LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
+
+int
+LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+{
+       char       *str;
+       size_t          resultlen;
+       unsigned char *result;
+
+       if (size == 0)
+               return 0;
+
+       /* PQunescapeBytea expects a NUL-terminated string */
+       str = malloc(size + 1);
+       if (!str)
+               return 0;
+       memcpy(str, data, size);
+       str[size] = '\0';
+
+       result = PQunescapeBytea((const unsigned char *) str, &resultlen);
+       if (result)
+               PQfreemem(result);
+
+       free(str);
+       return 0;
+}
+
+#ifdef STANDALONE_FUZZ_TARGET
+int
+main(int argc, char **argv)
+{
+       int                     i;
+       int                     ret = 0;
+
+       for (i = 1; i < argc; i++)
+       {
+               FILE       *f = fopen(argv[i], "rb");
+               long            len;
+               uint8_t    *buf;
+               size_t          n_read;
+
+               if (!f)
+               {
+                       fprintf(stderr, "%s: could not open %s: %m\n", argv[0], argv[i]);
+                       ret = 1;
+                       continue;
+               }
+
+               fseek(f, 0, SEEK_END);
+               len = ftell(f);
+               fseek(f, 0, SEEK_SET);
+
+               if (len < 0)
+               {
+                       fprintf(stderr, "%s: could not determine size of %s\n",
+                                       argv[0], argv[i]);
+                       fclose(f);
+                       ret = 1;
+                       continue;
+               }
+
+               buf = malloc(len);
+               if (!buf)
+               {
+                       fprintf(stderr, "%s: out of memory\n", argv[0]);
+                       fclose(f);
+                       return 1;
+               }
+
+               n_read = fread(buf, 1, len, f);
+               fclose(f);
+
+               LLVMFuzzerTestOneInput(buf, n_read);
+               free(buf);
+       }
+
+       return ret;
+}
+#endif                                                 /* STANDALONE_FUZZ_TARGET */
diff --git a/src/test/fuzzing/meson.build b/src/test/fuzzing/meson.build
new file mode 100644 (file)
index 0000000..f05267d
--- /dev/null
@@ -0,0 +1,203 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Fuzzing harnesses for security testing.
+#
+# Build with:
+#   meson setup build -Dfuzzing=true
+#
+# For libFuzzer (recommended), also pass sanitizer flags:
+#   meson setup build -Dfuzzing=true \
+#     -Dc_args='-fsanitize=fuzzer-no-link' \
+#     -Dc_link_args='-fsanitize=fuzzer'
+#
+# Without a fuzzer engine the harnesses are built in standalone mode:
+# each reads input from files named on the command line.
+
+if not get_option('fuzzing')
+  subdir_done()
+endif
+
+# Detect whether a fuzzer engine (e.g. libFuzzer) is available.
+# If so, link fuzzer executables with -fsanitize=fuzzer so that the
+# engine provides main().  Otherwise compile with STANDALONE_FUZZ_TARGET
+# so the harnesses supply their own main() that reads from files.
+
+fuzz_c_args = []
+fuzz_link_args = []
+
+if cc.has_argument('-fsanitize=fuzzer-no-link')
+  fuzzer_has_engine = cc.links('''
+    #include <stdint.h>
+    #include <stddef.h>
+    int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+    { return 0; }
+  ''',
+    args: ['-fsanitize=fuzzer'],
+    name: 'libFuzzer support')
+else
+  fuzzer_has_engine = false
+endif
+
+if fuzzer_has_engine
+  fuzz_link_args += ['-fsanitize=fuzzer']
+else
+  fuzz_c_args += ['-DSTANDALONE_FUZZ_TARGET']
+endif
+
+# --- Frontend targets (no backend dependencies) ---
+
+fuzz_json = executable('fuzz_json',
+  'fuzz_json.c',
+  c_args: fuzz_c_args,
+  link_args: fuzz_link_args,
+  dependencies: [frontend_code],
+  kwargs: default_bin_args + {
+    'install': false,
+  },
+)
+
+fuzz_json_incremental = executable('fuzz_json_incremental',
+  'fuzz_json_incremental.c',
+  c_args: fuzz_c_args,
+  link_args: fuzz_link_args,
+  dependencies: [frontend_code],
+  kwargs: default_bin_args + {
+    'install': false,
+  },
+)
+
+fuzz_conninfo = executable('fuzz_conninfo',
+  'fuzz_conninfo.c',
+  c_args: fuzz_c_args,
+  link_args: fuzz_link_args,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args + {
+    'install': false,
+  },
+)
+
+fuzz_pglz = executable('fuzz_pglz',
+  'fuzz_pglz.c',
+  c_args: fuzz_c_args,
+  link_args: fuzz_link_args,
+  dependencies: [frontend_code],
+  kwargs: default_bin_args + {
+    'install': false,
+  },
+)
+
+fuzz_unescapebytea = executable('fuzz_unescapebytea',
+  'fuzz_unescapebytea.c',
+  c_args: fuzz_c_args,
+  link_args: fuzz_link_args,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args + {
+    'install': false,
+  },
+)
+
+fuzz_b64decode = executable('fuzz_b64decode',
+  'fuzz_b64decode.c',
+  c_args: fuzz_c_args,
+  link_args: fuzz_link_args,
+  dependencies: [frontend_code],
+  kwargs: default_bin_args + {
+    'install': false,
+  },
+)
+
+fuzz_saslprep = executable('fuzz_saslprep',
+  'fuzz_saslprep.c',
+  c_args: fuzz_c_args,
+  link_args: fuzz_link_args,
+  dependencies: [frontend_code],
+  kwargs: default_bin_args + {
+    'install': false,
+  },
+)
+
+fuzz_parsepgarray = executable('fuzz_parsepgarray',
+  'fuzz_parsepgarray.c',
+  c_args: fuzz_c_args,
+  link_args: fuzz_link_args,
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args + {
+    'install': false,
+  },
+)
+
+# The pgbench expression parser is built from generated Bison/Flex
+# sources.  We reference the same custom_target outputs used by the
+# pgbench build, and provide our own syntax_error() / strtoint64() /
+# strtodouble() so we don't pull in pgbench.c (which has its own
+# main() and calls exit() on parse errors).
+
+exprscan_fuzz = custom_target('exprscan_fuzz',
+  input: files('../../bin/pgbench/exprscan.l'),
+  output: 'exprscan.c',
+  command: flex_cmd,
+)
+
+exprparse_fuzz = custom_target('exprparse_fuzz',
+  input: files('../../bin/pgbench/exprparse.y'),
+  kwargs: bison_kw,
+)
+
+fuzz_pgbench_expr = executable('fuzz_pgbench_expr',
+  'fuzz_pgbench_expr.c',
+  exprscan_fuzz,
+  exprparse_fuzz,
+  c_args: fuzz_c_args,
+  link_args: fuzz_link_args,
+  include_directories: include_directories('../../bin/pgbench'),
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args + {
+    'install': false,
+  },
+)
+
+# --- Backend targets ---
+#
+# These link against postgres_lib using standard archive semantics
+# (link_with), so only objects needed to resolve symbols are pulled in.
+# The harness provides stub definitions for symbols exported by
+# main/main.c, preventing the archive linker from pulling in main.o
+# (which would conflict with the harness's own main()).
+#
+# Backend code uses function-pointer casts in hash tables (dynahash.c)
+# that trigger UBSan's -fsanitize=function check.  This is a known
+# benign pattern; when using -fsanitize=undefined, also pass
+# -fno-sanitize=function in the top-level c_args to suppress it.
+
+fuzz_rawparser = executable('fuzz_rawparser',
+  'fuzz_rawparser.c',
+  c_args: fuzz_c_args,
+  link_args: fuzz_link_args,
+  link_with: [postgres_lib],
+  dependencies: backend_build_deps,
+  kwargs: default_bin_args + {
+    'install': false,
+  },
+)
+
+fuzz_regex = executable('fuzz_regex',
+  'fuzz_regex.c',
+  c_args: fuzz_c_args,
+  link_args: fuzz_link_args,
+  link_with: [postgres_lib],
+  dependencies: backend_build_deps,
+  kwargs: default_bin_args + {
+    'install': false,
+  },
+)
+
+fuzz_typeinput = executable('fuzz_typeinput',
+  'fuzz_typeinput.c',
+  c_args: fuzz_c_args,
+  link_args: fuzz_link_args,
+  link_with: [postgres_lib],
+  dependencies: backend_build_deps,
+  kwargs: default_bin_args + {
+    'install': false,
+  },
+)
index cd45cbf57fb0f8e94965c7743bfc41fe937cfcbf..76387524c4cbf906dce0b5b4af069e630e75d0b7 100644 (file)
@@ -25,4 +25,6 @@ if icu.found()
   subdir('icu')
 endif
 
+subdir('fuzzing')
+
 subdir('perl')