Skip to content

18: stack-depth guard, PL/pgSQL alias leak, scan out of memory (combined) - #13

Open
pyramation wants to merge 7 commits into
18-constructivefrom
feat/combined-hardening-18
Open

pyramation wants to merge 7 commits into
18-constructivefrom
feat/combined-hardening-18

Conversation

@pyramation

@pyramation pyramation commented Oct 25, 2026 •

Copy link
Copy Markdown

Summary

One PR for every pending 18-constructive fix. Each folded PR is merged in with a merge commit, so its original commits keep their SHAs. Head: 36067881e52e94e892e7a9d154e1531fef6dbc39.

Folded PR Broken → fixed
#10 feat/stack-depth-guard-18 check_stack_depth() was a no-op for embedders, so deep output, fingerprint, deparse or PL/pgSQL recursion overflowed the C stack → pg_query_set_stack_limit() plus guards raise stack depth limit exceeded. Also frees the PL/pgSQL alias list (the valgrind still-reachable block).
#12 fix/scan-out-of-memory-18 pg_query_scan wrote tokens and the packed result through NULL when malloc failed (segfault natively, memory corruption in WASM) → result.error = "out of memory".
#14 feat/plpgsql-no-body-18 PL/pgSQL parsing of a statement without a body string (e.g. BEGIN ATOMIC) hit an assertion and aborted → returns an error (cherry-pick of upstream pganalyze#363).

Not changed: pg_query_nodes_to_protobuf also packs into an unchecked malloc. It runs after pg_query_raw_parse's PG_TRY, though, so returning an error needs its own PG_TRY in pg_query_parse_protobuf_opts. That is a restructure, not a one-line check, so it is left for a separate PR.

Local results

make test and make test VALGRIND=1 both pass (0 INVALID, valgrind exit 0).

Merge order

  1. Merge this PR and Make check_stack_depth() effective for embedders; guard recursive output paths (17) #11 (17-constructive) with a merge commit, not squash. libpg-query-node pins 36067881 (v18) and ab619b8 (v17).
  2. Then merge the combined libpg-query-node PR and re-run its CI.

pyramation and others added 6 commits October 24, 2026 01:39
…ive output paths

libpg_query never calls set_stack_base(), so stack_base_ptr stays NULL and
check_stack_depth() is a no-op: deep recursion in the JSON/protobuf output
functions, the fingerprinter, the deparser and the PL/pgSQL JSON dump runs
until the C stack overflows (a trap in WebAssembly builds).

Add pg_query_set_stack_limit(bytes) so an embedder can set the reference
point and limit at each entry, call check_stack_depth() at the top of
_outNode, _fingerprintNode, dump_stmt/dump_expr and deparseExpr, and run
pg_query_nodes_to_json in pg_query_parse_opts under PG_TRY so the
'stack depth limit exceeded' error is returned instead of being fatal.
pg_query_plpgsql_reset_aliases() freed the names but kept the array, and
nothing called it after the last compile, so valgrind reported a
still-reachable block from pg_query_plpgsql_record_alias at exit. Free the
array in reset and reset at the end of pg_query_parse_plpgsql().
* Fix PL/pgSQL parsing without function bodies

Return PostgreSQL errors when CREATE FUNCTION or DO omits its AS clause instead of asserting or dereferencing a null source pointer in release builds.

Add regression coverage for both malformed statement types.

Fixes pganalyze#362.

* Skip non-PL/pgSQL functions before checking for a body, add more checks

Check the language before requiring an AS clause, so that valid
SQL-standard function bodies (LANGUAGE sql RETURN ... / BEGIN ATOMIC)
are skipped as before instead of returning an error. For PL/pgSQL
functions, also raise upstream's "duplicate function body specified"
and "inline SQL function body only valid for language SQL" errors, as
in Postgres' interpret_AS_clause function.

In passing, fix a related memory leak for raw parser's stderr buffer when
the input contains no CREATE FUNCTION or DO statements, which previously
leaked.

---------

Co-authored-by: Darren Carreras <[email protected]>
Co-authored-by: Lukas Fittl <[email protected]>
(cherry picked from commit e275398)
@devin-ai-integration

Copy link
Copy Markdown

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@tenki-reviewer

tenki-reviewer Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Review complete. 🟠 3 high · 🟡 1 medium

💬 Inline comments (2)

📍 Findings outside the diff (2) — 🟠 2 high — defects on lines GitHub can't attach comments to

🟠 High — check_stack_depth() throws in fingerprint paths with no PG_TRY · pg_query_fingerprint.c:350–351 · unchanged line

// src/pg_query_fingerprint.c
350				_fingerprintNode(&ctx, parsetree_and_error.tree, NULL, NULL, 0);
351			}

The new check_stack_depth() in _fingerprintNode (src/pg_query_fingerprint.c:267) raises a Postgres ERROR, which longjmps to the innermost PG_TRY - but the fingerprint entry points pg_query_fingerprint_node (src/pg_query_fingerprint.c:314) and pg_query_fingerprint_with_opts (src/pg_query_fingerprint.c:329) contain no PG_TRY/PG_CATCH. With PG_exception_stack == NULL the error is promoted to FATAL and aborts the embedding process, also skipping pg_query_exit_memory_context and _fingerprintFreeContext.


