postgres-github.git / summary / log / commit / refs

commit    34a307862930056e1976471d6d81a5e2efc148df
Author:   Amit Langote <amitlan@postgresql.org>
Date:     Fri Apr 10 03:40:09 2026 +0000

    Fix RI fast-path crash under nested C-level SPI
    
    When a C-language function uses SPI_connect/SPI_execute/SPI_finish to
    INSERT into a table with FK constraints, the FK AFTER triggers fire
    and schedule ri_FastPathEndBatch via
    RegisterAfterTriggerBatchCallback(), opening PK relations under
    CurrentResourceOwner at the time of the SPI call.  The query_depth > 0
    guard in FireAfterTriggerBatchCallbacks suppresses the callback at
    that nesting level, deferring teardown to the outer query's
    AfterTriggerEndQuery. By then the resource owner active during the SPI
    call may have been released, decrementing the cached relations'
    refcounts to zero. ri_FastPathTeardown, running under the outer
    query's resource owner, then crashes in assert builds when it attempts
    to close relations whose refcounts are already zero:
    
      TRAP: failed Assert("rel->rd_refcnt > 0")
    
    Fix by storing batch callbacks at the level where they should fire:
    in AfterTriggersQueryData.batch_callbacks for immediate constraints
    (fired by AfterTriggerEndQuery) and in AfterTriggersData.batch_callbacks
    for deferred constraints (fired by AfterTriggerFireDeferred and
    AfterTriggerSetState).  RegisterAfterTriggerBatchCallback() routes the
    callback to the current query-level list when query_depth >= 0, and to
    the top-level list otherwise.  FireAfterTriggerBatchCallbacks() takes a
    list parameter and simply iterates and invokes it; memory cleanup is
    handled by the caller.  This replaces the query_depth > 0 guard with
    list-level scoping.  Note that deferred constraints are unaffected by
    this bug: their callbacks fire at commit via AfterTriggerFireDeferred,
    under the outer transaction's resource owner, which remains valid
    throughout.
    
    Also add firing_batch_callbacks to AfterTriggersData to enforce that
    callbacks do not register new callbacks during
    FireAfterTriggerBatchCallbacks(), which would be unsafe as it could
    modify the list being iterated.  An Assert in
    RegisterAfterTriggerBatchCallback() enforces this discipline for
    future callers.  The flag is reset at transaction and subtransaction
    boundaries to handle cases where an error thrown by a callback is
    caught and the subtransaction is rolled back.
    
    While at it, ensure callbacks are properly accounted for at all
    transaction boundaries, as cleanup of b7b27eb41a5c: discard any
    remaining top-level callbacks on both commit and abort in
    AfterTriggerEndXact(), and clean up query-level callbacks in
    AfterTriggerFreeQuery().
    
    Note that ri_PerformCheck() calls SPI with fire_triggers=false, which
    skips AfterTriggerBeginQuery/EndQuery for that SPI command.  Any
    triggers queued during that SPI command are not fired immediately but
    deferred to the outer query level.  Since the fast-path check for
    those triggers runs under the outer query's resource owner rather than
    a nested SPI resource owner, and ri_PerformCheck() does not create
    a dedicated child resource owner, the bug described above does not
    apply.
    
    Reported-by: Evan Montgomery-Recht <montge@mianetworks.net>
    Reported-by: Sandro Santilli <strk@kbt.io>
    Analyzed-by: Evan Montgomery-Recht <montge@mianetworks.net>
    Author: Amit Langote <amitlangote09@gmail.com>
    Reviewed-by: Chao Li <li.evan.chao@gmail.com>
    Discussion: https://postgr.es/m/CAEg7pwcKf01FmDqFAf-Hzu_pYnMYScY_Otid-pe9uw3BJ6gq9g@mail.gmail.com

[parent: 90630ec42939]