[Python-checkins] gh-112536: Add TSAN builds on Github Actions (#116872)
https://github.com/python/cpython/commit/20578a1f68c841a264b72b00591b11ab2fa77b43 commit: 20578a1f68c841a264b72b00591b11ab2fa77b43 branch: main author: Donghee Na committer: pitrou date: 2024-03-16T11:10:37+01:00 summary: gh-112536: Add TSAN builds on Github Actions (#116872) files: A .github/workflows/reusable-tsan.yml M .github/workflows/build.yml M Lib/test/test_concurrent_futures/util.py M Lib/test/test_logging.py M Lib/test/test_threading.py M Python/thread_pthread.h diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d43b83e830e1fb..e36859e728b67f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -484,6 +484,24 @@ jobs: - name: Tests run: xvfb-run make test + build_tsan: +name: 'Thread sanitizer' +needs: check_source +if: needs.check_source.outputs.run_tests == 'true' +uses: ./.github/workflows/reusable-tsan.yml +with: + config_hash: ${{ needs.check_source.outputs.config_hash }} + options: ./configure --config-cache --with-thread-sanitizer --with-pydebug + + build_tsan_free_threading: +name: 'Thread sanitizer (free-threading)' +needs: check_source +if: needs.check_source.outputs.run_tests == 'true' +uses: ./.github/workflows/reusable-tsan.yml +with: + config_hash: ${{ needs.check_source.outputs.config_hash }} + options: ./configure --config-cache --disable-gil --with-thread-sanitizer --with-pydebug + # CIFuzz job based on https://google.github.io/oss-fuzz/getting-started/continuous-integration/ cifuzz: name: CIFuzz @@ -542,6 +560,8 @@ jobs: - build_windows_free_threading - test_hypothesis - build_asan +- build_tsan +- build_tsan_free_threading - cifuzz runs-on: ubuntu-latest @@ -575,6 +595,8 @@ jobs: build_windows, build_windows_free_threading, build_asan, +build_tsan, +build_tsan_free_threading, ' || '' }} diff --git a/.github/workflows/reusable-tsan.yml b/.github/workflows/reusable-tsan.yml new file mode 100644 index 00..96a9c1b0cda3c3 --- /dev/null +++ b/.github/workflows/reusable-tsan.yml @@ -0,0 +1,51 @@ +on: + workflow_call: +inputs: + config_hash: +required: true +type: string + options: +required: true +type: string + +jobs: + build_tsan_reusable: +name: 'Thread sanitizer' +runs-on: ubuntu-22.04 +timeout-minutes: 60 +steps: +- uses: actions/checkout@v4 +- name: Runner image version + run: echo "IMAGE_VERSION=${ImageVersion}" >> $GITHUB_ENV +- name: Restore config.cache + uses: actions/cache@v4 + with: +path: config.cache +key: ${{ github.job }}-${{ runner.os }}-${{ env.IMAGE_VERSION }}-${{ inputs.config_hash }} +- name: Install Dependencies + run: | +sudo ./.github/workflows/posix-deps-apt.sh +sudo apt install -y clang +# Reduce ASLR to avoid TSAN crashing +sudo sysctl -w vm.mmap_rnd_bits=28 +- name: TSAN Option Setup + run: | +echo "TSAN_OPTIONS=suppressions=${GITHUB_WORKSPACE}/Tools/tsan/supressions.txt" >> $GITHUB_ENV +echo "CC=clang" >> $GITHUB_ENV +echo "CXX=clang++" >> $GITHUB_ENV +- name: Add ccache to PATH + run: | +echo "PATH=/usr/lib/ccache:$PATH" >> $GITHUB_ENV +- name: Configure ccache action + uses: hendrikmuhs/ccache-action@v1.2 + with: +save: ${{ github.event_name == 'push' }} +max-size: "200M" +- name: Configure CPython + run: ${{ inputs.options }} +- name: Build CPython + run: make -j4 +- name: Display build info + run: make pythoninfo +- name: Tests + run: ./python -m test --tsan -j4 diff --git a/Lib/test/test_concurrent_futures/util.py b/Lib/test/test_concurrent_futures/util.py index 3e855031913042..3b8ec3e205d5aa 100644 --- a/Lib/test/test_concurrent_futures/util.py +++ b/Lib/test/test_concurrent_futures/util.py @@ -85,6 +85,8 @@ def get_context(self): self.skipTest("ProcessPoolExecutor unavailable on this system") if sys.platform == "win32": self.skipTest("require unix system") +if support.check_sanitizer(thread=True): +self.skipTest("TSAN doesn't support threads after fork") return super().get_context() @@ -111,6 +113,8 @@ def get_context(self): self.skipTest("ProcessPoolExecutor unavailable on this system") if sys.platform == "win32": self.skipTest("require unix system") +if support.check_sanitizer(thread=True): +self.skipTest("TSAN doesn't suppor
[Python-checkins] gh-112536: Add test_threading to TSAN tests (#116898)
https://github.com/python/cpython/commit/86bc40dd414bceb3f93382cc9f670936de9d68be commit: 86bc40dd414bceb3f93382cc9f670936de9d68be branch: main author: Antoine Pitrou committer: pitrou date: 2024-03-16T11:55:46Z summary: gh-112536: Add test_threading to TSAN tests (#116898) files: M Lib/test/libregrtest/tsan.py M Lib/test/support/script_helper.py M Lib/test/test_threading.py diff --git a/Lib/test/libregrtest/tsan.py b/Lib/test/libregrtest/tsan.py index 150fcec8816179..fde8ba937c0e4b 100644 --- a/Lib/test/libregrtest/tsan.py +++ b/Lib/test/libregrtest/tsan.py @@ -14,6 +14,7 @@ 'test_syslog', 'test_thread', 'test_threadedtempfile', +'test_threading', 'test_threading_local', 'test_threadsignals', ] diff --git a/Lib/test/support/script_helper.py b/Lib/test/support/script_helper.py index 759020c33aa700..65e0bc199e7f0b 100644 --- a/Lib/test/support/script_helper.py +++ b/Lib/test/support/script_helper.py @@ -63,8 +63,8 @@ class _PythonRunResult(collections.namedtuple("_PythonRunResult", """Helper for reporting Python subprocess run results""" def fail(self, cmd_line): """Provide helpful details about failed subcommand runs""" -# Limit to 80 lines to ASCII characters -maxlen = 80 * 100 +# Limit to 300 lines of ASCII characters +maxlen = 300 * 100 out, err = self.out, self.err if len(out) > maxlen: out = b'(... truncated stdout ...)' + out[-maxlen:] diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 9769cb41e3e689..886866626dc9f8 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -47,14 +47,11 @@ def skip_unless_reliable_fork(test): return unittest.skip("due to known OS bug related to thread+fork")(test) if support.HAVE_ASAN_FORK_BUG: return unittest.skip("libasan has a pthread_create() dead lock related to thread+fork")(test) +if support.check_sanitizer(thread=True): +return unittest.skip("TSAN doesn't support threads after fork") return test -skip_if_tsan_fork = unittest.skipIf( -support.check_sanitizer(thread=True), -"TSAN doesn't support threads after fork") - - def requires_subinterpreters(meth): """Decorator to skip a test if subinterpreters are not supported.""" return unittest.skipIf(interpreters is None, @@ -428,6 +425,10 @@ def test_finalize_running_thread(self): # Issue 1402: the PyGILState_Ensure / _Release functions may be called # very late on python exit: on deallocation of a running thread for # example. +if support.check_sanitizer(thread=True): +# the thread running `time.sleep(100)` below will still be alive +# at process exit +self.skipTest("TSAN would report thread leak") import_module("ctypes") rc, out, err = assert_python_failure("-c", """if 1: @@ -460,6 +461,11 @@ def waitingThread(): def test_finalize_with_trace(self): # Issue1733757 # Avoid a deadlock when sys.settrace steps into threading._shutdown +if support.check_sanitizer(thread=True): +# the thread running `time.sleep(2)` below will still be alive +# at process exit +self.skipTest("TSAN would report thread leak") + assert_python_ok("-c", """if 1: import sys, threading @@ -639,7 +645,6 @@ def test_daemon_param(self): self.assertTrue(t.daemon) @skip_unless_reliable_fork -@skip_if_tsan_fork def test_dummy_thread_after_fork(self): # Issue #14308: a dummy thread in the active list doesn't mess up # the after-fork mechanism. @@ -709,7 +714,6 @@ def f(): @skip_unless_reliable_fork @unittest.skipUnless(hasattr(os, 'waitpid'), "test needs os.waitpid()") -@skip_if_tsan_fork def test_main_thread_after_fork(self): code = """if 1: import os, threading @@ -1278,7 +1282,6 @@ def test_2_join_in_forked_process(self): self._run_and_join(script) @skip_unless_reliable_fork -@skip_if_tsan_fork def test_3_join_in_forked_from_thread(self): # Like the test above, but fork() was called from a worker thread # In the forked process, the main Thread object must be marked as stopped. @@ -1311,6 +1314,11 @@ def test_4_daemon_threads(self): # Check that a daemon thread cannot crash the interpreter on shutdown # by manipulating internal structures that are being disposed of in # the main thread. +if support.check_sanitizer(thread=True): +# some o
[Python-checkins] gh-114271: Fix race in `Thread.join()` (#114839)
https://github.com/python/cpython/commit/33da0e844c922b3dcded75fbb9b7be67cb013a17 commit: 33da0e844c922b3dcded75fbb9b7be67cb013a17 branch: main author: mpage committer: pitrou date: 2024-03-16T13:56:30+01:00 summary: gh-114271: Fix race in `Thread.join()` (#114839) There is a race between when `Thread._tstate_lock` is released[^1] in `Thread._wait_for_tstate_lock()` and when `Thread._stop()` asserts[^2] that it is unlocked. Consider the following execution involving threads A, B, and C: 1. A starts. 2. B joins A, blocking on its `_tstate_lock`. 3. C joins A, blocking on its `_tstate_lock`. 4. A finishes and releases its `_tstate_lock`. 5. B acquires A's `_tstate_lock` in `_wait_for_tstate_lock()`, releases it, but is swapped out before calling `_stop()`. 6. C is scheduled, acquires A's `_tstate_lock` in `_wait_for_tstate_lock()` but is swapped out before releasing it. 7. B is scheduled, calls `_stop()`, which asserts that A's `_tstate_lock` is not held. However, C holds it, so the assertion fails. The race can be reproduced[^3] by inserting sleeps at the appropriate points in the threading code. To do so, run the `repro_join_race.py` from the linked repo. There are two main parts to this PR: 1. `_tstate_lock` is replaced with an event that is attached to `PyThreadState`. The event is set by the runtime prior to the thread being cleared (in the same place that `_tstate_lock` was released). `Thread.join()` blocks waiting for the event to be set. 2. `_PyInterpreterState_WaitForThreads()` provides the ability to wait for all non-daemon threads to exit. To do so, an `is_daemon` predicate was added to `PyThreadState`. This field is set each time a thread is created. `threading._shutdown()` now calls into `_PyInterpreterState_WaitForThreads()` instead of waiting on `_tstate_lock`s. [^1]: https://github.com/python/cpython/blob/441affc9e7f419ef0b68f734505fa2f79fe653c7/Lib/threading.py#L1201 [^2]: https://github.com/python/cpython/blob/441affc9e7f419ef0b68f734505fa2f79fe653c7/Lib/threading.py#L1115 [^3]: https://github.com/mpage/cpython/commit/81946532792f938cd6f6ab4c4ff92a4edf61314f - Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com> Co-authored-by: Antoine Pitrou files: A Misc/NEWS.d/next/Library/2024-02-01-03-09-38.gh-issue-114271.raCkt5.rst M Include/cpython/pystate.h M Include/internal/pycore_lock.h M Include/internal/pycore_pythread.h M Lib/test/test_audit.py M Lib/test/test_concurrent_futures/test_process_pool.py M Lib/test/test_thread.py M Lib/test/test_threading.py M Lib/threading.py M Modules/_threadmodule.c M Python/lock.c M Python/pystate.c diff --git a/Include/cpython/pystate.h b/Include/cpython/pystate.h index ac7ff83748dbfc..38d0897ea13161 100644 --- a/Include/cpython/pystate.h +++ b/Include/cpython/pystate.h @@ -161,32 +161,6 @@ struct _ts { */ uintptr_t critical_section; -/* Called when a thread state is deleted normally, but not when it - * is destroyed after fork(). - * Pain: to prevent rare but fatal shutdown errors (issue 18808), - * Thread.join() must wait for the join'ed thread's tstate to be unlinked - * from the tstate chain. That happens at the end of a thread's life, - * in pystate.c. - * The obvious way doesn't quite work: create a lock which the tstate - * unlinking code releases, and have Thread.join() wait to acquire that - * lock. The problem is that we _are_ at the end of the thread's life: - * if the thread holds the last reference to the lock, decref'ing the - * lock will delete the lock, and that may trigger arbitrary Python code - * if there's a weakref, with a callback, to the lock. But by this time - * _PyRuntime.gilstate.tstate_current is already NULL, so only the simplest - * of C code can be allowed to run (in particular it must not be possible to - * release the GIL). - * So instead of holding the lock directly, the tstate holds a weakref to - * the lock: that's the value of on_delete_data below. Decref'ing a - * weakref is harmless. - * on_delete points to _threadmodule.c's static release_sentinel() function. - * After the tstate is unlinked, release_sentinel is called with the - * weakref-to-lock (on_delete_data) argument, and release_sentinel releases - * the indirectly held lock. - */ -void (*on_delete)(void *); -void *on_delete_data; - int coroutine_origin_tracking_depth; PyObject *async_gen_firstiter; diff --git a/Include/internal/pycore_lock.h b/Include/internal/pycore_lock.h index 971b4611a87f3b..f993c95ecbf75a 100644 --- a/Include/internal/pycore_lock.h +++ b/Include/internal/pycore_lock.h @@ -153,16 +153,6 @@ PyAPI_FUNC(void) PyEvent_Wait(PyEvent *evt); // and 0 if the timeout expired or thread was interrupted. PyAPI_FUNC(int) PyEvent_WaitTimed(PyEvent *evt, PyTime_t t
[Python-checkins] gh-112536: Add more TSAN tests (#116896)
https://github.com/python/cpython/commit/bee7e290cdedb17e06f473a2f318c720ba766852 commit: bee7e290cdedb17e06f473a2f318c720ba766852 branch: main author: Donghee Na committer: pitrou date: 2024-03-16T14:52:44Z summary: gh-112536: Add more TSAN tests (#116896) - Co-authored-by: Antoine Pitrou files: M Lib/test/libregrtest/tsan.py diff --git a/Lib/test/libregrtest/tsan.py b/Lib/test/libregrtest/tsan.py index fde8ba937c0e4b..c5aed436b829d1 100644 --- a/Lib/test/libregrtest/tsan.py +++ b/Lib/test/libregrtest/tsan.py @@ -10,6 +10,7 @@ 'test_importlib', 'test_io', 'test_logging', +'test_queue', 'test_ssl', 'test_syslog', 'test_thread', @@ -17,6 +18,7 @@ 'test_threading', 'test_threading_local', 'test_threadsignals', +'test_weakref', ] ___ Python-checkins mailing list -- python-checkins@python.org To unsubscribe send an email to python-checkins-le...@python.org https://mail.python.org/mailman3/lists/python-checkins.python.org/ Member address: arch...@mail-archive.com
[Python-checkins] GH-112536: Add more TSan tests (#116911)
https://github.com/python/cpython/commit/b8d808ddd77f84de9f93adcc2aede2879eb5241e commit: b8d808ddd77f84de9f93adcc2aede2879eb5241e branch: main author: Antoine Pitrou committer: pitrou date: 2024-03-17T09:47:14+01:00 summary: GH-112536: Add more TSan tests (#116911) These may all exercise some non-trivial aspects of thread synchronization. files: M Lib/test/libregrtest/tsan.py M Modules/_testinternalcapi.c diff --git a/Lib/test/libregrtest/tsan.py b/Lib/test/libregrtest/tsan.py index c5aed436b829d1..dd18ae2584f5d8 100644 --- a/Lib/test/libregrtest/tsan.py +++ b/Lib/test/libregrtest/tsan.py @@ -2,6 +2,9 @@ # chosen because they use threads and run in a reasonable amount of time. TSAN_TESTS = [ +# TODO: enable more of test_capi once bugs are fixed (GH-116908, GH-116909). +'test_capi.test_mem', +'test_capi.test_pyatomic', 'test_code', 'test_enum', 'test_functools', @@ -11,6 +14,9 @@ 'test_io', 'test_logging', 'test_queue', +'test_signal', +'test_socket', +'test_sqlite3', 'test_ssl', 'test_syslog', 'test_thread', diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index b3076a8f548b62..1c10dd02138f3a 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -1287,8 +1287,8 @@ check_pyobject_forbidden_bytes_is_freed(PyObject *self, static PyObject * check_pyobject_freed_is_freed(PyObject *self, PyObject *Py_UNUSED(args)) { -/* This test would fail if run with the address sanitizer */ -#ifdef _Py_ADDRESS_SANITIZER +/* ASan or TSan would report an use-after-free error */ +#if defined(_Py_ADDRESS_SANITIZER) || defined(_Py_THREAD_SANITIZER) Py_RETURN_NONE; #else PyObject *op = PyObject_CallNoArgs((PyObject *)&PyBaseObject_Type); ___ Python-checkins mailing list -- python-checkins@python.org To unsubscribe send an email to python-checkins-le...@python.org https://mail.python.org/mailman3/lists/python-checkins.python.org/ Member address: arch...@mail-archive.com
[Python-checkins] [3.12] gh-112536: Add support for thread sanitizer (TSAN) (gh-112648) (#116924)
https://github.com/python/cpython/commit/2ac1b48a044429d7a290310348b53a87b9f2033a commit: 2ac1b48a044429d7a290310348b53a87b9f2033a branch: 3.12 author: Antoine Pitrou committer: pitrou date: 2024-03-17T16:33:35+01:00 summary: [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 88cb9720001295f82c7771ab4ebf20f3cd0b31fb) * Remove doc for configure option (leave it hidden in this branch) - Co-authored-by: Samet YASLAN files: A Misc/NEWS.d/next/Build/2023-12-17-18-23-02.gh-issue-112536.8lr3Ep.rst M Include/pyport.h M Lib/test/libregrtest/utils.py M Lib/test/support/__init__.py M Lib/test/test_io.py M configure M configure.ac diff --git a/Include/pyport.h b/Include/pyport.h index 35eca7234ca094..30b9c8ebc409f0 100644 --- a/Include/pyport.h +++ b/Include/pyport.h @@ -748,6 +748,11 @@ extern char * _getpty(int *, int, mode_t, int); # define _Py_ADDRESS_SANITIZER #endif # endif +# if __has_feature(thread_sanitizer) +#if !defined(_Py_THREAD_SANITIZER) +# define _Py_THREAD_SANITIZER +#endif +# endif #elif defined(__GNUC__) # if defined(__SANITIZE_ADDRESS__) #define _Py_ADDRESS_SANITIZER diff --git a/Lib/test/libregrtest/utils.py b/Lib/test/libregrtest/utils.py index 1be5abd8828be8..25017e8717f47c 100644 --- a/Lib/test/libregrtest/utils.py +++ b/Lib/test/libregrtest/utils.py @@ -349,6 +349,9 @@ def get_build_info(): # --with-undefined-behavior-sanitizer if support.check_sanitizer(ub=True): sanitizers.append("UBSAN") +# --with-thread-sanitizer +if support.check_sanitizer(thread=True): +sanitizers.append("TSAN") if sanitizers: build.append('+'.join(sanitizers)) @@ -649,6 +652,7 @@ def display_header(use_resources: tuple[str, ...], asan = support.check_sanitizer(address=True) msan = support.check_sanitizer(memory=True) ubsan = support.check_sanitizer(ub=True) +tsan = support.check_sanitizer(thread=True) sanitizers = [] if asan: sanitizers.append("address") @@ -656,12 +660,15 @@ def display_header(use_resources: tuple[str, ...], sanitizers.append("memory") if ubsan: sanitizers.append("undefined behavior") +if tsan: +sanitizers.append("thread") if sanitizers: print(f"== sanitizers: {', '.join(sanitizers)}") for sanitizer, env_var in ( (asan, "ASAN_OPTIONS"), (msan, "MSAN_OPTIONS"), (ubsan, "UBSAN_OPTIONS"), +(tsan, "TSAN_OPTIONS"), ): options= os.environ.get(env_var) if sanitizer and options is not None: diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index cb5a84aa74e05f..4e793f154940e3 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -391,10 +391,10 @@ def skip_if_buildbot(reason=None): isbuildbot = False 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""" -if not (address or memory or ub): -raise ValueError('At least one of address, memory, or ub must be True') +if not (address or memory or ub or thread): +raise ValueError('At least one of address, memory, ub or thread must be True') 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 '--with-undefined-behavior-sanitizer' in config_args ) +thread_sanitizer = ( +'-fsanitize=thread' in cflags or +'--with-thread-sanitizer' in config_args +) return ( (memory and memory_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.""" if not reason: 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) # gh-89363: True if fork() can hang if Python is built wi
[Python-checkins] [3.12] gh-112536: Add --tsan test for reasonable TSAN execution times. (gh-116601) (#116929)
https://github.com/python/cpython/commit/fcb230180faff828aa67ca2ee1aa94fc522e6daa commit: fcb230180faff828aa67ca2ee1aa94fc522e6daa branch: 3.12 author: Antoine Pitrou committer: pitrou date: 2024-03-18T10:22:19+01:00 summary: [3.12] gh-112536: Add --tsan test for reasonable TSAN execution times. (gh-116601) (#116929) (cherry picked from commit ebf29b3) Co-authored-by: Donghee Na files: A Lib/test/libregrtest/tsan.py A Misc/NEWS.d/next/Tests/2024-03-11-23-20-28.gh-issue-112536.Qv1RrX.rst A Tools/tsan/supressions.txt M Lib/test/libregrtest/cmdline.py M Lib/test/libregrtest/main.py M Lib/test/support/script_helper.py M Lib/test/test_threading.py M Modules/_testcapi/mem.c diff --git a/Lib/test/libregrtest/cmdline.py b/Lib/test/libregrtest/cmdline.py index 23ca3565662146..ccf5c2e7d77184 100644 --- a/Lib/test/libregrtest/cmdline.py +++ b/Lib/test/libregrtest/cmdline.py @@ -164,6 +164,7 @@ def __init__(self, **kwargs) -> None: self.match_tests: TestFilter = [] self.pgo = False self.pgo_extended = False +self.tsan = False self.worker_json = None self.start = None self.timeout = None @@ -333,6 +334,8 @@ def _create_parser(): help='enable Profile Guided Optimization (PGO) training') group.add_argument('--pgo-extended', action='store_true', help='enable extended PGO training (slower training)') +group.add_argument('--tsan', dest='tsan', action='store_true', + help='run a subset of test cases that are proper for the TSAN test') group.add_argument('--fail-env-changed', action='store_true', help='if a test file alters the environment, mark ' 'the test as failed') diff --git a/Lib/test/libregrtest/main.py b/Lib/test/libregrtest/main.py index a9725fa967370a..1dcf7474163787 100644 --- a/Lib/test/libregrtest/main.py +++ b/Lib/test/libregrtest/main.py @@ -18,6 +18,7 @@ from .runtests import RunTests, HuntRefleak from .setup import setup_process, setup_test_dir from .single import run_single_test, PROGRESS_MIN_TIME +from .tsan import setup_tsan_tests from .utils import ( StrPath, StrJSON, TestName, TestList, TestTuple, TestFilter, strip_py_suffix, count, format_duration, @@ -56,6 +57,7 @@ def __init__(self, ns: Namespace, _add_python_opts: bool = False): self.quiet: bool = ns.quiet self.pgo: bool = ns.pgo self.pgo_extended: bool = ns.pgo_extended +self.tsan: bool = ns.tsan # Test results self.results: TestResults = TestResults() @@ -182,6 +184,9 @@ def find_tests(self, tests: TestList | None = None) -> tuple[TestTuple, TestList # add default PGO tests if no tests are specified setup_pgo_tests(self.cmdline_args, self.pgo_extended) +if self.tsan: +setup_tsan_tests(self.cmdline_args) + exclude_tests = set() if self.exclude: for arg in self.cmdline_args: diff --git a/Lib/test/libregrtest/tsan.py b/Lib/test/libregrtest/tsan.py new file mode 100644 index 00..99cef88e6801a3 --- /dev/null +++ b/Lib/test/libregrtest/tsan.py @@ -0,0 +1,31 @@ +# Set of tests run by default if --tsan is specified. The tests below were +# chosen because they use threads and run in a reasonable amount of time. + +TSAN_TESTS = [ +'test_capi', +'test_code', +'test_enum', +'test_functools', +'test_httpservers', +'test_imaplib', +'test_importlib', +'test_io', +'test_logging', +'test_queue', +'test_signal', +'test_socket', +'test_sqlite3', +'test_ssl', +'test_syslog', +'test_thread', +'test_threadedtempfile', +'test_threading', +'test_threading_local', +'test_threadsignals', +'test_weakref', +] + + +def setup_tsan_tests(cmdline_args): +if not cmdline_args: +cmdline_args[:] = TSAN_TESTS[:] diff --git a/Lib/test/support/script_helper.py b/Lib/test/support/script_helper.py index c2b43f4060eb55..565f3b54a04be5 100644 --- a/Lib/test/support/script_helper.py +++ b/Lib/test/support/script_helper.py @@ -64,8 +64,8 @@ class _PythonRunResult(collections.namedtuple("_PythonRunResult", """Helper for reporting Python subprocess run results""" def fail(self, cmd_line): """Provide helpful details about failed subcommand runs""" -# Limit to 80 lines to ASCII characters -maxlen = 80 * 100 +# Limit to 300 lines of ASCII characters +maxlen = 300 * 100 out,
[Python-checkins] [3.12] gh-112536: Add TSAN build on Github Actions (GH-116872)
https://github.com/python/cpython/commit/25243b1461e524560639ebe54bab9b689b6cc31e commit: 25243b1461e524560639ebe54bab9b689b6cc31e branch: 3.12 author: Antoine Pitrou committer: pitrou date: 2024-03-18T09:52:54Z summary: [3.12] gh-112536: Add TSAN build on Github Actions (GH-116872) (cherry picked from commit 20578a1f68c841a264b72b00591b11ab2fa77b43) Co-authored-by: Donghee Na files: A .github/workflows/reusable-tsan.yml M .github/workflows/build.yml M Lib/test/test_concurrent_futures/util.py M Lib/test/test_logging.py M Python/thread_pthread.h diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d9c399476e24a6..cd56c7d84ab8f1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -471,6 +471,15 @@ jobs: - name: Tests run: xvfb-run make buildbottest TESTOPTS="-j4 -uall,-cpu" + build_tsan: +name: 'Thread sanitizer' +needs: check_source +if: needs.check_source.outputs.run_tests == 'true' +uses: ./.github/workflows/reusable-tsan.yml +with: + config_hash: ${{ needs.check_source.outputs.config_hash }} + options: ./configure --config-cache --with-thread-sanitizer --with-pydebug + all-required-green: # This job does nothing and is only used for the branch protection name: All required checks pass if: always() @@ -485,6 +494,7 @@ jobs: - build_windows - test_hypothesis - build_asan +- build_tsan runs-on: ubuntu-latest @@ -513,6 +523,7 @@ jobs: build_ubuntu_ssltests, build_windows, build_asan, +build_tsan, ' || '' }} diff --git a/.github/workflows/reusable-tsan.yml b/.github/workflows/reusable-tsan.yml new file mode 100644 index 00..96a9c1b0cda3c3 --- /dev/null +++ b/.github/workflows/reusable-tsan.yml @@ -0,0 +1,51 @@ +on: + workflow_call: +inputs: + config_hash: +required: true +type: string + options: +required: true +type: string + +jobs: + build_tsan_reusable: +name: 'Thread sanitizer' +runs-on: ubuntu-22.04 +timeout-minutes: 60 +steps: +- uses: actions/checkout@v4 +- name: Runner image version + run: echo "IMAGE_VERSION=${ImageVersion}" >> $GITHUB_ENV +- name: Restore config.cache + uses: actions/cache@v4 + with: +path: config.cache +key: ${{ github.job }}-${{ runner.os }}-${{ env.IMAGE_VERSION }}-${{ inputs.config_hash }} +- name: Install Dependencies + run: | +sudo ./.github/workflows/posix-deps-apt.sh +sudo apt install -y clang +# Reduce ASLR to avoid TSAN crashing +sudo sysctl -w vm.mmap_rnd_bits=28 +- name: TSAN Option Setup + run: | +echo "TSAN_OPTIONS=suppressions=${GITHUB_WORKSPACE}/Tools/tsan/supressions.txt" >> $GITHUB_ENV +echo "CC=clang" >> $GITHUB_ENV +echo "CXX=clang++" >> $GITHUB_ENV +- name: Add ccache to PATH + run: | +echo "PATH=/usr/lib/ccache:$PATH" >> $GITHUB_ENV +- name: Configure ccache action + uses: hendrikmuhs/ccache-action@v1.2 + with: +save: ${{ github.event_name == 'push' }} +max-size: "200M" +- name: Configure CPython + run: ${{ inputs.options }} +- name: Build CPython + run: make -j4 +- name: Display build info + run: make pythoninfo +- name: Tests + run: ./python -m test --tsan -j4 diff --git a/Lib/test/test_concurrent_futures/util.py b/Lib/test/test_concurrent_futures/util.py index dc48bec796b87f..fc6030e375fb6b 100644 --- a/Lib/test/test_concurrent_futures/util.py +++ b/Lib/test/test_concurrent_futures/util.py @@ -85,6 +85,8 @@ def get_context(self): self.skipTest("ProcessPoolExecutor unavailable on this system") if sys.platform == "win32": self.skipTest("require unix system") +if support.check_sanitizer(thread=True): +self.skipTest("TSAN doesn't support threads after fork") return super().get_context() @@ -111,6 +113,8 @@ def get_context(self): self.skipTest("ProcessPoolExecutor unavailable on this system") if sys.platform == "win32": self.skipTest("require unix system") +if support.check_sanitizer(thread=True): +self.skipTest("TSAN doesn't support threads after fork") return super().get_context() diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index 463bbc575c7275..6bff48c39df7b8 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -80,6 +80,9 @@ skip_if_asan_fork = unittest.skipIf( support.HAVE_ASAN_FORK_BUG, "libasan has a pthread_create() de
[Python-checkins] gh-125541: Make Ctrl-C interrupt `threading.Lock.acquire()` on Windows (#125546)
https://github.com/python/cpython/commit/d8c864816121547338efa43c56e3f75ead98a924 commit: d8c864816121547338efa43c56e3f75ead98a924 branch: main author: Sam Gross committer: pitrou date: 2024-10-17T20:10:55+02:00 summary: gh-125541: Make Ctrl-C interrupt `threading.Lock.acquire()` on Windows (#125546) files: A Misc/NEWS.d/next/Library/2024-10-15-16-50-03.gh-issue-125541.FfhmWo.rst M Doc/library/_thread.rst M Doc/library/threading.rst M Python/parking_lot.c diff --git a/Doc/library/_thread.rst b/Doc/library/_thread.rst index 6a66fc4c64bc45..ed29ac70035597 100644 --- a/Doc/library/_thread.rst +++ b/Doc/library/_thread.rst @@ -187,6 +187,9 @@ Lock objects have the following methods: .. versionchanged:: 3.2 Lock acquires can now be interrupted by signals on POSIX. + .. versionchanged:: 3.14 + Lock acquires can now be interrupted by signals on Windows. + .. method:: lock.release() @@ -219,12 +222,6 @@ In addition to these methods, lock objects can also be used via the * Calling :func:`sys.exit` or raising the :exc:`SystemExit` exception is equivalent to calling :func:`_thread.exit`. -* It is platform-dependent whether the :meth:`~threading.Lock.acquire` method - on a lock can be interrupted (so that the :exc:`KeyboardInterrupt` exception - will happen immediately, rather than only after the lock has been acquired or - the operation has timed out). It can be interrupted on POSIX, but not on - Windows. - * When the main thread exits, it is system defined whether the other threads survive. On most systems, they are killed without executing :keyword:`try` ... :keyword:`finally` clauses or executing object diff --git a/Doc/library/threading.rst b/Doc/library/threading.rst index cb82fea377697b..d4b343db36efb3 100644 --- a/Doc/library/threading.rst +++ b/Doc/library/threading.rst @@ -567,6 +567,9 @@ All methods are executed atomically. Lock acquisition can now be interrupted by signals on POSIX if the underlying threading implementation supports it. + .. versionchanged:: 3.14 + Lock acquisition can now be interrupted by signals on Windows. + .. method:: release() diff --git a/Misc/NEWS.d/next/Library/2024-10-15-16-50-03.gh-issue-125541.FfhmWo.rst b/Misc/NEWS.d/next/Library/2024-10-15-16-50-03.gh-issue-125541.FfhmWo.rst new file mode 100644 index 00..7a20bca1739869 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-10-15-16-50-03.gh-issue-125541.FfhmWo.rst @@ -0,0 +1,4 @@ +Pressing :kbd:`Ctrl-C` while blocked in :meth:`threading.Lock.acquire`, +:meth:`threading.RLock.acquire`, and :meth:`threading.Thread.join` now +interrupts the function call and raises a :exc:`KeyboardInterrupt` exception +on Windows, similar to how those functions behave on macOS and Linux. diff --git a/Python/parking_lot.c b/Python/parking_lot.c index a7e9760e35d87a..bffc959e5d0978 100644 --- a/Python/parking_lot.c +++ b/Python/parking_lot.c @@ -111,15 +111,28 @@ _PySemaphore_PlatformWait(_PySemaphore *sema, PyTime_t timeout) millis = (DWORD) div; } } -wait = WaitForSingleObjectEx(sema->platform_sem, millis, FALSE); + +// NOTE: we wait on the sigint event even in non-main threads to match the +// behavior of the other platforms. Non-main threads will ignore the +// Py_PARK_INTR result. +HANDLE sigint_event = _PyOS_SigintEvent(); +HANDLE handles[2] = { sema->platform_sem, sigint_event }; +DWORD count = sigint_event != NULL ? 2 : 1; +wait = WaitForMultipleObjects(count, handles, FALSE, millis); if (wait == WAIT_OBJECT_0) { res = Py_PARK_OK; } +else if (wait == WAIT_OBJECT_0 + 1) { +ResetEvent(sigint_event); +res = Py_PARK_INTR; +} else if (wait == WAIT_TIMEOUT) { res = Py_PARK_TIMEOUT; } else { -res = Py_PARK_INTR; +_Py_FatalErrorFormat(__func__, +"unexpected error from semaphore: %u (error: %u)", +wait, GetLastError()); } #elif defined(_Py_USE_SEMAPHORES) int err; ___ Python-checkins mailing list -- python-checkins@python.org To unsubscribe send an email to python-checkins-le...@python.org https://mail.python.org/mailman3/lists/python-checkins.python.org/ Member address: arch...@mail-archive.com
[Python-checkins] gh-130115: fix thread identifiers for 32-bit musl (#130391)
https://github.com/python/cpython/commit/72123063ddee84bb2c9d591a23f420997e35af5a commit: 72123063ddee84bb2c9d591a23f420997e35af5a branch: main author: Vincent Fazio committer: pitrou date: 2025-04-04T16:31:37+02:00 summary: gh-130115: fix thread identifiers for 32-bit musl (#130391) CPython's pthread-based thread identifier relies on pthread_t being able to be represented as an unsigned integer type. This is true in most Linux libc implementations where it's defined as an unsigned long, however musl typedefs it as a struct *. If the pointer has the high bit set and is cast to PyThread_ident_t, the resultant value can be sign-extended [0]. This can cause issues when comparing against threading._MainThread's identifier. The main thread's identifier value is retrieved via _get_main_thread_ident which is backed by an unsigned long which truncates sign extended bits. >>> hex(threading.main_thread().ident) '0xb6f33f3c' >>> hex(threading.current_thread().ident) '0xb6f33f3c' Work around this by conditionally compiling in some code for non-glibc based Linux platforms that are at risk of sign-extension to return a PyLong based on the main thread's unsigned long thread identifier if the current thread is the main thread. [0]: https://gcc.gnu.org/onlinedocs/gcc-14.2.0/gcc/Arrays-and-pointers-implementation.html - Signed-off-by: Vincent Fazio files: A Misc/NEWS.d/next/Core_and_Builtins/2025-02-21-00-12-24.gh-issue-130115.mF-rP6.rst M Python/thread_pthread.h diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-02-21-00-12-24.gh-issue-130115.mF-rP6.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-02-21-00-12-24.gh-issue-130115.mF-rP6.rst new file mode 100644 index 00..124da33f8836f6 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-02-21-00-12-24.gh-issue-130115.mF-rP6.rst @@ -0,0 +1 @@ +Fix an issue with thread identifiers being sign-extended on some platforms. diff --git a/Python/thread_pthread.h b/Python/thread_pthread.h index c010b3a827757f..da4058242448f3 100644 --- a/Python/thread_pthread.h +++ b/Python/thread_pthread.h @@ -306,6 +306,24 @@ do_start_joinable_thread(void (*func)(void *), void *arg, pthread_t* out_id) return 0; } +/* Helper to convert pthread_t to PyThread_ident_t. POSIX allows pthread_t to be + non-arithmetic, e.g., musl typedefs it as a pointer. */ +static PyThread_ident_t +_pthread_t_to_ident(pthread_t value) { +// Cast through an integer type of the same size to avoid sign-extension. +#if SIZEOF_PTHREAD_T == SIZEOF_VOID_P +return (uintptr_t) value; +#elif SIZEOF_PTHREAD_T == SIZEOF_LONG +return (unsigned long) value; +#elif SIZEOF_PTHREAD_T == SIZEOF_INT +return (unsigned int) value; +#elif SIZEOF_PTHREAD_T == SIZEOF_LONG_LONG +return (unsigned long long) value; +#else +#error "Unsupported SIZEOF_PTHREAD_T value" +#endif +} + int PyThread_start_joinable_thread(void (*func)(void *), void *arg, PyThread_ident_t* ident, PyThread_handle_t* handle) { @@ -313,9 +331,8 @@ PyThread_start_joinable_thread(void (*func)(void *), void *arg, if (do_start_joinable_thread(func, arg, &th)) { return -1; } -*ident = (PyThread_ident_t) th; +*ident = _pthread_t_to_ident(th); *handle = (PyThread_handle_t) th; -assert(th == (pthread_t) *ident); assert(th == (pthread_t) *handle); return 0; } @@ -328,11 +345,7 @@ PyThread_start_new_thread(void (*func)(void *), void *arg) return PYTHREAD_INVALID_THREAD_ID; } pthread_detach(th); -#if SIZEOF_PTHREAD_T <= SIZEOF_LONG -return (unsigned long) th; -#else -return (unsigned long) *(unsigned long *) &th; -#endif +return (unsigned long) _pthread_t_to_ident(th);; } int @@ -357,8 +370,7 @@ PyThread_get_thread_ident_ex(void) { if (!initialized) PyThread_init_thread(); threadid = pthread_self(); -assert(threadid == (pthread_t) (PyThread_ident_t) threadid); -return (PyThread_ident_t) threadid; +return _pthread_t_to_ident(threadid); } unsigned long ___ Python-checkins mailing list -- python-checkins@python.org To unsubscribe send an email to python-checkins-le...@python.org https://mail.python.org/mailman3/lists/python-checkins.python.org/ Member address: arch...@mail-archive.com
[Python-checkins] [3.13] gh-130115: fix thread identifiers for 32-bit musl (GH-130391) (GH-132089)
https://github.com/python/cpython/commit/240c200ccef9ad31dea3977d70d3ab4177a1212d commit: 240c200ccef9ad31dea3977d70d3ab4177a1212d branch: 3.13 author: Miss Islington (bot) <31488909+miss-isling...@users.noreply.github.com> committer: pitrou date: 2025-04-04T22:57:35+02:00 summary: [3.13] gh-130115: fix thread identifiers for 32-bit musl (GH-130391) (GH-132089) CPython's pthread-based thread identifier relies on pthread_t being able to be represented as an unsigned integer type. This is true in most Linux libc implementations where it's defined as an unsigned long, however musl typedefs it as a struct *. If the pointer has the high bit set and is cast to PyThread_ident_t, the resultant value can be sign-extended [0]. This can cause issues when comparing against threading._MainThread's identifier. The main thread's identifier value is retrieved via _get_main_thread_ident which is backed by an unsigned long which truncates sign extended bits. >>> hex(threading.main_thread().ident) '0xb6f33f3c' >>> hex(threading.current_thread().ident) '0xb6f33f3c' Work around this by conditionally compiling in some code for non-glibc based Linux platforms that are at risk of sign-extension to return a PyLong based on the main thread's unsigned long thread identifier if the current thread is the main thread. [0]: https://gcc.gnu.org/onlinedocs/gcc-14.2.0/gcc/Arrays-and-pointers-implementation.html - (cherry picked from commit 72123063ddee84bb2c9d591a23f420997e35af5a) Signed-off-by: Vincent Fazio Co-authored-by: Vincent Fazio files: A Misc/NEWS.d/next/Core_and_Builtins/2025-02-21-00-12-24.gh-issue-130115.mF-rP6.rst M Python/thread_pthread.h diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-02-21-00-12-24.gh-issue-130115.mF-rP6.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-02-21-00-12-24.gh-issue-130115.mF-rP6.rst new file mode 100644 index 00..124da33f8836f6 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-02-21-00-12-24.gh-issue-130115.mF-rP6.rst @@ -0,0 +1 @@ +Fix an issue with thread identifiers being sign-extended on some platforms. diff --git a/Python/thread_pthread.h b/Python/thread_pthread.h index f588b4620da0d3..de95e733ce09f3 100644 --- a/Python/thread_pthread.h +++ b/Python/thread_pthread.h @@ -307,6 +307,24 @@ do_start_joinable_thread(void (*func)(void *), void *arg, pthread_t* out_id) return 0; } +/* Helper to convert pthread_t to PyThread_ident_t. POSIX allows pthread_t to be + non-arithmetic, e.g., musl typedefs it as a pointer. */ +static PyThread_ident_t +_pthread_t_to_ident(pthread_t value) { +// Cast through an integer type of the same size to avoid sign-extension. +#if SIZEOF_PTHREAD_T == SIZEOF_VOID_P +return (uintptr_t) value; +#elif SIZEOF_PTHREAD_T == SIZEOF_LONG +return (unsigned long) value; +#elif SIZEOF_PTHREAD_T == SIZEOF_INT +return (unsigned int) value; +#elif SIZEOF_PTHREAD_T == SIZEOF_LONG_LONG +return (unsigned long long) value; +#else +#error "Unsupported SIZEOF_PTHREAD_T value" +#endif +} + int PyThread_start_joinable_thread(void (*func)(void *), void *arg, PyThread_ident_t* ident, PyThread_handle_t* handle) { @@ -314,9 +332,8 @@ PyThread_start_joinable_thread(void (*func)(void *), void *arg, if (do_start_joinable_thread(func, arg, &th)) { return -1; } -*ident = (PyThread_ident_t) th; +*ident = _pthread_t_to_ident(th); *handle = (PyThread_handle_t) th; -assert(th == (pthread_t) *ident); assert(th == (pthread_t) *handle); return 0; } @@ -329,11 +346,7 @@ PyThread_start_new_thread(void (*func)(void *), void *arg) return PYTHREAD_INVALID_THREAD_ID; } pthread_detach(th); -#if SIZEOF_PTHREAD_T <= SIZEOF_LONG -return (unsigned long) th; -#else -return (unsigned long) *(unsigned long *) &th; -#endif +return (unsigned long) _pthread_t_to_ident(th);; } int @@ -358,8 +371,7 @@ PyThread_get_thread_ident_ex(void) { if (!initialized) PyThread_init_thread(); threadid = pthread_self(); -assert(threadid == (pthread_t) (PyThread_ident_t) threadid); -return (PyThread_ident_t) threadid; +return _pthread_t_to_ident(threadid); } unsigned long ___ Python-checkins mailing list -- python-checkins@python.org To unsubscribe send an email to python-checkins-le...@python.org https://mail.python.org/mailman3/lists/python-checkins.python.org/ Member address: arch...@mail-archive.com