From d9eae7649c8f0550d26a3ef46c704cd30d72eac7 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 3 Nov 2026 18:25:48 +0100 Subject: [PATCH 1/6] gh-160962: Fix PyInitConfig_SetStr() with NULL value Previously, passing NULL to an optional string option crashed. Add tests on PyInitConfig_GetInt(), PyInitConfig_GetStr() and PyInitConfig_GetStrList() functions. --- ...-11-03-18-27-42.gh-issue-160962.06816I.rst | 2 + Programs/_testembed.c | 37 +++++++++++++++++++ Python/initconfig.c | 13 +++++-- 3 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/C_API/2026-11-03-18-27-42.gh-issue-160962.06816I.rst diff --git a/Misc/NEWS.d/next/C_API/2026-11-03-18-27-42.gh-issue-160962.06816I.rst b/Misc/NEWS.d/next/C_API/2026-11-03-18-27-42.gh-issue-160962.06816I.rst new file mode 100644 index 000000000000000..3b043a8d495f1c6 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2026-11-03-18-27-42.gh-issue-160962.06816I.rst @@ -0,0 +1,2 @@ +Fix :c:func:`PyInitConfig_SetStr` when setting an optional string option to +``NULL``. Previously, the function crashed. Patch by Victor Stinner. diff --git a/Programs/_testembed.c b/Programs/_testembed.c index 08bbcd4cfec1543..e5d36866da89a40 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -1720,10 +1720,18 @@ static int test_initconfig_api(void) goto error; } + // Set an integer option if (PyInitConfig_SetInt(config, "hash_seed", 10) < 0) { goto error; } + // Get an integer option + int64_t value; + if (PyInitConfig_GetInt(config, "hash_seed", &value) < 0) { + goto error; + } + assert(value == 10); + if (PyInitConfig_SetInt(config, "perf_profiling", 2) < 0) { goto error; } @@ -1733,6 +1741,14 @@ static int test_initconfig_api(void) goto error; } + // Get a non-empty string + char *str; + if (PyInitConfig_GetStr(config, "program_name", &str) < 0) { + goto error; + } + assert(strcmp(str, PROGRAM_NAME_UTF8) == 0); + free(str); + // Set a UTF-8 string (pycache_prefix) if (PyInitConfig_SetStr(config, "pycache_prefix", "conf_pycache_prefix") < 0) { @@ -1746,6 +1762,27 @@ static int test_initconfig_api(void) goto error; } + // Get a string list + char **items; + size_t length; + if (PyInitConfig_GetStrList(config, "xoptions", &length, &items) < 0) { + goto error; + } + assert(length == 1); + assert(strcmp(items[0], "faulthandler") == 0); + PyInitConfig_FreeStrList(length, items); + + // Get an optional string (which is NULL). + if (PyInitConfig_GetStr(config, "prefix", &str) < 0) { + goto error; + } + assert(str == NULL); + + // Set an optional string to NULL explicitly (must not crash). + if (PyInitConfig_SetStr(config, "prefix", NULL) < 0) { + goto error; + } + if (Py_InitializeFromInitConfig(config) < 0) { goto error; } diff --git a/Python/initconfig.c b/Python/initconfig.c index 225ee0a3fe5ff2a..a768fdf0ca61c4d 100644 --- a/Python/initconfig.c +++ b/Python/initconfig.c @@ -4372,10 +4372,15 @@ PyInitConfig_SetStr(PyInitConfig *config, const char *name, const char* value) } wchar_t **member = raw_member; - - wchar_t *wstr = utf8_to_wstr(config, value); - if (wstr == NULL) { - return -1; + wchar_t *wstr; + if (value != NULL) { + wstr = utf8_to_wstr(config, value); + if (wstr == NULL) { + return -1; + } + } + else { + wstr = NULL; } free(*member); *member = wstr; From 3893f2568a7b0b49854b2595c7e27787b0c81499 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 4 Nov 2026 00:26:17 +0100 Subject: [PATCH 2/6] Remove redundant get tests Instead, add some tests to test_initconfig_get_api(). --- Programs/_testembed.c | 38 ++++++-------------------------------- 1 file changed, 6 insertions(+), 32 deletions(-) diff --git a/Programs/_testembed.c b/Programs/_testembed.c index e5d36866da89a40..00b01adfd38e8f3 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -1725,13 +1725,6 @@ static int test_initconfig_api(void) goto error; } - // Get an integer option - int64_t value; - if (PyInitConfig_GetInt(config, "hash_seed", &value) < 0) { - goto error; - } - assert(value == 10); - if (PyInitConfig_SetInt(config, "perf_profiling", 2) < 0) { goto error; } @@ -1741,14 +1734,6 @@ static int test_initconfig_api(void) goto error; } - // Get a non-empty string - char *str; - if (PyInitConfig_GetStr(config, "program_name", &str) < 0) { - goto error; - } - assert(strcmp(str, PROGRAM_NAME_UTF8) == 0); - free(str); - // Set a UTF-8 string (pycache_prefix) if (PyInitConfig_SetStr(config, "pycache_prefix", "conf_pycache_prefix") < 0) { @@ -1762,22 +1747,6 @@ static int test_initconfig_api(void) goto error; } - // Get a string list - char **items; - size_t length; - if (PyInitConfig_GetStrList(config, "xoptions", &length, &items) < 0) { - goto error; - } - assert(length == 1); - assert(strcmp(items[0], "faulthandler") == 0); - PyInitConfig_FreeStrList(length, items); - - // Get an optional string (which is NULL). - if (PyInitConfig_GetStr(config, "prefix", &str) < 0) { - goto error; - } - assert(str == NULL); - // Set an optional string to NULL explicitly (must not crash). if (PyInitConfig_SetStr(config, "prefix", NULL) < 0) { goto error; @@ -1815,6 +1784,7 @@ static int test_initconfig_get_api(void) assert(PyInitConfig_HasOption(config, "verbose") == 1); assert(PyInitConfig_HasOption(config, "utf8_mode") == 1); assert(PyInitConfig_HasOption(config, "non-existent") == 0); + assert(PyInitConfig_HasOption(config, "") == 0); // test PyInitConfig_GetInt() assert(initconfig_getint(config, "dev_mode") == 0); @@ -1826,7 +1796,7 @@ static int test_initconfig_get_api(void) assert(PyInitConfig_SetInt(config, "utf8_mode", 0) == 0); assert(initconfig_getint(config, "utf8_mode") == 0); - // test PyInitConfig_GetStr() + // test PyInitConfig_GetStr() on an optional string (can be NULL) char *str; assert(PyInitConfig_GetStr(config, "program_name", &str) == 0); assert(str == NULL); @@ -1852,8 +1822,11 @@ static int test_initconfig_get_api(void) // Setting hash_seed sets use_hash_seed assert(initconfig_getint(config, "use_hash_seed") == 0); + assert(initconfig_getint(config, "hash_seed") == 0); + assert(PyInitConfig_SetInt(config, "hash_seed", 123) == 0); assert(initconfig_getint(config, "use_hash_seed") == 1); + assert(initconfig_getint(config, "hash_seed") == 123); // Setting module_search_paths sets module_search_paths_set assert(initconfig_getint(config, "module_search_paths_set") == 0); @@ -1862,6 +1835,7 @@ static int test_initconfig_get_api(void) Py_ARRAY_LENGTH(paths), paths) == 0); assert(initconfig_getint(config, "module_search_paths_set") == 1); + PyInitConfig_Free(config); return 0; } From 210148898f170a7c686880482f77d506dd75e409 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 4 Nov 2026 00:36:55 +0100 Subject: [PATCH 3/6] Mention NULL in SetStr() documentation --- Doc/c-api/init_config.rst | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/Doc/c-api/init_config.rst b/Doc/c-api/init_config.rst index ad3571262168003..058664505ebf25e 100644 --- a/Doc/c-api/init_config.rst +++ b/Doc/c-api/init_config.rst @@ -204,6 +204,9 @@ only implemented when ``Py_InitializeFromInitConfig()`` is called, not by the Set a string configuration option from a null-terminated UTF-8 encoded string. The string is copied. + Options documented with the type ``str | None`` in the :ref:`Configuration + Options ` table can be set to ``NULL``. + * Return ``0`` on success. * Set an error in *config* and return ``-1`` on error. @@ -275,15 +278,15 @@ Configuration Options - Public * - ``"base_exec_prefix"`` - :c:member:`base_exec_prefix ` - - ``str`` + - ``str | None`` - Public * - ``"base_executable"`` - :c:member:`base_executable ` - - ``str`` + - ``str | None`` - Public * - ``"base_prefix"`` - :c:member:`base_prefix ` - - ``str`` + - ``str | None`` - Public * - ``"buffered_stdio"`` - :c:member:`buffered_stdio ` @@ -331,15 +334,15 @@ Configuration Options - Read-only * - ``"dump_refs_file"`` - :c:member:`dump_refs_file ` - - ``str`` + - ``str | None`` - Read-only * - ``"exec_prefix"`` - :c:member:`exec_prefix ` - - ``str`` + - ``str | None`` - Public * - ``"executable"`` - :c:member:`executable ` - - ``str`` + - ``str | None`` - Public * - ``"faulthandler"`` - :c:member:`faulthandler ` @@ -359,7 +362,7 @@ Configuration Options - Read-only * - ``"home"`` - :c:member:`home ` - - ``str`` + - ``str | None`` - Read-only * - ``"import_time"`` - :c:member:`import_time ` @@ -431,7 +434,7 @@ Configuration Options - Public * - ``"prefix"`` - :c:member:`prefix ` - - ``str`` + - ``str | None`` - Public * - ``"program_name"`` - :c:member:`program_name ` @@ -439,7 +442,7 @@ Configuration Options - Read-only * - ``"pycache_prefix"`` - :c:member:`pycache_prefix ` - - ``str`` + - ``str | None`` - Public * - ``"quiet"`` - :c:member:`quiet ` @@ -447,19 +450,19 @@ Configuration Options - Public * - ``"run_command"`` - :c:member:`run_command ` - - ``str`` + - ``str | None`` - Read-only * - ``"run_filename"`` - :c:member:`run_filename ` - - ``str`` + - ``str | None`` - Read-only * - ``"run_module"`` - :c:member:`run_module ` - - ``str`` + - ``str | None`` - Read-only * - ``"run_presite"`` - :c:member:`run_presite ` - - ``str`` + - ``str | None`` - Read-only * - ``"safe_path"`` - :c:member:`safe_path ` @@ -479,7 +482,7 @@ Configuration Options - Read-only * - ``"stdio_encoding"`` - :c:member:`stdio_encoding ` - - ``str`` + - ``str | None`` - Read-only * - ``"stdio_errors"`` - :c:member:`stdio_errors ` @@ -487,7 +490,7 @@ Configuration Options - Read-only * - ``"stdlib_dir"`` - :c:member:`stdlib_dir ` - - ``str`` + - ``str | None`` - Public * - ``"tracemalloc"`` - :c:member:`tracemalloc ` @@ -542,6 +545,8 @@ Configuration Options - ``bool`` - Read-only +The type ``str | None`` means that a string can be ``NULL``. + Visibility: * Public: Can be retrieved by :c:func:`PyConfig_Get` and set by From b75a5f4d7a62270ed9a2c61993bddfd2d08decc4 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 4 Nov 2026 00:39:06 +0100 Subject: [PATCH 4/6] Update also GetStr() doc --- Doc/c-api/init_config.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Doc/c-api/init_config.rst b/Doc/c-api/init_config.rst index 058664505ebf25e..2af1687831b4a34 100644 --- a/Doc/c-api/init_config.rst +++ b/Doc/c-api/init_config.rst @@ -157,6 +157,8 @@ UTF-8 encoded string. See :ref:`Configuration Options `. *\*value* can be set to ``NULL`` if the option is an optional string and the option is unset. + Optional strings are documented with the type ``str | None`` in the + :ref:`Configuration Options ` table. On success, the string must be released with ``free(value)`` if it's not ``NULL``. From b6800112d0771ecdd2c5d851c906abc372545d64 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 4 Nov 2026 01:57:18 +0100 Subject: [PATCH 5/6] Fix GetStr() doc; add tests on set errors --- Doc/c-api/init_config.rst | 4 +- Lib/test/test_embed.py | 3 ++ Programs/_testembed.c | 96 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 100 insertions(+), 3 deletions(-) diff --git a/Doc/c-api/init_config.rst b/Doc/c-api/init_config.rst index 2af1687831b4a34..25c87e830e1fd64 100644 --- a/Doc/c-api/init_config.rst +++ b/Doc/c-api/init_config.rst @@ -155,8 +155,8 @@ UTF-8 encoded string. See :ref:`Configuration Options `. * Set *\*value*, and return ``0`` on success. * Set an error in *config* and return ``-1`` on error. - *\*value* can be set to ``NULL`` if the option is an optional string and the - option is unset. + *\*value* can be set to ``NULL`` if the option is unset or if the option is + an optional string. Optional strings are documented with the type ``str | None`` in the :ref:`Configuration Options ` table. diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index 6c51b79bc44a0f5..e373a256f0d252f 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -1977,6 +1977,9 @@ def test_initconfig_api(self): self.check_all_configs("test_initconfig_api", config, preconfig, api=API_ISOLATED, ignore_stderr=True) + def test_initconfig_set_errors(self): + self.run_embedded_interpreter("test_initconfig_set_errors") + def test_initconfig_get_api(self): self.run_embedded_interpreter("test_initconfig_get_api") diff --git a/Programs/_testembed.c b/Programs/_testembed.c index 00b01adfd38e8f3..6431bfda53c95c1 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -1772,6 +1772,99 @@ static int test_initconfig_api(void) } +static void +check_initconfig_error(PyInitConfig *config, const char *expected) +{ + const char *err_msg; + assert(PyInitConfig_GetError(config, &err_msg) == 1); + assert(strcmp(err_msg, expected) == 0); +} + + +// Test errors of PyInitConfig_Set*() functions +static int test_initconfig_set_errors(void) +{ + char* strlist[] = {"value"}; + + PyInitConfig *config = PyInitConfig_Create(); + if (config == NULL) { + printf("Init allocation error\n"); + return 1; + } + + // Unknown option + assert(PyInitConfig_SetInt(config, "non-existent", 123) < 0); + check_initconfig_error(config, "unknown config option name"); + assert(PyInitConfig_SetStr(config, "non-existent", "abc") < 0); + check_initconfig_error(config, "unknown config option name"); + assert(PyInitConfig_SetStrList(config, "non-existent", + Py_ARRAY_LENGTH(strlist), strlist) < 0); + check_initconfig_error(config, "unknown config option name"); + + + // Wrong type + assert(PyInitConfig_SetInt(config, "platlibdir", 123) == -1); + check_initconfig_error(config, "config option type is not int"); + + assert(PyInitConfig_SetStr(config, "cpu_count", "abc") == -1); + check_initconfig_error(config, "config option type is not string"); + + assert(PyInitConfig_SetStrList(config, "cpu_count", + Py_ARRAY_LENGTH(strlist), strlist) < 0); + check_initconfig_error(config, "config option type is not strings list"); + + + // Test SetInt() range: cpu_count option type is PyConfig_MEMBER_INT + assert(PyInitConfig_SetInt(config, "cpu_count", INT_MIN) == 0); + assert(PyInitConfig_SetInt(config, "cpu_count", INT_MAX) == 0); + + assert(PyInitConfig_SetInt(config, "cpu_count", INT64_MIN) == -1); + check_initconfig_error(config, "config option value is out of int range"); + assert(PyInitConfig_SetInt(config, "cpu_count", INT64_MAX) == -1); + check_initconfig_error(config, "config option value is out of int range"); + + + // Test SetInt() range: verbose option type is PyConfig_MEMBER_UINT + assert(PyInitConfig_SetInt(config, "verbose", 0) == 0); + assert(PyInitConfig_SetInt(config, "verbose", UINT_MAX) == 0); + + assert(PyInitConfig_SetInt(config, "verbose", -1) == -1); + check_initconfig_error(config, "config option value is out of unsigned int range"); + assert(PyInitConfig_SetInt(config, "verbose", INT64_MAX) == -1); + check_initconfig_error(config, "config option value is out of unsigned int range"); + + + // SetStr() decode error + assert(PyInitConfig_SetStr(config, "platlibdir", "\xff") == -1); + check_initconfig_error(config, "decoding error"); + + + // Setting a non-optional string to NULL fails with an error, + // but leaves the optional unchanged. + if (PyInitConfig_SetStr(config, "program_name", PROGRAM_NAME_UTF8) < 0) { + goto error; + } + assert(PyInitConfig_SetStr(config, "program_name", NULL) < 0); + check_initconfig_error(config, "config option string cannot be NULL"); + char *str; + assert(PyInitConfig_GetStr(config, "program_name", &str) == 0); + assert(strcmp(str, PROGRAM_NAME_UTF8) == 0); + free(str); + + + PyInitConfig_Free(config); + return 0; + +error: + { + const char *err_msg; + (void)PyInitConfig_GetError(config, &err_msg); + printf("Python init failed: %s\n", err_msg); + exit(1); + } +} + + static int test_initconfig_get_api(void) { PyInitConfig *config = PyInitConfig_Create(); @@ -1796,7 +1889,7 @@ static int test_initconfig_get_api(void) assert(PyInitConfig_SetInt(config, "utf8_mode", 0) == 0); assert(initconfig_getint(config, "utf8_mode") == 0); - // test PyInitConfig_GetStr() on an optional string (can be NULL) + // test PyInitConfig_GetStr() on an unset string (NULL by default) char *str; assert(PyInitConfig_GetStr(config, "program_name", &str) == 0); assert(str == NULL); @@ -3199,6 +3292,7 @@ static struct TestCase TestCases[] = { {"test_init_is_python_build", test_init_is_python_build}, {"test_init_warnoptions", test_init_warnoptions}, {"test_initconfig_api", test_initconfig_api}, + {"test_initconfig_set_errors", test_initconfig_set_errors}, {"test_initconfig_get_api", test_initconfig_get_api}, {"test_initconfig_exit", test_initconfig_exit}, {"test_initconfig_module", test_initconfig_module}, From 996d0e5eb7656a1e920d751b34962e764ce7cc64 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 4 Nov 2026 02:05:17 +0100 Subject: [PATCH 6/6] Clarify doc --- Doc/c-api/init_config.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Doc/c-api/init_config.rst b/Doc/c-api/init_config.rst index 25c87e830e1fd64..af74e2d0a8369b8 100644 --- a/Doc/c-api/init_config.rst +++ b/Doc/c-api/init_config.rst @@ -547,7 +547,8 @@ Configuration Options - ``bool`` - Read-only -The type ``str | None`` means that a string can be ``NULL``. +The type ``str | None`` means that the option is an optional string which can +be set to ``NULL``. Visibility: