https://github.com/python/cpython/commit/6d69297b2b1007991640310e8699979a2ef66287
commit: 6d69297b2b1007991640310e8699979a2ef66287
branch: main
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-11-04T01:47:50Z
summary:

gh-160962: Fix PyInitConfig_SetStr() with NULL value (#161258)

Previously, passing NULL to an optional string option crashed.

Document optional string options.

Add more tests on the PyInitConfig API.

files:
A Misc/NEWS.d/next/C_API/2026-11-03-18-27-42.gh-issue-160962.06816I.rst
M Doc/c-api/init_config.rst
M Lib/test/test_embed.py
M Programs/_testembed.c
M Python/initconfig.c

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 
<pyinitconfig-opts>`.
    * 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 <pyinitconfig-opts>` 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 <pyinitconfig-opts>` 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 <PyConfig.base_exec_prefix>`
-     - ``str``
+     - ``str | None``
      - Public
    * - ``"base_executable"``
      - :c:member:`base_executable <PyConfig.base_executable>`
-     - ``str``
+     - ``str | None``
      - Public
    * - ``"base_prefix"``
      - :c:member:`base_prefix <PyConfig.base_prefix>`
-     - ``str``
+     - ``str | None``
      - Public
    * - ``"buffered_stdio"``
      - :c:member:`buffered_stdio <PyConfig.buffered_stdio>`
@@ -331,15 +336,15 @@ Configuration Options
      - Read-only
    * - ``"dump_refs_file"``
      - :c:member:`dump_refs_file <PyConfig.dump_refs_file>`
-     - ``str``
+     - ``str | None``
      - Read-only
    * - ``"exec_prefix"``
      - :c:member:`exec_prefix <PyConfig.exec_prefix>`
-     - ``str``
+     - ``str | None``
      - Public
    * - ``"executable"``
      - :c:member:`executable <PyConfig.executable>`
-     - ``str``
+     - ``str | None``
      - Public
    * - ``"faulthandler"``
      - :c:member:`faulthandler <PyConfig.faulthandler>`
@@ -359,7 +364,7 @@ Configuration Options
      - Read-only
    * - ``"home"``
      - :c:member:`home <PyConfig.home>`
-     - ``str``
+     - ``str | None``
      - Read-only
    * - ``"import_time"``
      - :c:member:`import_time <PyConfig.import_time>`
@@ -431,7 +436,7 @@ Configuration Options
      - Public
    * - ``"prefix"``
      - :c:member:`prefix <PyConfig.prefix>`
-     - ``str``
+     - ``str | None``
      - Public
    * - ``"program_name"``
      - :c:member:`program_name <PyConfig.program_name>`
@@ -439,7 +444,7 @@ Configuration Options
      - Read-only
    * - ``"pycache_prefix"``
      - :c:member:`pycache_prefix <PyConfig.pycache_prefix>`
-     - ``str``
+     - ``str | None``
      - Public
    * - ``"quiet"``
      - :c:member:`quiet <PyConfig.quiet>`
@@ -447,19 +452,19 @@ Configuration Options
      - Public
    * - ``"run_command"``
      - :c:member:`run_command <PyConfig.run_command>`
-     - ``str``
+     - ``str | None``
      - Read-only
    * - ``"run_filename"``
      - :c:member:`run_filename <PyConfig.run_filename>`
-     - ``str``
+     - ``str | None``
      - Read-only
    * - ``"run_module"``
      - :c:member:`run_module <PyConfig.run_module>`
-     - ``str``
+     - ``str | None``
      - Read-only
    * - ``"run_presite"``
      - :c:member:`run_presite <PyConfig.run_presite>`
-     - ``str``
+     - ``str | None``
      - Read-only
    * - ``"safe_path"``
      - :c:member:`safe_path <PyConfig.safe_path>`
@@ -479,7 +484,7 @@ Configuration Options
      - Read-only
    * - ``"stdio_encoding"``
      - :c:member:`stdio_encoding <PyConfig.stdio_encoding>`
-     - ``str``
+     - ``str | None``
      - Read-only
    * - ``"stdio_errors"``
      - :c:member:`stdio_errors <PyConfig.stdio_errors>`
@@ -487,7 +492,7 @@ Configuration Options
      - Read-only
    * - ``"stdlib_dir"``
      - :c:member:`stdlib_dir <PyConfig.stdlib_dir>`
-     - ``str``
+     - ``str | None``
      - Public
    * - ``"tracemalloc"``
      - :c:member:`tracemalloc <PyConfig.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 b5a441b7e94fb49..0317e4900ab0acc 100644
--- a/Lib/test/test_embed.py
+++ b/Lib/test/test_embed.py
@@ -1973,6 +1973,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;

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to