public inbox for [email protected]
help / color / mirror / Atom feedBUG #19506: LOAD '$libdir/...' inside extension scripts ignores dynamic_library_path with extension_control_path
6+ messages / 4 participants
[nested] [flat]
* BUG #19506: LOAD '$libdir/...' inside extension scripts ignores dynamic_library_path with extension_control_path
@ 2026-06-03 22:21 PG Bug reporting form <[email protected]>
2026-06-05 04:27 ` Re: BUG #19506: LOAD '$libdir/...' inside extension scripts ignores dynamic_library_path with extension_control_path Ayush Tiwari <[email protected]>
2026-06-05 12:12 ` Re: BUG #19506: LOAD '$libdir/...' inside extension scripts ignores dynamic_library_path with extension_control_path Matheus Alcantara <[email protected]>
0 siblings, 2 replies; 6+ messages in thread
From: PG Bug reporting form @ 2026-06-03 22:21 UTC (permalink / raw)
To: [email protected]; +Cc: [email protected]
The following bug has been logged on the website:
Bug reference: 19506
Logged by: Gabriele Bartolini
Email address: [email protected]
PostgreSQL version: 18.4
Operating system: Linux (reproduced under CloudNativePG/Kubernetes)
Description:
When an extension is installed in a location reached via
`extension_control_path` / `dynamic_library_path` (rather than the
compiled-in package library directory), a LOAD '$libdir/foo' hardcoded
inside an extension's SQL script fails to find the library. PostGIS does
this in its upgrade scripts, so a PostGIS upgrade fails:
```
app=# SELECT postgis_extensions_upgrade();
NOTICE: Updating extension postgis 3.6.1
ERROR: could not access file "$libdir/postgis-3": No such file or directory
CONTEXT: SQL statement "LOAD '$libdir/postgis-3'"
extension script file "postgis--ANY--3.6.3.sql", near line 1530
```
This is a side effect of the fix for bug #18920 (commit f777d773878). Commit
4f7f7b03758 (`extension_control_path`) made the feature work by stripping
the '$libdir/' prefix so that dynamic_library_path is consulted. #18920 then
restricted that stripping to the function-load path so that a user-issued
`LOAD` keeps the literal '$libdir/' prefix. As a result, a `LOAD` inside an
extension script now also keeps the literal prefix, so
`dynamic_library_path` is never consulted, and the library cannot be found.
A `LOAD` running inside an extension script should behave like the
extension's function loads (strip '$libdir/'), while a LOAD issued directly
by a user should keep it (the #18920 behaviour). The two can be
distinguished by `creating_extension`.
Reproduced with the CloudNativePG operator on Kubernetes, but it applies to
any setup using `extension_control_path` / `dynamic_library_path`.
^ permalink raw reply [nested|flat] 6+ messages in thread
* Re: BUG #19506: LOAD '$libdir/...' inside extension scripts ignores dynamic_library_path with extension_control_path
2026-06-03 22:21 BUG #19506: LOAD '$libdir/...' inside extension scripts ignores dynamic_library_path with extension_control_path PG Bug reporting form <[email protected]>
@ 2026-06-05 04:27 ` Ayush Tiwari <[email protected]>
2026-06-05 04:34 ` Re: BUG #19506: LOAD '$libdir/...' inside extension scripts ignores dynamic_library_path with extension_control_path Gabriele Bartolini <[email protected]>
1 sibling, 1 reply; 6+ messages in thread
From: Ayush Tiwari @ 2026-06-05 04:27 UTC (permalink / raw)
To: [email protected]; [email protected]; Peter Eisentraut <[email protected]>
Hi,
On Thu, 4 Jun 2026 at 21:23, PG Bug reporting form <[email protected]>
wrote:
> The following bug has been logged on the website:
>
> Bug reference: 19506
> Logged by: Gabriele Bartolini
> Email address: [email protected]
> PostgreSQL version: 18.4
> Operating system: Linux (reproduced under CloudNativePG/Kubernetes)
> Description:
>
> When an extension is installed in a location reached via
> `extension_control_path` / `dynamic_library_path` (rather than the
> compiled-in package library directory), a LOAD '$libdir/foo' hardcoded
> inside an extension's SQL script fails to find the library. PostGIS does
> this in its upgrade scripts, so a PostGIS upgrade fails:
>
> ```
> app=# SELECT postgis_extensions_upgrade();
> NOTICE: Updating extension postgis 3.6.1
> ERROR: could not access file "$libdir/postgis-3": No such file or
> directory
> CONTEXT: SQL statement "LOAD '$libdir/postgis-3'"
> extension script file "postgis--ANY--3.6.3.sql", near line 1530
> ```
>
> This is a side effect of the fix for bug #18920 (commit f777d773878).
> Commit
> 4f7f7b03758 (`extension_control_path`) made the feature work by stripping
> the '$libdir/' prefix so that dynamic_library_path is consulted. #18920
> then
> restricted that stripping to the function-load path so that a user-issued
> `LOAD` keeps the literal '$libdir/' prefix. As a result, a `LOAD` inside an
> extension script now also keeps the literal prefix, so
> `dynamic_library_path` is never consulted, and the library cannot be found.
>
> A `LOAD` running inside an extension script should behave like the
> extension's function loads (strip '$libdir/'), while a LOAD issued directly
> by a user should keep it (the #18920 behaviour). The two can be
> distinguished by `creating_extension`.
>
> Reproduced with the CloudNativePG operator on Kubernetes, but it applies to
> any setup using `extension_control_path` / `dynamic_library_path`.
>
Thanks for the report and the very clear diagnosis, I could
reproduce the issue and your analysis matches what I see.
The attached patch implements exactly what you suggested: a
LOAD running while creating_extension is true strips the simple
"$libdir/" prefix (so dynamic_library_path is consulted, like
the extension's own function loads do), while a user-issued
LOAD keeps the literal prefix and therefore preserves the
#18920 behaviour.
To avoid duplicating the existing prefix-stripping logic in
load_external_function(), I factored it out into a small
static helper and reused it from load_file(). Nested paths
(e.g. "$libdir/foo/bar") are still left untouched and continue
to be expanded by expand_dynamic_library_name() as before.
Verified locally that the PostGIS-style reproducer now succeeds
when the extension is installed via extension_control_path /
dynamic_library_path, that a plain user-issued
LOAD '$libdir/foo' still behaves as on HEAD, and that the
existing extension regression suites still pass.
Regards,
Ayush
Attachments:
[application/octet-stream] v1-0001-dfmgr-let-extension-script-LOAD-use-dynamic_lib.patch (4.2K, 3-v1-0001-dfmgr-let-extension-script-LOAD-use-dynamic_lib.patch)
download | inline diff:
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Ayush Tiwari <[email protected]>
Date: Fri, 5 Jun 2026 12:00:00 +0530
Subject: [PATCH v1] dfmgr: let extension-script LOAD use dynamic_library_path
Commit f777d773878 (fix for bug #18920) restricted the simple "$libdir/"
prefix stripping to load_external_function() so that a user-issued
LOAD '$libdir/foo' would keep the literal "$libdir/" prefix and not
consult dynamic_library_path.
A side effect is that a LOAD '$libdir/foo' executed *inside* an
extension script also keeps the literal prefix, so a hardcoded
"$libdir/" library reference in an extension's SQL script can no
longer be resolved through dynamic_library_path. This breaks
extensions installed via extension_control_path /
dynamic_library_path (for example PostGIS, whose upgrade scripts do
LOAD '$libdir/postgis-3').
A LOAD running inside an extension script should behave like the
extension's function loads: strip the simple "$libdir/" prefix and
let dynamic_library_path be consulted. A LOAD issued directly by a
user should keep the literal prefix. The two are distinguished by
creating_extension.
Factor the existing prefix stripping into a small helper and use it
from load_external_function() unchanged and from load_file() only
while an extension script is being executed.
Reported-by: Gabriele Bartolini
Discussion: https://postgr.es/m/[email protected]
---
src/backend/utils/fmgr/dfmgr.c | 38 ++++++++++++++++++++++------------
1 file changed, 25 insertions(+), 13 deletions(-)
diff --git a/src/backend/utils/fmgr/dfmgr.c b/src/backend/utils/fmgr/dfmgr.c
index e636cc81cf8..14b6e5b5c4c 100644
--- a/src/backend/utils/fmgr/dfmgr.c
+++ b/src/backend/utils/fmgr/dfmgr.c
@@ -20,6 +20,7 @@
#include <dlfcn.h>
#endif /* !WIN32 */
+#include "commands/extension.h"
#include "fmgr.h"
#include "lib/stringinfo.h"
#include "miscadmin.h"
@@ -71,6 +72,7 @@ char *Dynamic_library_path;
static void *internal_load_library(const char *libname);
pg_noreturn static void incompatible_module_error(const char *libname,
const Pg_abi_values *module_magic_data);
+static const char *strip_libdir_prefix(const char *filename);
static char *expand_dynamic_library_name(const char *name);
static void check_restricted_library_name(const char *name);
@@ -99,20 +101,7 @@ load_external_function(const char *filename, const char *funcname,
void *lib_handle;
void *retval;
- /*
- * For extensions with hardcoded '$libdir/' library names, we strip the
- * prefix to allow the library search path to be used. This is done only
- * for simple names (e.g., "$libdir/foo"), not for nested paths (e.g.,
- * "$libdir/foo/bar").
- *
- * For nested paths, 'expand_dynamic_library_name' directly expands the
- * '$libdir' macro, so we leave them untouched.
- */
- if (strncmp(filename, "$libdir/", 8) == 0)
- {
- if (first_dir_separator(filename + 8) == NULL)
- filename += 8;
- }
+ filename = strip_libdir_prefix(filename);
/* Expand the possibly-abbreviated filename to an exact path name */
fullname = expand_dynamic_library_name(filename);
@@ -150,6 +139,9 @@ load_file(const char *filename, bool restricted)
{
char *fullname;
+ if (creating_extension)
+ filename = strip_libdir_prefix(filename);
+
/* Apply security restriction if requested */
if (restricted)
check_restricted_library_name(filename);
@@ -309,6 +301,24 @@ internal_load_library(const char *libname)
return file_scanner->handle;
}
+/*
+ * For extensions with hardcoded '$libdir/' library names, strip the prefix to
+ * allow the library search path to be used. This is done only for simple names
+ * (e.g., "$libdir/foo"), not for nested paths (e.g., "$libdir/foo/bar").
+ *
+ * For nested paths, expand_dynamic_library_name() directly expands the
+ * '$libdir' macro, so leave them untouched.
+ */
+static const char *
+strip_libdir_prefix(const char *filename)
+{
+ if (strncmp(filename, "$libdir/", 8) == 0 &&
+ first_dir_separator(filename + 8) == NULL)
+ filename += 8;
+
+ return filename;
+}
+
/*
* Report a suitable error for an incompatible magic block.
*/
--
2.43.0
base-commit: 7598b5383b16a562bd7e7732eda7d783b34f4615
^ permalink raw reply [nested|flat] 6+ messages in thread
* Re: BUG #19506: LOAD '$libdir/...' inside extension scripts ignores dynamic_library_path with extension_control_path
2026-06-03 22:21 BUG #19506: LOAD '$libdir/...' inside extension scripts ignores dynamic_library_path with extension_control_path PG Bug reporting form <[email protected]>
2026-06-05 04:27 ` Re: BUG #19506: LOAD '$libdir/...' inside extension scripts ignores dynamic_library_path with extension_control_path Ayush Tiwari <[email protected]>
@ 2026-06-05 04:34 ` Gabriele Bartolini <[email protected]>
2026-06-05 04:39 ` Re: BUG #19506: LOAD '$libdir/...' inside extension scripts ignores dynamic_library_path with extension_control_path Ayush Tiwari <[email protected]>
0 siblings, 1 reply; 6+ messages in thread
From: Gabriele Bartolini @ 2026-06-05 04:34 UTC (permalink / raw)
To: Ayush Tiwari <[email protected]>; +Cc: [email protected]; [email protected]; Peter Eisentraut <[email protected]>
Hi Ayush,
I was about to draft an email to hackers about this. I had already done a
patch: https://github.com/gbartolini/postgres/pull/1
Thanks for your reply. I will verify yours later.
Cheers,
Gabriele
On Fri, 5 Jun 2026 at 14:27, Ayush Tiwari <[email protected]>
wrote:
> Hi,
>
> On Thu, 4 Jun 2026 at 21:23, PG Bug reporting form <[email protected]>
> wrote:
>
>> The following bug has been logged on the website:
>>
>> Bug reference: 19506
>> Logged by: Gabriele Bartolini
>> Email address: [email protected]
>> PostgreSQL version: 18.4
>> Operating system: Linux (reproduced under CloudNativePG/Kubernetes)
>> Description:
>>
>> When an extension is installed in a location reached via
>> `extension_control_path` / `dynamic_library_path` (rather than the
>> compiled-in package library directory), a LOAD '$libdir/foo' hardcoded
>> inside an extension's SQL script fails to find the library. PostGIS does
>> this in its upgrade scripts, so a PostGIS upgrade fails:
>>
>> ```
>> app=# SELECT postgis_extensions_upgrade();
>> NOTICE: Updating extension postgis 3.6.1
>> ERROR: could not access file "$libdir/postgis-3": No such file or
>> directory
>> CONTEXT: SQL statement "LOAD '$libdir/postgis-3'"
>> extension script file "postgis--ANY--3.6.3.sql", near line 1530
>> ```
>>
>> This is a side effect of the fix for bug #18920 (commit f777d773878).
>> Commit
>> 4f7f7b03758 (`extension_control_path`) made the feature work by stripping
>> the '$libdir/' prefix so that dynamic_library_path is consulted. #18920
>> then
>> restricted that stripping to the function-load path so that a user-issued
>> `LOAD` keeps the literal '$libdir/' prefix. As a result, a `LOAD` inside
>> an
>> extension script now also keeps the literal prefix, so
>> `dynamic_library_path` is never consulted, and the library cannot be
>> found.
>>
>> A `LOAD` running inside an extension script should behave like the
>> extension's function loads (strip '$libdir/'), while a LOAD issued
>> directly
>> by a user should keep it (the #18920 behaviour). The two can be
>> distinguished by `creating_extension`.
>>
>> Reproduced with the CloudNativePG operator on Kubernetes, but it applies
>> to
>> any setup using `extension_control_path` / `dynamic_library_path`.
>>
>
> Thanks for the report and the very clear diagnosis, I could
> reproduce the issue and your analysis matches what I see.
>
> The attached patch implements exactly what you suggested: a
> LOAD running while creating_extension is true strips the simple
> "$libdir/" prefix (so dynamic_library_path is consulted, like
> the extension's own function loads do), while a user-issued
> LOAD keeps the literal prefix and therefore preserves the
> #18920 behaviour.
>
> To avoid duplicating the existing prefix-stripping logic in
> load_external_function(), I factored it out into a small
> static helper and reused it from load_file(). Nested paths
> (e.g. "$libdir/foo/bar") are still left untouched and continue
> to be expanded by expand_dynamic_library_name() as before.
>
> Verified locally that the PostGIS-style reproducer now succeeds
> when the extension is installed via extension_control_path /
> dynamic_library_path, that a plain user-issued
> LOAD '$libdir/foo' still behaves as on HEAD, and that the
> existing extension regression suites still pass.
>
> Regards,
> Ayush
>
--
Gabriele Bartolini
VP, Chief Architect, Kubernetes
enterprisedb.com / Melbourne, Australia
^ permalink raw reply [nested|flat] 6+ messages in thread
* Re: BUG #19506: LOAD '$libdir/...' inside extension scripts ignores dynamic_library_path with extension_control_path
2026-06-03 22:21 BUG #19506: LOAD '$libdir/...' inside extension scripts ignores dynamic_library_path with extension_control_path PG Bug reporting form <[email protected]>
2026-06-05 04:27 ` Re: BUG #19506: LOAD '$libdir/...' inside extension scripts ignores dynamic_library_path with extension_control_path Ayush Tiwari <[email protected]>
2026-06-05 04:34 ` Re: BUG #19506: LOAD '$libdir/...' inside extension scripts ignores dynamic_library_path with extension_control_path Gabriele Bartolini <[email protected]>
@ 2026-06-05 04:39 ` Ayush Tiwari <[email protected]>
0 siblings, 0 replies; 6+ messages in thread
From: Ayush Tiwari @ 2026-06-05 04:39 UTC (permalink / raw)
To: Gabriele Bartolini <[email protected]>; +Cc: [email protected]; [email protected]; Peter Eisentraut <[email protected]>
Hi,
On Fri, 5 Jun 2026 at 10:04, Gabriele Bartolini <
[email protected]> wrote:
> Hi Ayush,
>
> I was about to draft an email to hackers about this. I had already done a
> patch: https://github.com/gbartolini/postgres/pull/1
>
> Thanks for your reply. I will verify yours later.
>
> Cheers,
> Gabriele
>
Your patch looks good to me, we can go ahead with that.
I dont think this needs a dedicated tap test file though,
you can use the existing 001_extension_control_path.pl
Happy to review, once you post it.
Regards,
Ayush
^ permalink raw reply [nested|flat] 6+ messages in thread
* Re: BUG #19506: LOAD '$libdir/...' inside extension scripts ignores dynamic_library_path with extension_control_path
2026-06-03 22:21 BUG #19506: LOAD '$libdir/...' inside extension scripts ignores dynamic_library_path with extension_control_path PG Bug reporting form <[email protected]>
@ 2026-06-05 12:12 ` Matheus Alcantara <[email protected]>
2026-06-24 09:47 ` Re: BUG #19506: LOAD '$libdir/...' inside extension scripts ignores dynamic_library_path with extension_control_path Gabriele Bartolini <[email protected]>
1 sibling, 1 reply; 6+ messages in thread
From: Matheus Alcantara @ 2026-06-05 12:12 UTC (permalink / raw)
To: [email protected]; [email protected]; PG Bug reporting form <[email protected]>
On 03/06/26 19:21, PG Bug reporting form wrote:
> The following bug has been logged on the website:
>
> Bug reference: 19506
> Logged by: Gabriele Bartolini
> Email address: [email protected]
> PostgreSQL version: 18.4
> Operating system: Linux (reproduced under CloudNativePG/Kubernetes)
> Description:
>
> When an extension is installed in a location reached via
> `extension_control_path` / `dynamic_library_path` (rather than the
> compiled-in package library directory), a LOAD '$libdir/foo' hardcoded
> inside an extension's SQL script fails to find the library. PostGIS does
> this in its upgrade scripts, so a PostGIS upgrade fails:
>
> ```
> app=# SELECT postgis_extensions_upgrade();
> NOTICE: Updating extension postgis 3.6.1
> ERROR: could not access file "$libdir/postgis-3": No such file or directory
> CONTEXT: SQL statement "LOAD '$libdir/postgis-3'"
> extension script file "postgis--ANY--3.6.3.sql", near line 1530
> ```
>
> This is a side effect of the fix for bug #18920 (commit f777d773878). Commit
> 4f7f7b03758 (`extension_control_path`) made the feature work by stripping
> the '$libdir/' prefix so that dynamic_library_path is consulted. #18920 then
> restricted that stripping to the function-load path so that a user-issued
> `LOAD` keeps the literal '$libdir/' prefix. As a result, a `LOAD` inside an
> extension script now also keeps the literal prefix, so
> `dynamic_library_path` is never consulted, and the library cannot be found.
>
The only reason that it was decided that we should strip the $libdir
was that almost all popular extensions use $libdir prefix on
module_pathname on .control files, so the extension_control_path would
not work and waiting for extensions to change this will make the
extension_control_path almost useless (I hope that we can remove this
in the future once 18 is the minimum supported version).
I'm not sure if we also want this for the LOAD command, I'm wondering
if the postgis could remove the $libdir prefix from the LOAD command
instead of Postgres striping this, what do you think? IIRC a simple
LOAD 'postgis-3' on versions before 18 will still works.
I think that extensions should start to remove the $libdir prefix
since extension_control_path is going to the second release cycle, but
I agree that it seems more complicated than it actually is.
--
Matheus Alcantara
EDB: https://www.enterprisedb.com
^ permalink raw reply [nested|flat] 6+ messages in thread
* Re: BUG #19506: LOAD '$libdir/...' inside extension scripts ignores dynamic_library_path with extension_control_path
2026-06-03 22:21 BUG #19506: LOAD '$libdir/...' inside extension scripts ignores dynamic_library_path with extension_control_path PG Bug reporting form <[email protected]>
2026-06-05 12:12 ` Re: BUG #19506: LOAD '$libdir/...' inside extension scripts ignores dynamic_library_path with extension_control_path Matheus Alcantara <[email protected]>
@ 2026-06-24 09:47 ` Gabriele Bartolini <[email protected]>
0 siblings, 0 replies; 6+ messages in thread
From: Gabriele Bartolini @ 2026-06-24 09:47 UTC (permalink / raw)
To: Matheus Alcantara <[email protected]>; +Cc: [email protected]; [email protected]; PG Bug reporting form <[email protected]>
Hi everyone,
I realised I hadn't sent this email with the patch. Please find the
attached patch.
Thanks,
Gabriele
On Fri, 5 Jun 2026 at 22:12, Matheus Alcantara <[email protected]>
wrote:
> On 03/06/26 19:21, PG Bug reporting form wrote:
> > The following bug has been logged on the website:
> >
> > Bug reference: 19506
> > Logged by: Gabriele Bartolini
> > Email address: [email protected]
> > PostgreSQL version: 18.4
> > Operating system: Linux (reproduced under CloudNativePG/Kubernetes)
> > Description:
> >
> > When an extension is installed in a location reached via
> > `extension_control_path` / `dynamic_library_path` (rather than the
> > compiled-in package library directory), a LOAD '$libdir/foo' hardcoded
> > inside an extension's SQL script fails to find the library. PostGIS does
> > this in its upgrade scripts, so a PostGIS upgrade fails:
> >
> > ```
> > app=# SELECT postgis_extensions_upgrade();
> > NOTICE: Updating extension postgis 3.6.1
> > ERROR: could not access file "$libdir/postgis-3": No such file or
> directory
> > CONTEXT: SQL statement "LOAD '$libdir/postgis-3'"
> > extension script file "postgis--ANY--3.6.3.sql", near line 1530
> > ```
> >
> > This is a side effect of the fix for bug #18920 (commit f777d773878).
> Commit
> > 4f7f7b03758 (`extension_control_path`) made the feature work by stripping
> > the '$libdir/' prefix so that dynamic_library_path is consulted. #18920
> then
> > restricted that stripping to the function-load path so that a user-issued
> > `LOAD` keeps the literal '$libdir/' prefix. As a result, a `LOAD` inside
> an
> > extension script now also keeps the literal prefix, so
> > `dynamic_library_path` is never consulted, and the library cannot be
> found.
> >
>
> The only reason that it was decided that we should strip the $libdir
> was that almost all popular extensions use $libdir prefix on
> module_pathname on .control files, so the extension_control_path would
> not work and waiting for extensions to change this will make the
> extension_control_path almost useless (I hope that we can remove this
> in the future once 18 is the minimum supported version).
>
> I'm not sure if we also want this for the LOAD command, I'm wondering
> if the postgis could remove the $libdir prefix from the LOAD command
> instead of Postgres striping this, what do you think? IIRC a simple
> LOAD 'postgis-3' on versions before 18 will still works.
>
> I think that extensions should start to remove the $libdir prefix
> since extension_control_path is going to the second release cycle, but
> I agree that it seems more complicated than it actually is.
>
> --
> Matheus Alcantara
> EDB: https://www.enterprisedb.com
>
>
>
--
Gabriele Bartolini
VP, Chief Architect, Kubernetes
enterprisedb.com / Melbourne, Australia
Attachments:
[application/octet-stream] 0001-Strip-libdir-from-LOAD-inside-extension-scripts.patch (11.2K, 3-0001-Strip-libdir-from-LOAD-inside-extension-scripts.patch)
download | inline diff:
From 2bdddbb827e83f8e06f3f322349dd9301e135347 Mon Sep 17 00:00:00 2001
From: Gabriele Bartolini <[email protected]>
Date: Sat, 30 May 2026 00:17:17 +1000
Subject: [PATCH] Strip $libdir from LOAD inside extension scripts
Commit 4f7f7b03758 introduced the extension_control_path GUC, and to
make it work the $libdir/ prefix of an extension's module_pathname is
stripped so that the library search path (dynamic_library_path) is
consulted, allowing the extension's shared library to be found in a
custom location. Commit f777d773878 (bug #18920) later restricted that
stripping to the function-load path so that a LOAD command issued
directly by a user keeps the literal $libdir/ prefix.
However, some extensions hardcode a "LOAD '$libdir/foo'" in their own
SQL scripts (PostGIS does this in its upgrade scripts, for example).
Such a LOAD runs while creating_extension is set, and because the prefix
is no longer stripped on the LOAD path, the library search path is not
consulted and the extension fails to load when it lives in a directory
reached via extension_control_path / dynamic_library_path.
Strip the $libdir/ prefix in load_file() as well, but only while
creating_extension is true, so a LOAD inside an extension script behaves
like the extension's function loads. A LOAD issued directly by a user
is left untouched and keeps the bug #18920 behavior. The stripping is
factored out of load_external_function() into a new helper,
strip_libdir_prefix(), and is performed after the restricted-name
security check so that check still sees the original name.
Add a TAP test exercising both paths.
---
src/backend/utils/fmgr/dfmgr.c | 52 ++++++--
src/test/modules/test_extensions/Makefile | 3 +
src/test/modules/test_extensions/meson.build | 5 +
.../t/002_load_extension_control_path.pl | 111 ++++++++++++++++++
4 files changed, 159 insertions(+), 12 deletions(-)
create mode 100644 src/test/modules/test_extensions/t/002_load_extension_control_path.pl
diff --git a/src/backend/utils/fmgr/dfmgr.c b/src/backend/utils/fmgr/dfmgr.c
index e636cc81cf8..b4828bbf1a2 100644
--- a/src/backend/utils/fmgr/dfmgr.c
+++ b/src/backend/utils/fmgr/dfmgr.c
@@ -20,6 +20,7 @@
#include <dlfcn.h>
#endif /* !WIN32 */
+#include "commands/extension.h"
#include "fmgr.h"
#include "lib/stringinfo.h"
#include "miscadmin.h"
@@ -68,6 +69,7 @@ static DynamicFileList *file_tail = NULL;
char *Dynamic_library_path;
+static const char *strip_libdir_prefix(const char *filename);
static void *internal_load_library(const char *libname);
pg_noreturn static void incompatible_module_error(const char *libname,
const Pg_abi_values *module_magic_data);
@@ -78,6 +80,27 @@ static void check_restricted_library_name(const char *name);
static const Pg_abi_values magic_data = PG_MODULE_ABI_DATA;
+/*
+ * Strip a leading '$libdir/' prefix from a hardcoded library name so that the
+ * library search path (dynamic_library_path) is consulted instead of resolving
+ * '$libdir' straight to the package library directory. This is what allows
+ * extensions located via extension_control_path to be loaded.
+ *
+ * The stripping is done only for simple names (e.g., "$libdir/foo"), not for
+ * nested paths (e.g., "$libdir/foo/bar"). For nested paths,
+ * expand_dynamic_library_name() expands the '$libdir' macro directly, so we
+ * leave them untouched.
+ */
+static const char *
+strip_libdir_prefix(const char *filename)
+{
+ if (strncmp(filename, "$libdir/", 8) == 0 &&
+ first_dir_separator(filename + 8) == NULL)
+ filename += 8;
+
+ return filename;
+}
+
/*
* Load the specified dynamic-link library file, and look for a function
* named funcname in it.
@@ -100,19 +123,12 @@ load_external_function(const char *filename, const char *funcname,
void *retval;
/*
- * For extensions with hardcoded '$libdir/' library names, we strip the
- * prefix to allow the library search path to be used. This is done only
- * for simple names (e.g., "$libdir/foo"), not for nested paths (e.g.,
- * "$libdir/foo/bar").
- *
- * For nested paths, 'expand_dynamic_library_name' directly expands the
- * '$libdir' macro, so we leave them untouched.
+ * Extensions typically hardcode a '$libdir/' prefix in their library
+ * names (via MODULE_PATHNAME in their SQL scripts). Strip it so that the
+ * library search path can be used, which is what makes extensions found
+ * via extension_control_path work. See strip_libdir_prefix().
*/
- if (strncmp(filename, "$libdir/", 8) == 0)
- {
- if (first_dir_separator(filename + 8) == NULL)
- filename += 8;
- }
+ filename = strip_libdir_prefix(filename);
/* Expand the possibly-abbreviated filename to an exact path name */
fullname = expand_dynamic_library_name(filename);
@@ -154,6 +170,18 @@ load_file(const char *filename, bool restricted)
if (restricted)
check_restricted_library_name(filename);
+ /*
+ * When a LOAD comes from within an extension script (e.g., a hardcoded
+ * "LOAD '$libdir/foo'" in the script), strip the '$libdir/' prefix just as
+ * we do for an extension's functions, so that extensions found via
+ * extension_control_path can be loaded. A LOAD issued directly by a user
+ * is left untouched, so that an explicit '$libdir/' prefix keeps referring
+ * to the package library directory. The strip is done after the security
+ * check so that the latter still sees the original name.
+ */
+ if (creating_extension)
+ filename = strip_libdir_prefix(filename);
+
/* Expand the possibly-abbreviated filename to an exact path name */
fullname = expand_dynamic_library_name(filename);
diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile
index d1b0b81e5fd..4ee53d200c9 100644
--- a/src/test/modules/test_extensions/Makefile
+++ b/src/test/modules/test_extensions/Makefile
@@ -33,6 +33,9 @@ DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
REGRESS = test_extensions test_extdepend
TAP_TESTS = 1
+# required for 002_load_extension_control_path.pl
+export TEST_EXT_LIB = $(abs_top_builddir)/src/test/modules/test_extensions/test_ext$(DLSUFFIX)
+
# force C locale for output stability
NO_LOCALE = 1
diff --git a/src/test/modules/test_extensions/meson.build b/src/test/modules/test_extensions/meson.build
index 2c7cea189e2..cc755a1b0b8 100644
--- a/src/test/modules/test_extensions/meson.build
+++ b/src/test/modules/test_extensions/meson.build
@@ -71,8 +71,13 @@ tests += {
'regress_args': ['--no-locale'],
},
'tap': {
+ 'env': {
+ 'TEST_EXT_LIB': test_ext.full_path(),
+ },
'tests': [
't/001_extension_control_path.pl',
+ 't/002_load_extension_control_path.pl',
],
+ 'deps': [test_ext],
},
}
diff --git a/src/test/modules/test_extensions/t/002_load_extension_control_path.pl b/src/test/modules/test_extensions/t/002_load_extension_control_path.pl
new file mode 100644
index 00000000000..e0584be4c2a
--- /dev/null
+++ b/src/test/modules/test_extensions/t/002_load_extension_control_path.pl
@@ -0,0 +1,111 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test that a LOAD command with a hardcoded '$libdir/' prefix issued from
+# within an extension script honors the library search path, so that
+# extensions located via extension_control_path can be loaded. This mirrors
+# what e.g. PostGIS does in its upgrade scripts ("LOAD '$libdir/postgis-3'").
+#
+# A LOAD issued directly by a user must keep the literal '$libdir/' prefix and
+# is therefore not affected (see commit f777d773878 / bug #18920).
+
+use strict;
+use warnings FATAL => 'all';
+use File::Copy;
+use File::Path qw(mkpath);
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Make sure the test_ext shared library path is provided.
+my $ext_lib_so = $ENV{TEST_EXT_LIB}
+ or die "couldn't get the test_ext shared library path";
+
+my $ext_name = "test_ext";
+
+# Create the custom extension directory layout:
+# $ext_dir/extension/ -- .control and .sql files
+# $ext_dir/lib/ -- .so file
+my $ext_dir = PostgreSQL::Test::Utils::tempdir();
+mkpath("$ext_dir/extension");
+mkpath("$ext_dir/lib");
+my $ext_lib = "$ext_dir/lib";
+
+# Copy the .so file into the lib/ subdirectory.
+copy($ext_lib_so, $ext_lib)
+ or die "could not copy '$ext_lib_so' to '$ext_lib': $!";
+
+create_extension_files($ext_name, $ext_dir);
+
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init;
+
+# Use the correct separator and escape '\' when running on Windows.
+my $sep = $windows_os ? ";" : ":";
+my $ext_path = $windows_os ? ($ext_dir =~ s/\\/\\\\/gr) : $ext_dir;
+my $ext_lib_path = $windows_os ? ($ext_lib =~ s/\\/\\\\/gr) : $ext_lib;
+
+# Configure extension_control_path so the .control file is found in our
+# extension/ directory, and dynamic_library_path so the .so is found in lib/.
+$node->append_conf(
+ 'postgresql.conf', qq(
+extension_control_path = '\$system$sep$ext_path'
+dynamic_library_path = '\$libdir$sep$ext_lib_path'
+));
+
+$node->start;
+
+# CREATE EXTENSION runs the script, which contains a hardcoded
+# "LOAD '\$libdir/test_ext'". Before the fix this failed because the
+# '\$libdir/' prefix was not stripped for LOAD, so the library search path
+# (and thus the custom lib/ directory) was never consulted.
+$node->safe_psql('postgres', "CREATE EXTENSION $ext_name");
+
+# The function added by the extension exercises the function-load path, which
+# already stripped '$libdir/'.
+my ($code, $stdout, $stderr) = $node->psql('postgres', 'SELECT test_ext()');
+is($code, 0, 'extension function works');
+like($stderr, qr/NOTICE: running successful/, 'extension function loaded');
+
+# A LOAD issued directly by a user (outside any extension script) must keep
+# the literal '$libdir/' prefix, so it resolves to the package library
+# directory and does not find the library installed in our custom lib/ dir.
+($code, $stdout, $stderr) =
+ $node->psql('postgres', "LOAD '\$libdir/$ext_name'");
+isnt($code, 0, 'direct LOAD with $libdir prefix is not redirected to the path');
+like(
+ $stderr,
+ qr{could not access file "\$libdir/$ext_name"},
+ 'direct LOAD keeps the literal $libdir prefix');
+
+$node->stop;
+
+# Write .control and .sql files into $ext_dir/extension/.
+# The script uses a hardcoded "LOAD '$libdir/...'" and a '$libdir/' prefixed
+# module_pathname to reproduce what most extensions do by default.
+sub create_extension_files
+{
+ my ($ext_name, $ext_dir) = @_;
+
+ open my $cf, '>', "$ext_dir/extension/$ext_name.control"
+ or die "could not create control file: $!";
+ print $cf "comment = 'Test C extension for extension_control_path + LOAD'\n";
+ print $cf "default_version = '1.0'\n";
+ print $cf "module_pathname = '\$libdir/$ext_name'\n";
+ print $cf "relocatable = true\n";
+ close $cf;
+
+ open my $sqlf, '>', "$ext_dir/extension/$ext_name--1.0.sql"
+ or die "could not create SQL file: $!";
+ print $sqlf "/* $ext_name--1.0.sql */\n";
+ print $sqlf
+ "-- complain if script is sourced in psql, rather than via CREATE EXTENSION\n";
+ print $sqlf
+ qq'\\echo Use "CREATE EXTENSION $ext_name" to load this file. \\quit\n';
+ print $sqlf "LOAD '\$libdir/$ext_name';\n";
+ print $sqlf "CREATE FUNCTION test_ext()\n";
+ print $sqlf "RETURNS void AS 'MODULE_PATHNAME'\n";
+ print $sqlf "LANGUAGE C;\n";
+ close $sqlf;
+}
+
+done_testing();
--
2.54.0
^ permalink raw reply [nested|flat] 6+ messages in thread
end of thread, other threads:[~2026-06-24 09:47 UTC | newest]
Thread overview: 6+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-06-03 22:21 BUG #19506: LOAD '$libdir/...' inside extension scripts ignores dynamic_library_path with extension_control_path PG Bug reporting form <[email protected]>
2026-06-05 04:27 ` Ayush Tiwari <[email protected]>
2026-06-05 04:34 ` Gabriele Bartolini <[email protected]>
2026-06-05 04:39 ` Ayush Tiwari <[email protected]>
2026-06-05 12:12 ` Matheus Alcantara <[email protected]>
2026-06-24 09:47 ` Gabriele Bartolini <[email protected]>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox