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/