Skip to content

Commit 7ee3aca

Browse files
gareth-reesFFY00
andauthored
gh-92452: Avoid race in initialization of sysconfig._CONFIG_VARS
Co-authored-by: Filipe Laíns <[email protected]>
1 parent b27b57c commit 7ee3aca

File tree

2 files changed

+82
-60
lines changed

2 files changed

+82
-60
lines changed

Lib/sysconfig.py

Lines changed: 80 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import os
44
import sys
5+
import threading
56
from os.path import pardir, realpath
67

78
__all__ = [
@@ -172,7 +173,11 @@ def joinuser(*args):
172173
_BASE_PREFIX = os.path.normpath(sys.base_prefix)
173174
_EXEC_PREFIX = os.path.normpath(sys.exec_prefix)
174175
_BASE_EXEC_PREFIX = os.path.normpath(sys.base_exec_prefix)
176+
# Mutex guarding initialization of _CONFIG_VARS.
177+
_CONFIG_VARS_LOCK = threading.RLock()
175178
_CONFIG_VARS = None
179+
# True iff _CONFIG_VARS has been fully initialized.
180+
_CONFIG_VARS_INITIALIZED = False
176181
_USER_BASE = None
177182

178183
# Regexes needed for parsing Makefile (and similar syntaxes,
@@ -626,6 +631,71 @@ def get_path(name, scheme=get_default_scheme(), vars=None, expand=True):
626631
return get_paths(scheme, vars, expand)[name]
627632

628633

634+
def _init_config_vars():
635+
global _CONFIG_VARS
636+
_CONFIG_VARS = {}
637+
# Normalized versions of prefix and exec_prefix are handy to have;
638+
# in fact, these are the standard versions used most places in the
639+
# Distutils.
640+
_CONFIG_VARS['prefix'] = _PREFIX
641+
_CONFIG_VARS['exec_prefix'] = _EXEC_PREFIX
642+
_CONFIG_VARS['py_version'] = _PY_VERSION
643+
_CONFIG_VARS['py_version_short'] = _PY_VERSION_SHORT
644+
_CONFIG_VARS['py_version_nodot'] = _PY_VERSION_SHORT_NO_DOT
645+
_CONFIG_VARS['installed_base'] = _BASE_PREFIX
646+
_CONFIG_VARS['base'] = _PREFIX
647+
_CONFIG_VARS['installed_platbase'] = _BASE_EXEC_PREFIX
648+
_CONFIG_VARS['platbase'] = _EXEC_PREFIX
649+
_CONFIG_VARS['projectbase'] = _PROJECT_BASE
650+
_CONFIG_VARS['platlibdir'] = sys.platlibdir
651+
try:
652+
_CONFIG_VARS['abiflags'] = sys.abiflags
653+
except AttributeError:
654+
# sys.abiflags may not be defined on all platforms.
655+
_CONFIG_VARS['abiflags'] = ''
656+
try:
657+
_CONFIG_VARS['py_version_nodot_plat'] = sys.winver.replace('.', '')
658+
except AttributeError:
659+
_CONFIG_VARS['py_version_nodot_plat'] = ''
660+
661+
if os.name == 'nt':
662+
_init_non_posix(_CONFIG_VARS)
663+
_CONFIG_VARS['VPATH'] = sys._vpath
664+
if os.name == 'posix':
665+
_init_posix(_CONFIG_VARS)
666+
if _HAS_USER_BASE:
667+
# Setting 'userbase' is done below the call to the
668+
# init function to enable using 'get_config_var' in
669+
# the init-function.
670+
_CONFIG_VARS['userbase'] = _getuserbase()
671+
672+
# Always convert srcdir to an absolute path
673+
srcdir = _CONFIG_VARS.get('srcdir', _PROJECT_BASE)
674+
if os.name == 'posix':
675+
if _PYTHON_BUILD:
676+
# If srcdir is a relative path (typically '.' or '..')
677+
# then it should be interpreted relative to the directory
678+
# containing Makefile.
679+
base = os.path.dirname(get_makefile_filename())
680+
srcdir = os.path.join(base, srcdir)
681+
else:
682+
# srcdir is not meaningful since the installation is
683+
# spread about the filesystem. We choose the
684+
# directory containing the Makefile since we know it
685+
# exists.
686+
srcdir = os.path.dirname(get_makefile_filename())
687+
_CONFIG_VARS['srcdir'] = _safe_realpath(srcdir)
688+
689+
# OS X platforms require special customization to handle
690+
# multi-architecture, multi-os-version installers
691+
if sys.platform == 'darwin':
692+
import _osx_support
693+
_osx_support.customize_config_vars(_CONFIG_VARS)
694+
695+
global _CONFIG_VARS_INITIALIZED
696+
_CONFIG_VARS_INITIALIZED = True
697+
698+
629699
def get_config_vars(*args):
630700
"""With no arguments, return a dictionary of all configuration
631701
variables relevant for the current platform.
@@ -636,66 +706,16 @@ def get_config_vars(*args):
636706
With arguments, return a list of values that result from looking up
637707
each argument in the configuration variable dictionary.
638708
"""
639-
global _CONFIG_VARS
640-
if _CONFIG_VARS is None:
641-
_CONFIG_VARS = {}
642-
# Normalized versions of prefix and exec_prefix are handy to have;
643-
# in fact, these are the standard versions used most places in the
644-
# Distutils.
645-
_CONFIG_VARS['prefix'] = _PREFIX
646-
_CONFIG_VARS['exec_prefix'] = _EXEC_PREFIX
647-
_CONFIG_VARS['py_version'] = _PY_VERSION
648-
_CONFIG_VARS['py_version_short'] = _PY_VERSION_SHORT
649-
_CONFIG_VARS['py_version_nodot'] = _PY_VERSION_SHORT_NO_DOT
650-
_CONFIG_VARS['installed_base'] = _BASE_PREFIX
651-
_CONFIG_VARS['base'] = _PREFIX
652-
_CONFIG_VARS['installed_platbase'] = _BASE_EXEC_PREFIX
653-
_CONFIG_VARS['platbase'] = _EXEC_PREFIX
654-
_CONFIG_VARS['projectbase'] = _PROJECT_BASE
655-
_CONFIG_VARS['platlibdir'] = sys.platlibdir
656-
try:
657-
_CONFIG_VARS['abiflags'] = sys.abiflags
658-
except AttributeError:
659-
# sys.abiflags may not be defined on all platforms.
660-
_CONFIG_VARS['abiflags'] = ''
661-
try:
662-
_CONFIG_VARS['py_version_nodot_plat'] = sys.winver.replace('.', '')
663-
except AttributeError:
664-
_CONFIG_VARS['py_version_nodot_plat'] = ''
665-
666-
if os.name == 'nt':
667-
_init_non_posix(_CONFIG_VARS)
668-
_CONFIG_VARS['VPATH'] = sys._vpath
669-
if os.name == 'posix':
670-
_init_posix(_CONFIG_VARS)
671-
if _HAS_USER_BASE:
672-
# Setting 'userbase' is done below the call to the
673-
# init function to enable using 'get_config_var' in
674-
# the init-function.
675-
_CONFIG_VARS['userbase'] = _getuserbase()
676-
677-
# Always convert srcdir to an absolute path
678-
srcdir = _CONFIG_VARS.get('srcdir', _PROJECT_BASE)
679-
if os.name == 'posix':
680-
if _PYTHON_BUILD:
681-
# If srcdir is a relative path (typically '.' or '..')
682-
# then it should be interpreted relative to the directory
683-
# containing Makefile.
684-
base = os.path.dirname(get_makefile_filename())
685-
srcdir = os.path.join(base, srcdir)
686-
else:
687-
# srcdir is not meaningful since the installation is
688-
# spread about the filesystem. We choose the
689-
# directory containing the Makefile since we know it
690-
# exists.
691-
srcdir = os.path.dirname(get_makefile_filename())
692-
_CONFIG_VARS['srcdir'] = _safe_realpath(srcdir)
693-
694-
# OS X platforms require special customization to handle
695-
# multi-architecture, multi-os-version installers
696-
if sys.platform == 'darwin':
697-
import _osx_support
698-
_osx_support.customize_config_vars(_CONFIG_VARS)
709+
710+
# Avoid claiming the lock once initialization is complete.
711+
if not _CONFIG_VARS_INITIALIZED:
712+
with _CONFIG_VARS_LOCK:
713+
# Test again with the lock held to avoid races. Note that
714+
# we test _CONFIG_VARS here, not _CONFIG_VARS_INITIALIZED,
715+
# to ensure that recursive calls to get_config_vars()
716+
# don't re-enter init_config_vars().
717+
if _CONFIG_VARS is None:
718+
_init_config_vars()
699719

700720
if args:
701721
vals = []
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fixed a race condition that could cause :func:`sysconfig.get_config_var` to
2+
incorrectly return :const:`None` in multi-threaded programs.

0 commit comments

Comments
 (0)