From 2ac4805888d25dc83efc768c9ebeea30183183e0 Mon Sep 17 00:00:00 2001 From: "Joshua (D) Drake" <136637981+ChronicallyJD@users.noreply.github.com> Date: Sun, 5 Jul 2026 20:27:48 -0600 Subject: [PATCH 1/2] Add error CONTEXT lines; fix crash when errors cross nested calls Errors, warnings, and notices raised while PL/php code runs now carry a CONTEXT line, like every other PL: ERROR: division by zero at line 7 CONTEXT: PL/php function "div_by_zero" via error_context_stack callbacks in the call handler ("PL/php function \"name\""), the inline handler ("PL/php anonymous code block"), and the validator ("compilation of PL/php function"). Exercising nested calls surfaced a longstanding crasher, fixed here: a PostgreSQL error longjmp'ing out of a handler's zend_try block skips zend_end_try(), leaving EG(bailout) pointing into the dying stack frame. When one PL/php function called another via SPI and the inner one failed, execution continued in the outer function with the stale bailout environment; its eventual bailout (an uncaught error) longjmp'ed into garbage -- glibc's fortify checks abort with "longjmp causes uninitialized stack frame", and without them it is arbitrary stack corruption. Save EG(bailout) before each handler's zend_try and restore it in the PG_CATCH that re-throws, in all three entry points. Present since the zend_try blocks were introduced; the test suite never exercised an error crossing nested calls. Also guard the "at line N" suffix against double-appending, which the nested flow made visible. Co-Authored-By: Claude Fable 5 --- plphp.c | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 102 insertions(+), 5 deletions(-) diff --git a/plphp.c b/plphp.c index a070c1c..4a831a6 100644 --- a/plphp.c +++ b/plphp.c @@ -258,6 +258,30 @@ static void plphp_error_cb(int type, zend_string *error_filename, static bool is_valid_php_identifier(char *name); static char *plphp_pop_exception_message(void); +/* + * Error context callbacks: every message raised while PL/php code runs gets + * a CONTEXT line naming the function (or code block), like the other PLs. + */ +static void +plphp_exec_error_callback(void *arg) +{ + errcontext("PL/php function \"%s\"", + arg != NULL ? (const char *) arg : "unknown"); +} + +static void +plphp_inline_error_callback(void *arg) +{ + errcontext("PL/php anonymous code block"); +} + +static void +plphp_compile_error_callback(void *arg) +{ + errcontext("compilation of PL/php function \"%s\"", + arg != NULL ? (const char *) arg : "unknown"); +} + /* * Each compiled function's descriptor and all its subsidiary data (name, * fmgr info records, ...) live in a private memory context, prodesc->fn_cxt, @@ -742,6 +766,9 @@ plphp_call_handler(PG_FUNCTION_ARGS) { Datum retval; bool nonatomic = false; + ErrorContextCallback plerrcontext; + char *fname; + JMP_BUF *save_bailout; /* Initialize interpreter */ plphp_init_all(); @@ -754,6 +781,15 @@ plphp_call_handler(PG_FUNCTION_ARGS) if (fcinfo->context && IsA(fcinfo->context, CallContext)) nonatomic = !((CallContext *) fcinfo->context)->atomic; + /* Messages raised below carry a CONTEXT line naming the function */ + fname = get_func_name(fcinfo->flinfo->fn_oid); + plerrcontext.callback = plphp_exec_error_callback; + plerrcontext.arg = fname; + plerrcontext.previous = error_context_stack; + error_context_stack = &plerrcontext; + + save_bailout = EG(bailout); + PG_TRY(); { /* Connect to SPI manager */ @@ -820,10 +856,21 @@ plphp_call_handler(PG_FUNCTION_ARGS) } PG_CATCH(); { + /* + * The error may have longjmp'ed out of the zend_try block above, + * skipping zend_end_try(): EG(bailout) would then still point into + * this (now dying) stack frame, and the next zend_bailout() would + * jump into garbage. Put the caller's bailout environment back. + */ + EG(bailout) = save_bailout; PG_RE_THROW(); } PG_END_TRY(); + error_context_stack = plerrcontext.previous; + if (fname) + pfree(fname); + return retval; } @@ -842,10 +889,20 @@ plphp_inline_handler(PG_FUNCTION_ARGS) char *complete_source; zval funcname_zv; zval retval; + ErrorContextCallback plerrcontext; + JMP_BUF *save_bailout; /* Initialize interpreter */ plphp_init_all(); + /* Messages raised below carry a CONTEXT line */ + plerrcontext.callback = plphp_inline_error_callback; + plerrcontext.arg = NULL; + plerrcontext.previous = error_context_stack; + error_context_stack = &plerrcontext; + + save_bailout = EG(bailout); + PG_TRY(); { if (SPI_connect() != SPI_OK_CONNECT) @@ -922,10 +979,14 @@ plphp_inline_handler(PG_FUNCTION_ARGS) } PG_CATCH(); { + /* see plphp_call_handler about the bailout environment */ + EG(bailout) = save_bailout; PG_RE_THROW(); } PG_END_TRY(); + error_context_stack = plerrcontext.previous; + PG_RETURN_VOID(); } @@ -946,10 +1007,21 @@ plphp_validator(PG_FUNCTION_ARGS) char *tmpsrc = NULL, *prosrc; Datum prosrcdatum; + ErrorContextCallback plerrcontext; + JMP_BUF *save_bailout; /* Initialize interpreter */ plphp_init_all(); + /* Messages raised below carry a CONTEXT line naming the function */ + plerrcontext.callback = plphp_compile_error_callback; + plerrcontext.arg = funcname; + plerrcontext.previous = error_context_stack; + error_context_stack = &plerrcontext; + funcname[0] = '\0'; + + save_bailout = EG(bailout); + PG_TRY(); { bool isnull; @@ -1041,15 +1113,19 @@ plphp_validator(PG_FUNCTION_ARGS) return 0; } zend_end_try(); - - /* The result of a validator is ignored */ - PG_RETURN_VOID(); } PG_CATCH(); { + /* see plphp_call_handler about the bailout environment */ + EG(bailout) = save_bailout; PG_RE_THROW(); } PG_END_TRY(); + + error_context_stack = plerrcontext.previous; + + /* The result of a validator is ignored */ + PG_RETURN_VOID(); } /* @@ -2212,6 +2288,21 @@ plphp_func_build_args(plphp_proc_desc *desc, FunctionCallInfo fcinfo) * through zend_error_cb, so we must check for them explicitly after invoking * user code and translate them into Postgres errors. */ +/* Does the string already end in " at line "? */ +static bool +message_has_line_suffix(const char *msg) +{ + const char *p = strrchr(msg, ' '); + + if (p == NULL || !isdigit((unsigned char) p[1])) + return false; + for (p++; *p; p++) + if (!isdigit((unsigned char) *p)) + return false; + p = strstr(msg, " at line "); + return p != NULL; +} + static char * plphp_pop_exception_message(void) { @@ -2238,7 +2329,13 @@ plphp_pop_exception_message(void) zval *zline = zend_read_property(ex->ce, ex, "line", sizeof("line") - 1, 1, &rvl); - if (zline != NULL && Z_TYPE_P(zline) == IS_LONG) + /* + * Skip the suffix when the message already ends in one: an + * error crossing nested PL/php calls has it from the inner + * function, and repeating it just adds noise. + */ + if (zline != NULL && Z_TYPE_P(zline) == IS_LONG && + !message_has_line_suffix(Z_STRVAL_P(zmsg))) msg = psprintf("%s at line %ld", Z_STRVAL_P(zmsg), (long) Z_LVAL_P(zline)); else @@ -2494,7 +2591,7 @@ plphp_error_cb(int type, zend_string *error_filename, */ if (elevel >= ERROR) { - if (error_lineno != 0) + if (error_lineno != 0 && !message_has_line_suffix(str)) { char msgline[1024]; snprintf(msgline, sizeof(msgline), "%s at line %u", str, error_lineno); From c9d6bc95d1731ab8161800c9917b1ea86779b87b Mon Sep 17 00:00:00 2001 From: "Joshua (D) Drake" <136637981+ChronicallyJD@users.noreply.github.com> Date: Sun, 5 Jul 2026 20:28:18 -0600 Subject: [PATCH 2/2] Regression tests and CHANGELOG for error context and the nested fix Every existing error and notice in the expected files gains its CONTEXT line -- including the base_1/trigger_1 alternates that absorb older servers' message wording. New pgerror cases cover an uncaught error crossing nested calls (the former crasher), session health afterwards, and catching the nested failure as PgError in the outer function. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 15 ++++++++++++ expected/base.out | 8 +++++++ expected/base_1.out | 8 +++++++ expected/compat.out | 3 +++ expected/coverage.out | 3 +++ expected/cursor.out | 1 + expected/evttrig.out | 1 + expected/modules.out | 0 expected/oninit.out | 1 + expected/out.out | 1 + expected/pgerror.out | 35 ++++++++++++++++++++++++++++ expected/raise.out | 4 ++++ expected/shared.out | 1 + expected/spi.out | 1 + expected/srf.out | 1 + expected/subxact.out | 0 expected/trigger.out | 8 +++++++ expected/trigger_1.out | 8 +++++++ expected/txn.out | 2 ++ expected/validator.out | 4 ++++ expected/varnames.out | 1 + jsonb_plphp/expected/jsonb_plphp.out | 1 + sql/pgerror.sql | 23 ++++++++++++++++++ 23 files changed, 130 insertions(+) mode change 100755 => 100644 expected/base.out mode change 100755 => 100644 expected/base_1.out mode change 100755 => 100644 expected/compat.out mode change 100755 => 100644 expected/evttrig.out mode change 100755 => 100644 expected/modules.out mode change 100755 => 100644 expected/shared.out mode change 100755 => 100644 expected/subxact.out mode change 100755 => 100644 expected/trigger.out mode change 100755 => 100644 expected/txn.out mode change 100755 => 100644 expected/validator.out mode change 100755 => 100644 expected/varnames.out diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ebdd57..1563f30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,21 @@ and the project aims to follow [Semantic Versioning](https://semver.org/). function cache used to briefly point at freed memory during recompilation, which an unluckily timed statement cancel could have hit. +## [Unreleased] + +### Added + +- **Error CONTEXT lines.** Messages raised while PL/php code runs carry a + `CONTEXT: PL/php function "name"` line (or the anonymous-block/compilation + variants), like every other procedural language. + +### Fixed + +- **Backend crash when an error crossed nested PL/php calls.** A PostgreSQL + error unwinding out of a handler's `zend_try` left Zend's bailout + environment pointing into a dead stack frame; the next uncaught error then + jumped into garbage. Nested regression cases added. + ## [2.3.0] — 2026-07-06 ### Added diff --git a/expected/base.out b/expected/base.out old mode 100755 new mode 100644 index 727e2ed..296752f --- a/expected/base.out +++ b/expected/base.out @@ -50,16 +50,20 @@ LANGUAGE plphp AS $$ $$; SELECT test_a_bogus_array(); ERROR: function declared to return array must return an array +CONTEXT: PL/php function "test_a_bogus_array" SELECT * FROM test_a_bogus_array(); ERROR: function declared to return array must return an array +CONTEXT: PL/php function "test_a_bogus_array" CREATE FUNCTION test_a_bogus_int() RETURNS integer LANGUAGE plphp AS $$ return array(1); $$; SELECT test_a_bogus_int(); ERROR: this plphp function cannot return arrays +CONTEXT: PL/php function "test_a_bogus_int" SELECT * FROM test_a_bogus_int(); ERROR: this plphp function cannot return arrays +CONTEXT: PL/php function "test_a_bogus_int" CREATE FUNCTION test_ndim_array(int, int) RETURNS int[] LANGUAGE plphp AS $$ if (!function_exists('bar')) { @@ -112,6 +116,7 @@ SELECT test_ndim_array(6, 1); SELECT test_ndim_array(7, 1); ERROR: number of array dimensions exceeds the maximum allowed (6) +CONTEXT: PL/php function "test_ndim_array" CREATE FUNCTION php_max (integer, integer) RETURNS integer STRICT LANGUAGE plphp AS $$ if ($args[0] > $args[1]) { @@ -290,14 +295,17 @@ create function foo() returns record language plphp as $$ $$; select foo(); ERROR: function declared to return tuple must return an array +CONTEXT: PL/php function "foo" select * FROM foo() as (a int); ERROR: function declared to return tuple must return an array +CONTEXT: PL/php function "foo" create or replace function foo(anyelement) returns anyarray language plphp as $$ return 1; $$; select foo(1); ERROR: function declared to return array must return an array +CONTEXT: PL/php function "foo" -- test recursive functions create function php_fib(int) returns int language plphp as $$ if ($args[0] <= 1) { return 1; } diff --git a/expected/base_1.out b/expected/base_1.out old mode 100755 new mode 100644 index 3889de8..b2a1721 --- a/expected/base_1.out +++ b/expected/base_1.out @@ -50,16 +50,20 @@ LANGUAGE plphp AS $$ $$; SELECT test_a_bogus_array(); ERROR: function declared to return array must return an array +CONTEXT: PL/php function "test_a_bogus_array" SELECT * FROM test_a_bogus_array(); ERROR: function declared to return array must return an array +CONTEXT: PL/php function "test_a_bogus_array" CREATE FUNCTION test_a_bogus_int() RETURNS integer LANGUAGE plphp AS $$ return array(1); $$; SELECT test_a_bogus_int(); ERROR: this plphp function cannot return arrays +CONTEXT: PL/php function "test_a_bogus_int" SELECT * FROM test_a_bogus_int(); ERROR: this plphp function cannot return arrays +CONTEXT: PL/php function "test_a_bogus_int" CREATE FUNCTION test_ndim_array(int, int) RETURNS int[] LANGUAGE plphp AS $$ if (!function_exists('bar')) { @@ -112,6 +116,7 @@ SELECT test_ndim_array(6, 1); SELECT test_ndim_array(7, 1); ERROR: number of array dimensions (7) exceeds the maximum allowed (6) +CONTEXT: PL/php function "test_ndim_array" CREATE FUNCTION php_max (integer, integer) RETURNS integer STRICT LANGUAGE plphp AS $$ if ($args[0] > $args[1]) { @@ -290,14 +295,17 @@ create function foo() returns record language plphp as $$ $$; select foo(); ERROR: function declared to return tuple must return an array +CONTEXT: PL/php function "foo" select * FROM foo() as (a int); ERROR: function declared to return tuple must return an array +CONTEXT: PL/php function "foo" create or replace function foo(anyelement) returns anyarray language plphp as $$ return 1; $$; select foo(1); ERROR: function declared to return array must return an array +CONTEXT: PL/php function "foo" -- test recursive functions create function php_fib(int) returns int language plphp as $$ if ($args[0] <= 1) { return 1; } diff --git a/expected/compat.out b/expected/compat.out old mode 100755 new mode 100644 index 73bc619..162b1ce --- a/expected/compat.out +++ b/expected/compat.out @@ -16,6 +16,7 @@ NOTICE: plphp: DO via SPI: 42 -- A syntactically invalid DO block is rejected DO $$ this is not php $$ LANGUAGE plphp; ERROR: unable to compile inline code block: syntax error, unexpected identifier "is" +CONTEXT: PL/php anonymous code block -- Prepared statements: prepare / exec_prepared / freeplan CREATE FUNCTION prep_test(int) RETURNS int LANGUAGE plphp AS $$ $plan = spi_prepare('select $1 * 10 as v', 'int4'); @@ -111,6 +112,7 @@ CREATE FUNCTION elog_error() RETURNS void LANGUAGE plphp AS $$ $$; SELECT elog_error(); ERROR: boom from elog at line 2 +CONTEXT: PL/php function "elog_error" -- A prepared statement with a NULL argument CREATE FUNCTION prep_null() RETURNS text LANGUAGE plphp AS $$ $plan = spi_prepare('select $1::text as v', 'text'); @@ -155,3 +157,4 @@ CREATE FUNCTION elog_bad() RETURNS void LANGUAGE plphp AS $$ $$; SELECT elog_bad(); ERROR: elog: unrecognized level "BOGUS" at line 2 +CONTEXT: PL/php function "elog_bad" diff --git a/expected/coverage.out b/expected/coverage.out index 110a54a..b459864 100644 --- a/expected/coverage.out +++ b/expected/coverage.out @@ -13,6 +13,7 @@ LINE 1: SELECT cov_no_such_fn() ^ HINT: No function matches the given name and argument types. You might need to add explicit type casts. QUERY: SELECT cov_no_such_fn() +CONTEXT: PL/php function "cov_ident" RESET plphp.start_proc; SELECT cov_ident(1); cov_ident @@ -144,6 +145,7 @@ SELECT cov_catch(); -- A DO block can fail at runtime (not just at parse time) DO $$ pg_raise('error', 'do block runtime error'); $$ LANGUAGE plphp; ERROR: do block runtime error at line 1 +CONTEXT: PL/php anonymous code block DROP TABLE cov_t; -- Redefinition churn: each CREATE OR REPLACE (and ALTER) discards the old -- compiled function and its memory context and recompiles on next use @@ -176,6 +178,7 @@ SELECT churn(); -- a body that fails to compile poisons the cache entry; fixing it recovers CREATE OR REPLACE FUNCTION churn() RETURNS int LANGUAGE plphp AS $$ return $$; ERROR: function "churn" does not validate: syntax error, unexpected token "}", expecting ";" +CONTEXT: compilation of PL/php function "churn" SELECT churn(); churn ------- diff --git a/expected/cursor.out b/expected/cursor.out index 065c7ac..f9faf2e 100644 --- a/expected/cursor.out +++ b/expected/cursor.out @@ -130,6 +130,7 @@ select test_cursor_error(1); select test_cursor_error(0); ERROR: division by zero at line 2 +CONTEXT: PL/php function "test_cursor_error" select test_cursor_error(1); test_cursor_error ------------------- diff --git a/expected/evttrig.out b/expected/evttrig.out old mode 100755 new mode 100644 index e55f869..943d70c --- a/expected/evttrig.out +++ b/expected/evttrig.out @@ -32,5 +32,6 @@ CREATE EVENT TRIGGER guard ON ddl_command_start WHEN TAG IN ('CREATE TABLE') EXECUTE FUNCTION no_create(); CREATE TABLE blocked (x int); ERROR: no new tables allowed at line 2 +CONTEXT: PL/php function "no_create" DROP EVENT TRIGGER guard; DROP FUNCTION no_create(); diff --git a/expected/modules.out b/expected/modules.out old mode 100755 new mode 100644 diff --git a/expected/oninit.out b/expected/oninit.out index d65ef76..aaac7e9 100644 --- a/expected/oninit.out +++ b/expected/oninit.out @@ -7,6 +7,7 @@ $$; SET plphp.on_init = 'throw new Exception("on_init boom");'; SELECT oninit_victim(); ERROR: plphp: on_init failed: on_init boom +CONTEXT: PL/php function "oninit_victim" RESET plphp.on_init; SELECT oninit_victim(); oninit_victim diff --git a/expected/out.out b/expected/out.out index 2d9b44d..9e21811 100644 --- a/expected/out.out +++ b/expected/out.out @@ -81,6 +81,7 @@ $$ $$ LANGUAGE plphp; SELECT * FROM plphp.out_invalid(); ERROR: "long identifier" can not be used as a PHP variable name +CONTEXT: PL/php function "out_invalid" -- INOUT parameters in procedures: a procedure's result is always a record -- (even with one INOUT), returned via the same assignment convention CREATE PROCEDURE proc_one(INOUT x int) LANGUAGE plphp AS $$ diff --git a/expected/pgerror.out b/expected/pgerror.out index cf710c8..e01cc0e 100644 --- a/expected/pgerror.out +++ b/expected/pgerror.out @@ -89,6 +89,7 @@ CREATE FUNCTION err_uncaught() RETURNS void LANGUAGE plphp AS $$ $$; SELECT err_uncaught(); ERROR: division by zero at line 2 +CONTEXT: PL/php function "err_uncaught" -- Errors surfacing mid-iteration from a cursor are catchable too CREATE FUNCTION err_cursor() RETURNS text LANGUAGE plphp AS $$ $c = spi_query("select 1/(g-2) as x from generate_series(1,3) g"); @@ -108,4 +109,38 @@ SELECT err_cursor(); -1 then division by zero (1 row) +-- An error crossing nested PL/php calls: propagates cleanly (this used to +-- crash the backend via a stale Zend bailout environment), is catchable in +-- the outer function, and the session stays healthy +CREATE FUNCTION err_inner() RETURNS int LANGUAGE plphp AS $$ + spi_exec("select 1/0"); +$$; +CREATE FUNCTION err_outer() RETURNS int LANGUAGE plphp AS $$ + $r = spi_exec("select err_inner() as v"); + return 1; +$$; +SELECT err_outer(); +ERROR: division by zero at line 2 +CONTEXT: PL/php function "err_outer" +SELECT 1 AS session_alive; + session_alive +--------------- + 1 +(1 row) + +CREATE FUNCTION err_outer_catch() RETURNS text LANGUAGE plphp AS $$ + try { + spi_exec("select err_inner()"); + } catch (PgError $e) { + return "caught nested: " . $e->getMessage(); + } + return "not reached"; +$$; +SELECT err_outer_catch(); + err_outer_catch +------------------------------------------- + caught nested: division by zero at line 2 +(1 row) + +DROP FUNCTION err_inner(), err_outer(), err_outer_catch(); DROP TABLE errt; diff --git a/expected/raise.out b/expected/raise.out index 4fd819e..4f8c815 100644 --- a/expected/raise.out +++ b/expected/raise.out @@ -20,14 +20,18 @@ insert into b values (1); insert into b values (2); insert into b values (3); ERROR: value a=3 is not allowed at line 13 +CONTEXT: PL/php function "trigfunc_ins" insert into b values (4); insert into b values (5); ERROR: expected trigger function to return NULL, 'SKIP' or 'MODIFY' +CONTEXT: PL/php function "trigfunc_ins" insert into b values (6); ERROR: value a=6 is not allowed at line 13 +CONTEXT: PL/php function "trigfunc_ins" insert into b values (1); insert into b values (3); ERROR: value a=3 is not allowed at line 13 +CONTEXT: PL/php function "trigfunc_ins" insert into b values (2); select * from b; a diff --git a/expected/shared.out b/expected/shared.out old mode 100755 new mode 100644 index 0de5c2f..54666ac --- a/expected/shared.out +++ b/expected/shared.out @@ -67,6 +67,7 @@ SELECT php_get_shared('fourth'); SELECT php_get_shared_ary('fourth'); ERROR: function declared to return array must return an array +CONTEXT: PL/php function "php_get_shared_ary" -- A real array shared between calls CREATE OR REPLACE FUNCTION php_set_shared_ary (text, text[]) RETURNS text AS $$ global $_SHARED; diff --git a/expected/spi.out b/expected/spi.out index 51454ea..c5836e0 100644 --- a/expected/spi.out +++ b/expected/spi.out @@ -120,6 +120,7 @@ NOTICE: plphp: got value 3.000000 -- oops, division by zero select div_by_zero('{1,2,3}', 0); ERROR: division by zero at line 7 +CONTEXT: PL/php function "div_by_zero" -- 1/2 + 2/2 + 3/2 = 3 select div_by_zero('{1,2,3}', 2); NOTICE: plphp: got value 0.500000 diff --git a/expected/srf.out b/expected/srf.out index a08753c..a02d0d4 100644 --- a/expected/srf.out +++ b/expected/srf.out @@ -106,6 +106,7 @@ language plphp as $$ $$; select * from retset5(5, 2); ERROR: cannot use return_next in functions not declared to return a set +CONTEXT: PL/php function "retset5" -- Try to return setof array create or replace function retset6(int, int) returns setof int[] language plphp as $$ diff --git a/expected/subxact.out b/expected/subxact.out old mode 100755 new mode 100644 diff --git a/expected/trigger.out b/expected/trigger.out old mode 100755 new mode 100644 index b17377b..d850b1f --- a/expected/trigger.out +++ b/expected/trigger.out @@ -90,6 +90,7 @@ $$; -- Invalid attribute "a" in NEW INSERT INTO trigfunc_test1 VALUES (1); ERROR: $_TD['new'] does not contain attribute "b" +CONTEXT: PL/php function "trigfunc_test1" -- Create NEW as the correct array, but with a NULL value CREATE OR REPLACE FUNCTION trigfunc_test1() RETURNS trigger LANGUAGE plphp AS $$ $_TD['new'] = array('b' => NULL); @@ -113,6 +114,7 @@ $$; INSERT INTO trigfunc_test1 VALUES (1); ERROR: insufficient number of keys in $_TD['new'] DETAIL: At least 1 expected, 0 found. +CONTEXT: PL/php function "trigfunc_test1" -- Make "b" a literal of an incorrect type CREATE OR REPLACE FUNCTION trigfunc_test1() RETURNS trigger LANGUAGE plphp AS $$ $_TD['new'] = array('b' => 'foobar'); @@ -120,6 +122,7 @@ CREATE OR REPLACE FUNCTION trigfunc_test1() RETURNS trigger LANGUAGE plphp AS $$ $$; INSERT INTO trigfunc_test1 VALUES (1); ERROR: invalid input syntax for type integer: "foobar" +CONTEXT: PL/php function "trigfunc_test1" -- Make "b" an array CREATE OR REPLACE FUNCTION trigfunc_test1() RETURNS trigger LANGUAGE plphp AS $$ $_TD['new'] = array('b' => array(1, 2, 3)); @@ -127,6 +130,7 @@ CREATE OR REPLACE FUNCTION trigfunc_test1() RETURNS trigger LANGUAGE plphp AS $$ $$; INSERT INTO trigfunc_test1 VALUES (1); ERROR: invalid input syntax for type integer: "{1,2,3}" +CONTEXT: PL/php function "trigfunc_test1" -- What happens if we create a table whose columns are numbers? -- Does PHP regular array handling work? CREATE TABLE numbers ("0" int, "1" text, "2" float, "3" numeric[]); @@ -186,6 +190,7 @@ create trigger foo before insert on foo for each row execute procedure foo_trig(); insert into foo values (1, 'one'); ERROR: $_TD is not an array +CONTEXT: PL/php function "foo_trig" create or replace function foo_trig() returns trigger language plphp as $$ $_TD = array(); @@ -193,6 +198,7 @@ language plphp as $$ $$; insert into foo values (1, 'one'); ERROR: $_TD['new'] not found +CONTEXT: PL/php function "foo_trig" create or replace function foo_trig() returns trigger language plphp as $$ $_TD = array('new' => 1); @@ -200,6 +206,7 @@ language plphp as $$ $$; insert into foo values (1, 'one'); ERROR: $_TD['new'] must be an array +CONTEXT: PL/php function "foo_trig" create or replace function foo_trig() returns trigger language plphp as $$ $_TD = array('new' => array()); @@ -208,3 +215,4 @@ $$; insert into foo values (1, 'one'); ERROR: insufficient number of keys in $_TD['new'] DETAIL: At least 2 expected, 0 found. +CONTEXT: PL/php function "foo_trig" diff --git a/expected/trigger_1.out b/expected/trigger_1.out index 2f84b6d..a7f067e 100644 --- a/expected/trigger_1.out +++ b/expected/trigger_1.out @@ -90,6 +90,7 @@ $$; -- Invalid attribute "a" in NEW INSERT INTO trigfunc_test1 VALUES (1); ERROR: $_TD['new'] does not contain attribute "b" +CONTEXT: PL/php function "trigfunc_test1" -- Create NEW as the correct array, but with a NULL value CREATE OR REPLACE FUNCTION trigfunc_test1() RETURNS trigger LANGUAGE plphp AS $$ $_TD['new'] = array('b' => NULL); @@ -113,6 +114,7 @@ $$; INSERT INTO trigfunc_test1 VALUES (1); ERROR: insufficient number of keys in $_TD['new'] DETAIL: At least 1 expected, 0 found. +CONTEXT: PL/php function "trigfunc_test1" -- Make "b" a literal of an incorrect type CREATE OR REPLACE FUNCTION trigfunc_test1() RETURNS trigger LANGUAGE plphp AS $$ $_TD['new'] = array('b' => 'foobar'); @@ -120,6 +122,7 @@ CREATE OR REPLACE FUNCTION trigfunc_test1() RETURNS trigger LANGUAGE plphp AS $$ $$; INSERT INTO trigfunc_test1 VALUES (1); ERROR: invalid input syntax for integer: "foobar" +CONTEXT: PL/php function "trigfunc_test1" -- Make "b" an array CREATE OR REPLACE FUNCTION trigfunc_test1() RETURNS trigger LANGUAGE plphp AS $$ $_TD['new'] = array('b' => array(1, 2, 3)); @@ -127,6 +130,7 @@ CREATE OR REPLACE FUNCTION trigfunc_test1() RETURNS trigger LANGUAGE plphp AS $$ $$; INSERT INTO trigfunc_test1 VALUES (1); ERROR: invalid input syntax for integer: "{1,2,3}" +CONTEXT: PL/php function "trigfunc_test1" -- What happens if we create a table whose columns are numbers? -- Does PHP regular array handling work? CREATE TABLE numbers ("0" int, "1" text, "2" float, "3" numeric[]); @@ -186,6 +190,7 @@ create trigger foo before insert on foo for each row execute procedure foo_trig(); insert into foo values (1, 'one'); ERROR: $_TD is not an array +CONTEXT: PL/php function "foo_trig" create or replace function foo_trig() returns trigger language plphp as $$ $_TD = array(); @@ -193,6 +198,7 @@ language plphp as $$ $$; insert into foo values (1, 'one'); ERROR: $_TD['new'] not found +CONTEXT: PL/php function "foo_trig" create or replace function foo_trig() returns trigger language plphp as $$ $_TD = array('new' => 1); @@ -200,6 +206,7 @@ language plphp as $$ $$; insert into foo values (1, 'one'); ERROR: $_TD['new'] must be an array +CONTEXT: PL/php function "foo_trig" create or replace function foo_trig() returns trigger language plphp as $$ $_TD = array('new' => array()); @@ -208,3 +215,4 @@ $$; insert into foo values (1, 'one'); ERROR: insufficient number of keys in $_TD['new'] DETAIL: At least 2 expected, 0 found. +CONTEXT: PL/php function "foo_trig" diff --git a/expected/txn.out b/expected/txn.out old mode 100755 new mode 100644 index 4957320..793dab8 --- a/expected/txn.out +++ b/expected/txn.out @@ -37,9 +37,11 @@ CREATE FUNCTION cannot_commit() RETURNS void LANGUAGE plphp AS $$ $$; SELECT cannot_commit(); ERROR: invalid transaction termination at line 2 +CONTEXT: PL/php function "cannot_commit" -- Nor in a procedure called inside an explicit transaction block. BEGIN; CALL do_commits(); ERROR: invalid transaction termination at line 4 +CONTEXT: PL/php function "do_commits" ROLLBACK; DROP TABLE txn_test; diff --git a/expected/validator.out b/expected/validator.out old mode 100755 new mode 100644 index 0cda518..f5c1f01 --- a/expected/validator.out +++ b/expected/validator.out @@ -5,17 +5,21 @@ CREATE FUNCTION invalid() RETURNS void LANGUAGE plphp AS $$ asdasd $$; ERROR: function "invalid" does not validate: syntax error, unexpected token "}" +CONTEXT: compilation of PL/php function "invalid" CREATE FUNCTION invalid() RETURNS void LANGUAGE plphp AS $$ { $$; ERROR: function "invalid" does not validate: Unclosed '{' on line 1 +CONTEXT: compilation of PL/php function "invalid" CREATE OR REPLACE FUNCTION valid() RETURNS void LANGUAGE plphp AS $$ array_append(); $$; select valid(); ERROR: Uncaught Error: Call to undefined function array_append() in plphp function source:2 at line 2 +CONTEXT: PL/php function "valid" CREATE FUNCTION valid2() RETURNS void LANGUAGE plphp AS $$ return array(); $$; select valid2(); ERROR: this plphp function cannot return arrays +CONTEXT: PL/php function "valid2" diff --git a/expected/varnames.out b/expected/varnames.out old mode 100755 new mode 100644 index aca8f35..d658104 --- a/expected/varnames.out +++ b/expected/varnames.out @@ -93,4 +93,5 @@ CREATE FUNCTION vany(VARIADIC "any") RETURNS int LANGUAGE plphp AS $$ $$; SELECT vany(1, 'x'::text, true); ERROR: PL/php functions cannot accept VARIADIC "any" arguments +CONTEXT: PL/php function "vany" DROP FUNCTION vsum(int[]), vjoin(text, text[]), vany("any"); diff --git a/jsonb_plphp/expected/jsonb_plphp.out b/jsonb_plphp/expected/jsonb_plphp.out index b2f8781..051993e 100644 --- a/jsonb_plphp/expected/jsonb_plphp.out +++ b/jsonb_plphp/expected/jsonb_plphp.out @@ -159,5 +159,6 @@ $$; SELECT badret(); ERROR: cannot transform this PHP value to jsonb DETAIL: PHP type 8 has no jsonb representation. +CONTEXT: PL/php function "badret" DROP FUNCTION roundtrip(jsonb), typeinfo(jsonb), makedoc(), makenum(), redact(jsonb, text), astext(jsonb), badret(); diff --git a/sql/pgerror.sql b/sql/pgerror.sql index bb17f5f..4ef9a97 100644 --- a/sql/pgerror.sql +++ b/sql/pgerror.sql @@ -83,4 +83,27 @@ CREATE FUNCTION err_cursor() RETURNS text LANGUAGE plphp AS $$ $$; SELECT err_cursor(); +-- An error crossing nested PL/php calls: propagates cleanly (this used to +-- crash the backend via a stale Zend bailout environment), is catchable in +-- the outer function, and the session stays healthy +CREATE FUNCTION err_inner() RETURNS int LANGUAGE plphp AS $$ + spi_exec("select 1/0"); +$$; +CREATE FUNCTION err_outer() RETURNS int LANGUAGE plphp AS $$ + $r = spi_exec("select err_inner() as v"); + return 1; +$$; +SELECT err_outer(); +SELECT 1 AS session_alive; +CREATE FUNCTION err_outer_catch() RETURNS text LANGUAGE plphp AS $$ + try { + spi_exec("select err_inner()"); + } catch (PgError $e) { + return "caught nested: " . $e->getMessage(); + } + return "not reached"; +$$; +SELECT err_outer_catch(); + +DROP FUNCTION err_inner(), err_outer(), err_outer_catch(); DROP TABLE errt;