From 34a0786045cd1b2d83801d229deb1a8910e27d7e Mon Sep 17 00:00:00 2001 From: "Joshua D. Drake" Date: Mon, 9 Nov 2026 19:50:36 +0000 Subject: [PATCH 1/2] Schema-qualify plx's own objects in generated code; make plx non-relocatable A string append lowered to a plx_strbuild variable and a plx_sb_append() call, both unqualified, so they resolved through search_path at run time. A function hardened with SET search_path = pg_catalog then failed, as did one whose search_path omitted plx's schema, and a same-named function earlier on the path ran instead (with the definer's privileges for a SECURITY DEFINER function). The validator and inline handler now record plx's schema (the namespace of the running validator/inline handler, which are extension members; this works on PG 13+, where get_extension_schema is not exported before 16), and the transpiler emits .plx_strbuild and .plx_sb_append. Stored functions now carry the schema name, so the extension is made non-relocatable. PostgreSQL records relocatability when an extension is created or updated, so this adds extension version 2.1.0 with a no-catalog-change update script; running ALTER EXTENSION plx UPDATE is what records it for an existing installation. Regression tests cover a hardened search_path, an impostor plx_sb_append earlier on the path, and SET SCHEMA being refused. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 28 +++++ META.json | 4 +- Makefile | 2 +- plx--2.0.0--2.1.0.sql | 17 +++ plx--2.1.0.sql | 209 +++++++++++++++++++++++++++++++++ plx.control | 4 +- src/plx.h | 8 ++ src/plx_core.c | 6 + src/plx_dialect_cobol.c | 6 +- src/plx_engine.h | 1 + src/plx_transpile.c | 25 +++- test/expected/plx_strbuild.out | 62 ++++++++-- test/sql/plx_strbuild.sql | 32 +++++ 13 files changed, 382 insertions(+), 22 deletions(-) create mode 100644 plx--2.0.0--2.1.0.sql create mode 100644 plx--2.1.0.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 1930387..5db4986 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,34 @@ All notable changes to plx are recorded here. The format follows [Keep a Changelog](https://keepachangelog.com/), and plx uses the extension version in `plx.control` (currently `1.0`). +## [Unreleased] + +Extension version 2.1.0. Install the new build, then run +`ALTER EXTENSION plx UPDATE` in each database. The update changes no SQL +objects; it records the extension as non-relocatable (below). + +### Security + +- **Generated code names plx's own objects with plx's schema.** A string + append (`s << x`, `$s .= x`, `s += x`, `STRING-APPEND`) declared the + variable as `plx_strbuild` and called `plx_sb_append(...)` unqualified, so + they were resolved through `search_path` every time the function ran. That + broke the standard hardening for a `SECURITY DEFINER` function + (`SET search_path = pg_catalog`: `type "plx_strbuild" does not exist`), broke + functions when plx was installed in a schema not on the path, and let a + same-named function planted earlier on the path run in their place, with the + definer's privileges for a `SECURITY DEFINER` function. Generated code now + writes `.plx_strbuild` and `.plx_sb_append`. + +### Changed + +- **The extension is no longer relocatable** (`relocatable = false`). + Functions now store plx's schema name, so `ALTER EXTENSION plx SET SCHEMA` + would leave them pointing at the old schema; it now fails with + `extension "plx" does not support SET SCHEMA`. Choose the schema at + `CREATE EXTENSION plx SCHEMA ...` time. Existing functions keep the + unqualified names they were generated with until their DDL is run again. + ## [2.0.1] - 2026-08-25 Packaging only. **The extension is unchanged at 2.0.0**, so there is no diff --git a/META.json b/META.json index 22c079d..4b8d72d 100644 --- a/META.json +++ b/META.json @@ -10,9 +10,9 @@ "provides": { "plx": { "abstract": "Dialect-pluggable procedural language over plpgsql (Ruby, PHP, JavaScript, TypeScript, Python, Go, COBOL, Oracle PL/SQL, Transact-SQL)", - "file": "plx--2.0.0.sql", + "file": "plx--2.1.0.sql", "docfile": "README.md", - "version": "2.0.0" + "version": "2.1.0" } }, "prereqs": { diff --git a/Makefile b/Makefile index f01d495..34d5093 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ MODULE_big = plx OBJS = src/plx_core.o src/plx_transpile.o src/plx_strbuild.o src/plx_dialect_ruby.o src/plx_dialect_php.o src/plx_dialect_js.o src/plx_dialect_python.o src/plx_dialect_cobol.o src/plx_dialect_plsql.o src/plx_dialect_ts.o src/plx_dialect_tsql.o src/plx_dialect_go.o src/plx_parse_brace.o EXTENSION = plx -DATA = plx--1.0.sql plx--1.1.sql plx--1.1.1.sql plx--1.2.sql plx--1.2.1.sql plx--1.2.2.sql plx--1.3.0.sql plx--1.3.1.sql plx--1.3.2.sql plx--2.0.0.sql plx--1.0--1.1.sql plx--1.1--1.1.1.sql plx--1.1.1--1.2.sql plx--1.2--1.2.1.sql plx--1.2.1--1.2.2.sql plx--1.2.2--1.3.0.sql plx--1.3.0--1.3.1.sql plx--1.3.1--1.3.2.sql plx--1.3.2--2.0.0.sql +DATA = plx--1.0.sql plx--1.1.sql plx--1.1.1.sql plx--1.2.sql plx--1.2.1.sql plx--1.2.2.sql plx--1.3.0.sql plx--1.3.1.sql plx--1.3.2.sql plx--2.0.0.sql plx--2.1.0.sql plx--1.0--1.1.sql plx--1.1--1.1.1.sql plx--1.1.1--1.2.sql plx--1.2--1.2.1.sql plx--1.2.1--1.2.2.sql plx--1.2.2--1.3.0.sql plx--1.3.0--1.3.1.sql plx--1.3.1--1.3.2.sql plx--1.3.2--2.0.0.sql plx--2.0.0--2.1.0.sql # pg_regress suite (make installcheck). test/run_corpus.py is an additional # Ruby corpus runner, and test/differential.py checks each dialect against a diff --git a/plx--2.0.0--2.1.0.sql b/plx--2.0.0--2.1.0.sql new file mode 100644 index 0000000..630996d --- /dev/null +++ b/plx--2.0.0--2.1.0.sql @@ -0,0 +1,17 @@ +/* plx 2.0.0 -> 2.1.0: no catalog changes. + * + * 2.1.0 makes the extension non-relocatable (relocatable = false in + * plx.control). Generated code now names the extension's own objects + * (plx_strbuild, plx_sb_append) qualified with the schema plx is installed in, + * so a function with its own SET search_path still finds them and an object of + * the same name earlier on the caller's path cannot stand in for them. Moving + * the extension with ALTER EXTENSION ... SET SCHEMA would leave those stored + * names pointing at the old schema, so it is no longer allowed. + * + * PostgreSQL records relocatability in pg_extension when the extension is + * created or updated, which is why this otherwise empty update exists: running + * it is what records the new setting for an existing installation. + * + * Functions already in the catalog keep the unqualified names they were + * generated with; they pick up the qualified names the next time their DDL is + * run. See the 2.1.0 entry in CHANGELOG.md. */ diff --git a/plx--2.1.0.sql b/plx--2.1.0.sql new file mode 100644 index 0000000..c1a49c1 --- /dev/null +++ b/plx--2.1.0.sql @@ -0,0 +1,209 @@ +/* plx 2.1.0: registers the plx dialect languages and the string-builder type */ + +\echo Use "CREATE EXTENSION plx" to load this file. \quit + +/* + * plx_strbuild: an expanded-object string builder. Its flattened form is a + * text-compatible varlena, so it casts to and from text without a function. + * plx_sb_append grows the buffer in place across a loop (amortized O(1)), + * avoiding the quadratic cost of `s := s || 'x'` on plain text. + */ +CREATE TYPE plx_strbuild; + +CREATE FUNCTION plx_sb_in(cstring) RETURNS plx_strbuild + AS '$libdir/plx', 'plx_sb_in' LANGUAGE C IMMUTABLE STRICT; +CREATE FUNCTION plx_sb_out(plx_strbuild) RETURNS cstring + AS '$libdir/plx', 'plx_sb_out' LANGUAGE C IMMUTABLE STRICT; + +CREATE TYPE plx_strbuild ( + INPUT = plx_sb_in, + OUTPUT = plx_sb_out, + LIKE = text, + STORAGE = extended +); + +CREATE FUNCTION plx_sb_append_support(internal) RETURNS internal + AS '$libdir/plx', 'plx_sb_append_support' LANGUAGE C; + +CREATE FUNCTION plx_sb_append(plx_strbuild, text) RETURNS plx_strbuild + AS '$libdir/plx', 'plx_sb_append' LANGUAGE C IMMUTABLE + SUPPORT plx_sb_append_support; + +-- plx_strbuild is implicitly usable as text (so it works in RETURN, string +-- functions, and concatenation). text becomes a builder only in assignment +-- context, which avoids operator ambiguity such as `builder || text`. +CREATE CAST (plx_strbuild AS text) WITHOUT FUNCTION AS IMPLICIT; +CREATE CAST (text AS plx_strbuild) WITHOUT FUNCTION AS ASSIGNMENT; + +/* + * Runtime call handler = plpgsql's OWN handler. Binding a fresh pg_proc row to + * plpgsql.so's exported symbol is legal (CreateProceduralLanguage only checks + * the handler RETURNS language_handler). Execution is therefore 100% stock + * plpgsql; plx contributes zero call-path C. + */ +CREATE FUNCTION plx_call_handler() + RETURNS language_handler + AS '$libdir/plpgsql', 'plpgsql_call_handler' + LANGUAGE C; + +/* Ruby dialect validator + inline handler live in plx.so */ +CREATE FUNCTION plx_ruby_validator(oid) + RETURNS void + AS '$libdir/plx', 'plx_ruby_validator' + LANGUAGE C STRICT; + +CREATE FUNCTION plx_ruby_inline_handler(internal) + RETURNS void + AS '$libdir/plx', 'plx_ruby_inline_handler' + LANGUAGE C; + +CREATE TRUSTED LANGUAGE plxruby + HANDLER plx_call_handler + INLINE plx_ruby_inline_handler + VALIDATOR plx_ruby_validator; + +COMMENT ON LANGUAGE plxruby IS 'plx Ruby dialect (transpiles to plpgsql)'; + +/* PHP dialect */ +CREATE FUNCTION plx_php_validator(oid) + RETURNS void + AS '$libdir/plx', 'plx_php_validator' + LANGUAGE C STRICT; + +CREATE FUNCTION plx_php_inline_handler(internal) + RETURNS void + AS '$libdir/plx', 'plx_php_inline_handler' + LANGUAGE C; + +CREATE TRUSTED LANGUAGE plxphp + HANDLER plx_call_handler + INLINE plx_php_inline_handler + VALIDATOR plx_php_validator; + +COMMENT ON LANGUAGE plxphp IS 'plx PHP dialect (transpiles to plpgsql)'; + +/* JavaScript dialect */ +CREATE FUNCTION plx_js_validator(oid) + RETURNS void + AS '$libdir/plx', 'plx_js_validator' + LANGUAGE C STRICT; + +CREATE FUNCTION plx_js_inline_handler(internal) + RETURNS void + AS '$libdir/plx', 'plx_js_inline_handler' + LANGUAGE C; + +CREATE TRUSTED LANGUAGE plxjs + HANDLER plx_call_handler + INLINE plx_js_inline_handler + VALIDATOR plx_js_validator; + +COMMENT ON LANGUAGE plxjs IS 'plx JavaScript dialect (transpiles to plpgsql)'; + +/* Python dialect */ +CREATE FUNCTION plx_py_validator(oid) + RETURNS void + AS '$libdir/plx', 'plx_py_validator' + LANGUAGE C STRICT; + +CREATE FUNCTION plx_py_inline_handler(internal) + RETURNS void + AS '$libdir/plx', 'plx_py_inline_handler' + LANGUAGE C; + +CREATE TRUSTED LANGUAGE plxpython3 + HANDLER plx_call_handler + INLINE plx_py_inline_handler + VALIDATOR plx_py_validator; + +COMMENT ON LANGUAGE plxpython3 IS 'plx Python dialect (transpiles to plpgsql)'; + +/* COBOL dialect (ISO/IEC 1989:2023) validator + inline handler live in plx.so */ +CREATE FUNCTION plx_cob_validator(oid) + RETURNS void + AS '$libdir/plx', 'plx_cob_validator' + LANGUAGE C STRICT; + +CREATE FUNCTION plx_cob_inline_handler(internal) + RETURNS void + AS '$libdir/plx', 'plx_cob_inline_handler' + LANGUAGE C; + +CREATE TRUSTED LANGUAGE plxcobol + HANDLER plx_call_handler + INLINE plx_cob_inline_handler + VALIDATOR plx_cob_validator; + +COMMENT ON LANGUAGE plxcobol IS 'plx COBOL dialect (ISO/IEC 1989:2023, transpiles to plpgsql)'; + +/* PL/SQL dialect (Oracle) validator + inline handler live in plx.so */ +CREATE FUNCTION plx_plsql_validator(oid) + RETURNS void + AS '$libdir/plx', 'plx_plsql_validator' + LANGUAGE C STRICT; + +CREATE FUNCTION plx_plsql_inline_handler(internal) + RETURNS void + AS '$libdir/plx', 'plx_plsql_inline_handler' + LANGUAGE C; + +CREATE TRUSTED LANGUAGE plxplsql + HANDLER plx_call_handler + INLINE plx_plsql_inline_handler + VALIDATOR plx_plsql_validator; + +COMMENT ON LANGUAGE plxplsql IS 'plx Oracle PL/SQL dialect (transpiles to plpgsql)'; + +/* TypeScript dialect validator + inline handler live in plx.so */ +CREATE FUNCTION plx_ts_validator(oid) + RETURNS void + AS '$libdir/plx', 'plx_ts_validator' + LANGUAGE C STRICT; + +CREATE FUNCTION plx_ts_inline_handler(internal) + RETURNS void + AS '$libdir/plx', 'plx_ts_inline_handler' + LANGUAGE C; + +CREATE TRUSTED LANGUAGE plxts + HANDLER plx_call_handler + INLINE plx_ts_inline_handler + VALIDATOR plx_ts_validator; + +COMMENT ON LANGUAGE plxts IS 'plx TypeScript dialect (transpiles to plpgsql)'; + +/* T-SQL dialect (Transact-SQL) validator + inline handler live in plx.so */ +CREATE FUNCTION plx_tsql_validator(oid) + RETURNS void + AS '$libdir/plx', 'plx_tsql_validator' + LANGUAGE C STRICT; + +CREATE FUNCTION plx_tsql_inline_handler(internal) + RETURNS void + AS '$libdir/plx', 'plx_tsql_inline_handler' + LANGUAGE C; + +CREATE TRUSTED LANGUAGE plxtsql + HANDLER plx_call_handler + INLINE plx_tsql_inline_handler + VALIDATOR plx_tsql_validator; + +COMMENT ON LANGUAGE plxtsql IS 'plx Transact-SQL (SQL Server) dialect (transpiles to plpgsql)'; + +/* Go dialect validator + inline handler live in plx.so */ +CREATE FUNCTION plx_go_validator(oid) + RETURNS void + AS '$libdir/plx', 'plx_go_validator' + LANGUAGE C STRICT; + +CREATE FUNCTION plx_go_inline_handler(internal) + RETURNS void + AS '$libdir/plx', 'plx_go_inline_handler' + LANGUAGE C; + +CREATE TRUSTED LANGUAGE plxgo + HANDLER plx_call_handler + INLINE plx_go_inline_handler + VALIDATOR plx_go_validator; + +COMMENT ON LANGUAGE plxgo IS 'plx Go dialect (transpiles to plpgsql)'; diff --git a/plx.control b/plx.control index 110bcb6..b6e63f3 100644 --- a/plx.control +++ b/plx.control @@ -1,4 +1,4 @@ comment = 'plx (plExcellent): dialect-pluggable procedural language over plpgsql' -default_version = '2.0.0' +default_version = '2.1.0' module_pathname = '$libdir/plx' -relocatable = true +relocatable = false diff --git a/src/plx.h b/src/plx.h index 74c2720..a7d85bc 100644 --- a/src/plx.h +++ b/src/plx.h @@ -73,6 +73,14 @@ extern Datum plx_generic_inline_handler(FunctionCallInfo fcinfo, const PlxDialec /* True if prosrc already carries the plx sentinel (already transpiled). */ extern bool plx_has_sentinel(const char *prosrc); +/* + * Schema the plx extension is installed in, set by the validator and inline + * handler before they transpile. Generated code names the extension's own + * objects (plx_strbuild, plx_sb_append) qualified with it, so they resolve + * the same under any search_path. InvalidOid means "emit unqualified". + */ +extern Oid plx_ext_namespace; + /* (plx_transpile + PlxSurface live in plx_int.h, shared with the transpiler.) */ #endif /* PLX_H */ diff --git a/src/plx_core.c b/src/plx_core.c index a359e82..51ae377 100644 --- a/src/plx_core.c +++ b/src/plx_core.c @@ -23,6 +23,7 @@ #include "commands/proclang.h" #include "nodes/parsenodes.h" #include "utils/builtins.h" +#include "utils/lsyscache.h" #include "utils/rel.h" #include "utils/syscache.h" @@ -36,6 +37,8 @@ PG_MODULE_MAGIC; #define PLX_MAX_DIALECTS 32 static const PlxDialect *plx_dialects[PLX_MAX_DIALECTS]; + +Oid plx_ext_namespace = InvalidOid; static int plx_ndialects = 0; void @@ -126,6 +129,8 @@ plx_generic_validator(FunctionCallInfo fcinfo, const PlxDialect *d) bool repl[Natts_pg_proc]; build_meta(&meta, tuple, funcoid); + /* the validator is an extension member, so it lives in our schema */ + plx_ext_namespace = get_func_namespace(fcinfo->flinfo->fn_oid); plpgsql = d->transpile(rubysrc, &meta); memset(nulls, false, sizeof(nulls)); @@ -162,6 +167,7 @@ plx_generic_inline_handler(FunctionCallInfo fcinfo, const PlxDialect *d) Form_pg_language langform; Oid inline_oid; + plx_ext_namespace = get_func_namespace(fcinfo->flinfo->fn_oid); transpiled = d->transpile(codeblock->source_text, NULL); plpgsql_lang = get_language_oid("plpgsql", false); diff --git a/src/plx_dialect_cobol.c b/src/plx_dialect_cobol.c index 33ffa49..8ed9902 100644 --- a/src/plx_dialect_cobol.c +++ b/src/plx_dialect_cobol.c @@ -2089,10 +2089,10 @@ cob_string_append(Cb *cb, int ind) cb->pos++; l = plx_local_find(cb->cx, tgt, strlen(tgt)); if (l && !l->is_record) - l->typ = pstrdup("plx_strbuild"); /* the accumulator becomes a builder */ + l->typ = plx_ext_object("plx_strbuild"); /* the accumulator becomes a builder */ plx_indent(&cb->cx->out, ind); - appendStringInfo(&cb->cx->out, "%s := plx_sb_append(%s, (%s )::text);\n", - tgt, tgt, v.data); + appendStringInfo(&cb->cx->out, "%s := %s(%s, (%s )::text);\n", + tgt, plx_ext_object("plx_sb_append"), tgt, v.data); } static void diff --git a/src/plx_engine.h b/src/plx_engine.h index 434be02..5ad4386 100644 --- a/src/plx_engine.h +++ b/src/plx_engine.h @@ -135,6 +135,7 @@ int plx_stmt_end(Ctx *cx, int from); void plx_indent(StringInfo o, int n); PlxLocal2 *plx_local_add(Ctx *cx, const char *name, int len); PlxLocal2 *plx_local_find(Ctx *cx, const char *name, int len); +char *plx_ext_object(const char *name); bool plx_is_param(Ctx *cx, const char *name, int len); /* ---------------------------------------------- dialect front ends (vtable) */ diff --git a/src/plx_transpile.c b/src/plx_transpile.c index 0a23018..262a5aa 100644 --- a/src/plx_transpile.c +++ b/src/plx_transpile.c @@ -26,6 +26,7 @@ #include "mb/pg_wchar.h" #include "miscadmin.h" #include "utils/builtins.h" +#include "utils/lsyscache.h" #include "plx.h" #include "plx_int.h" @@ -564,6 +565,23 @@ plx_is_param(Ctx *cx, const char *name, int len) return false; } +/* + * An extension object's name as generated code should spell it: qualified + * with the extension's schema, so a function with its own SET search_path + * (the usual hardening for SECURITY DEFINER) still finds it, and an object + * of the same name earlier on the caller's path cannot stand in for it. + */ +char * +plx_ext_object(const char *name) +{ + char *nsp = OidIsValid(plx_ext_namespace) + ? get_namespace_name(plx_ext_namespace) : NULL; + + if (!nsp) + return pstrdup(name); + return psprintf("%s.%s", quote_identifier(nsp), quote_identifier(name)); +} + PlxLocal2 * plx_local_find(Ctx *cx, const char *name, int len) { @@ -2298,13 +2316,14 @@ plx_emit_core(Ctx *cx, int a, int b, int ind, bool toplevel) l = plx_local_add(cx, t0->s, t0->len); if (l->is_const) plx_err(cx, t0->line, "cannot append to a constant"); - l->typ = pstrdup("plx_strbuild"); + l->typ = plx_ext_object("plx_strbuild"); rhs = plx_span_text(cx, app_rhs, rb); r = plx_rewrite_expr(cx, rhs, (int) strlen(rhs), false); plx_indent(o, ind); /* cast the appended value to text (int, numeric, etc. render) */ - appendStringInfo(o, "%.*s := plx_sb_append(%.*s, (%s)::text);\n", - t0->len, t0->s, t0->len, t0->s, r); + appendStringInfo(o, "%.*s := %s(%.*s, (%s)::text);\n", + t0->len, t0->s, plx_ext_object("plx_sb_append"), + t0->len, t0->s, r); return; } } diff --git a/test/expected/plx_strbuild.out b/test/expected/plx_strbuild.out index 9be2b82..25db252 100644 --- a/test/expected/plx_strbuild.out +++ b/test/expected/plx_strbuild.out @@ -96,17 +96,17 @@ SELECT acc_rb(5); -- the lowered plpgsql uses plx_strbuild + plx_sb_append SELECT regexp_replace(prosrc, '/\*plx-orig:[^*]*\*/\s*', '', 'g') FROM pg_proc WHERE proname = 'acc_rb'; - regexp_replace ---------------------------------------------- - /*plx:v1:plxruby:9849bb14*/ + - DECLARE + - s plx_strbuild := ''; + - BEGIN + - FOR i IN 1..n LOOP + - s := plx_sb_append(s, (i::text)::text);+ - END LOOP; + - RETURN s; + - END; + + regexp_replace +---------------------------------------------------- + /*plx:v1:plxruby:9849bb14*/ + + DECLARE + + s public.plx_strbuild := ''; + + BEGIN + + FOR i IN 1..n LOOP + + s := public.plx_sb_append(s, (i::text)::text);+ + END LOOP; + + RETURN s; + + END; + (1 row) @@ -222,3 +222,43 @@ SELECT acc_reset(4); Raa (1 row) +-- generated code names plx_strbuild/plx_sb_append with the extension's schema, +-- so a function with a hardened search_path still finds them ... +CREATE FUNCTION sb_hardened(n int) RETURNS text LANGUAGE plxruby + SECURITY DEFINER SET search_path = pg_catalog AS $$ + s = "" + for i in 1..n + s << "x" + end + return s +$$; +SELECT sb_hardened(3); + sb_hardened +------------- + xxx +(1 row) + +-- ... and an object of the same name earlier on the path cannot stand in +CREATE SCHEMA sb_evil; +CREATE FUNCTION sb_evil.plx_sb_append(plx_strbuild, text) RETURNS plx_strbuild + LANGUAGE sql AS $$ SELECT 'hijacked'::text::plx_strbuild $$; +CREATE FUNCTION sb_path(n int) RETURNS text LANGUAGE plxruby + SET search_path = sb_evil, public AS $$ + s = "" + for i in 1..n + s << "y" + end + return s +$$; +SELECT sb_path(3); + sb_path +--------- + yyy +(1 row) + +DROP SCHEMA sb_evil CASCADE; +-- the extension is not relocatable, since stored functions name its schema +CREATE SCHEMA sb_other; +ALTER EXTENSION plx SET SCHEMA sb_other; +ERROR: extension "plx" does not support SET SCHEMA +DROP SCHEMA sb_other; diff --git a/test/sql/plx_strbuild.sql b/test/sql/plx_strbuild.sql index 837499c..8ec4f2c 100644 --- a/test/sql/plx_strbuild.sql +++ b/test/sql/plx_strbuild.sql @@ -135,3 +135,35 @@ end return s $$; SELECT acc_reset(4); + +-- generated code names plx_strbuild/plx_sb_append with the extension's schema, +-- so a function with a hardened search_path still finds them ... +CREATE FUNCTION sb_hardened(n int) RETURNS text LANGUAGE plxruby + SECURITY DEFINER SET search_path = pg_catalog AS $$ + s = "" + for i in 1..n + s << "x" + end + return s +$$; +SELECT sb_hardened(3); + +-- ... and an object of the same name earlier on the path cannot stand in +CREATE SCHEMA sb_evil; +CREATE FUNCTION sb_evil.plx_sb_append(plx_strbuild, text) RETURNS plx_strbuild + LANGUAGE sql AS $$ SELECT 'hijacked'::text::plx_strbuild $$; +CREATE FUNCTION sb_path(n int) RETURNS text LANGUAGE plxruby + SET search_path = sb_evil, public AS $$ + s = "" + for i in 1..n + s << "y" + end + return s +$$; +SELECT sb_path(3); +DROP SCHEMA sb_evil CASCADE; + +-- the extension is not relocatable, since stored functions name its schema +CREATE SCHEMA sb_other; +ALTER EXTENSION plx SET SCHEMA sb_other; +DROP SCHEMA sb_other; From f77f4df67cb6a1eb4093f842e5d39ac1d6107d30 Mon Sep 17 00:00:00 2001 From: "Joshua D. Drake" Date: Tue, 10 Nov 2026 19:18:56 +0000 Subject: [PATCH 2/2] Docs: restoring into another plx schema, and updating straight away A function that uses a string append stores plx's schema name, so restoring it into a database where plx is in a different schema leaves it pointing at a schema that is not there. Say so in the README install section and the CHANGELOG, with the two ways out. The CHANGELOG upgrade note now says to run ALTER EXTENSION plx UPDATE straight after installing the build: until then the extension is still relocatable, and moving it breaks functions created with the new build. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 10 +++++++++- README.md | 10 ++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5db4986..a59fe25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,10 @@ version in `plx.control` (currently `1.0`). Extension version 2.1.0. Install the new build, then run `ALTER EXTENSION plx UPDATE` in each database. The update changes no SQL -objects; it records the extension as non-relocatable (below). +objects; it records the extension as non-relocatable (below). Run it straight +after installing the build: until it runs, the new build already writes plx's +schema into functions, but `ALTER EXTENSION plx SET SCHEMA` is still allowed and +would break them. ### Security @@ -31,6 +34,11 @@ objects; it records the extension as non-relocatable (below). `extension "plx" does not support SET SCHEMA`. Choose the schema at `CREATE EXTENSION plx SCHEMA ...` time. Existing functions keep the unqualified names they were generated with until their DDL is run again. + For the same reason, a function that uses a string append and is restored + into a database where plx is in a different schema (a `pg_dump -n` of one + schema, say) fails when it runs, with `schema "..." does not exist`: install + plx in the same schema in the target, or run the function's DDL again after + restoring. Functions without a string append are not affected. ## [2.0.1] - 2026-08-25 diff --git a/README.md b/README.md index 59a2762..4af2167 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,16 @@ Then, in a database: CREATE EXTENSION plx; ``` +plx cannot be moved to another schema once created, so choose the schema at +`CREATE EXTENSION plx SCHEMA ...` time. A function that appends to a string +(`s << x`, `$s .= x`, `s += x`, `STRING-APPEND`) stores plx's schema name in its +generated code. If such a function is restored into a database where plx is in +a different schema (from a `pg_dump -n` of one schema, say), it fails when it +runs with `schema "..." does not exist`. Install plx in the same schema in the +target database, or run the function's DDL again there; its original source +can be recovered from the function itself (see +[DEBUGGING.md](doc/DEBUGGING.md#recover-your-original-source)). + ## Documentation - Per-dialect chapters: [plxruby](doc/plxruby.md), [plxphp](doc/plxphp.md),