From c9c95203328990e2cf4f25b2ad4b9267d44cd23d Mon Sep 17 00:00:00 2001 From: "Chao Li (Evan)" Date: Mon, 9 Nov 2026 16:15:01 +0800 Subject: [PATCH v4 2/2] Reject relative paths consistently in extension_control_path The available-extension views accepted relative paths, while CREATE EXTENSION rejected them only when its search reached the relative element. Move the absolute-path check into get_extension_control_directories() so ordinary extension lookups validate the complete search path before using it. When skip_invalid_path is true, skip relative elements as well as empty ones, preserving unknown-language errors and allowing hints from the remaining directories. 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. Add TAP coverage for the view and CREATE EXTENSION, including a case where an earlier valid directory contains the requested extension, and for unknown-language errors and hints with relative path elements. 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 --- src/backend/commands/extension.c | 62 ++++++++++--------- .../t/001_extension_control_path.pl | 40 +++++++++--- 2 files changed, 63 insertions(+), 39 deletions(-) diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index 57bff97319a..f2d467dac32 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,8 +510,11 @@ is_extension_script_filename(const char *filename) /* * Return a list of directories declared in the extension_control_path GUC. - * If skip_invalid_path is true, ignore empty elements instead of reporting an - * error. This is useful when looking for extensions only to provide a hint. + * All returned directory paths are absolute and canonicalized. + * + * If skip_invalid_path is true, ignore empty or relative elements instead of + * reporting an error. This is useful when looking for extensions only to + * provide a hint. */ static List * get_extension_control_directories(bool skip_invalid_path) @@ -526,6 +528,7 @@ get_extension_control_directories(bool skip_invalid_path) get_share_path(my_exec_path, sharepath); system_dir = psprintf("%s/extension", sharepath); + canonicalize_path(system_dir); if (strlen(Extension_control_path) == 0) { @@ -563,7 +566,7 @@ get_extension_control_directories(bool skip_invalid_path) else { char *mangled; - ExtensionLocation *location = palloc_object(ExtensionLocation); + bool is_system; /* Copy the next path found on ecp */ piece = palloc(len + 1); @@ -573,22 +576,32 @@ get_extension_control_directories(bool skip_invalid_path) * 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)) + { + if (!skip_invalid_path) + ereport(ERROR, + errcode(ERRCODE_INVALID_NAME), + errmsg("component in parameter \"%s\" is not an absolute path", + "extension_control_path")); + 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 */ @@ -4238,12 +4251,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; @@ -4251,26 +4262,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 94f9ae961a6..4f62a3756c8 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 @@ -131,21 +131,43 @@ for my $path ( "error identifies the empty path element"); } -# 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 $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"); + } +} + +# Reject relative paths even if an earlier directory contains the extension. +my $relative_ext = 'test_relative_path'; +create_extension($relative_ext, $ext_dir); for my $sql ( - "create function test_invalid_language() returns int language $ext_name as 'x'", - "do language $ext_name 'x'") + 'select count(*) from pg_available_extensions', + "create extension $relative_ext") { 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 = '$ext_dir_canonicalized${sep}relative'; + $sql"); + is($code, 3, "relative path is rejected by $sql"); like($stderr, - qr/HINT: Use CREATE EXTENSION to load the language into the database\./, - "remaining directory provides the extension hint"); + qr/ERROR: component in parameter "extension_control_path" is not an absolute path/, + "error identifies the relative path element"); } # Test with an extension that does not exists -- 2.50.1 (Apple Git-155)