Skip to content

Commit 6d69297

Browse files
authored
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.
1 parent 1db5966 commit 6d69297

5 files changed

Lines changed: 145 additions & 22 deletions

File tree

‎Doc/c-api/init_config.rst‎

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,10 @@ UTF-8 encoded string. See :ref:`Configuration Options <pyinitconfig-opts>`.
155155
* Set *\*value*, and return ``0`` on success.
156156
* Set an error in *config* and return ``-1`` on error.
157157
158-
*\*value* can be set to ``NULL`` if the option is an optional string and the
159-
option is unset.
158+
*\*value* can be set to ``NULL`` if the option is unset or if the option is
159+
an optional string.
160+
Optional strings are documented with the type ``str | None`` in the
161+
:ref:`Configuration Options <pyinitconfig-opts>` table.
160162
161163
On success, the string must be released with ``free(value)`` if it's not
162164
``NULL``.
@@ -204,6 +206,9 @@ only implemented when ``Py_InitializeFromInitConfig()`` is called, not by the
204206
Set a string configuration option from a null-terminated UTF-8
205207
encoded string. The string is copied.
206208
209+
Options documented with the type ``str | None`` in the :ref:`Configuration
210+
Options <pyinitconfig-opts>` table can be set to ``NULL``.
211+
207212
* Return ``0`` on success.
208213
* Set an error in *config* and return ``-1`` on error.
209214
@@ -275,15 +280,15 @@ Configuration Options
275280
- Public
276281
* - ``"base_exec_prefix"``
277282
- :c:member:`base_exec_prefix <PyConfig.base_exec_prefix>`
278-
- ``str``
283+
- ``str | None``
279284
- Public
280285
* - ``"base_executable"``
281286
- :c:member:`base_executable <PyConfig.base_executable>`
282-
- ``str``
287+
- ``str | None``
283288
- Public
284289
* - ``"base_prefix"``
285290
- :c:member:`base_prefix <PyConfig.base_prefix>`
286-
- ``str``
291+
- ``str | None``
287292
- Public
288293
* - ``"buffered_stdio"``
289294
- :c:member:`buffered_stdio <PyConfig.buffered_stdio>`
@@ -331,15 +336,15 @@ Configuration Options
331336
- Read-only
332337
* - ``"dump_refs_file"``
333338
- :c:member:`dump_refs_file <PyConfig.dump_refs_file>`
334-
- ``str``
339+
- ``str | None``
335340
- Read-only
336341
* - ``"exec_prefix"``
337342
- :c:member:`exec_prefix <PyConfig.exec_prefix>`
338-
- ``str``
343+
- ``str | None``
339344
- Public
340345
* - ``"executable"``
341346
- :c:member:`executable <PyConfig.executable>`
342-
- ``str``
347+
- ``str | None``
343348
- Public
344349
* - ``"faulthandler"``
345350
- :c:member:`faulthandler <PyConfig.faulthandler>`
@@ -359,7 +364,7 @@ Configuration Options
359364
- Read-only
360365
* - ``"home"``
361366
- :c:member:`home <PyConfig.home>`
362-
- ``str``
367+
- ``str | None``
363368
- Read-only
364369
* - ``"import_time"``
365370
- :c:member:`import_time <PyConfig.import_time>`
@@ -431,35 +436,35 @@ Configuration Options
431436
- Public
432437
* - ``"prefix"``
433438
- :c:member:`prefix <PyConfig.prefix>`
434-
- ``str``
439+
- ``str | None``
435440
- Public
436441
* - ``"program_name"``
437442
- :c:member:`program_name <PyConfig.program_name>`
438443
- ``str``
439444
- Read-only
440445
* - ``"pycache_prefix"``
441446
- :c:member:`pycache_prefix <PyConfig.pycache_prefix>`
442-
- ``str``
447+
- ``str | None``
443448
- Public
444449
* - ``"quiet"``
445450
- :c:member:`quiet <PyConfig.quiet>`
446451
- ``bool``
447452
- Public
448453
* - ``"run_command"``
449454
- :c:member:`run_command <PyConfig.run_command>`
450-
- ``str``
455+
- ``str | None``
451456
- Read-only
452457
* - ``"run_filename"``
453458
- :c:member:`run_filename <PyConfig.run_filename>`
454-
- ``str``
459+
- ``str | None``
455460
- Read-only
456461
* - ``"run_module"``
457462
- :c:member:`run_module <PyConfig.run_module>`
458-
- ``str``
463+
- ``str | None``
459464
- Read-only
460465
* - ``"run_presite"``
461466
- :c:member:`run_presite <PyConfig.run_presite>`
462-
- ``str``
467+
- ``str | None``
463468
- Read-only
464469
* - ``"safe_path"``
465470
- :c:member:`safe_path <PyConfig.safe_path>`
@@ -479,15 +484,15 @@ Configuration Options
479484
- Read-only
480485
* - ``"stdio_encoding"``
481486
- :c:member:`stdio_encoding <PyConfig.stdio_encoding>`
482-
- ``str``
487+
- ``str | None``
483488
- Read-only
484489
* - ``"stdio_errors"``
485490
- :c:member:`stdio_errors <PyConfig.stdio_errors>`
486491
- ``str``
487492
- Read-only
488493
* - ``"stdlib_dir"``
489494
- :c:member:`stdlib_dir <PyConfig.stdlib_dir>`
490-
- ``str``
495+
- ``str | None``
491496
- Public
492497
* - ``"tracemalloc"``
493498
- :c:member:`tracemalloc <PyConfig.tracemalloc>`
@@ -542,6 +547,9 @@ Configuration Options
542547
- ``bool``
543548
- Read-only
544549
550+
The type ``str | None`` means that the option is an optional string which can
551+
be set to ``NULL``.
552+
545553
Visibility:
546554
547555
* Public: Can be retrieved by :c:func:`PyConfig_Get` and set by

