Repository navigation
18: stack-depth guard, PL/pgSQL alias leak, scan out of memory (combined) - #13
pyramation wants to merge 7 commits into
Conversation
…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().
…feat/combined-hardening-18
…feat/combined-hardening-18
* 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)
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
|
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 — // src/pg_query_fingerprint.c
350 _fingerprintNode(&ctx, parsetree_and_error.tree, NULL, NULL, 0);
351 }The new 🟠 High — Protobuf path aborts process on stack-depth error · pg_query_parse.c:195 · // src/pg_query_parse.c
195 result.parse_tree = pg_query_nodes_to_protobuf(parsetree_and_error.tree);
The change introduces a
Reviewed commit: d7ff7fc |
There was a problem hiding this comment.
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
- 🟠
check_stack_depth()throws in fingerprint paths with noPG_TRY— pg_query_fingerprint.c:350 - 🟠 Protobuf path aborts process on stack-depth error — pg_query_parse.c:195
- 🟠
check_stack_depth()in plpgsql dump can longjmp uncaught — pg_query_json_plpgsql.c:143 - 🟡 New
pg_query_set_stack_limitmissing frompg_query.h— src_backend_utils_misc_stack_depth.c:151
| 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); | ||
| } |
There was a problem hiding this comment.
🟡 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(); |
There was a problem hiding this comment.
🟠 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.
…at/combined-hardening-18
Summary
One PR for every pending
18-constructivefix. Each folded PR is merged in with a merge commit, so its original commits keep their SHAs. Head:36067881e52e94e892e7a9d154e1531fef6dbc39.feat/stack-depth-guard-18check_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 raisestack depth limit exceeded. Also frees the PL/pgSQL alias list (the valgrind still-reachable block).fix/scan-out-of-memory-18pg_query_scanwrote tokens and the packed result through NULL whenmallocfailed (segfault natively, memory corruption in WASM) →result.error = "out of memory".feat/plpgsql-no-body-18BEGIN ATOMIC) hit an assertion and aborted → returns an error (cherry-pick of upstream pganalyze#363).Not changed:
pg_query_nodes_to_protobufalso packs into an uncheckedmalloc. It runs afterpg_query_raw_parse'sPG_TRY, though, so returning an error needs its ownPG_TRYinpg_query_parse_protobuf_opts. That is a restructure, not a one-line check, so it is left for a separate PR.Local results
make testandmake test VALGRIND=1both pass (0INVALID, valgrind exit 0).Merge order
17-constructive) with a merge commit, not squash. libpg-query-node pins36067881(v18) andab619b8(v17).