[PATCH] Add reentrancy guards in ri_triggers.c

First seen: 2026-05-20 13:14:36+00:00 · Messages: 1 · Participants: 1

Latest Update

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

Technical Analysis: Reentrancy Guards in ri_triggers.c

The Core Problem

This thread addresses a use-after-free vulnerability in PostgreSQL's referential integrity trigger system (ri_triggers.c) that manifests as a segmentation fault during cascading deletes on self-referential tables.

Architectural Context

PostgreSQL's foreign key enforcement is implemented via system-generated triggers in ri_triggers.c. When a row is deleted from a table with cascading foreign keys, RI_FKey_cascade_del fires to delete dependent rows. These triggers use SPI (Server Programming Interface) to execute internally-generated plans, which are cached in the plan cache for performance.

The Bug Mechanism

The crash requires a specific but realistic configuration:

  1. Self-referential table: A table with a foreign key column referencing its own primary key (e.g., a tree/hierarchy structure like parent_id REFERENCES self(id)).
  2. BEFORE DELETE trigger: A user-defined trigger that itself performs DELETE operations on the same table.

The sequence of events leading to the crash:

  1. A DELETE is issued on the self-referential table.
  2. The RI cascade trigger (RI_FKey_cascade_del) fires, acquiring a cached plan via SPI and beginning execution.
  3. The cascade delete triggers the user's BEFORE DELETE trigger on a dependent row.
  4. The BEFORE DELETE trigger deletes another row from the same table, causing reentrant invocation of RI_FKey_cascade_del.
  5. During this reentrant execution, something causes ResetPlanCache() to be called (e.g., a catalog invalidation, DDL in a concurrent session, or the trigger's own actions).
  6. The plan cache invalidation frees the plan that the outer (first) invocation of RI_FKey_cascade_del is still actively executing.
  7. When control returns to the outer invocation, it attempts to continue executing a freed plan — resulting in a use-after-free and segfault in _SPI_execute_plan.

Why This Matters Architecturally

This bug exposes a fundamental gap in the lifecycle management of RI trigger plans. The RI module maintains its own plan cache (via ri_PlanHashTable) separate from the general plan cache, but it lacks awareness of whether a plan is currently in-flight. The SPI layer expects plans to remain valid for the duration of execution, but the RI layer can free/replace plans underneath active execution frames during reentrancy.

Self-referential tables with cascading foreign keys are common in real-world schemas (organizational hierarchies, category trees, threaded comments), making this a practically exploitable crash scenario rather than a theoretical concern.

Proposed Solution

The patch introduces reentrancy guards — essentially a reference counting mechanism for RI plans that are currently being executed.

Design Approach

The solution maintains a reference count on each cached plan entry. The key invariant is:

This is analogous to how the general plan cache uses ResourceOwner references to pin plans during execution, but adapted to the RI module's private plan hash table.

Patch Structure

Design Tradeoffs

  1. Reference counting vs. plan pinning: The approach uses simple reference counts rather than integrating with ResourceOwner, which keeps the change localized to ri_triggers.c but doesn't leverage existing infrastructure.
  2. Performance: Adding increment/decrement operations on every RI trigger execution adds minimal overhead (single atomic or protected integer operation).
  3. Deferred cleanup complexity: Plans marked for deferred freeing need a mechanism to eventually be cleaned up — likely checked at the point where the reference count drops to zero.

Relationship to Existing Infrastructure

The general plan cache (plancache.c) already handles this scenario for user-prepared statements via CachedPlanIsValid checks and plan pinning. The RI module's private cache predates some of these mechanisms and has historically assumed non-reentrant execution — an assumption violated by self-referential FK + user triggers.

This is conceptually similar to past bugs in SPI plan management where nested SPI calls could invalidate outer frames' plans, which were addressed by the SPI_keepplan / SPI_saveplan mechanisms.