‎Lib/test/test_embed.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1973,6 +1973,9 @@ def test_initconfig_api(self):
19731973
self.check_all_configs("test_initconfig_api", config, preconfig,
19741974
api=API_ISOLATED, ignore_stderr=True)
19751975

1976+
def test_initconfig_set_errors(self):
1977+
self.run_embedded_interpreter("test_initconfig_set_errors")
1978+
19761979
def test_initconfig_get_api(self):
19771980
self.run_embedded_interpreter("test_initconfig_get_api")
19781981

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :c:func:`PyInitConfig_SetStr` when setting an optional string option to
2+
``NULL``. Previously, the function crashed. Patch by Victor Stinner.

‎Programs/_testembed.c‎

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1720,6 +1720,7 @@ static int test_initconfig_api(void)
17201720
goto error;
17211721
}
17221722

1723+
// Set an integer option
17231724
if (PyInitConfig_SetInt(config, "hash_seed", 10) < 0) {
17241725
goto error;
17251726
}
@@ -1746,6 +1747,11 @@ static int test_initconfig_api(void)
17461747
goto error;
17471748
}
17481749

1750+
// Set an optional string to NULL explicitly (must not crash).
1751+
if (PyInitConfig_SetStr(config, "prefix", NULL) < 0) {
1752+
goto error;
1753+
}
1754+
17491755
if (Py_InitializeFromInitConfig(config) < 0) {
17501756
goto error;
17511757
}
@@ -1766,6 +1772,99 @@ static int test_initconfig_api(void)
17661772
}
17671773

17681774

1775+
static void
1776+
check_initconfig_error(PyInitConfig *config, const char *expected)
1777+
{
1778+
const char *err_msg;
1779+
assert(PyInitConfig_GetError(config, &err_msg) == 1);
1780+
assert(strcmp(err_msg, expected) == 0);
1781+
}
1782+
1783+
1784+
// Test errors of PyInitConfig_Set*() functions
1785+
static int test_initconfig_set_errors(void)
1786+
{
1787+
char* strlist[] = {"value"};
1788+
1789+
PyInitConfig *config = PyInitConfig_Create();
1790+
if (config == NULL) {
1791+
printf("Init allocation error\n");
1792+
return 1;
1793+
}
1794+
1795+
// Unknown option
1796+
assert(PyInitConfig_SetInt(config, "non-existent", 123) < 0);
1797+
check_initconfig_error(config, "unknown config option name");
1798+
assert(PyInitConfig_SetStr(config, "non-existent", "abc") < 0);
1799+
check_initconfig_error(config, "unknown config option name");
1800+
assert(PyInitConfig_SetStrList(config, "non-existent",
1801+
Py_ARRAY_LENGTH(strlist), strlist) < 0);
1802+
check_initconfig_error(config, "unknown config option name");
1803+
1804+
1805+
// Wrong type
1806+
assert(PyInitConfig_SetInt(config, "platlibdir", 123) == -1);
1807+
check_initconfig_error(config, "config option type is not int");
1808+
1809+
assert(PyInitConfig_SetStr(config, "cpu_count", "abc") == -1);
1810+
check_initconfig_error(config, "config option type is not string");
1811+
1812+
assert(PyInitConfig_SetStrList(config, "cpu_count",
1813+
Py_ARRAY_LENGTH(strlist), strlist) < 0);
1814+
check_initconfig_error(config, "config option type is not strings list");
1815+
1816+
1817+
// Test SetInt() range: cpu_count option type is PyConfig_MEMBER_INT
1818+
assert(PyInitConfig_SetInt(config, "cpu_count", INT_MIN) == 0);
1819+
assert(PyInitConfig_SetInt(config, "cpu_count", INT_MAX) == 0);
1820+
1821+
assert(PyInitConfig_SetInt(config, "cpu_count", INT64_MIN) == -1);
1822+
check_initconfig_error(config, "config option value is out of int range");
1823+
assert(PyInitConfig_SetInt(config, "cpu_count", INT64_MAX) == -1);
1824+
check_initconfig_error(config, "config option value is out of int range");
1825+
1826+
1827+
// Test SetInt() range: verbose option type is PyConfig_MEMBER_UINT
1828+
assert(PyInitConfig_SetInt(config, "verbose", 0) == 0);
1829+
assert(PyInitConfig_SetInt(config, "verbose", UINT_MAX) == 0);
1830+
1831+
assert(PyInitConfig_SetInt(config, "verbose", -1) == -1);
1832+
check_initconfig_error(config, "config option value is out of unsigned int range");
1833+
assert(PyInitConfig_SetInt(config, "verbose", INT64_MAX) == -1);
1834+
check_initconfig_error(config, "config option value is out of unsigned int range");
1835+
1836+
1837+
// SetStr() decode error
1838+
assert(PyInitConfig_SetStr(config, "platlibdir", "\xff") == -1);
1839+
check_initconfig_error(config, "decoding error");
1840+
1841+
1842+
// Setting a non-optional string to NULL fails with an error,
1843+
// but leaves the optional unchanged.
1844+
if (PyInitConfig_SetStr(config, "program_name", PROGRAM_NAME_UTF8) < 0) {
1845+
goto error;
1846+
}
1847+
assert(PyInitConfig_SetStr(config, "program_name", NULL) < 0);
1848+
check_initconfig_error(config, "config option string cannot be NULL");
1849+
char *str;
1850+
assert(PyInitConfig_GetStr(config, "program_name", &str) == 0);
1851+
assert(strcmp(str, PROGRAM_NAME_UTF8) == 0);
1852+
free(str);
1853+
1854+
1855+
PyInitConfig_Free(config);
1856+
return 0;
1857+
1858+
error:
1859+
{
1860+
const char *err_msg;
1861+
(void)PyInitConfig_GetError(config, &err_msg);
1862+
printf("Python init failed: %s\n", err_msg);
1863+
exit(1);
1864+
}
1865+
}
1866+
1867+
17691868
static int test_initconfig_get_api(void)
17701869
{
17711870
PyInitConfig *config = PyInitConfig_Create();
@@ -1778,6 +1877,7 @@ static int test_initconfig_get_api(void)
17781877
assert(PyInitConfig_HasOption(config, "verbose") == 1);
17791878
assert(PyInitConfig_HasOption(config, "utf8_mode") == 1);
17801879
assert(PyInitConfig_HasOption(config, "non-existent") == 0);
1880+
assert(PyInitConfig_HasOption(config, "") == 0);
17811881

17821882
// test PyInitConfig_GetInt()
17831883
assert(initconfig_getint(config, "dev_mode") == 0);
@@ -1789,7 +1889,7 @@ static int test_initconfig_get_api(void)
17891889
assert(PyInitConfig_SetInt(config, "utf8_mode", 0) == 0);
17901890
assert(initconfig_getint(config, "utf8_mode") == 0);
17911891

1792-
// test PyInitConfig_GetStr()
1892+
// test PyInitConfig_GetStr() on an unset string (NULL by default)
17931893
char *str;
17941894
assert(PyInitConfig_GetStr(config, "program_name", &str) == 0);
17951895
assert(str == NULL);
@@ -1815,8 +1915,11 @@ static int test_initconfig_get_api(void)
18151915

18161916
// Setting hash_seed sets use_hash_seed
18171917
assert(initconfig_getint(config, "use_hash_seed") == 0);
1918+
assert(initconfig_getint(config, "hash_seed") == 0);
1919+
18181920
assert(PyInitConfig_SetInt(config, "hash_seed", 123) == 0);
18191921
assert(initconfig_getint(config, "use_hash_seed") == 1);
1922+
assert(initconfig_getint(config, "hash_seed") == 123);
18201923

18211924
// Setting module_search_paths sets module_search_paths_set
18221925
assert(initconfig_getint(config, "module_search_paths_set") == 0);
@@ -1825,6 +1928,7 @@ static int test_initconfig_get_api(void)
18251928
Py_ARRAY_LENGTH(paths), paths) == 0);
18261929
assert(initconfig_getint(config, "module_search_paths_set") == 1);
18271930

