Skip to content

check_stack_depth() is inert: set_stack_base() is never called, so deep statements overflow the stack instead of erroring #52

Description

@ewhauser

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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions