Skip to content

Commit cb57ba8

Browse files
gh-155247: Use raw string content for the extensions cache key
The key of the extensions cache was built with _PyUnicode_AsUTF8NoNUL(), so importing an extension module failed with UnicodeEncodeError if its path contained characters unencodable in UTF-8, e.g. surrogate escapes of an undecodable file name. The raw content of the strings is now used. The rejection of embedded null characters, which was a side effect of the UTF-8 encoding, is now explicit in the extension module loader. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 1d90627 commit cb57ba8

4 files changed

Lines changed: 84 additions & 31 deletions

File tree

Lib/test/test_import/__init__.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1262,6 +1262,34 @@ class Spec2:
12621262
origin = "a\x00b"
12631263
_imp.create_dynamic(Spec2())
12641264

1265+
@unittest.skipUnless(_testsinglephase is not None,
1266+
'requires _testsinglephase')
1267+
@unittest.skipUnless(os_helper.TESTFN_UNDECODABLE,
1268+
'requires undecodable file names')
1269+
def test_import_from_undecodable_path(self):
1270+
# gh-155247: the path of the extension module is not encodable
1271+
# in UTF-8.
1272+
origin = _testsinglephase.__file__
1273+
# The module is cached by its path, so restore it afterwards.
1274+
self.addCleanup(restore__testsinglephase)
1275+
with os_helper.temp_dir() as tempdir:
1276+
subdir = os.path.join(os.fsencode(tempdir),
1277+
os_helper.TESTFN_UNDECODABLE)
1278+
try:
1279+
os.mkdir(subdir)
1280+
except OSError:
1281+
self.skipTest('undecodable paths are not supported')
1282+
path = os.path.join(subdir, os.fsencode(os.path.basename(origin)))
1283+
shutil.copyfile(origin, path)
1284+
path = os.fsdecode(path)
1285+
spec = importlib.util.spec_from_file_location('_testsinglephase',
1286+
path)
1287+
module = importlib.util.module_from_spec(spec)
1288+
spec.loader.exec_module(module)
1289+
self.assertEqual(module.__name__, '_testsinglephase')
1290+
self.assertEqual(module.__file__, path)
1291+
_testinternalcapi.clear_extension('_testsinglephase', path)
1292+
12651293
def test_create_builtin(self):
12661294
class Spec:
12671295
pass
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix importing an extension module whose path contains characters unencodable
2+
in UTF-8, e.g. undecodable bytes of a file name. Previously it failed with
3+
:exc:`UnicodeEncodeError`, which made it impossible to build or run Python in
4+
a directory with such name.

Python/import.c

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1276,50 +1276,66 @@ del_extensions_cache_value(void *raw)
12761276
}
12771277
}
12781278

1279+
/* The key of the extensions cache: the raw content of two strings.
1280+
1281+
The UTF-8 encoding is not used, because the strings can contain lone
1282+
surrogates, e.g. a file name undecodable in the filesystem encoding. */
1283+
struct hashtable_key {
1284+
size_t size; /* the total size of the key */
1285+
unsigned char kind1;
1286+
unsigned char kind2;
1287+
Py_ssize_t len1;
1288+
/* followed by the raw content of both strings */
1289+
};
1290+
12791291
static void *
1280-
hashtable_key_from_2_strings(PyObject *str1, PyObject *str2, const char sep)
1292+
hashtable_key_from_2_strings(PyObject *str1, PyObject *str2)
12811293
{
1282-
const char *str1_data = _PyUnicode_AsUTF8NoNUL(str1);
1283-
const char *str2_data = _PyUnicode_AsUTF8NoNUL(str2);
1284-
if (str1_data == NULL || str2_data == NULL) {
1285-
return NULL;
1286-
}
1287-
Py_ssize_t str1_len = strlen(str1_data);
1288-
Py_ssize_t str2_len = strlen(str2_data);
1294+
Py_ssize_t len1 = PyUnicode_GET_LENGTH(str1);
1295+
Py_ssize_t len2 = PyUnicode_GET_LENGTH(str2);
1296+
int kind1 = PyUnicode_KIND(str1);
1297+
int kind2 = PyUnicode_KIND(str2);
1298+
size_t size1 = (size_t)len1 * kind1;
1299+
size_t size2 = (size_t)len2 * kind2;
12891300

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

12941304
// XXX Use a buffer if it's a temp value (every case but "set").
1295-
char *key = PyMem_RawMalloc(size);
1305+
struct hashtable_key *key = PyMem_RawMalloc(size);
12961306
if (key == NULL) {
12971307
PyErr_NoMemory();
12981308
return NULL;
12991309
}
13001310

1301-
memcpy(key, str1_data, str1_len);
1302-
key[str1_len] = sep;
1303-
memcpy(key + str1_len + 1, str2_data, str2_len);
1304-
key[size - 1] = '\0';
1305-
assert(strlen(key) == size - 1);
1311+
key->size = size;
1312+
key->kind1 = (unsigned char)kind1;
1313+
key->kind2 = (unsigned char)kind2;
1314+
key->len1 = len1;
1315+
char *data = (char *)(key + 1);
1316+
memcpy(data, PyUnicode_DATA(str1), size1);
1317+
memcpy(data + size1, PyUnicode_DATA(str2), size2);
13061318
return key;
13071319
}
13081320

