From e8fa780d1a7a1bbe4a56e2d5d8390500a5d13c55 Mon Sep 17 00:00:00 2001 From: Abhinav Agarwal Date: Thu, 22 Oct 2026 02:38:09 -0700 Subject: [PATCH] ci: add ASan/LSan and UBSan jobs Build with clang and -Db_sanitize and run the test suite under ASan with LeakSanitizer and under UBSan. Reports are written to files and a final step prints them and fails the job if there are any, so tests that expect sshfs to fail can't hide one. The job also fails if any test was skipped. LSan uses the existing test/lsan_suppress.txt; it caught the free_node leak fixed in #390. --- .github/workflows/sanitizers.yml | 125 +++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 .github/workflows/sanitizers.yml diff --git a/.github/workflows/sanitizers.yml b/.github/workflows/sanitizers.yml new file mode 100644 index 00000000..49fcfe1e --- /dev/null +++ b/.github/workflows/sanitizers.yml @@ -0,0 +1,125 @@ +name: sanitizers + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + sanitize: + name: ${{ matrix.name }} + runs-on: ubuntu-24.04 + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - name: ASan + LeakSanitizer + sanitizer: asan + san_flag: address + san_opts: "detect_leaks=1:halt_on_error=1:symbolize=1" + extra_cflags: "-fno-omit-frame-pointer -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0" + - name: UBSan + sanitizer: ubsan + san_flag: undefined + san_opts: "halt_on_error=1:print_stacktrace=1" + extra_cflags: "-fno-omit-frame-pointer" + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y clang llvm ninja-build pkg-config libglib2.0-dev libfuse3-dev fuse3 openssh-client openssh-server + pip3 install meson pytest pytest-timeout + + - name: Setup SSH + run: | + mkdir -p ~/.ssh + chmod 700 ~/.ssh + ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -q -N "" + cat ~/.ssh/id_ed25519.pub > ~/.ssh/authorized_keys + chmod 600 ~/.ssh/authorized_keys + sudo systemctl start ssh || sudo service ssh start + ssh -o StrictHostKeyChecking=no -o BatchMode=yes localhost true + + - name: Check FUSE availability + run: | + test -e /dev/fuse + command -v fusermount3 + # test/util.py looks for the legacy name and skips the mount tests without it + command -v fusermount || sudo ln -s "$(command -v fusermount3)" /usr/local/bin/fusermount + + - name: Build + env: + CC: clang + CFLAGS: ${{ matrix.extra_cflags }} + run: | + meson setup build -Db_sanitize=${{ matrix.san_flag }} -Db_lundef=false -Dwerror=true + ninja -C build + + - name: Test + env: + ASAN_OPTIONS: "${{ matrix.sanitizer == 'asan' && format('{0}:log_path={1}/sanitizer/asan', matrix.san_opts, runner.temp) || '' }}" + LSAN_OPTIONS: "${{ matrix.sanitizer == 'asan' && format('suppressions={0}/test/lsan_suppress.txt', github.workspace) || '' }}" + UBSAN_OPTIONS: "${{ matrix.sanitizer == 'ubsan' && format('{0}:log_path={1}/sanitizer/ubsan', matrix.san_opts, runner.temp) || '' }}" + ASAN_SYMBOLIZER_PATH: /usr/bin/llvm-symbolizer + run: | + mkdir -p "$RUNNER_TEMP/sanitizer" + cd build + python3 -m pytest test/ --timeout=180 --maxfail=99 --junitxml=test-results.xml + timeout-minutes: 20 + + # Some tests expect sshfs to fail and only look at its exit status, so + # check for reports separately instead of relying on pytest. + - name: Check sanitizer reports + if: always() + run: | + shopt -s nullglob + reports=("$RUNNER_TEMP"/sanitizer/*) + for f in "${reports[@]}"; do + echo "::group::$f" + cat "$f" + echo "::endgroup::" + done + if [ ${#reports[@]} -gt 0 ]; then + echo "::error::${#reports[@]} sanitizer report(s)" + exit 1 + fi + + # A missing FUSE prerequisite makes the mount tests skip, not fail. + - name: Check no tests were skipped + run: | + python3 - build/test-results.xml <<'EOF' + import sys + import xml.etree.ElementTree as ET + root = ET.parse(sys.argv[1]).getroot() + suites = [root] if root.tag == "testsuite" else root.findall("testsuite") + skipped = sum(int(s.get("skipped", 0)) for s in suites) + if skipped: + print(f"::error::{skipped} test(s) were skipped") + sys.exit(1) + EOF + + - name: Upload test results + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: always() + with: + name: test-results-${{ matrix.sanitizer }} + path: | + build/test-results.xml + build/meson-logs/ + ${{ runner.temp }}/sanitizer/