Don't corrupt plpython's "TD" dictionary in a recursive trigger call.
authorTom Lane <[email protected]>
Tue, 7 May 2024 22:15:00 +0000 (18:15 -0400)
committerTom Lane <[email protected]>
Tue, 7 May 2024 22:15:00 +0000 (18:15 -0400)
If a plpython-language trigger caused another one to be invoked,
the "TD" dictionary created for the inner one would overwrite the
outer one's "TD" dictionary.  This is more or less the same problem
that 1d2fe56e4 fixed for ordinary functions in plpython, so fix it
the same way, by saving and restoring "TD" during a recursive
invocation.

This fix makes an ABI-incompatible change in struct PLySavedArgs.
I'm not too worried about that because it seems highly unlikely that
any extension is messing with those structs.  We could imagine doing
something weird to preserve nominal ABI compatibility in the back
branches, like keeping the saved TD object in an extra element of
namedargs[].  However, that would only be very nominal compatibility:
if anything *is* touching PLySavedArgs, it would likely do the wrong
thing due to not knowing about the additional value.  So I judge it
not worth the ugliness to do something different there.

(I also changed struct PLyProcedure, but its added field fits
into formerly-padding space, so that should be safe.)

Per bug #18456 from Jacques Combrink.  This bug is very ancient,
so back-patch to all supported branches.

Discussion: https://postgr.es/m/3008982.1714853799@sss.pgh.pa.us

src/pl/plpython/expected/plpython_trigger.out
src/pl/plpython/plpy_exec.c
src/pl/plpython/plpy_procedure.c
src/pl/plpython/plpy_procedure.h
src/pl/plpython/sql/plpython_trigger.sql

index 742988a5b59eb0b19388035ecc4a98acab7596b3..f81aa4ade5e44e367f609f0e2d559ea5021011fe 100644 (file)
@@ -618,3 +618,30 @@ SELECT * FROM trigger_test_generated;
 ---+---
 (0 rows)
 
+-- recursive call of a trigger mustn't corrupt TD (bug #18456)
+CREATE TABLE recursive_trigger_test (a int, b int);
+CREATE FUNCTION recursive_trigger_func() RETURNS trigger
+LANGUAGE plpythonu
+AS $$
+if TD["event"] == "UPDATE":
+    plpy.execute("INSERT INTO recursive_trigger_test VALUES (1, 2)")
+    plpy.notice("TD[event] => " + str(TD["event"]) + ", expecting UPDATE");
+else:
+    plpy.notice("TD[event] => " + str(TD["event"]) + ", expecting INSERT");
+return None
+$$;
+CREATE TRIGGER recursive_trigger_trig
+  AFTER INSERT OR UPDATE ON recursive_trigger_test
+  FOR EACH ROW EXECUTE PROCEDURE recursive_trigger_func();
+INSERT INTO recursive_trigger_test VALUES (0, 0);
+NOTICE:  TD[event] => INSERT, expecting INSERT
+UPDATE recursive_trigger_test SET a = 11 WHERE b = 0;
+NOTICE:  TD[event] => INSERT, expecting INSERT
+NOTICE:  TD[event] => UPDATE, expecting UPDATE
+SELECT * FROM recursive_trigger_test;
+ a  | b 
+----+---
+ 11 | 0
+  1 | 2
+(2 rows)
+
index 30f929f220c4391e4f2f6c14cef9a851780b8ff7..697726162a8b502e304915a025887a88386bfca5 100644 (file)
@@ -339,6 +339,13 @@ PLy_exec_trigger(FunctionCallInfo fcinfo, PLyProcedure *proc)
    PLy_output_setup_tuple(&proc->result, rel_descr, proc);
    PLy_input_setup_tuple(&proc->result_in, rel_descr, proc);
 
+   /*
+    * If the trigger is called recursively, we must push outer-level
+    * arguments into the stack.  This must be immediately before the PG_TRY
+    * to ensure that the corresponding pop happens.
+    */
+   PLy_global_args_push(proc);
+
    PG_TRY();
    {
        int         rc PG_USED_FOR_ASSERTS_ONLY;
@@ -405,6 +412,7 @@ PLy_exec_trigger(FunctionCallInfo fcinfo, PLyProcedure *proc)
    }
    PG_CATCH();
    {
+       PLy_global_args_pop(proc);
        Py_XDECREF(plargs);
        Py_XDECREF(plrv);
 
@@ -412,6 +420,7 @@ PLy_exec_trigger(FunctionCallInfo fcinfo, PLyProcedure *proc)
    }
    PG_END_TRY();
 
+   PLy_global_args_pop(proc);
    Py_DECREF(plargs);
    Py_DECREF(plrv);
 
@@ -514,6 +523,13 @@ PLy_function_save_args(PLyProcedure *proc)
    result->args = PyDict_GetItemString(proc->globals, "args");
    Py_XINCREF(result->args);
 
