[3.12] gh-112536: Add support for thread sanitizer (TSAN) (gh-112648) (#116924)

* [3.12] gh-112536: Add support for thread sanitizer (TSAN) (gh-112648)
(cherry picked from commit 88cb972000)

* Remove doc for configure option (leave it hidden in this branch)

---------

Co-authored-by: Samet YASLAN <sametyaslan@gmail.com>
This commit is contained in:
Antoine Pitrou 2024-03-17 16:33:35 +01:00 committed by GitHub
parent 2dbc77e1ec
commit 2ac1b48a04
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 74 additions and 10 deletions

View file

@ -748,6 +748,11 @@ extern char * _getpty(int *, int, mode_t, int);
# define _Py_ADDRESS_SANITIZER # define _Py_ADDRESS_SANITIZER
# endif # endif
# endif # endif
# if __has_feature(thread_sanitizer)
# if !defined(_Py_THREAD_SANITIZER)
# define _Py_THREAD_SANITIZER
# endif
# endif
#elif defined(__GNUC__) #elif defined(__GNUC__)
# if defined(__SANITIZE_ADDRESS__) # if defined(__SANITIZE_ADDRESS__)
# define _Py_ADDRESS_SANITIZER # define _Py_ADDRESS_SANITIZER

View file

@ -349,6 +349,9 @@ def get_build_info():
# --with-undefined-behavior-sanitizer # --with-undefined-behavior-sanitizer
if support.check_sanitizer(ub=True): if support.check_sanitizer(ub=True):
sanitizers.append("UBSAN") sanitizers.append("UBSAN")
# --with-thread-sanitizer
if support.check_sanitizer(thread=True):
sanitizers.append("TSAN")
if sanitizers: if sanitizers:
build.append('+'.join(sanitizers)) build.append('+'.join(sanitizers))
@ -649,6 +652,7 @@ def display_header(use_resources: tuple[str, ...],
asan = support.check_sanitizer(address=True) asan = support.check_sanitizer(address=True)
msan = support.check_sanitizer(memory=True) msan = support.check_sanitizer(memory=True)
ubsan = support.check_sanitizer(ub=True) ubsan = support.check_sanitizer(ub=True)
tsan = support.check_sanitizer(thread=True)
sanitizers = [] sanitizers = []
if asan: if asan:
sanitizers.append("address") sanitizers.append("address")
@ -656,12 +660,15 @@ def display_header(use_resources: tuple[str, ...],
sanitizers.append("memory") sanitizers.append("memory")
if ubsan: if ubsan:
sanitizers.append("undefined behavior") sanitizers.append("undefined behavior")
if tsan:
sanitizers.append("thread")
if sanitizers: if sanitizers:
print(f"== sanitizers: {', '.join(sanitizers)}") print(f"== sanitizers: {', '.join(sanitizers)}")
for sanitizer, env_var in ( for sanitizer, env_var in (
(asan, "ASAN_OPTIONS"), (asan, "ASAN_OPTIONS"),
(msan, "MSAN_OPTIONS"), (msan, "MSAN_OPTIONS"),
(ubsan, "UBSAN_OPTIONS"), (ubsan, "UBSAN_OPTIONS"),
(tsan, "TSAN_OPTIONS"),
): ):
options= os.environ.get(env_var) options= os.environ.get(env_var)
if sanitizer and options is not None: if sanitizer and options is not None:

View file

@ -391,10 +391,10 @@ def skip_if_buildbot(reason=None):
isbuildbot = False isbuildbot = False
return unittest.skipIf(isbuildbot, reason) return unittest.skipIf(isbuildbot, reason)
def check_sanitizer(*, address=False, memory=False, ub=False): def check_sanitizer(*, address=False, memory=False, ub=False, thread=False):
"""Returns True if Python is compiled with sanitizer support""" """Returns True if Python is compiled with sanitizer support"""
if not (address or memory or ub): if not (address or memory or ub or thread):
raise ValueError('At least one of address, memory, or ub must be True') raise ValueError('At least one of address, memory, ub or thread must be True')
cflags = sysconfig.get_config_var('CFLAGS') or '' cflags = sysconfig.get_config_var('CFLAGS') or ''
@ -411,18 +411,23 @@ def check_sanitizer(*, address=False, memory=False, ub=False):
'-fsanitize=undefined' in cflags or '-fsanitize=undefined' in cflags or
'--with-undefined-behavior-sanitizer' in config_args '--with-undefined-behavior-sanitizer' in config_args
) )
thread_sanitizer = (
'-fsanitize=thread' in cflags or
'--with-thread-sanitizer' in config_args
)
return ( return (
(memory and memory_sanitizer) or (memory and memory_sanitizer) or
(address and address_sanitizer) or (address and address_sanitizer) or
(ub and ub_sanitizer) (ub and ub_sanitizer) or
(thread and thread_sanitizer)
) )
def skip_if_sanitizer(reason=None, *, address=False, memory=False, ub=False): def skip_if_sanitizer(reason=None, *, address=False, memory=False, ub=False, thread=False):
"""Decorator raising SkipTest if running with a sanitizer active.""" """Decorator raising SkipTest if running with a sanitizer active."""
if not reason: if not reason:
reason = 'not working with sanitizers active' reason = 'not working with sanitizers active'
skip = check_sanitizer(address=address, memory=memory, ub=ub) skip = check_sanitizer(address=address, memory=memory, ub=ub, thread=thread)
return unittest.skipIf(skip, reason) return unittest.skipIf(skip, reason)
# gh-89363: True if fork() can hang if Python is built with Address Sanitizer # gh-89363: True if fork() can hang if Python is built with Address Sanitizer
@ -431,7 +436,7 @@ HAVE_ASAN_FORK_BUG = check_sanitizer(address=True)
def set_sanitizer_env_var(env, option): def set_sanitizer_env_var(env, option):
for name in ('ASAN_OPTIONS', 'MSAN_OPTIONS', 'UBSAN_OPTIONS'): for name in ('ASAN_OPTIONS', 'MSAN_OPTIONS', 'UBSAN_OPTIONS', 'TSAN_OPTIONS'):
if name in env: if name in env:
env[name] += f':{option}' env[name] += f':{option}'
else: else:

View file

@ -1708,7 +1708,8 @@ class BufferedReaderTest(unittest.TestCase, CommonBufferedTests):
class CBufferedReaderTest(BufferedReaderTest, SizeofTest): class CBufferedReaderTest(BufferedReaderTest, SizeofTest):
tp = io.BufferedReader tp = io.BufferedReader
@skip_if_sanitizer(memory=True, address=True, reason= "sanitizer defaults to crashing " @skip_if_sanitizer(memory=True, address=True, thread=True,
reason="sanitizer defaults to crashing "
"instead of returning NULL for malloc failure.") "instead of returning NULL for malloc failure.")
def test_constructor(self): def test_constructor(self):
BufferedReaderTest.test_constructor(self) BufferedReaderTest.test_constructor(self)
@ -2075,7 +2076,8 @@ class BufferedWriterTest(unittest.TestCase, CommonBufferedTests):
class CBufferedWriterTest(BufferedWriterTest, SizeofTest): class CBufferedWriterTest(BufferedWriterTest, SizeofTest):
tp = io.BufferedWriter tp = io.BufferedWriter
@skip_if_sanitizer(memory=True, address=True, reason= "sanitizer defaults to crashing " @skip_if_sanitizer(memory=True, address=True, thread=True,
reason="sanitizer defaults to crashing "
"instead of returning NULL for malloc failure.") "instead of returning NULL for malloc failure.")
def test_constructor(self): def test_constructor(self):
BufferedWriterTest.test_constructor(self) BufferedWriterTest.test_constructor(self)
@ -2596,7 +2598,8 @@ class BufferedRandomTest(BufferedReaderTest, BufferedWriterTest):
class CBufferedRandomTest(BufferedRandomTest, SizeofTest): class CBufferedRandomTest(BufferedRandomTest, SizeofTest):
tp = io.BufferedRandom tp = io.BufferedRandom
@skip_if_sanitizer(memory=True, address=True, reason= "sanitizer defaults to crashing " @skip_if_sanitizer(memory=True, address=True, thread=True,
reason="sanitizer defaults to crashing "
"instead of returning NULL for malloc failure.") "instead of returning NULL for malloc failure.")
def test_constructor(self): def test_constructor(self):
BufferedRandomTest.test_constructor(self) BufferedRandomTest.test_constructor(self)

View file

@ -0,0 +1 @@
Add support for thread sanitizer (TSAN)

25
configure generated vendored
View file

@ -1089,6 +1089,7 @@ with_dsymutil
with_address_sanitizer with_address_sanitizer
with_memory_sanitizer with_memory_sanitizer
with_undefined_behavior_sanitizer with_undefined_behavior_sanitizer
with_thread_sanitizer
with_hash_algorithm with_hash_algorithm
with_tzpath with_tzpath
with_libs with_libs
@ -1868,6 +1869,8 @@ Optional Packages:
--with-undefined-behavior-sanitizer --with-undefined-behavior-sanitizer
enable UndefinedBehaviorSanitizer undefined enable UndefinedBehaviorSanitizer undefined
behaviour detector, 'ubsan' (default is no) behaviour detector, 'ubsan' (default is no)
--with-thread-sanitizer enable ThreadSanitizer data race detector, 'tsan'
(default is no)
--with-hash-algorithm=[fnv|siphash13|siphash24] --with-hash-algorithm=[fnv|siphash13|siphash24]
select hash algorithm for use in Python/pyhash.c select hash algorithm for use in Python/pyhash.c
(default is SipHash13) (default is SipHash13)
@ -12661,6 +12664,28 @@ with_ubsan="no"
fi fi
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for --with-thread-sanitizer" >&5
printf %s "checking for --with-thread-sanitizer... " >&6; }
# Check whether --with-thread_sanitizer was given.
if test ${with_thread_sanitizer+y}
then :
withval=$with_thread_sanitizer;
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $withval" >&5
printf "%s\n" "$withval" >&6; }
BASECFLAGS="-fsanitize=thread $BASECFLAGS"
LDFLAGS="-fsanitize=thread $LDFLAGS"
with_tsan="yes"
else $as_nop
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
printf "%s\n" "no" >&6; }
with_tsan="no"
fi
# Set info about shared libraries. # Set info about shared libraries.

View file

@ -3230,6 +3230,24 @@ AC_MSG_RESULT([no])
with_ubsan="no" with_ubsan="no"
]) ])
AC_MSG_CHECKING([for --with-thread-sanitizer])
AC_ARG_WITH(
[thread_sanitizer],
[AS_HELP_STRING(
[--with-thread-sanitizer],
[enable ThreadSanitizer data race detector, 'tsan' (default is no)]
)],
[
AC_MSG_RESULT([$withval])
BASECFLAGS="-fsanitize=thread $BASECFLAGS"
LDFLAGS="-fsanitize=thread $LDFLAGS"
with_tsan="yes"
],
[
AC_MSG_RESULT([no])
with_tsan="no"
])
# Set info about shared libraries. # Set info about shared libraries.
AC_SUBST([SHLIB_SUFFIX]) AC_SUBST([SHLIB_SUFFIX])
AC_SUBST([LDSHARED]) AC_SUBST([LDSHARED])