[PATCH] Fix use-after-free of propgraph orphan static lists on xact abort

First seen: 2026-05-12 17:52:52+00:00 · Messages: 3 · Participants: 2

Latest Update

2026-05-14 · claude-opus-4-6

Technical Analysis: Use-After-Free in Property Graph Orphan Static Lists on Transaction Abort

Overview

This thread presents a short-lived bug report that was ultimately retracted. The submitter reported a use-after-free vulnerability in PostgreSQL's dependency cascade machinery related to the SQL/PGQ Property Graph feature, but later acknowledged the issue existed only in their local patch set and not in upstream HEAD.

The Reported Problem (As Described)

Core Issue: Static Pointer Lifetime vs. Memory Context Lifetime

The reported bug follows a classic PostgreSQL memory management anti-pattern: static (file-scoped) pointers holding references to memory allocated in a transient MemoryContext.

The claimed scenario:

  1. dependency.c maintains static List * pointers (propgraph_orphan_label_graphids and propgraph_orphan_property_graphids) that accumulate OIDs of property graph elements orphaned during a dependency cascade.
  2. These lists are allocated in TopTransactionContext during deleteOneObject().
  3. The lists are only reset (set to NIL) on the success path — specifically in run_deferred_propgraph_orphan_cleanup(), called at the end of performDeletion()/performMultipleDeletions().
  4. If the transaction aborts (e.g., due to statement_timeout) after the lists have been populated but before cleanup runs, AbortTransaction() destroys TopTransactionContext, freeing the underlying memory.
  5. The static pointers still point to the now-freed memory.
  6. A subsequent property graph cascade in the same backend session calls list_append_unique_oid() with the dangling pointer, which dereferences freed memory.

Why This Pattern Is Dangerous

This is a well-known hazard in PostgreSQL's memory management model. The rules are:

The List infrastructure in PostgreSQL uses a tagged-union header (IsOidList(list) checks list->type == T_OidList), so dereferencing freed memory either triggers an assertion failure or silently corrupts the heap — the latter being far worse.

Proposed Fix

The submitter proposed resetting both static pointers to NIL in PG_FINALLY() blocks wrapping performDeletion() and performMultipleDeletions(). This ensures cleanup occurs on both success and error paths. This is the standard PostgreSQL idiom for this pattern — PG_FINALLY blocks execute regardless of whether an error was thrown, making them ideal for resource cleanup that must happen unconditionally.

Architectural Context: Property Graphs (SQL/PGQ)

The SQL/PGQ property graph feature (being developed for PG19) introduces CREATE PROPERTY GRAPH which defines graph views over existing tables. When a base table is dropped with CASCADE, the property graph's vertex/edge tables and associated labels become orphaned. The dependency system needs to track these orphans and clean them up in a deferred fashion — hence the static accumulator lists.

The deferred cleanup pattern is necessary because property graph cleanup may involve catalog modifications that shouldn't happen mid-cascade (to avoid re-entrant catalog access or deadlock scenarios).

Resolution

The thread was quickly resolved as a false alarm. Ashutosh Bapat attempted to reproduce and could not find the referenced static variables in dependency.c at HEAD. The original submitter then acknowledged they had been working with local patches that introduced this code, and the issue does not exist in upstream PostgreSQL HEAD.

Technical Lessons

Despite being retracted, the thread illustrates an important pattern:

  1. Static pointers to transaction-scoped memory are a recurring source of bugs in PostgreSQL. The correct mitigation is either: (a) allocate in a longer-lived context and free explicitly, (b) use PG_FINALLY or AtEOXact callbacks to reset pointers, or (c) use TopMemoryContext with explicit lifecycle management.

  2. Fuzz testing with statement_timeout is an effective technique for finding cleanup-path bugs. The timeout fires asynchronously via CHECK_FOR_INTERRUPTS(), simulating mid-operation failures that exercise error recovery paths.

  3. Property Graph (SQL/PGQ) integration with the dependency system will need careful attention to these lifetime issues as the feature matures toward commit.