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/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); 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;