13091321
static Py_uhash_t
1310-
hashtable_hash_str(const void *key)
1322+
hashtable_hash_key(const void *key)
13111323
{
1312-
return Py_HashBuffer(key, strlen((const char *)key));
1324+
return Py_HashBuffer(key, ((const struct hashtable_key *)key)->size);
13131325
}
13141326

13151327
static int
1316-
hashtable_compare_str(const void *key1, const void *key2)
1328+
hashtable_compare_key(const void *key1, const void *key2)
13171329
{
1318-
return strcmp((const char *)key1, (const char *)key2) == 0;
1330+
size_t size = ((const struct hashtable_key *)key1)->size;
1331+
if (size != ((const struct hashtable_key *)key2)->size) {
1332+
return 0;
1333+
}
1334+
return memcmp(key1, key2, size) == 0;
13191335
}
13201336

13211337
static void
1322-
hashtable_destroy_str(void *ptr)
1338+
hashtable_destroy_key(void *ptr)
13231339
{
13241340
PyMem_RawFree(ptr);
13251341
}
@@ -1359,16 +1375,15 @@ _find_cached_def(PyModuleDef *def)
13591375
}
13601376
#endif
13611377

1362-
#define HTSEP ':'
13631378

13641379
static int
13651380
_extensions_cache_init(void)
13661381
{
13671382
_Py_hashtable_allocator_t alloc = {PyMem_RawMalloc, PyMem_RawFree};
13681383
EXTENSIONS.hashtable = _Py_hashtable_new_full(
1369-
hashtable_hash_str,
1370-
hashtable_compare_str,
1371-
hashtable_destroy_str, // key
1384+
hashtable_hash_key,
1385+
hashtable_compare_key,
1386+
hashtable_destroy_key, // key
13721387
del_extensions_cache_value, // value
13731388
&alloc
13741389
);
@@ -1386,7 +1401,7 @@ _extensions_cache_find_unlocked(PyObject *path, PyObject *name,
13861401
if (EXTENSIONS.hashtable == NULL) {
13871402
return NULL;
13881403
}
1389-
void *key = hashtable_key_from_2_strings(path, name, HTSEP);
1404+
void *key = hashtable_key_from_2_strings(path, name);
13901405
if (key == NULL) {
13911406
return NULL;
13921407
}
@@ -1396,7 +1411,7 @@ _extensions_cache_find_unlocked(PyObject *path, PyObject *name,
13961411
*p_key = key;
13971412
}
13981413
else {
1399-
hashtable_destroy_str(key);
1414+
hashtable_destroy_key(key);
14001415
}
14011416
return entry;
14021417
}
@@ -1534,7 +1549,7 @@ _extensions_cache_set(PyObject *path, PyObject *name,
15341549
finally_oldvalue:
15351550
extensions_lock_release();
15361551
if (key != NULL) {
1537-
hashtable_destroy_str(key);
1552+
hashtable_destroy_key(key);
15381553
}
15391554

15401555
return value;
@@ -1578,7 +1593,6 @@ _extensions_cache_clear_all(void)
15781593
EXTENSIONS.hashtable = NULL;
15791594
}
15801595

1581-
#undef HTSEP
15821596

15831597

15841598
static bool

Python/importdl.c

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "pycore_moduleobject.h" // _PyModule_GetDefOrNull()
99
#include "pycore_pyerrors.h" // _PyErr_FormatFromCause()
1010
#include "pycore_runtime.h" // _Py_ID()
11+
#include "pycore_unicodeobject.h" // _PyUnicode_AsUTF8NoNUL()
1112

1213

1314
/***********************************/
@@ -117,7 +118,7 @@ _Py_ext_module_loader_info_init(struct _Py_ext_module_loader_info *p_info,
117118
return -1;
118119
}
119120

120-
info.newcontext = PyUnicode_AsUTF8(info.name);
121+
info.newcontext = _PyUnicode_AsUTF8NoNUL(info.name);
121122
if (info.newcontext == NULL) {
122123
_Py_ext_module_loader_info_clear(&info);
123124
return -1;
@@ -130,6 +131,12 @@ _Py_ext_module_loader_info_init(struct _Py_ext_module_loader_info *p_info,
130131
_Py_ext_module_loader_info_clear(&info);
131132
return -1;
132133
}
134+
if (PyUnicode_FindChar(filename, 0, 0,
135+
PyUnicode_GET_LENGTH(filename), 1) != -1) {
136+
PyErr_SetString(PyExc_ValueError, "embedded null character");
137+
_Py_ext_module_loader_info_clear(&info);
138+
return -1;
139+
}
133140
info.filename = Py_NewRef(filename);
134141

135142
#ifndef MS_WINDOWS

0 commit comments

Comments
 (0)