Skip to content

PYTHON-6048 Add sanitizer variants to Evergreen - #32

Draft
blink1073 wants to merge 34 commits into
mainfrom
PYTHON-6048
Draft

blink1073 wants to merge 34 commits into
mainfrom
PYTHON-6048

Conversation

@blink1073

@blink1073 blink1073 commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

PYTHON-6048

Changes in this PR

Adds a sanitizers-ubuntu-22 Evergreen variant so memory-safety, undefined-behavior, and data-race bugs in the C extensions are caught in CI instead of only in manual runs.

  • Adds an ASan+UBSan task and a TSan task (the latter builds a TSan-instrumented free-threaded CPython 3.14 from source), both failing on any sanitizer diagnostic.
  • Fixes monitor shutdown races and latency found by TSan.
  • Adds sanitizer-exercising tests for the C encode/decode paths: type and size boundary round-trips, hostile length fields, deterministic byte mutations, and direct wire-builder coverage. Includes a test-only hook (never compiled into released wheels) that fails a chosen buffer growth so allocation-failure handling runs under ASan.
  • Adds regression tests for concurrent access to a shared zero-copy RawBSONDocument and buffer-lifetime edge cases in the C decoder's error paths.

Test Plan

  • Passing patch build (test-sanitizer-asan with the new tests, no sanitizer diagnostics).
  • The core suites pass on 3.14-asan-ubsan and 3.14t-tsan toolchain builds, and the new raw-bson and monitor tests pass on plain CPython 3.14 and 3.14t.
  • just lint and just typing pass.

Checklist

Checklist for Author

  • Did you update the changelog (if necessary)?
  • Is there test coverage?
  • Is any followup work tracked in a JIRA ticket? If so, add link(s). — TODO: file the _memory_release memoryview race against CPython.

Checklist for Reviewer

  • Does the title of the PR reference a JIRA Ticket?
  • Do you fully understand the implementation? (Would you be comfortable explaining how this code works to someone else?)
  • Is all relevant documentation (README or docstring) updated?

…n close()

TSan caught a data race: Server.close() could return, and the caller
could then close monitor sockets, while the monitor's background
executor was still mid socket.recv() on the same connection.
gc_safe_close() only signals the executor to stop (it must stay
non-blocking since it also runs from a GC weakref callback); it never
waited for the executor to actually exit before proceeding to reset
the pool. Join the executor with the same 1-second bound already used
by periodic_executor.py's shutdown path in the three affected
close() overrides (MonitorBase, Monitor, _RttMonitor) before touching
the pool.
…or join

MonitorBase.close()'s join only ever runs for SrvMonitor, which has no
pool/socket to race with, so it just added a guaranteed timeout to every
mongodb+srv:// client close; drop it and leave the joins in Monitor.close()
and _RttMonitor.close(), the classes that actually own the raced socket.
Also wake() the executor there before joining so the thread rechecks the
stop flag sooner instead of waiting out a full sleep chunk.
The TSan task LD_PRELOADed libtsan.so onto a prebuilt, uninstrumented
free-threaded interpreter, which reported false races in CPython's own
free-threading internals because TSan cannot see synchronization in code
compiled without -fsanitize=thread. It now builds CPython 3.14 from
source with --with-thread-sanitizer, matching CPython's own CI, so all
five suppressions come out.

pip keeps build isolation so it resolves hatchling's build dependencies
itself; CFLAGS and LDFLAGS are shell env vars, which the isolated build
subprocess inherits. The task gets a 7200 second exec timeout because
the source build does not fit in the project-wide 3600.
The suppressions file was emptied on the assumption that upstream's
Tools/tsan/suppressions_free_threading.txt is empty for an instrumented
build. That is true on CPython main but not on the pinned v3.14.0 tag,
where it carries 24 entries that upstream's own TSan CI needs. Build a
combined file from the cloned tag's copy plus this repo's additions and
point TSAN_OPTIONS at that, with handle_segv=0 to match upstream.

Also fix the interpreter glob, which missed the real python3.14td name
that --disable-gil plus --with-pydebug produces; extend apt-get to
CPython's official dependency list; and abort before the 20-30 minute
build when the OpenSSL headers pip needs are missing.
Monitor.close() and _RttMonitor.close() no longer block. Joining there
burned up to a second while Topology.close() and _update_servers() held
the topology lock, stalling every thread in server selection.

Instead, extend the existing _monitor_tasks deferral to sync: closed
monitors are queued under the lock and joined by cleanup_monitors() once
it is released, from Topology.close() and the next select_servers().
Monitor.join() and cleanup_monitors() drop asyncio.gather, which synchro
could not translate and which left a broken call in the generated sync
monitor.
MongoClient.close() now waits (bounded) for monitor threads to stop
before returning, fixing a real race between monitor threads and pool
teardown. Document the resulting latency change for users.
A real TSan CI run finished in 16m3s, well under the project's 60
minute default. Drop the 7200s override and the comment justifying it.
… packages

Production Evergreen hosts for this task can't run apt-get install.
Drop both calls and let the existing OpenSSL header check fail fast
when the headers aren't already on the host.
The free-threaded GC's delete_garbage path and a worker thread's
deferred refcount merge can both enter _memory_release for the same
memoryview. Both stacks are entirely within CPython.
The C-specific tests skip when has_c() is False, so a silently broken
rebuild would report a false-green task.
clang's -print-file-name=libasan.so can return a bare name or linker
script that ld.so silently refuses to preload, leaving the instrumented
extensions unable to import. Resolve real ELF candidates instead, and
preload only the ASan runtime: it carries the UBSan handlers, and
preloading libubsan_standalone alongside it aborts during init when the
two sanitizer_common copies fight over sigaction.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant