Subject: BUG #19506: LOAD '$libdir/...' inside extension scripts ignores dynamic_library_path with extension_control_path

From: PG Bug reporting form <[email-masked]>
Date: Wed, 03 Jun 2026 22:21:28 +0000

The following bug has been logged on the website:

Bug reference:      19506
Logged by:          Gabriele Bartolini
Email address:      [email-masked]
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`.

---

From: Ayush Tiwari <[email-masked]>
Date: Fri, 5 Jun 2026 09:57:16 +0530

Hi,

> 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

---

From: Gabriele Bartolini <[email-masked]>
Date: Fri, 5 Jun 2026 14:34:27 +1000

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

-- 
Gabriele Bartolini
VP, Chief Architect, Kubernetes
enterprisedb.com / Melbourne, Australia

---

From: Ayush Tiwari <[email-masked]>
Date: Fri, 5 Jun 2026 10:09:55 +0530

Hi,

> Thanks for your reply. I will verify yours later.

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

---

From: Matheus Alcantara <[email-masked]>
Date: Fri, 5 Jun 2026 09:12:37 -0300

> `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

---

From: Gabriele Bartolini <[email-masked]>
Date: Wed, 24 Jun 2026 19:47:11 +1000

Hi everyone,

I realised I hadn't sent this email with the patch. Please find the
attached patch.

Thanks,
Gabriele

---

From: Gabriele Bartolini <[email-masked]>
Date: Tue, 7 Jul 2026 11:37:42 +1000

Hi Matheus, do you think we can apply this patch to the next Postgres minor
release?

---

From: "Matheus Alcantara" <[email-masked]>
Date: Tue, 07 Jul 2026 07:02:26 -0300

> Hi Matheus, do you think we can apply this patch to the next Postgres minor

Hi Gabriele, did you saw my reply on [1]?

Although we have this strip logic when reading module_pathname I'm not
sure if we should replicate this for the LOAD command.

I think that this should be fixed on postgis update scripts instead of
Postgres since the $libdir on module_pathname and LOAD commands is not
necessary anymore after 18 release. 

Also I think that we need more opinions on this from other hackers.

[1] https://www.postgresql.org/message-id/9ef850b9-d127-44db-9a6c-4db64436b056%40gmail.com