From c5bec5426af989f2a9b0c1f4b97340798fe153f7 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Tue, 7 May 2024 18:15:00 -0400 Subject: [PATCH] Don't corrupt plpython's "TD" dictionary in a recursive trigger call. 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 | 27 +++++++++++++++++++++++++ src/pl/plpython/plpy_exec.c | 29 ++++++++++++++++++++++++--- src/pl/plpython/plpy_procedure.c | 1 + src/pl/plpython/plpy_procedure.h | 2 ++ src/pl/plpython/sql/plpython_trigger.sql | 24 ++++++++++++++++++++++ 5 files changed, 80 insertions(+), 3 deletions(-) diff --git a/src/pl/plpython/expected/plpython_trigger.out b/src/pl/plpython/expected/plpython_trigger.out index dd1ca32fa4..4cb90cb520 100644 --- a/src/pl/plpython/expected/plpython_trigger.out +++ b/src/pl/plpython/expected/plpython_trigger.out @@ -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 plpython3u +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) + diff --git a/src/pl/plpython/plpy_exec.c b/src/pl/plpython/plpy_exec.c index 3145c69699..6f7b5e121d 100644 --- a/src/pl/plpython/plpy_exec.c +++ b/src/pl/plpython/plpy_exec.c @@ -335,6 +335,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; @@ -397,6 +404,7 @@ PLy_exec_trigger(FunctionCallInfo fcinfo, PLyProcedure *proc) } PG_FINALLY(); { + PLy_global_args_pop(proc); Py_XDECREF(plargs); Py_XDECREF(plrv); } @@ -501,6 +509,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) { @@ -550,6 +565,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); } @@ -568,8 +590,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); @@ -578,9 +601,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. diff --git a/src/pl/plpython/plpy_procedure.c b/src/pl/plpython/plpy_procedure.c index 9cbbe7e3c8..ba7786d31c 100644 --- a/src/pl/plpython/plpy_procedure.c +++ b/src/pl/plpython/plpy_procedure.c @@ -183,6 +183,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; diff --git a/src/pl/plpython/plpy_procedure.h b/src/pl/plpython/plpy_procedure.h index 8968b5c92e..5db854fc8b 100644 --- a/src/pl/plpython/plpy_procedure.h +++ b/src/pl/plpython/plpy_procedure.h @@ -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 */ diff --git a/src/pl/plpython/sql/plpython_trigger.sql b/src/pl/plpython/sql/plpython_trigger.sql index e5504b9ab1..f6c2ef8d6a 100644 --- a/src/pl/plpython/sql/plpython_trigger.sql +++ b/src/pl/plpython/sql/plpython_trigger.sql @@ -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 plpython3u +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; -- 2.11.4.GIT