Skip to content

Commit 0ddeaba

Browse files
committed
Avoid dump/reload problems when using both plpython2 and plpython3.
Commit 8037160 installed a safeguard against loading plpython2 and plpython3 at the same time, but asserted that both could still be used in the same database, just not in the same session. However, that's not actually all that practical because dumping and reloading will fail (since both libraries necessarily get loaded into the restoring session). pg_upgrade is even worse, because it checks for missing libraries by loading every .so library mentioned in the entire installation into one session, so that you can have only one across the whole cluster. We can improve matters by not throwing the error immediately in _PG_init, but only when and if we're asked to do something that requires calling into libpython. This ameliorates both of the above situations, since while execution of CREATE LANGUAGE, CREATE FUNCTION, etc will result in loading plpython, it isn't asked to do anything interesting (at least not if check_function_bodies is off, as it will be during a restore). It's possible that this opens some corner-case holes in which a crash could be provoked with sufficient effort. However, since plpython only exists as an untrusted language, any such crash would require superuser privileges, making it "don't do that" not a security issue. To reduce the hazards in this area, the error is still FATAL when it does get thrown. Per a report from Paul Jones. Back-patch to 9.2, which is as far back as the patch applies without work. (It could be made to work in 9.1, but given the lack of previous complaints, I'm disinclined to expend effort so far back. We've been pretty desultory about support for Python 3 in 9.1 anyway.)
1 parent 8b5cc3e commit 0ddeaba

File tree

1 file changed

+71
-10
lines changed

1 file changed

+71
-10
lines changed

src/pl/plpython/plpy_main.c

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ static void PLy_init_interp(void);
7272
static PLyExecutionContext *PLy_push_execution_context(void);
7373
static void PLy_pop_execution_context(void);
7474

75+
/* static state for Python library conflict detection */
76+
static int *plpython_version_bitmask_ptr = NULL;
77+
static int plpython_version_bitmask = 0;
7578
static const int plpython_python_version = PY_MAJOR_VERSION;
7679

7780
/* initialize global variables */
@@ -84,28 +87,81 @@ static PLyExecutionContext *PLy_execution_contexts = NULL;
8487
void
8588
_PG_init(void)
8689
{
87-
/* Be sure we do initialization only once (should be redundant now) */
88-
static bool inited = false;
90+
int **bitmask_ptr;
8991
const int **version_ptr;
9092

91-
if (inited)
92-
return;
93+
/*
94+
* Set up a shared bitmask variable telling which Python version(s) are
95+
* loaded into this process's address space. If there's more than one, we
96+
* cannot call into libpython for fear of causing crashes. But postpone
97+
* the actual failure for later, so that operations like pg_restore can
98+
* load more than one plpython library so long as they don't try to do
99+
* anything much with the language.
100+
*/
101+
bitmask_ptr = (int **) find_rendezvous_variable("plpython_version_bitmask");
102+
if (!(*bitmask_ptr)) /* am I the first? */
103+
*bitmask_ptr = &plpython_version_bitmask;
104+
/* Retain pointer to the agreed-on shared variable ... */
105+
plpython_version_bitmask_ptr = *bitmask_ptr;
106+
/* ... and announce my presence */
107+
*plpython_version_bitmask_ptr |= (1 << PY_MAJOR_VERSION);
93108

94-
/* Be sure we don't run Python 2 and 3 in the same session (might crash) */
109+
/*
110+
* This should be safe even in the presence of conflicting plpythons, and
111+
* it's necessary to do it here for the next error to be localized.
112+
*/
113+
pg_bindtextdomain(TEXTDOMAIN);
114+
115+
/*
116+
* We used to have a scheme whereby PL/Python would fail immediately if
117+
* loaded into a session in which a conflicting libpython is already
118+
* present. We don't like to do that anymore, but it seems possible that
119+
* a plpython library adhering to the old convention is present in the
120+
* session, in which case we have to fail. We detect an old library if
121+
* plpython_python_version is already defined but the indicated version
122+
* isn't reflected in plpython_version_bitmask. Otherwise, set the
123+
* variable so that the right thing happens if an old library is loaded
124+
* later.
125+
*/
95126
version_ptr = (const int **) find_rendezvous_variable("plpython_python_version");
96127
if (!(*version_ptr))
97128
*version_ptr = &plpython_python_version;
98129
else
99130
{
100-
if (**version_ptr != plpython_python_version)
131+
if ((*plpython_version_bitmask_ptr & (1 << **version_ptr)) == 0)
101132
ereport(FATAL,
102133
(errmsg("Python major version mismatch in session"),
103134
errdetail("This session has previously used Python major version %d, and it is now attempting to use Python major version %d.",
104135
**version_ptr, plpython_python_version),
105136
errhint("Start a new session to use a different Python major version.")));
106137
}
138+
}
107139

108-
pg_bindtextdomain(TEXTDOMAIN);
140+
/*
141+
* Perform one-time setup of PL/Python, after checking for a conflict
142+
* with other versions of Python.
143+
*/
144+
static void
145+
PLy_initialize(void)
146+
{
147+
static bool inited = false;
148+
149+
/*
150+
* Check for multiple Python libraries before actively doing anything with
151+
* libpython. This must be repeated on each entry to PL/Python, in case a
152+
* conflicting library got loaded since we last looked.
153+
*
154+
* It is attractive to weaken this error from FATAL to ERROR, but there
155+
* would be corner cases, so it seems best to be conservative.
156+
*/
157+
if (*plpython_version_bitmask_ptr != (1 << PY_MAJOR_VERSION))
158+
ereport(FATAL,
159+
(errmsg("multiple Python libraries are present in session"),
160+
errdetail("Only one Python major version can be used in one session.")));
161+
162+
/* The rest should only be done once per session */
163+
if (inited)
164+
return;
109165

110166
#if PY_MAJOR_VERSION >= 3
111167
PyImport_AppendInittab("plpy", PyInit_plpy);
@@ -129,7 +185,7 @@ _PG_init(void)
129185
}
130186

131187
/*
132-
* This should only be called once from _PG_init. Initialize the Python
188+
* This should be called only once, from PLy_initialize. Initialize the Python
133189
* interpreter and global data.
134190
*/
135191
static void
@@ -164,9 +220,10 @@ plpython_validator(PG_FUNCTION_ARGS)
164220
PG_RETURN_VOID();
165221

166222
if (!check_function_bodies)
167-
{
168223
PG_RETURN_VOID();
169-
}
224+
225+
/* Do this only after making sure we need to do something */
226+
PLy_initialize();
170227

171228
/* Get the new function's pg_proc entry */
172229
tuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcoid));
@@ -200,6 +257,8 @@ plpython_call_handler(PG_FUNCTION_ARGS)
200257
PLyExecutionContext *exec_ctx;
201258
ErrorContextCallback plerrcontext;
202259

260+
PLy_initialize();
261+
203262
/* Note: SPI_finish() happens in plpy_exec.c, which is dubious design */
204263
if (SPI_connect() != SPI_OK_CONNECT)
205264
elog(ERROR, "SPI_connect failed");
@@ -275,6 +334,8 @@ plpython_inline_handler(PG_FUNCTION_ARGS)
275334
PLyExecutionContext *exec_ctx;
276335
ErrorContextCallback plerrcontext;
277336

337+
PLy_initialize();
338+
278339
/* Note: SPI_finish() happens in plpy_exec.c, which is dubious design */
279340
if (SPI_connect() != SPI_OK_CONNECT)
280341
elog(ERROR, "SPI_connect failed");

0 commit comments

Comments
 (0)