Summary
PostgreSQL's recursion guard is compiled into the vendored libpg_query, but it is inert in pg_raw_parse: check_stack_depth() / stack_is_too_deep() compare against stack_base_ptr, which only set_stack_base() initializes, and nothing in this crate or its libpg_query tree ever calls set_stack_base().
/* libpg_query/src/postgres/src_backend_utils_misc_stack_depth.c */
static __thread char *stack_base_ptr = NULL;
...
if (stack_depth > max_stack_depth_bytes &&
stack_base_ptr != NULL) /* never true: stack_base_ptr stays NULL */
return true;
Consequently every recursive entry point that relies on the guard (copyObject via normalize, raw_expression_tree_walker via walk, the deparser, raw_parser's post-processing) recurses without limit on the caller's stack. A statement that nests deeply enough does not produce a "stack depth limit exceeded" error; it overflows the thread stack and the process dies (SIGSEGV, or a Rust "has overflowed its stack" abort when the guard page is hit).
Reproduction
fn nested(levels: usize) -> String {
let mut sql = "SELECT 1".to_owned();
for _ in 0..levels { sql = format!("SELECT ({sql})"); }
sql
}
// On a 2 MiB thread (Tokio's default worker stack in release builds):
std::thread::Builder::new().stack_size(2 << 20).spawn(|| {
let tree = pg_raw_parse::parse(&nested(1500)).unwrap();
for stmt in tree.iter() {
let n = pg_raw_parse::normalize::normalize(stmt); // copyObject
let _ = pg_raw_parse::deparse(&*n); // deparse
}
}).unwrap().join().unwrap();
Measured on Linux aarch64 with PgDog v0.1.56 (release binary, 2 MiB worker stacks, query_parser = "on"): PgDog itself, with no plugins loaded, aborts with thread '<unknown>' has overflowed its stack (exit 134) for roughly 450–650 nested subselects, and for roughly 1,100 nested jsonb_set(...) calls. The parser accepts these statements (bison's own limit is higher), so any authenticated client can take the proxy down with one statement. In debug builds the frames are larger and the threshold correspondingly lower.
Suggestion
Call set_stack_base() on each thread before entering the parser/normalizer/deparser (for example in pg_query_init() or wherever the per-thread MemoryContext is set up), and set max_stack_depth_bytes relative to the actual thread stack size (or expose a setter) so check_stack_depth() raises the normal PostgreSQL error, which pg_raw_parse already turns into a PgError/panic that callers can catch. With that in place callers get an error for pathological statements instead of a process crash.
If keeping the guard off is intentional, it would help to document that callers are responsible for bounding tree depth before calling normalize/deparse/walk.
Summary
PostgreSQL's recursion guard is compiled into the vendored
libpg_query, but it is inert inpg_raw_parse:check_stack_depth()/stack_is_too_deep()compare againststack_base_ptr, which onlyset_stack_base()initializes, and nothing in this crate or itslibpg_querytree ever callsset_stack_base().Consequently every recursive entry point that relies on the guard (
copyObjectvianormalize,raw_expression_tree_walkerviawalk, the deparser,raw_parser's post-processing) recurses without limit on the caller's stack. A statement that nests deeply enough does not produce a "stack depth limit exceeded" error; it overflows the thread stack and the process dies (SIGSEGV, or a Rust "has overflowed its stack" abort when the guard page is hit).Reproduction
Measured on Linux aarch64 with PgDog v0.1.56 (release binary, 2 MiB worker stacks,
query_parser = "on"): PgDog itself, with no plugins loaded, aborts withthread '<unknown>' has overflowed its stack(exit 134) for roughly 450–650 nested subselects, and for roughly 1,100 nestedjsonb_set(...)calls. The parser accepts these statements (bison's own limit is higher), so any authenticated client can take the proxy down with one statement. In debug builds the frames are larger and the threshold correspondingly lower.Suggestion
Call
set_stack_base()on each thread before entering the parser/normalizer/deparser (for example inpg_query_init()or wherever the per-threadMemoryContextis set up), and setmax_stack_depth_bytesrelative to the actual thread stack size (or expose a setter) socheck_stack_depth()raises the normal PostgreSQL error, whichpg_raw_parsealready turns into aPgError/panic that callers can catch. With that in place callers get an error for pathological statements instead of a process crash.If keeping the guard off is intentional, it would help to document that callers are responsible for bounding tree depth before calling
normalize/deparse/walk.