+   /* If it's a trigger, also save "TD" */
+   if (proc->is_trigger)
+   {
+       result->td = PyDict_GetItemString(proc->globals, "TD");
+       Py_XINCREF(result->td);
+   }
+
    /* Fetch all the named arguments */
    if (proc->argnames)
    {
@@ -563,6 +579,13 @@ PLy_function_restore_args(PLyProcedure *proc, PLySavedArgs *savedargs)
        Py_DECREF(savedargs->args);
    }
 
+   /* Restore the "TD" object, too */
+   if (savedargs->td)
+   {
+       PyDict_SetItemString(proc->globals, "TD", savedargs->td);
+       Py_DECREF(savedargs->td);
+   }
+
    /* And free the PLySavedArgs struct */
    pfree(savedargs);
 }
@@ -581,8 +604,9 @@ PLy_function_drop_args(PLySavedArgs *savedargs)
        Py_XDECREF(savedargs->namedargs[i]);
    }
 
-   /* Drop ref to the "args" object, too */
+   /* Drop refs to the "args" and "TD" objects, too */
    Py_XDECREF(savedargs->args);
+   Py_XDECREF(savedargs->td);
 
    /* And free the PLySavedArgs struct */
    pfree(savedargs);
@@ -591,9 +615,9 @@ PLy_function_drop_args(PLySavedArgs *savedargs)
 /*
  * Save away any existing arguments for the given procedure, so that we can
  * install new values for a recursive call.  This should be invoked before
- * doing PLy_function_build_args().
+ * doing PLy_function_build_args() or PLy_trigger_build_args().
  *
- * NB: caller must ensure that PLy_global_args_pop gets invoked once, and
+ * NB: callers must ensure that PLy_global_args_pop gets invoked once, and
  * only once, per successful completion of PLy_global_args_push.  Otherwise
  * we'll end up out-of-sync between the actual call stack and the contents
  * of proc->argstack.
index 50b07cad8207e1e13c841d2875fc93e869873239..e8eff38d3d047b7a265d012544a521a136d59492 100644 (file)
@@ -188,6 +188,7 @@ PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger)
        proc->fn_readonly = (procStruct->provolatile != PROVOLATILE_VOLATILE);
        proc->is_setof = procStruct->proretset;
        proc->is_procedure = (procStruct->prokind == PROKIND_PROCEDURE);
+       proc->is_trigger = is_trigger;
        proc->src = NULL;
        proc->argnames = NULL;
        proc->args = NULL;
index 8968b5c92e5a56e1da90deef15d37e2ba0f80ebc..5db854fc8bd2dfef857c72013f26e9552cfba330 100644 (file)
@@ -16,6 +16,7 @@ typedef struct PLySavedArgs
 {
    struct PLySavedArgs *next;  /* linked-list pointer */
    PyObject   *args;           /* "args" element of globals dict */
+   PyObject   *td;             /* "TD" element of globals dict, if trigger */
    int         nargs;          /* length of namedargs array */
    PyObject   *namedargs[FLEXIBLE_ARRAY_MEMBER];   /* named args */
 } PLySavedArgs;
@@ -32,6 +33,7 @@ typedef struct PLyProcedure
    bool        fn_readonly;
    bool        is_setof;       /* true, if function returns result set */
    bool        is_procedure;
+   bool        is_trigger;     /* called as trigger? */
    PLyObToDatum result;        /* Function result output conversion info */
    PLyDatumToOb result_in;     /* For converting input tuples in a trigger */
    char       *src;            /* textual procedure code, after mangling */
index 19852dc58510f2974be0106eef7329a564167cc1..69e96d2aad0de005360cd06f42721c6aa1224d6b 100644 (file)
@@ -467,3 +467,27 @@ FOR EACH ROW EXECUTE PROCEDURE generated_test_func1();
 TRUNCATE trigger_test_generated;
 INSERT INTO trigger_test_generated (i) VALUES (1);
 SELECT * FROM trigger_test_generated;
+
+
+-- recursive call of a trigger mustn't corrupt TD (bug #18456)
+
+CREATE TABLE recursive_trigger_test (a int, b int);
+
+CREATE FUNCTION recursive_trigger_func() RETURNS trigger
+LANGUAGE plpythonu
+AS $$
+if TD["event"] == "UPDATE":
+    plpy.execute("INSERT INTO recursive_trigger_test VALUES (1, 2)")
+    plpy.notice("TD[event] => " + str(TD["event"]) + ", expecting UPDATE");
+else:
+    plpy.notice("TD[event] => " + str(TD["event"]) + ", expecting INSERT");
+return None
+$$;
+
+CREATE TRIGGER recursive_trigger_trig
+  AFTER INSERT OR UPDATE ON recursive_trigger_test
+  FOR EACH ROW EXECUTE PROCEDURE recursive_trigger_func();
+
+INSERT INTO recursive_trigger_test VALUES (0, 0);
+UPDATE recursive_trigger_test SET a = 11 WHERE b = 0;
+SELECT * FROM recursive_trigger_test;