diff --git a/CHANGELOG.md b/CHANGELOG.md index 1930387..a59fe25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,42 @@ 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). 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 + +- **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. + 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 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/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), 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;