Server crash: Use-after-free in AfterTriggerEndQuery()

First seen: 2026-05-05 06:37:19+00:00 · Messages: 3 · Participants: 3

Latest Update

2026-05-06 · opus 4.7

Core Problem: Use-After-Free in AfterTriggerEndQuery()

This thread reports a server crash caused by a classic use-after-free bug in PostgreSQL's AFTER trigger firing machinery, specifically within AfterTriggerEndQuery() in src/backend/commands/trigger.c. The bug manifests when AFTER triggers recursively fire additional triggers that cause the query_stack data structure to be reallocated mid-iteration.

The Bug Mechanics

AfterTriggerEndQuery() walks the per-query trigger event lists and repeatedly invokes afterTriggerInvokeEvents() until all deferred-until-end-of-query events have fired. The function holds a local pointer qs pointing into afterTriggers.query_stack[afterTriggers.query_depth].

The critical issue: afterTriggerInvokeEvents() can cause new triggers to fire, which in turn can recursively enter trigger-processing code paths that push onto query_stack. When the stack's capacity is exceeded, it gets repalloc'd to a larger size. This invalidates any pointers that were computed into the old allocation.

The code path that crashes is the "all fired" exit branch, where after the loop terminates, the code does:

FireAfterTriggerBatchCallbacks(qs->batch_callbacks);

If query_stack was reallocated during firing, qs now points into freed memory, and dereferencing qs->batch_callbacks either returns garbage or crashes (depending on whether the old chunk has been reused).

Why This Matters Architecturally

AFTER triggers are a foundational correctness mechanism in PostgreSQL — they implement:

The query_stack array supports recursive query execution: each level of a query that can fire triggers (e.g., a trigger function that itself runs INSERTs) gets its own slot. Because trigger execution is inherently reentrant — a foreign-key check trigger on table A can fire an INSERT into table B which fires another trigger — any code that holds a pointer into query_stack across a call that may fire triggers is making an unsafe aliasing assumption.

The reporter's reproducer (an AFTER INSERT trigger on a referenced table that recursively inserts into itself) is precisely the scenario that exercises this aliasing: the self-recursion forces query_depth to grow beyond the initial query_stack capacity, triggering repalloc.

Likely Fix Shape

Although no patch is shown in the two messages, the fix is structurally obvious and falls into one of two patterns used elsewhere in trigger.c:

  1. Re-fetch after the loop: Recompute qs = &afterTriggers.query_stack[afterTriggers.query_depth] immediately before the FireAfterTriggerBatchCallbacks() call, rather than relying on the stale pointer.
  2. Index rather than pointer: Refactor the function to address the slot by index on each access, never caching the pointer across a call that can fire triggers.

Pattern (1) is the minimal fix; pattern (2) is more defensive and matches how nearby code in AfterTriggerBeginQuery / afterTriggerInvokeEvents already handles reallocation. A grep of the file will likely reveal prior commits (by Álvaro Herrera or Tom Lane) that fixed analogous reallocation hazards — this class of bug has appeared multiple times in the AFTER trigger subsystem.

Severity and Backpatch Implications

This is a server crash triggerable from unprivileged SQL (any user who can define triggers on their own tables). It's almost certainly a backpatchable bug fix to all supported branches. The fact that it requires recursive self-referential inserts through an FK explains why it has survived — most trigger workloads don't grow query_stack deep enough to force reallocation.

Participant Weight

Amit Langote's immediate response ("I'll look at this on Thursday") is significant: Amit is a PostgreSQL committer with deep expertise in the trigger and partitioning code (he authored substantial portions of the partition-aware trigger firing logic, including work on transition tables for partitioned tables). His ownership of this investigation signals that the fix will likely be committed by him. The reporter, Amul Sul, is an experienced EDB engineer who regularly files well-characterized bug reports with reproducers — his diagnosis pointing directly at the dangling qs pointer is almost certainly correct and will frame the fix.