Skip to content

Commit b49ec81

Browse files
[3.14] gh-143750: Compile OpenSSL with TSan for TSan CI (#153316) (#153355)
Compile OpenSSL with TSan in the TSan CI job so that data races between Python code and OpenSSL internals are visible to the sanitizer. Also skip test_ssl_in_multiple_threads under TSan: concurrent calls to SSLContext.load_cert_chain on the same context race on the SSL_CTX default password callback. The race is fixed in 3.15+ (GH-143818), but with TSan-instrumented OpenSSL it is now reported on 3.14. (cherry picked from commit fc19ad7) Co-authored-by: Sam Gross <colesbury@gmail.com>
1 parent 6374772 commit b49ec81

3 files changed

Lines changed: 61 additions & 18 deletions

File tree

.github/workflows/reusable-san.yml

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ permissions:
1717

1818
env:
1919
FORCE_COLOR: 1
20+
OPENSSL_VER: 3.5.7
2021

2122
jobs:
2223
build-san-reusable:
@@ -45,28 +46,53 @@ jobs:
4546
sudo update-alternatives --set clang /usr/bin/clang-20
4647
sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-20 100
4748
sudo update-alternatives --set clang++ /usr/bin/clang++-20
48-
49-
if [ "${SANITIZER}" = "TSan" ]; then
50-
# Reduce ASLR to avoid TSan crashing
51-
sudo sysctl -w vm.mmap_rnd_bits=28
52-
fi
53-
54-
- name: Sanitizer option setup
55-
run: |
56-
if [ "${SANITIZER}" = "TSan" ]; then
57-
echo "TSAN_OPTIONS=${SAN_LOG_OPTION} suppressions=${GITHUB_WORKSPACE}/Tools/tsan/suppressions${{
58-
fromJSON(inputs.free-threading)
59-
&& '_free_threading'
60-
|| ''
61-
}}.txt handle_segv=0" >> "$GITHUB_ENV"
62-
else
63-
echo "UBSAN_OPTIONS=${SAN_LOG_OPTION}" >> "$GITHUB_ENV"
64-
fi
6549
echo "CC=clang" >> "$GITHUB_ENV"
6650
echo "CXX=clang++" >> "$GITHUB_ENV"
51+
- name: TSan option setup
52+
if: inputs.sanitizer == 'TSan'
53+
run: |
54+
sudo sysctl -w vm.mmap_rnd_bits=28 # Reduce ASLR to avoid TSan crashing
55+
56+
echo "MULTISSL_DIR=${GITHUB_WORKSPACE}/multissl" >> "$GITHUB_ENV"
57+
echo "OPENSSL_DIR=${GITHUB_WORKSPACE}/multissl/openssl/${OPENSSL_VER}" >> "$GITHUB_ENV"
58+
echo "TSAN_OPTIONS=${SAN_LOG_OPTION} suppressions=${GITHUB_WORKSPACE}/Tools/tsan/suppressions${SUPPRESSIONS_SUFFIX}.txt handle_segv=0" >> "$GITHUB_ENV"
6759
env:
68-
SANITIZER: ${{ inputs.sanitizer }}
6960
SAN_LOG_OPTION: log_path=${{ github.workspace }}/san_log
61+
SUPPRESSIONS_SUFFIX: >-
62+
${{
63+
fromJSON(inputs.free-threading)
64+
&& '_free_threading'
65+
|| ''
66+
}}
67+
- name: UBSan option setup
68+
if: inputs.sanitizer != 'TSan'
69+
run: >-
70+
echo
71+
"UBSAN_OPTIONS=${SAN_LOG_OPTION}"
72+
>> "$GITHUB_ENV"
73+
env:
74+
SAN_LOG_OPTION: log_path=${{ github.workspace }}/san_log
75+
- name: Add ccache to PATH
76+
run: |
77+
echo "PATH=/usr/lib/ccache:$PATH" >> "$GITHUB_ENV"
78+
- name: 'Restore OpenSSL build (TSan)'
79+
id: cache-openssl
80+
if: inputs.sanitizer == 'TSan'
81+
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
82+
with:
83+
path: ./multissl/openssl/${{ env.OPENSSL_VER }}
84+
key: ${{ env.IMAGE_OS_VERSION }}-multissl-openssl-tsan-${{ env.OPENSSL_VER }}
85+
- name: Install OpenSSL (TSan)
86+
if: >-
87+
inputs.sanitizer == 'TSan'
88+
&& steps.cache-openssl.outputs.cache-hit != 'true'
89+
run: >-
90+
python3 Tools/ssl/multissltests.py
91+
--steps=library
92+
--base-directory="${MULTISSL_DIR}"
93+
--openssl="${OPENSSL_VER}"
94+
--system=Linux
95+
--tsan
7096
- name: Configure CPython
7197
run: >-
7298
./configure
@@ -77,6 +103,7 @@ jobs:
77103
|| '--with-undefined-behavior-sanitizer'
78104
}}
79105
--with-pydebug
106+
${{ inputs.sanitizer == 'TSan' && '--with-openssl="$OPENSSL_DIR" --with-openssl-rpath=auto' || '' }}
80107
${{ fromJSON(inputs.free-threading) && '--disable-gil' || '' }}
81108
- name: Build CPython
82109
run: make -j4

Lib/test/test_ssl.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1311,6 +1311,10 @@ def getpass(self):
13111311
# Make sure the password function isn't called if it isn't needed
13121312
ctx.load_cert_chain(CERTFILE, password=getpass_exception)
13131313

1314+
@support.skip_if_sanitizer("gh-143756: data race in "
1315+
"SSLContext.load_cert_chain when called "
1316+
"concurrently on the same context",
1317+
thread=True)
13141318
@threading_helper.requires_working_threading()
13151319
def test_load_cert_chain_thread_safety(self):
13161320
# gh-134698: _ssl detaches the thread state (and as such,
@@ -3059,6 +3063,10 @@ def test_echo(self):
30593063
'Cannot create a client socket with a PROTOCOL_TLS_SERVER context',
30603064
str(e.exception))
30613065

3066+
@support.skip_if_sanitizer("gh-143756: data race in "
3067+
"SSLContext.load_cert_chain when called "
3068+
"concurrently on the same context",
3069+
thread=True)
30623070
@unittest.skipUnless(support.Py_GIL_DISABLED, "test is only useful if the GIL is disabled")
30633071
def test_ssl_in_multiple_threads(self):
30643072
# See GH-124984: OpenSSL is not thread safe.

Tools/ssl/multissltests.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,12 @@
147147
dest='keep_sources',
148148
help="Keep original sources for debugging."
149149
)
150+
parser.add_argument(
151+
'--tsan',
152+
action='store_true',
153+
dest='tsan',
154+
help="Build with thread sanitizer. (Disables fips in OpenSSL 3.x)."
155+
)
150156

151157

152158
class AbstractBuilder(object):
@@ -301,6 +307,8 @@ def _build_src(self, config_args=()):
301307
"""Now build openssl"""
302308
log.info("Running build in {}".format(self.build_dir))
303309
cwd = self.build_dir
310+
if self.args.tsan:
311+
config_args += ("-fsanitize=thread",)
304312
cmd = [
305313
"./config", *config_args,
306314
"shared", "--debug",

0 commit comments

Comments
 (0)