diff --git a/Doc/c-api/init_config.rst b/Doc/c-api/init_config.rst index ad3571262168003..af74e2d0a8369b8 100644 --- a/Doc/c-api/init_config.rst +++ b/Doc/c-api/init_config.rst @@ -155,8 +155,10 @@ 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. On success, the string must be released with ``free(value)`` if it's not ``NULL``. @@ -204,6 +206,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 +280,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 +336,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 +364,7 @@ Configuration Options - Read-only * - ``"home"`` - :c:member:`home ` - - ``str`` + - ``str | None`` - Read-only * - ``"import_time"`` - :c:member:`import_time ` @@ -431,7 +436,7 @@ Configuration Options - Public * - ``"prefix"`` - :c:member:`prefix ` - - ``str`` + - ``str | None`` - Public * - ``"program_name"`` - :c:member:`program_name ` @@ -439,7 +444,7 @@ Configuration Options - Read-only * - ``"pycache_prefix"`` - :c:member:`pycache_prefix ` - - ``str`` + - ``str | None`` - Public * - ``"quiet"`` - :c:member:`quiet ` @@ -447,19 +452,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 +484,7 @@ Configuration Options - Read-only * - ``"stdio_encoding"`` - :c:member:`stdio_encoding ` - - ``str`` + - ``str | None`` - Read-only * - ``"stdio_errors"`` - :c:member:`stdio_errors ` @@ -487,7 +492,7 @@ Configuration Options - Read-only * - ``"stdlib_dir"`` - :c:member:`stdlib_dir ` - - ``str`` + - ``str | None`` - Public * - ``"tracemalloc"`` - :c:member:`tracemalloc ` @@ -542,6 +547,9 @@ Configuration Options - ``bool`` - Read-only +The type ``str | None`` means that the option is an optional string which can +be set to ``NULL``. + Visibility: * Public: Can be retrieved by :c:func:`PyConfig_Get` and set by 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/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..6431bfda53c95c1 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -1720,6 +1720,7 @@ static int test_initconfig_api(void) goto error; } + // Set an integer option if (PyInitConfig_SetInt(config, "hash_seed", 10) < 0) { goto error; } @@ -1746,6 +1747,11 @@ static int test_initconfig_api(void) goto error; } + // 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; } @@ -1766,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(); @@ -1778,6 +1877,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); @@ -1789,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() + // test PyInitConfig_GetStr() on an unset string (NULL by default) char *str; assert(PyInitConfig_GetStr(config, "program_name", &str) == 0); assert(str == NULL); @@ -1815,8 +1915,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); @@ -1825,6 +1928,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; } @@ -3188,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}, 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;