Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions Lib/test/test_import/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1262,6 +1262,37 @@ class Spec2:
origin = "a\x00b"
_imp.create_dynamic(Spec2())

@unittest.skipUnless(_testsinglephase is not None,
'requires _testsinglephase')
@unittest.skipUnless(os_helper.TESTFN_UNDECODABLE,
'requires undecodable file names')
def test_import_from_undecodable_path(self):
# gh-155247: the path of the extension module is not encodable
# in UTF-8.
origin = _testsinglephase.__file__
with os_helper.temp_dir() as tempdir:
subdir = os.path.join(os.fsencode(tempdir),
os_helper.TESTFN_UNDECODABLE)
try:
os.mkdir(subdir)
except OSError:
self.skipTest('undecodable paths are not supported')
path = os.path.join(subdir, os.fsencode(os.path.basename(origin)))
shutil.copyfile(origin, path)
# Import it in a subprocess: the extension module stays loaded,
# and on Windows its file cannot be removed.
script = textwrap.dedent(f"""
import importlib.util
path = {os.fsdecode(path)!a}
spec = importlib.util.spec_from_file_location(
'_testsinglephase', path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
assert module.__name__ == '_testsinglephase', module.__name__
assert module.__file__ == path, module.__file__
""")
script_helper.assert_python_ok('-c', script)

def test_create_builtin(self):
class Spec:
pass
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix importing an extension module whose path contains characters unencodable
in UTF-8, e.g. undecodable bytes of a file name. Previously it failed with
:exc:`UnicodeEncodeError`, which made it impossible to build or run Python in
a directory with such name.
76 changes: 46 additions & 30 deletions Python/import.c
Original file line number Diff line number Diff line change
Expand Up @@ -1276,50 +1276,68 @@ del_extensions_cache_value(void *raw)
}
}

/* The key of the extensions cache: the raw content of two strings.

The UTF-8 encoding is not used, because the strings can contain lone
surrogates, e.g. a file name undecodable in the filesystem encoding. */
struct hashtable_key {
size_t size; /* the total size of the key */
unsigned char kind1;
unsigned char kind2;
Py_ssize_t len1;
/* followed by the raw content of both strings */
};

static void *
hashtable_key_from_2_strings(PyObject *str1, PyObject *str2, const char sep)
hashtable_key_from_2_strings(PyObject *str1, PyObject *str2)
{
const char *str1_data = _PyUnicode_AsUTF8NoNUL(str1);
const char *str2_data = _PyUnicode_AsUTF8NoNUL(str2);
if (str1_data == NULL || str2_data == NULL) {
return NULL;
}
Py_ssize_t str1_len = strlen(str1_data);
Py_ssize_t str2_len = strlen(str2_data);
Py_ssize_t len1 = PyUnicode_GET_LENGTH(str1);
Py_ssize_t len2 = PyUnicode_GET_LENGTH(str2);
int kind1 = PyUnicode_KIND(str1);
int kind2 = PyUnicode_KIND(str2);
size_t size1 = (size_t)len1 * kind1;
size_t size2 = (size_t)len2 * kind2;

/* Make sure sep and the NULL byte won't cause an overflow. */
assert(SIZE_MAX - str1_len - str2_len > 2);
size_t size = str1_len + 1 + str2_len + 1;
assert(SIZE_MAX - sizeof(struct hashtable_key) - size1 > size2);
size_t size = sizeof(struct hashtable_key) + size1 + size2;

// XXX Use a buffer if it's a temp value (every case but "set").
char *key = PyMem_RawMalloc(size);
struct hashtable_key *key = PyMem_RawMalloc(size);
if (key == NULL) {
PyErr_NoMemory();
return NULL;
}

memcpy(key, str1_data, str1_len);
key[str1_len] = sep;
memcpy(key + str1_len + 1, str2_data, str2_len);
key[size - 1] = '\0';
assert(strlen(key) == size - 1);
/* Clear the padding: the key is hashed and compared as raw bytes. */
memset(key, 0, sizeof(struct hashtable_key));
key->size = size;
key->kind1 = (unsigned char)kind1;
key->kind2 = (unsigned char)kind2;
key->len1 = len1;
char *data = (char *)(key + 1);
memcpy(data, PyUnicode_DATA(str1), size1);
memcpy(data + size1, PyUnicode_DATA(str2), size2);
return key;
}

static Py_uhash_t
hashtable_hash_str(const void *key)
hashtable_hash_key(const void *key)
{
return Py_HashBuffer(key, strlen((const char *)key));
return Py_HashBuffer(key, ((const struct hashtable_key *)key)->size);
}

static int
hashtable_compare_str(const void *key1, const void *key2)
hashtable_compare_key(const void *key1, const void *key2)
{
return strcmp((const char *)key1, (const char *)key2) == 0;
size_t size = ((const struct hashtable_key *)key1)->size;
if (size != ((const struct hashtable_key *)key2)->size) {
return 0;
}
return memcmp(key1, key2, size) == 0;
}

static void
hashtable_destroy_str(void *ptr)
hashtable_destroy_key(void *ptr)
{
PyMem_RawFree(ptr);
}
Expand Down Expand Up @@ -1359,16 +1377,15 @@ _find_cached_def(PyModuleDef *def)
}
#endif

#define HTSEP ':'

static int
_extensions_cache_init(void)
{
_Py_hashtable_allocator_t alloc = {PyMem_RawMalloc, PyMem_RawFree};
EXTENSIONS.hashtable = _Py_hashtable_new_full(
hashtable_hash_str,
hashtable_compare_str,
hashtable_destroy_str, // key
hashtable_hash_key,
hashtable_compare_key,
hashtable_destroy_key, // key
del_extensions_cache_value, // value
&alloc
);
Expand All @@ -1386,7 +1403,7 @@ _extensions_cache_find_unlocked(PyObject *path, PyObject *name,
if (EXTENSIONS.hashtable == NULL) {
return NULL;
}
void *key = hashtable_key_from_2_strings(path, name, HTSEP);
void *key = hashtable_key_from_2_strings(path, name);
if (key == NULL) {
return NULL;
}
Expand All @@ -1396,7 +1413,7 @@ _extensions_cache_find_unlocked(PyObject *path, PyObject *name,
*p_key = key;
}
else {
hashtable_destroy_str(key);
hashtable_destroy_key(key);
}
return entry;
}
Expand Down Expand Up @@ -1534,7 +1551,7 @@ _extensions_cache_set(PyObject *path, PyObject *name,
finally_oldvalue:
extensions_lock_release();
if (key != NULL) {
hashtable_destroy_str(key);
hashtable_destroy_key(key);
}

return value;
Expand Down Expand Up @@ -1578,7 +1595,6 @@ _extensions_cache_clear_all(void)
EXTENSIONS.hashtable = NULL;
}

#undef HTSEP


static bool
Expand Down
9 changes: 8 additions & 1 deletion Python/importdl.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "pycore_moduleobject.h" // _PyModule_GetDefOrNull()
#include "pycore_pyerrors.h" // _PyErr_FormatFromCause()
#include "pycore_runtime.h" // _Py_ID()
#include "pycore_unicodeobject.h" // _PyUnicode_AsUTF8NoNUL()


/***********************************/
Expand Down Expand Up @@ -117,7 +118,7 @@ _Py_ext_module_loader_info_init(struct _Py_ext_module_loader_info *p_info,
return -1;
}

info.newcontext = PyUnicode_AsUTF8(info.name);
info.newcontext = _PyUnicode_AsUTF8NoNUL(info.name);
if (info.newcontext == NULL) {
_Py_ext_module_loader_info_clear(&info);
return -1;
Expand All @@ -130,6 +131,12 @@ _Py_ext_module_loader_info_init(struct _Py_ext_module_loader_info *p_info,
_Py_ext_module_loader_info_clear(&info);
return -1;
}
if (PyUnicode_FindChar(filename, 0, 0,
PyUnicode_GET_LENGTH(filename), 1) != -1) {
PyErr_SetString(PyExc_ValueError, "embedded null character");
_Py_ext_module_loader_info_clear(&info);
return -1;
}
info.filename = Py_NewRef(filename);

#ifndef MS_WINDOWS
Expand Down
Loading