1931+
PyInitConfig_Free(config);
18281932
return 0;
18291933
}
18301934

@@ -3188,6 +3292,7 @@ static struct TestCase TestCases[] = {
31883292
{"test_init_is_python_build", test_init_is_python_build},
31893293
{"test_init_warnoptions", test_init_warnoptions},
31903294
{"test_initconfig_api", test_initconfig_api},
3295+
{"test_initconfig_set_errors", test_initconfig_set_errors},
31913296
{"test_initconfig_get_api", test_initconfig_get_api},
31923297
{"test_initconfig_exit", test_initconfig_exit},
31933298
{"test_initconfig_module", test_initconfig_module},

‎Python/initconfig.c‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4372,10 +4372,15 @@ PyInitConfig_SetStr(PyInitConfig *config, const char *name, const char* value)
43724372
}
43734373

43744374
wchar_t **member = raw_member;
4375-
4376-
wchar_t *wstr = utf8_to_wstr(config, value);
4377-
if (wstr == NULL) {
4378-
return -1;
4375+
wchar_t *wstr;
4376+
if (value != NULL) {
4377+
wstr = utf8_to_wstr(config, value);
4378+
if (wstr == NULL) {
4379+
return -1;
4380+
}
4381+
}
4382+
else {
4383+
wstr = NULL;
43794384
}
43804385
free(*member);
43814386
*member = wstr;

0 commit comments

Comments
 (0)