]> granicus.if.org Git - postgresql/commitdiff
Prevent leakage of SPI tuple tables during subtransaction abort.
authorTom Lane <tgl@sss.pgh.pa.us>
Thu, 25 Jul 2013 20:45:47 +0000 (16:45 -0400)
committerTom Lane <tgl@sss.pgh.pa.us>
Thu, 25 Jul 2013 20:45:47 +0000 (16:45 -0400)
plpgsql often just remembers SPI-result tuple tables in local variables,
and has no mechanism for freeing them if an ereport(ERROR) causes an escape
out of the execution function whose local variable it is.  In the original
coding, that wasn't a problem because the tuple table would be cleaned up
when the function's SPI context went away during transaction abort.
However, once plpgsql grew the ability to trap exceptions, repeated
trapping of errors within a function could result in significant
intra-function-call memory leakage, as illustrated in bug #8279 from
Chad Wagner.

We could fix this locally in plpgsql with a bunch of PG_TRY/PG_CATCH
coding, but that would be tedious, probably slow, and prone to bugs of
omission; moreover it would do nothing for similar risks elsewhere.
What seems like a better plan is to make SPI itself responsible for
freeing tuple tables at subtransaction abort.  This patch attacks the
problem that way, keeping a list of live tuple tables within each SPI
function context.  Currently, such freeing is automatic for tuple tables
made within the failed subtransaction.  We might later add a SPI call to
mark a tuple table as not to be freed this way, allowing callers to opt
out; but until someone exhibits a clear use-case for such behavior, it
doesn't seem worth bothering.

A very useful side-effect of this change is that SPI_freetuptable() can
now defend itself against bad calls, such as duplicate free requests;
this should make things more robust in many places.  (In particular,
this reduces the risks involved if a third-party extension contains
now-redundant SPI_freetuptable() calls in error cleanup code.)

Even though the leakage problem is of long standing, it seems imprudent
to back-patch this into stable branches, since it does represent an API
semantics change for SPI users.  We'll patch this in 9.3, but live with
the leakage in older branches.

doc/src/sgml/spi.sgml
src/backend/executor/spi.c
src/include/executor/spi.h
src/include/executor/spi_priv.h
src/pl/plpgsql/src/pl_exec.c
src/pl/plpython/plpy_cursorobject.c
src/pl/plpython/plpy_spi.c

index 4de6a2512d40bda46a24502954f5d5ecd9f71674..079a4571fb894c59c082c26d694d49f4da4e4ec4 100644 (file)
@@ -3934,8 +3934,8 @@ void SPI_freetuptable(SPITupleTable * <parameter>tuptable</parameter>)
   <para>
    <function>SPI_freetuptable</function> frees a row set created by a
    prior SPI command execution function, such as
-   <function>SPI_execute</>.  Therefore, this function is usually called
-   with the global variable <varname>SPI_tupletable</varname> as
+   <function>SPI_execute</>.  Therefore, this function is often called
+   with the global variable <varname>SPI_tuptable</varname> as
    argument.
   </para>
 
@@ -3944,6 +3944,16 @@ void SPI_freetuptable(SPITupleTable * <parameter>tuptable</parameter>)
    multiple commands and does not want to keep the results of earlier
    commands around until it ends.  Note that any unfreed row sets will
    be freed anyway at <function>SPI_finish</>.
+   Also, if a subtransaction is started and then aborted within execution
+   of a SPI procedure, SPI automatically frees any row sets created while
+   the subtransaction was running.
+  </para>
+
+  <para>
+   Beginning in <productname>PostgreSQL</> 9.3,
+   <function>SPI_freetuptable</function> contains guard logic to protect
+   against duplicate deletion requests for the same row set.  In previous
+   releases, duplicate deletions would lead to crashes.
   </para>
  </refsect1>
 
@@ -3955,7 +3965,7 @@ void SPI_freetuptable(SPITupleTable * <parameter>tuptable</parameter>)
     <term><literal>SPITupleTable * <parameter>tuptable</parameter></literal></term>
     <listitem>
      <para>
-      pointer to row set to free
+      pointer to row set to free, or NULL to do nothing
      </para>
     </listitem>
    </varlistentry>
index 2f9a94d01e5c40e2805fd5caf77099822ca5aa5a..8ba6e107a9ac3f4dbc33928cce339fe78c1a0bb2 100644 (file)
@@ -126,6 +126,7 @@ SPI_connect(void)
        _SPI_current->processed = 0;
        _SPI_current->lastoid = InvalidOid;
        _SPI_current->tuptable = NULL;