🟠 High — Protobuf path aborts process on stack-depth error · pg_query_parse.c:195 · unchanged line

// src/pg_query_parse.c
195		result.parse_tree = pg_query_nodes_to_protobuf(parsetree_and_error.tree);

_outNode now calls check_stack_depth() (src/pg_query_outfuncs_protobuf.c:250), which raises elog(ERROR) on deep recursion, but its caller pg_query_nodes_to_protobuf is invoked unguarded at src/pg_query_parse.c:195 - the JSON equivalent was wrapped in PG_TRY at src/pg_query_parse.c:140, but the protobuf path was not. With PG_exception_stack == NULL the error promotes to FATAL and terminates the entire embedding process instead of returning a PgQueryError, leaking the palloc'd partial tree.


The change introduces a pg_query_set_stack_limit API backed by new stack-depth plumbing, wires check_stack_depth() into recursive traversal paths (fingerprinting, JSON/protobuf serialization, PL/pgSQL dumping, deparse), adds alias-array cleanup and scan-token freeing on failure paths, and extends test/scan.c with an OOM/stack-limit scenario. The guard placement is incomplete: several guarded recursion entry points throw inside call trees that no caller wraps in PG_TRY/PG_CATCH, converting a recoverable deep-nesting input into a process abort. The new public setter is also missing from the public header, so it is not part of the declared API.

Files Change
src/postgres/src_backend_utils_misc_stack_depth.c, src/pg_query_scan.c Stack-limit configuration helper, token freeing on failure, and error propagation from the scanner
src/pg_query_fingerprint.c, src/pg_query_outfuncs_json.c, src/pg_query_outfuncs_protobuf.c Depth checks in recursive tree-walk serializers and the fingerprint visitor
src/pg_query_parse.c, src/pg_query_parse_plpgsql.c, src/pg_query_json_plpgsql.c, src/postgres_deparse.c Error-path cleanup, alias-array reset, PL/pgSQL dump and deparse depth guards
test/scan.c New OOM/stack-limit regression test

Reviewed commit: d7ff7fc

@tenki-reviewer tenki-reviewer Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR adds stack-depth and OOM hardening across libpg_query, but three of the new check_stack_depth() call sites can longjmp out of functions with no PG_TRY handler, aborting the process instead of returning an error.

Key findings

Comment on lines +151 to +159
void
pg_query_set_stack_limit(size_t bytes)
{
char here;

stack_base_ptr = &here;
max_stack_depth_bytes = (ssize_t) bytes;
max_stack_depth = (int) (bytes / 1024);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 bug · medium

New pg_query_set_stack_limit missing from pg_query.h

The PR adds an exported entry point pg_query_set_stack_limit() (src/postgres/src_backend_utils_misc_stack_depth.c:151) aimed at embedders, but no declaration is added to the public header pg_query.h or any internal header. Embedders must hand-write an extern declaration with no compile-time protection against signature drift, and the contract (per-thread semantics, meaning of bytes) is undocumented in the API surface.

📋 Prompt for AI Agents

In pg_query.h (repo root), add a declaration for void pg_query_set_stack_limit(size_t bytes); near the other public entry points, with a doc comment stating: it sets the reference stack frame and maximum stack depth (in bytes) used by check_stack_depth(); it is thread-local (all three backing variables are __thread) so it must be invoked on each thread that parses; and the base pointer is captured from the caller's frame, so call it from the outermost embedder frame, not from deep call stacks. This matches the new export added in src/postgres/src_backend_utils_misc_stack_depth.c lines 151-159.

static void
dump_stmt(StringInfo out, PLpgSQL_stmt *node)
{
check_stack_depth();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 bug · high

check_stack_depth() in plpgsql dump can longjmp uncaught

dump_stmt and dump_expr now call check_stack_depth(), which raises a Postgres ERROR when the configured limit is exceeded (src/pg_query_json_plpgsql.c:143 and src/pg_query_json_plpgsql.c:633). plpgsqlToJSON is invoked from the statement loop in pg_query_parse_plpgsql at src/pg_query_parse_plpgsql.c:651, outside every PG_TRY region in the file, so a deeply nested PL/pgSQL function aborts the embedding process instead of returning an error result.

📋 Prompt for AI Agents

In src/pg_query_parse_plpgsql.c around line 651, plpgsqlToJSON(func_and_error.func) runs outside any PG_TRY region, but the new check_stack_depth() calls in dump_stmt/dump_expr (src/pg_query_json_plpgsql.c:143 and :633) can raise a Postgres ERROR that longjmps with no exception handler once pg_query_set_stack_limit() has been used. Wrap the plpgsqlToJSON call and the surrounding body (lines ~646-667) in PG_TRY/PG_CATCH that switches to the outer memory context, builds a malloc'd PgQueryError via CopyErrorData()/strdup (same pattern as pg_query_raw_parse_plpgsql lines 528-548), assigns it to result.error, FlushErrorState()s, frees the partial function with plpgsql_free_function_memory, calls pg_query_plpgsql_reset_aliases() and pg_query_exit_memory_context(ctx), and returns the result.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants