From b0ee23f95f94caad8310356f7d6f31bc3c109d4a Mon Sep 17 00:00:00 2001 From: "Chao Li (Evan)" Date: Mon, 9 Nov 2026 16:15:01 +0800 Subject: [PATCH v5 2/2] Ignore relative elements in extension_control_path The available-extension views could list extensions found through relative paths, while CREATE EXTENSION rejected those paths when its search reached them. Discard relative elements in get_extension_control_directories() so all callers search only absolute paths. Extensions found only through relative elements are neither listed nor loaded, while valid absolute elements remain usable. Canonicalize system_dir in get_extension_control_directories() so all returned paths are canonicalized. Make find_in_paths() static and remove its redundant path copying and canonicalization, retaining an assertion that paths are absolute. Document the behavior and add TAP coverage using an actual relative directory, including checks that absolute directories remain usable with relative elements before or after them. Reported-by: Surya Poondla Author: Chao Li Reviewed-by: Surya Poondla Reviewed-by: Matheus Alcantara Discussion: https://postgr.es/m/DC2D9FA5-45C4-494F-99FC-F9B95AD4FF55@gmail.com --- doc/src/sgml/config.sgml | 1 + src/backend/commands/extension.c | 51 ++++++-------- .../t/001_extension_control_path.pl | 69 ++++++++++++++++--- 3 files changed, 82 insertions(+), 39 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 0c0510d306c..d3319710b1d 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -11784,6 +11784,7 @@ extension_control_path = 'C:\tools\postgresql;H:\my_project\share;$system' '$system'. If the value is set to an empty string, the default '$system' is also assumed. Empty elements within a nonempty value are ignored. + Relative path elements are also ignored. diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index 6fdaf6aa963..05b3e486278 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -195,8 +195,7 @@ static void ExecAlterExtensionContentsRecurse(AlterExtensionContentsStmt *stmt, ObjectAddress object); static char *read_whole_file(const char *filename, int *length); static ExtensionControlFile *new_ExtensionControlFile(const char *extname); - -char *find_in_paths(const char *basename, List *paths); +static char *find_in_paths(const char *basename, List *paths); /* * Return the extension location. If the current user doesn't have sufficient @@ -511,7 +510,8 @@ is_extension_script_filename(const char *filename) /* * Return a list of directories declared in the extension_control_path GUC. - * Empty elements are ignored. + * All returned directory paths are absolute and canonicalized. + * Empty and relative elements are ignored. */ static List * get_extension_control_directories(void) @@ -525,6 +525,7 @@ get_extension_control_directories(void) get_share_path(my_exec_path, sharepath); system_dir = psprintf("%s/extension", sharepath); + canonicalize_path(system_dir); if (strlen(Extension_control_path) == 0) { @@ -554,7 +555,7 @@ get_extension_control_directories(void) if (len > 0) { char *mangled; - ExtensionLocation *location = palloc_object(ExtensionLocation); + bool is_system; /* Copy the next path found on ecp */ piece = palloc(len + 1); @@ -564,22 +565,25 @@ get_extension_control_directories(void) * Substitute the path macro if needed or append "extension" * suffix if it is a custom extension control path. */ - if (strcmp(piece, EXTENSION_SYSTEM_MACRO) == 0) - { - location->macro = pstrdup(piece); + is_system = strcmp(piece, EXTENSION_SYSTEM_MACRO) == 0; + if (is_system) mangled = substitute_path_macro(piece, EXTENSION_SYSTEM_MACRO, system_dir); - } else - { - location->macro = NULL; mangled = psprintf("%s/extension", piece); - } pfree(piece); /* Canonicalize the path based on the OS and add to the list */ canonicalize_path(mangled); - location->loc = mangled; - paths = lappend(paths, location); + if (!is_absolute_path(mangled)) + pfree(mangled); + else + { + ExtensionLocation *location = palloc_object(ExtensionLocation); + + location->macro = is_system ? pstrdup(EXTENSION_SYSTEM_MACRO) : NULL; + location->loc = mangled; + paths = lappend(paths, location); + } } /* Break if ecp is empty or move to the next path on ecp */ @@ -4229,12 +4233,10 @@ new_ExtensionControlFile(const char *extname) } /* - * Search for the basename in the list of paths. - * - * Similar to find_in_path but for simplicity does not support custom error - * messages and expects that paths already have all macros replaced. + * Search for the basename in the list of absolute, canonicalized paths + * returned by get_extension_control_directories(). */ -char * +static char * find_in_paths(const char *basename, List *paths) { ListCell *cell; @@ -4242,26 +4244,17 @@ find_in_paths(const char *basename, List *paths) foreach(cell, paths) { ExtensionLocation *location = lfirst(cell); - char *path = location->loc; + const char *path = location->loc; char *full; Assert(path != NULL); - - path = pstrdup(path); - canonicalize_path(path); - - /* only absolute paths */ - if (!is_absolute_path(path)) - ereport(ERROR, - errcode(ERRCODE_INVALID_NAME), - errmsg("component in parameter \"%s\" is not an absolute path", "extension_control_path")); + Assert(is_absolute_path(path)); full = psprintf("%s/%s", path, basename); if (pg_file_exists(full)) return full; - pfree(path); pfree(full); } diff --git a/src/test/modules/test_extensions/t/001_extension_control_path.pl b/src/test/modules/test_extensions/t/001_extension_control_path.pl index 5fb811b8e8d..4fc29b72678 100644 --- a/src/test/modules/test_extensions/t/001_extension_control_path.pl +++ b/src/test/modules/test_extensions/t/001_extension_control_path.pl @@ -135,21 +135,70 @@ $ret = $node->safe_psql( select count(*) from pg_available_extensions"); is($ret, '0', 'separator-only path lists no extensions'); -# Empty elements must not replace an unknown-language error or prevent a hint +# Invalid elements must not replace an unknown-language error or prevent a hint # from being found using the remaining directories. -for my $sql ( - "create function test_invalid_language() returns int language $ext_name as 'x'", - "do language $ext_name 'x'") +for my $path ( + "$sep$ext_dir_canonicalized$sep", + "relative$sep$ext_dir_canonicalized${sep}relative") +{ + for my $sql ( + "create function test_invalid_language() returns int language $ext_name as 'x'", + "do language $ext_name 'x'") + { + my ($code, $stdout, $stderr) = $node->psql( + 'postgres', + "set extension_control_path = '$path'; $sql"); + is($code, 3, "unknown language is rejected by $sql"); + like($stderr, qr/ERROR: language "\Q$ext_name\E" does not exist/, + "invalid path does not replace the unknown-language error"); + like($stderr, + qr/HINT: Use CREATE EXTENSION to load the language into the database\./, + "remaining directory provides the extension hint"); + } +} + +# An extension found only through a relative path must not be listed or loaded. +my $relative_dir = $node->data_dir . '/relative'; +mkpath("$relative_dir/extension"); +my $relative_ext = 'test_relative_path'; +create_extension($relative_ext, $relative_dir); +$ret = $node->safe_psql( + 'postgres', + "set extension_control_path = '\$system${sep}relative'; + select count(*) from pg_available_extensions where name = '$relative_ext'"); +is($ret, '0', 'extension in a relative directory is not listed'); + { my ($code, $stdout, $stderr) = $node->psql( 'postgres', - "set extension_control_path = '$sep$ext_dir_canonicalized$sep'; $sql"); - is($code, 3, "unknown language is rejected by $sql"); - like($stderr, qr/ERROR: language "\Q$ext_name\E" does not exist/, - "invalid path does not replace the unknown-language error"); + "set extension_control_path = '\$system${sep}relative'; + create extension $relative_ext"); + is($code, 3, 'extension in a relative directory cannot be loaded'); like($stderr, - qr/HINT: Use CREATE EXTENSION to load the language into the database\./, - "remaining directory provides the extension hint"); + qr/ERROR: extension "\Q$relative_ext\E" is not available/, + 'relative directory is excluded from the extension search'); +} + +# The same extension can be found and loaded through an absolute path, even +# with relative elements before or after that path. +create_extension($relative_ext, $ext_dir); +for my $path ( + "relative$sep$ext_dir_canonicalized", + "$ext_dir_canonicalized${sep}relative") +{ + $ret = $node->safe_psql( + 'postgres', + "set extension_control_path = '$path'; + select location from pg_available_extensions where name = '$relative_ext'"); + is($ret, "$ext_dir_canonicalized/extension", + "extension is listed from the absolute directory in path '$path'"); + $ret = $node->safe_psql( + 'postgres', + "set extension_control_path = '$path'; + create extension $relative_ext; + select count(*) from pg_extension where extname = '$relative_ext'; + drop extension $relative_ext"); + is($ret, '1', "extension can be loaded using path '$path'"); } # Test with an extension that does not exists -- 2.50.1 (Apple Git-155)