+       slist_init(&_SPI_current->tuptables);
        _SPI_current->procCxt = NULL;           /* in case we fail to create 'em */
        _SPI_current->execCxt = NULL;
        _SPI_current->connectSubid = GetCurrentSubTransactionId();
@@ -166,7 +167,7 @@ SPI_finish(void)
        /* Restore memory context as it was before procedure call */
        MemoryContextSwitchTo(_SPI_current->savedcxt);
 
-       /* Release memory used in procedure call */
+       /* Release memory used in procedure call (including tuptables) */
        MemoryContextDelete(_SPI_current->execCxt);
        _SPI_current->execCxt = NULL;
        MemoryContextDelete(_SPI_current->procCxt);
@@ -282,11 +283,35 @@ AtEOSubXact_SPI(bool isCommit, SubTransactionId mySubid)
         */
        if (_SPI_current && !isCommit)
        {
+               slist_mutable_iter siter;
+
                /* free Executor memory the same as _SPI_end_call would do */
                MemoryContextResetAndDeleteChildren(_SPI_current->execCxt);
-               /* throw away any partially created tuple-table */
-               SPI_freetuptable(_SPI_current->tuptable);
-               _SPI_current->tuptable = NULL;
+
+               /* throw away any tuple tables created within current subxact */
+               slist_foreach_modify(siter, &_SPI_current->tuptables)
+               {
+                       SPITupleTable *tuptable;
+
+                       tuptable = slist_container(SPITupleTable, next, siter.cur);
+                       if (tuptable->subid >= mySubid)
+                       {
+                               /*
+                                * If we used SPI_freetuptable() here, its internal search of
+                                * the tuptables list would make this operation O(N^2).
+                                * Instead, just free the tuptable manually.  This should
+                                * match what SPI_freetuptable() does.
+                                */
+                               slist_delete_current(&siter);
+                               if (tuptable == _SPI_current->tuptable)
+                                       _SPI_current->tuptable = NULL;
+                               if (tuptable == SPI_tuptable)
+                                       SPI_tuptable = NULL;
+                               MemoryContextDelete(tuptable->tuptabcxt);
+                       }
+               }
+               /* in particular we should have gotten rid of any in-progress table */
+               Assert(_SPI_current->tuptable == NULL);
        }
 }
 
@@ -1015,8 +1040,59 @@ SPI_freetuple(HeapTuple tuple)
 void
 SPI_freetuptable(SPITupleTable *tuptable)
 {
-       if (tuptable != NULL)
-               MemoryContextDelete(tuptable->tuptabcxt);
+       bool            found = false;
+
+       /* ignore call if NULL pointer */
+       if (tuptable == NULL)
+               return;
+
+       /*
+        * Since this function might be called during error recovery, it seems
+        * best not to insist that the caller be actively connected.  We just
+        * search the topmost SPI context, connected or not.
+        */
+       if (_SPI_connected >= 0)
+       {
+               slist_mutable_iter siter;
+
+               if (_SPI_current != &(_SPI_stack[_SPI_connected]))
+                       elog(ERROR, "SPI stack corrupted");
+
+               /* find tuptable in active list, then remove it */
+               slist_foreach_modify(siter, &_SPI_current->tuptables)
+               {
+                       SPITupleTable *tt;
+
+                       tt = slist_container(SPITupleTable, next, siter.cur);
+                       if (tt == tuptable)
+                       {
+                               slist_delete_current(&siter);
+                               found = true;
+                               break;
+                       }
+               }
+       }
+
+       /*
+        * Refuse the deletion if we didn't find it in the topmost SPI context.
+        * This is primarily a guard against double deletion, but might prevent
+        * other errors as well.  Since the worst consequence of not deleting a
+        * tuptable would be a transient memory leak, this is just a WARNING.
+        */
+       if (!found)
+       {
+               elog(WARNING, "attempt to delete invalid SPITupleTable %p", tuptable);
+               return;
+       }
+
+       /* for safety, reset global variables that might point at tuptable */
+       if (tuptable == _SPI_current->tuptable)
+               _SPI_current->tuptable = NULL;
+       if (tuptable == SPI_tuptable)
+               SPI_tuptable = NULL;
+
+       /* release all memory belonging to tuptable */
+       MemoryContextDelete(tuptable->tuptabcxt);
 }
 
 
@@ -1650,6 +1726,8 @@ spi_dest_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
        if (_SPI_current->tuptable != NULL)
                elog(ERROR, "improper call to spi_dest_startup");
 
