Invent start_proc parameters for PL/Tcl.

Define GUCs pltcl.start_proc and pltclu.start_proc.  When set to a
nonempty value at the time a new Tcl interpreter is created, the
parameterless pltcl or pltclu function named by the GUC is called to
allow user-controlled initialization to occur within the interpreter.
This is modeled on plv8's start_proc parameter, and also has much in
common with plperl's on_init feature.  It allows users to fully
replace the "modules" feature that was removed in commit 817f2a586.

Since an initializer function could subvert later Tcl code in nearly
arbitrary ways, mark both GUCs as SUSET for now.  It would be nice
to find a way to relax that someday; but the corresponding GUCs in
plperl are also SUSET, and there's not been much complaint.

Discussion: https://postgr.es/m/22067.1488046447@sss.pgh.pa.us
This commit is contained in:
Tom Lane 2017-03-07 12:40:44 -05:00
parent 03cf221934
commit 0d2b1f305d
5 changed files with 286 additions and 9 deletions

View File

@ -902,6 +902,80 @@ if {[catch { spi_exec $sql_command }]} {
</para>
</sect1>
<sect1 id="pltcl-config">
<title>PL/Tcl Configuration</title>
<para>
This section lists configuration parameters that
affect <application>PL/Tcl</>.
</para>
<variablelist>
<varlistentry id="guc-pltcl-start-proc" xreflabel="pltcl.start_proc">
<term>
<varname>pltcl.start_proc</varname> (<type>string</type>)
<indexterm>
<primary><varname>pltcl.start_proc</> configuration parameter</primary>
</indexterm>
</term>
<listitem>
<para>
This parameter, if set to a nonempty string, specifies the name
(possibly schema-qualified) of a parameterless PL/Tcl function that
is to be executed whenever a new Tcl interpreter is created for
PL/Tcl. Such a function can perform per-session initialization, such
as loading additional Tcl code. A new Tcl interpreter is created
when a PL/Tcl function is first executed in a database session, or
when an additional interpreter has to be created because a PL/Tcl
function is called by a new SQL role.
</para>
<para>
The referenced function must be written in the <literal>pltcl</>
language, and must not be marked <literal>SECURITY DEFINER</>.
(These restrictions ensure that it runs in the interpreter it's
supposed to initialize.) The current user must have permission to
call it, too.
</para>
<para>
If the function fails with an error it will abort the function call
that caused the new interpreter to be created and propagate out to
the calling query, causing the current transaction or subtransaction
to be aborted. Any actions already done within Tcl won't be undone;
however, that interpreter won't be used again. If the language is
used again the initialization will be attempted again within a fresh
Tcl interpreter.
</para>
<para>
Only superusers can change this setting. Although this setting
can be changed within a session, such changes will not affect Tcl
interpreters that have already been created.
</para>
</listitem>
</varlistentry>
<varlistentry id="guc-pltclu-start-proc" xreflabel="pltclu.start_proc">
<term>
<varname>pltclu.start_proc</varname> (<type>string</type>)
<indexterm>
<primary><varname>pltclu.start_proc</> configuration parameter</primary>
</indexterm>
</term>
<listitem>
<para>
This parameter is exactly like <varname>pltcl.start_proc</varname>,
except that it applies to PL/TclU. The referenced function must
be written in the <literal>pltclu</> language.
</para>
</listitem>
</varlistentry>
</variablelist>
</sect1>
<sect1 id="pltcl-procnames">
<title>Tcl Procedure Names</title>

View File

@ -28,7 +28,7 @@ DATA = pltcl.control pltcl--1.0.sql pltcl--unpackaged--1.0.sql \
pltclu.control pltclu--1.0.sql pltclu--unpackaged--1.0.sql
REGRESS_OPTS = --dbname=$(PL_TESTDB) --load-extension=pltcl
REGRESS = pltcl_setup pltcl_queries pltcl_unicode
REGRESS = pltcl_setup pltcl_queries pltcl_start_proc pltcl_unicode
# Tcl on win32 ships with import libraries only for Microsoft Visual C++,
# which are not compatible with mingw gcc. Therefore we need to build a

View File

@ -0,0 +1,31 @@
--
-- Test start_proc execution
--
SET pltcl.start_proc = 'no_such_function';
select tcl_int4add(1, 2);
ERROR: function no_such_function() does not exist
CONTEXT: processing pltcl.start_proc parameter
select tcl_int4add(1, 2);
ERROR: function no_such_function() does not exist
CONTEXT: processing pltcl.start_proc parameter
create function tcl_initialize() returns void as
$$ elog NOTICE "in tcl_initialize" $$ language pltcl SECURITY DEFINER;
SET pltcl.start_proc = 'public.tcl_initialize';
select tcl_int4add(1, 2); -- fail
ERROR: function "public.tcl_initialize" must not be SECURITY DEFINER
CONTEXT: processing pltcl.start_proc parameter
create or replace function tcl_initialize() returns void as
$$ elog NOTICE "in tcl_initialize" $$ language pltcl;
select tcl_int4add(1, 2);
NOTICE: in tcl_initialize
tcl_int4add
-------------
3
(1 row)
select tcl_int4add(1, 2);
tcl_int4add
-------------
3
(1 row)

View File

@ -15,6 +15,7 @@
#include "access/htup_details.h"
#include "access/xact.h"
#include "catalog/objectaccess.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_type.h"
#include "commands/event_trigger.h"
@ -25,11 +26,14 @@
#include "mb/pg_wchar.h"
#include "miscadmin.h"
#include "nodes/makefuncs.h"
#include "parser/parse_func.h"
#include "parser/parse_type.h"
#include "pgstat.h"
#include "tcop/tcopprot.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/regproc.h"
#include "utils/rel.h"
#include "utils/syscache.h"
#include "utils/typcache.h"
@ -226,6 +230,8 @@ typedef struct pltcl_call_state
/**********************************************************************
* Global data
**********************************************************************/
static char *pltcl_start_proc = NULL;
static char *pltclu_start_proc = NULL;
static bool pltcl_pm_init_done = false;
static Tcl_Interp *pltcl_hold_interp = NULL;
static HTAB *pltcl_interp_htab = NULL;
@ -253,8 +259,11 @@ static const TclExceptionNameMap exception_name_map[] = {
**********************************************************************/
void _PG_init(void);
static void pltcl_init_interp(pltcl_interp_desc *interp_desc, bool pltrusted);
static pltcl_interp_desc *pltcl_fetch_interp(bool pltrusted);
static void pltcl_init_interp(pltcl_interp_desc *interp_desc,
Oid prolang, bool pltrusted);
static pltcl_interp_desc *pltcl_fetch_interp(Oid prolang, bool pltrusted);
static void call_pltcl_start_proc(Oid prolang, bool pltrusted);
static void start_proc_error_callback(void *arg);
static Datum pltcl_handler(PG_FUNCTION_ARGS, bool pltrusted);
@ -441,6 +450,24 @@ _PG_init(void)
&hash_ctl,
HASH_ELEM | HASH_BLOBS);
/************************************************************
* Define PL/Tcl's custom GUCs
************************************************************/
DefineCustomStringVariable("pltcl.start_proc",
gettext_noop("PL/Tcl function to call once when pltcl is first used."),
NULL,
&pltcl_start_proc,
NULL,
PGC_SUSET, 0,
NULL, NULL, NULL);
DefineCustomStringVariable("pltclu.start_proc",
gettext_noop("PL/TclU function to call once when pltclu is first used."),
NULL,
&pltclu_start_proc,
NULL,
PGC_SUSET, 0,
NULL, NULL, NULL);
pltcl_pm_init_done = true;
}
@ -448,7 +475,7 @@ _PG_init(void)
* pltcl_init_interp() - initialize a new Tcl interpreter
**********************************************************************/
static void
pltcl_init_interp(pltcl_interp_desc *interp_desc, bool pltrusted)
pltcl_init_interp(pltcl_interp_desc *interp_desc, Oid prolang, bool pltrusted)
{
Tcl_Interp *interp;
char interpname[32];
@ -462,7 +489,6 @@ pltcl_init_interp(pltcl_interp_desc *interp_desc, bool pltrusted)
if ((interp = Tcl_CreateSlave(pltcl_hold_interp, interpname,
pltrusted ? 1 : 0)) == NULL)
elog(ERROR, "could not create slave Tcl interpreter");
interp_desc->interp = interp;
/************************************************************
* Initialize the query hash table associated with interpreter
@ -490,16 +516,35 @@ pltcl_init_interp(pltcl_interp_desc *interp_desc, bool pltrusted)
pltcl_SPI_execute_plan, NULL, NULL);
Tcl_CreateObjCommand(interp, "spi_lastoid",
pltcl_SPI_lastoid, NULL, NULL);
/************************************************************
* Call the appropriate start_proc, if there is one.
*
* We must set interp_desc->interp before the call, else the start_proc
* won't find the interpreter it's supposed to use. But, if the
* start_proc fails, we want to abandon use of the interpreter.
************************************************************/
PG_TRY();
{
interp_desc->interp = interp;
call_pltcl_start_proc(prolang, pltrusted);
}
PG_CATCH();
{
interp_desc->interp = NULL;
Tcl_DeleteInterp(interp);
PG_RE_THROW();
}
PG_END_TRY();
}
/**********************************************************************
* pltcl_fetch_interp() - fetch the Tcl interpreter to use for a function
*
* This also takes care of any on-first-use initialization required.
* Note: we assume caller has already connected to SPI.
**********************************************************************/
static pltcl_interp_desc *
pltcl_fetch_interp(bool pltrusted)
pltcl_fetch_interp(Oid prolang, bool pltrusted)
{
Oid user_id;
pltcl_interp_desc *interp_desc;
@ -515,12 +560,117 @@ pltcl_fetch_interp(bool pltrusted)
HASH_ENTER,
&found);
if (!found)
pltcl_init_interp(interp_desc, pltrusted);
interp_desc->interp = NULL;
/* If we haven't yet successfully made an interpreter, try to do that */
if (!interp_desc->interp)
pltcl_init_interp(interp_desc, prolang, pltrusted);
return interp_desc;
}
/**********************************************************************
* call_pltcl_start_proc() - Call user-defined initialization proc, if any
**********************************************************************/
static void
call_pltcl_start_proc(Oid prolang, bool pltrusted)
{
char *start_proc;
const char *gucname;
ErrorContextCallback errcallback;
List *namelist;
Oid fargtypes[1]; /* dummy */
Oid procOid;
HeapTuple procTup;
Form_pg_proc procStruct;
AclResult aclresult;
FmgrInfo finfo;
FunctionCallInfoData fcinfo;
PgStat_FunctionCallUsage fcusage;
/* select appropriate GUC */
start_proc = pltrusted ? pltcl_start_proc : pltclu_start_proc;
gucname = pltrusted ? "pltcl.start_proc" : "pltclu.start_proc";
/* Nothing to do if it's empty or unset */
if (start_proc == NULL || start_proc[0] == '\0')
return;
/* Set up errcontext callback to make errors more helpful */
errcallback.callback = start_proc_error_callback;
errcallback.arg = (void *) gucname;
errcallback.previous = error_context_stack;
error_context_stack = &errcallback;
/* Parse possibly-qualified identifier and look up the function */
namelist = stringToQualifiedNameList(start_proc);
procOid = LookupFuncName(namelist, 0, fargtypes, false);
/* Current user must have permission to call function */
aclresult = pg_proc_aclcheck(procOid, GetUserId(), ACL_EXECUTE);
if (aclresult != ACLCHECK_OK)
aclcheck_error(aclresult, ACL_KIND_PROC, start_proc);
/* Get the function's pg_proc entry */
procTup = SearchSysCache1(PROCOID, ObjectIdGetDatum(procOid));
if (!HeapTupleIsValid(procTup))
elog(ERROR, "cache lookup failed for function %u", procOid);
procStruct = (Form_pg_proc) GETSTRUCT(procTup);
/* It must be same language as the function we're currently calling */
if (procStruct->prolang != prolang)
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("function \"%s\" is in the wrong language",
start_proc)));
/*
* It must not be SECURITY DEFINER, either. This together with the
* language match check ensures that the function will execute in the same
* Tcl interpreter we just finished initializing.
*/
if (procStruct->prosecdef)
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("function \"%s\" must not be SECURITY DEFINER",
start_proc)));
/* A-OK */
ReleaseSysCache(procTup);
/*
* Call the function using the normal SQL function call mechanism. We
* could perhaps cheat and jump directly to pltcl_handler(), but it seems
* better to do it this way so that the call is exposed to, eg, call
* statistics collection.
*/
InvokeFunctionExecuteHook(procOid);
fmgr_info(procOid, &finfo);
InitFunctionCallInfoData(fcinfo, &finfo,
0,
InvalidOid, NULL, NULL);
pgstat_init_function_usage(&fcinfo, &fcusage);
(void) FunctionCallInvoke(&fcinfo);
pgstat_end_function_usage(&fcusage, true);
/* Pop the error context stack */
error_context_stack = errcallback.previous;
}
/*
* Error context callback for errors occurring during start_proc processing.
*/
static void
start_proc_error_callback(void *arg)
{
const char *gucname = (const char *) arg;
/* translator: %s is "pltcl.start_proc" or "pltclu.start_proc" */
errcontext("processing %s parameter", gucname);
}
/**********************************************************************
* pltcl_call_handler - This is the only visible function
* of the PL interpreter. The PostgreSQL
@ -1319,7 +1469,8 @@ compile_pltcl_function(Oid fn_oid, Oid tgreloid,
/************************************************************
* Identify the interpreter to use for the function
************************************************************/
prodesc->interp_desc = pltcl_fetch_interp(prodesc->lanpltrusted);
prodesc->interp_desc = pltcl_fetch_interp(procStruct->prolang,
prodesc->lanpltrusted);
interp = prodesc->interp_desc->interp;
/************************************************************

View File

@ -0,0 +1,21 @@
--
-- Test start_proc execution
--
SET pltcl.start_proc = 'no_such_function';
select tcl_int4add(1, 2);
select tcl_int4add(1, 2);
create function tcl_initialize() returns void as
$$ elog NOTICE "in tcl_initialize" $$ language pltcl SECURITY DEFINER;
SET pltcl.start_proc = 'public.tcl_initialize';
select tcl_int4add(1, 2); -- fail
create or replace function tcl_initialize() returns void as
$$ elog NOTICE "in tcl_initialize" $$ language pltcl;
select tcl_int4add(1, 2);
select tcl_int4add(1, 2);