From: Andrew Dunstan Date: Thu, 9 Apr 2026 18:20:40 +0000 (-0400) Subject: Add built-in fuzzing harnesses for security testing. X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=4a18907b412e77684bf888ad6d1b4844d220196a;p=thirdparty%2Fpostgresql.git Add built-in fuzzing harnesses for security testing. 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) --- diff --git a/meson_options.txt b/meson_options.txt index 6a793f3e479..4f60abccdc3 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -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 index 00000000000..f388aaf6908 --- /dev/null +++ b/src/test/fuzzing/fuzz_b64decode.c @@ -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 + +#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 index 00000000000..61993704bb5 --- /dev/null +++ b/src/test/fuzzing/fuzz_conninfo.c @@ -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 + +#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 index 00000000000..b265ddf0b24 --- /dev/null +++ b/src/test/fuzzing/fuzz_json.c @@ -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 + +#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 index 00000000000..7692f5ca371 --- /dev/null +++ b/src/test/fuzzing/fuzz_json_incremental.c @@ -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 + +#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 index 00000000000..38c67dae419 --- /dev/null +++ b/src/test/fuzzing/fuzz_parsepgarray.c @@ -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 + +#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 index 00000000000..a326fa7d307 --- /dev/null +++ b/src/test/fuzzing/fuzz_pgbench_expr.c @@ -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 +#include + +#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 index 00000000000..b1912e342d8 --- /dev/null +++ b/src/test/fuzzing/fuzz_pglz.c @@ -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 +#include + +#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 index 00000000000..ba5f70c8d7c --- /dev/null +++ b/src/test/fuzzing/fuzz_rawparser.c @@ -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 + +#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 index 00000000000..584ee32bdd6 --- /dev/null +++ b/src/test/fuzzing/fuzz_regex.c @@ -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 + +#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 index 00000000000..77170769c7a --- /dev/null +++ b/src/test/fuzzing/fuzz_saslprep.c @@ -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 + +#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 index 00000000000..ee5e8135d5d --- /dev/null +++ b/src/test/fuzzing/fuzz_typeinput.c @@ -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 + +#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 index 00000000000..a900344b4c1 --- /dev/null +++ b/src/test/fuzzing/fuzz_unescapebytea.c @@ -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 + +#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 index 00000000000..f05267da57f --- /dev/null +++ b/src/test/fuzzing/meson.build @@ -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 + #include + 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, + }, +) diff --git a/src/test/meson.build b/src/test/meson.build index cd45cbf57fb..76387524c4c 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -25,4 +25,6 @@ if icu.found() subdir('icu') endif +subdir('fuzzing') + subdir('perl')