+       /* We create the tuple table context as a child of procCxt */
+
        oldcxt = _SPI_procmem();        /* switch to procedure memory context */
 
        tuptabcxt = AllocSetContextCreate(CurrentMemoryContext,
@@ -1660,8 +1738,18 @@ spi_dest_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
        MemoryContextSwitchTo(tuptabcxt);
 
        _SPI_current->tuptable = tuptable = (SPITupleTable *)
-               palloc(sizeof(SPITupleTable));
+               palloc0(sizeof(SPITupleTable));
        tuptable->tuptabcxt = tuptabcxt;
+       tuptable->subid = GetCurrentSubTransactionId();
+
+       /*
+        * The tuptable is now valid enough to be freed by AtEOSubXact_SPI, so put
+        * it onto the SPI context's tuptables list.  This will ensure it's not
+        * leaked even in the unlikely event the following few lines fail.
+        */
+       slist_push_head(&_SPI_current->tuptables, &tuptable->next);
+
+       /* set up initial allocations */
        tuptable->alloced = tuptable->free = 128;
        tuptable->vals = (HeapTuple *) palloc(tuptable->alloced * sizeof(HeapTuple));
        tuptable->tupdesc = CreateTupleDescCopy(typeinfo);
index d4f1272cd81ae13eb52a16327357bfdf15ad7621..81310e377f6b769c5aa306cf9aab932813dbeef8 100644 (file)
@@ -13,6 +13,7 @@
 #ifndef SPI_H
 #define SPI_H
 
+#include "lib/ilist.h"
 #include "nodes/parsenodes.h"
 #include "utils/portal.h"
 
@@ -24,6 +25,8 @@ typedef struct SPITupleTable
        uint32          free;                   /* # of free vals */
        TupleDesc       tupdesc;                /* tuple descriptor */
        HeapTuple  *vals;                       /* tuples */
+       slist_node      next;                   /* link for internal bookkeeping */
+       SubTransactionId subid;         /* subxact in which tuptable was created */
 } SPITupleTable;
 
 /* Plans are opaque structs for standard users of SPI */
index ef7903abd09a94906d48bfcbf3ea223aacdd9423..7d4c9e963933401bf278363bf51a9db63c78086f 100644 (file)
@@ -23,8 +23,10 @@ typedef struct
        /* current results */
        uint32          processed;              /* by Executor */
        Oid                     lastoid;
-       SPITupleTable *tuptable;
+       SPITupleTable *tuptable;        /* tuptable currently being built */
 
+       /* resources of this execution context */
+       slist_head      tuptables;              /* list of all live SPITupleTables */
        MemoryContext procCxt;          /* procedure context */
        MemoryContext execCxt;          /* executor context */
        MemoryContext savedcxt;         /* context of SPI_connect's caller */
index 57789fc365b140d4df67c1b39e8ce4fcd7bad4b6..156a1e2ce2155a6afd55ae8351cb6030114e635e 100644 (file)
@@ -1202,7 +1202,13 @@ exec_stmt_block(PLpgSQL_execstate *estate, PLpgSQL_stmt_block *block)
                         */
                        SPI_restore_connection();
 
-                       /* Must clean up the econtext too */
+                       /*
+                        * Must clean up the econtext too.      However, any tuple table made
+                        * in the subxact will have been thrown away by SPI during subxact
+                        * abort, so we don't need to (and mustn't try to) free the
+                        * eval_tuptable.
+                        */
+                       estate->eval_tuptable = NULL;
                        exec_eval_cleanup(estate);
 
                        /* Look for a matching exception handler */
index 910e63b19954cae697043d1641365de9b480cdc0..2c458d35fdb1a19fad1a46670487d6468f63384b 100644 (file)
@@ -377,8 +377,6 @@ PLy_cursor_iternext(PyObject *self)
        }
        PG_CATCH();
        {
-               SPI_freetuptable(SPI_tuptable);
-
                PLy_spi_subtransaction_abort(oldcontext, oldowner);
                return NULL;
        }
@@ -461,8 +459,6 @@ PLy_cursor_fetch(PyObject *self, PyObject *args)
        }
        PG_CATCH();
        {
-               SPI_freetuptable(SPI_tuptable);
-
                PLy_spi_subtransaction_abort(oldcontext, oldowner);
                return NULL;
        }
index ed1f21cd6a51a83bd57157f16af003654b47b46c..982bf84e0e544661f41d98ca7a6eeeddda288036 100644 (file)
@@ -439,7 +439,6 @@ PLy_spi_execute_fetch_result(SPITupleTable *tuptable, int rows, int status)
                {
                        MemoryContextSwitchTo(oldcontext);
                        PLy_typeinfo_dealloc(&args);
-                       SPI_freetuptable(tuptable);
                        Py_DECREF(result);
                        PG_RE_THROW();
                }