Skip to content

[Python] Add ASan/LSan coverage for bindings and C++ integration - #1063

Closed
ColinLeeo wants to merge 2 commits into
developfrom
fix/1027-python-asan-coverage
Closed

ColinLeeo wants to merge 2 commits into
developfrom
fix/1027-python-asan-coverage

Conversation

@ColinLeeo

Copy link
Copy Markdown
Contributor

Summary

Implements #1027.

  • Adds a coordinated sanitizer build mode (-Dtsfile.sanitizer=address) that instruments libtsfile and every Cython extension with the same ASan/LSan check set. C++ gets -fsanitize on compile and on the shared-library link with debug info and frame pointers; setup.py builds every extension (including python_random_access_read_file.cc) with -O1 -g -fno-omit-frame-pointer, links the sanitizer runtime, and verifies all artifacts with nm/readelf. Sanitizer builds refuse to produce wheels.
  • Fixes a pre-existing CMake issue where the sanitizer compile flags were applied before the bundled third-party build resets CMAKE_CXX_FLAGS and never restored, so TsFile sources were linked against the runtime but not compiled with instrumentation. The bindings mode now re-applies them after third_party; the C++-only ASan mode keeps its historical static-runtime setup to avoid mixing static and dynamic runtimes.
  • Fixes ownership leaks surfaced by the new checks: STRING/TEXT result buffers, BLOB (now length-aware, preserving embedded NUL bytes), writer-side schema structs, result-set metadata and schema arrays on error paths, and the C wrapper bad_alloc path. Adds tsfile_free_buffer so bindings release buffers with the allocating module's CRT.
  • Adds a Linux CI job with subprocess-based ownership scenarios (tree/table/dataset/Arrow reads, all value types including NULLs, early termination, exception cleanup, repeated open/close, reader/result lifetime, file-object adapter), an import-only baseline, and positive controls. Leak reports are attributed by allocation stack: only leaks reaching TsFile modules fail the run; interpreter/third-party noise is handled with narrow suppressions.
  • Documents build/run commands, runtime preloading, symbolization, and the suppression policy in python/SANITIZER.md.

Why

C++ sanitizer jobs did not exercise the bindings, so native ownership defects across the Python -> Cython -> C wrapper -> C++ chain were invisible. All binding leaks fixed here were present before this change.

Testing

  • Normal build (macOS): 603 Python tests pass (16 sanitizer tests skipped), 1412 C++ tests pass; normal wheel builds; a sanitizer wheel is refused.
  • Linux (Ubuntu 24.04, GCC 13, Python 3.12) with -Dtsfile.sanitizer=address -Dbuild.type=RelWithDebInfo:
    • Maven BUILD SUCCESS; check_sanitizer_build.py reports all six binaries instrumented (compile references plus linked runtime).
    • Ownership suite: 15/15 (13 scenarios and 2 positive controls).
    • Full Python suite under the instrumented build: 619 passed.
    • Temporarily reintroducing the old string-buffer leak makes tree_full fail with tsfile_result_set_get_value_by_index_string in the allocation stack; restoring the fix returns the suite to green.

Reviewer notes

  • TSFILE_SANITIZER_SHARED_RUNTIME keeps the sanitizer runtime dynamic in the shared library so one preloaded runtime serves Python, the extensions, and libtsfile. The C++-only ASan jobs do not enable it.
  • LD_PRELOAD must list libasan first and libstdc++ second; otherwise GCC's __cxa_throw interceptor aborts the first time a C++ exception is thrown. This is documented and used in CI.
  • The sanitizer job runs the new ownership suite; the normal Python matrix still runs the full suite.
  • Build-time instrumentation verification requires nm and readelf (binutils), installed in the CI job.

Follow-up

Supersedes #1047 (same content; the source branch was renamed so repository branch names stay descriptive).

Add a coordinated sanitizer build mode, -Dtsfile.sanitizer=<checks>, that
instruments libtsfile and every Cython extension with the same checks. The
C++ library gets -fsanitize on compile and on the shared-library link and
keeps debug info and frame pointers; setup.py builds each extension with
-O1 -g -fno-omit-frame-pointer, links the sanitizer runtime, and verifies
with nm/readelf that every artifact is instrumented. Sanitizer builds never
produce a wheel.

The sanitizer compile flags must be re-applied after the bundled third-party
build resets CMAKE_CXX_FLAGS; previously they were discarded and only the
link step saw them. The C++-only ASan mode keeps its historical static-runtime
setup so an instrumented libtsfile cannot mix runtimes with the test binary.

Fix ownership defects the new checks expose:
- free STRING/TEXT value buffers and use the length-aware binary getter for
  BLOB so embedded NUL bytes survive;
- free the writer-side schema structs, not only their members;
- free result-set metadata and schema arrays on every path;
- free the property array on the C wrapper's bad_alloc path;
- add tsfile_free_buffer so bindings release buffers with the allocating
  module's CRT.

Add subprocess ownership scenarios (tree/table/dataset/Arrow reads, value
types and NULLs, early termination, exception cleanup, repeated open/close,
reader/result lifetime, file-object adapter), two positive controls, narrow
suppressions, and a Linux CI job. Leak reports are attributed by allocation
stack so only leaks reaching TsFile modules fail the run. Document build and
run commands, runtime preloading (libasan before libstdc++), symbolization,
and the suppression policy.
- Skip the ordinary pytest execution in the with-sanitizer Maven profile:
  the instrumented extensions abort on import unless the sanitizer runtime
  is preloaded, which Maven's test execution does not do, so
  `mvn verify -Dtsfile.sanitizer=address` failed before running a test.
- Document the complete ASan/LSan command in python/CLAUDE.md, including
  TSFILE_SANITIZER (without it test_sanitizer_ownership.py skips silently)
  and the ASAN/LSAN options used by CI.
- Add tsfile_free_table_schema_array()/tsfile_free_device_schema_array()
  so bindings release reader-owned schema arrays with libtsfile's
  allocator, and release partial conversion results when a schema
  conversion raises mid-loop.
- Point the C API value-getter docs at tsfile_free_buffer() and document
  the binary getter's NULL-cell behavior.
- Note the nm/binutils wheel requirement and the test/verify limitation in
  python/SANITIZER.md.
@ColinLeeo ColinLeeo added this to the 2.5.1 milestone Nov 3, 2026
@ColinLeeo

Copy link
Copy Markdown
Contributor Author

Closing: #1047 was already merged (7b96800) and contains the same commits. No separate PR is needed.

@ColinLeeo ColinLeeo closed this Nov 3, 2026
@ColinLeeo
ColinLeeo deleted the fix/1027-python-asan-coverage branch November 3, 2026 04:12
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