fix: improve symbol sanitization, call argument synthesis, and type coercion robustness #1519
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # | |
| # Copyright (c) 2025, Trail of Bits, Inc. | |
| # | |
| # This source code is licensed in accordance with the terms specified in | |
| # the LICENSE file found in the root directory of this source tree. | |
| # | |
| name: CI | |
| on: | |
| push: | |
| branches: | |
| - 'main' | |
| tags: | |
| - '*' | |
| pull_request: | |
| branches: | |
| - '*' | |
| permissions: read-all | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| jobs: | |
| ghidra-script-tests: | |
| strategy: | |
| matrix: | |
| image-version: [22.04] | |
| arch: [amd64, arm64] | |
| runs-on: ${{ matrix.arch == 'arm64' && format('ubuntu-{0}-arm', matrix.image-version) || format('ubuntu-{0}', matrix.image-version) }} | |
| # Ghidra-from-source build (gradle buildGhidra + IntegrationTest) dominates | |
| # wall time; 60 min covers both arches with headroom. | |
| timeout-minutes: 60 | |
| env: | |
| CI: true | |
| steps: | |
| - name: Free up disk space | |
| uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 | |
| with: | |
| tool-cache: false | |
| android: true | |
| dotnet: true | |
| haskell: true | |
| large-packages: true | |
| docker-images: false | |
| swap-storage: true | |
| - name: Clone the Patchestry repository | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: true | |
| fetch-depth: 1 | |
| - name: Build the Ghidra testing Docker image and run tests via Gradle | |
| run: | | |
| DOCKER_BUILDKIT=1 docker build -t trailofbits/decompile-test:latest -f test/decompile-test.dockerfile . && \ | |
| docker run trailofbits/decompile-test | |
| - name: Clean up Docker artifacts | |
| if: always() | |
| run: | | |
| docker system prune -af --volumes | |
| llvm-build-and-test: | |
| strategy: | |
| matrix: | |
| llvm-version: [22] | |
| image-version: [22.04] | |
| build-type: [Release, Debug] | |
| sanitizers: [OFF] | |
| arch: [amd64, arm64] | |
| runs-on: ${{ matrix.arch == 'arm64' && format('ubuntu-{0}-arm', matrix.image-version) || format('ubuntu-{0}', matrix.image-version) }} | |
| timeout-minutes: 30 | |
| env: | |
| # `-dev:latest` is a multi-arch manifest published by devcontainer.yml; | |
| # Docker resolves the correct platform on pull. | |
| DEV_IMAGE: ghcr.io/lifting-bits/patchestry-ubuntu-${{ matrix.image-version }}-llvm-${{ matrix.llvm-version }}-dev:latest | |
| CMAKE_PREFIX_PATH: "/usr/lib/llvm-${{ matrix.llvm-version }}/lib/cmake/mlir/;/usr/lib/llvm-${{ matrix.llvm-version }}/lib/cmake/clang/" | |
| LLVM_EXTERNAL_LIT: "/usr/local/bin/lit" | |
| ENABLE_SANITIZER_UNDEFINED_BEHAVIOR: ${{ matrix.sanitizers }} | |
| ENABLE_SANITIZER_ADDRESS: ${{ matrix.sanitizers }} | |
| CI: true | |
| steps: | |
| - name: Free up disk space | |
| uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 | |
| with: | |
| tool-cache: false | |
| android: true | |
| dotnet: true | |
| haskell: true | |
| large-packages: true | |
| docker-images: false | |
| swap-storage: true | |
| - name: Clone the Patchestry repository | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: true | |
| fetch-depth: 1 | |
| - name: Pull dev container image | |
| run: docker pull "$DEV_IMAGE" | |
| - name: Create gitconfig for safe directories | |
| run: | | |
| cat > /tmp/.gitconfig << 'EOF' | |
| [safe] | |
| directory = /workspace | |
| directory = /workspace/vendor/glog/src | |
| directory = /workspace/vendor/gflags/src | |
| directory = /workspace/vendor/z3/src | |
| directory = /workspace/vendor/llvm-project/src | |
| EOF | |
| - name: Configure build - sanitizers ${{ matrix.sanitizers }} | |
| run: | | |
| docker run --rm \ | |
| -v ${{ github.workspace }}:/workspace \ | |
| -v /tmp/.gitconfig:/root/.gitconfig:ro \ | |
| -w /workspace \ | |
| -e CMAKE_PREFIX_PATH="${CMAKE_PREFIX_PATH}" \ | |
| -e LLVM_EXTERNAL_LIT="${LLVM_EXTERNAL_LIT}" \ | |
| -e ENABLE_SANITIZER_UNDEFINED_BEHAVIOR="${ENABLE_SANITIZER_UNDEFINED_BEHAVIOR}" \ | |
| -e ENABLE_SANITIZER_ADDRESS="${ENABLE_SANITIZER_ADDRESS}" \ | |
| -e CI=true \ | |
| "$DEV_IMAGE" \ | |
| cmake --preset ci -DPE_USE_VENDORED_Z3=OFF -DLLVM_EXTERNAL_LIT=/usr/local/bin/lit -DLLVM_Z3_INSTALL_DIR=/usr/local | |
| - name: Build ${{ matrix.build-type }} with sanitizers set ${{ matrix.sanitizers }} | |
| run: | | |
| docker run --rm \ | |
| -v ${{ github.workspace }}:/workspace \ | |
| -v /tmp/.gitconfig:/root/.gitconfig:ro \ | |
| -w /workspace \ | |
| -e CI=true \ | |
| "$DEV_IMAGE" \ | |
| cmake --build --preset ci --config ${{ matrix.build-type }} -j$(nproc) | |
| - name: Build the intrinsic library | |
| run: | | |
| docker run --rm \ | |
| -v ${{ github.workspace }}:/workspace \ | |
| -v /tmp/.gitconfig:/root/.gitconfig:ro \ | |
| -w /workspace/lib/patchestry/intrinsics \ | |
| "$DEV_IMAGE" \ | |
| bash -c "cmake -B build_standalone -DCMAKE_BUILD_TYPE=Release && cmake --build build_standalone -j\$(nproc)" | |
| - name: Build the headless decompilation docker image | |
| run: | | |
| bash ./scripts/ghidra/build-headless-docker.sh | |
| - name: Run cached patch matrix | |
| if: matrix.build-type == 'Debug' && matrix.sanitizers == 'OFF' | |
| run: | | |
| docker run --rm \ | |
| -v ${{ github.workspace }}:/workspace \ | |
| -v /tmp/.gitconfig:/root/.gitconfig:ro \ | |
| -w /workspace \ | |
| -v /var/run/docker.sock:/var/run/docker.sock \ | |
| -e CI=true \ | |
| -e HOST_WORKSPACE=${{ github.workspace }} \ | |
| "$DEV_IMAGE" \ | |
| bash ./scripts/test-patch-matrix.sh \ | |
| --build-type Debug \ | |
| --build-root ci \ | |
| --rebuild-firmware \ | |
| --rebuild-fixtures | |
| - name: Test ${{ matrix.build-type }} with sanitizers set ${{ matrix.sanitizers }} | |
| run: | | |
| docker run --rm \ | |
| -v ${{ github.workspace }}:/workspace \ | |
| -v /tmp/.gitconfig:/root/.gitconfig:ro \ | |
| -w /workspace \ | |
| -v /var/run/docker.sock:/var/run/docker.sock \ | |
| -e CI=true \ | |
| -e HOST_WORKSPACE=${{ github.workspace }} \ | |
| "$DEV_IMAGE" \ | |
| lit ./builds/ci/test -D BUILD_TYPE=${{ matrix.build-type }} -v -DCI_OUTPUT_FOLDER=/workspace/builds/ci/test/ghidra/Output | |
| - name: Run KLEE on generated harnesses | |
| # KLEE's POSIX runtime uses klee-uclibc, which is x86/x86_64-only. | |
| # Skip on arm64; re-enable if the runtime is ever ported. | |
| if: matrix.build-type == 'Debug' && matrix.sanitizers == 'OFF' && matrix.arch != 'arm64' | |
| continue-on-error: true | |
| env: | |
| KLEE_IMAGE: ghcr.io/lifting-bits/patchestry-klee-ubuntu-${{ matrix.image-version }}-llvm-${{ matrix.llvm-version }}:latest | |
| run: | | |
| # Pull KLEE image — skip this step if not available | |
| if ! docker pull "${KLEE_IMAGE}" 2>/dev/null; then | |
| echo "::warning::KLEE image not available — skipping KLEE execution tests" | |
| exit 0 | |
| fi | |
| VERIFIER="./builds/ci/tools/patchir-klee-verifier/Debug/patchir-klee-verifier" | |
| PASSED=0 | |
| FAILED=0 | |
| SKIPPED=0 | |
| for TEST_LL in ./test/patchir-klee-verifier/*.ll; do | |
| TEST_NAME="$(basename "${TEST_LL}" .ll)" | |
| # Extract --target-function from the RUN line | |
| TARGET_FN=$(grep -o '\-\-target-function [^ ]*' "${TEST_LL}" | head -1 | awk '{print $2}') | |
| if [ -z "${TARGET_FN}" ]; then | |
| echo "::warning::${TEST_NAME}: no --target-function found — skipping" | |
| SKIPPED=$((SKIPPED + 1)) | |
| continue | |
| fi | |
| echo "" | |
| echo "=== ${TEST_NAME} (target: ${TARGET_FN}) ===" | |
| WORK_DIR="/tmp/klee-ci/${TEST_NAME}" | |
| mkdir -p "${WORK_DIR}/klee-out" | |
| # Generate harness inside dev container | |
| if ! docker run --rm \ | |
| -v ${{ github.workspace }}:/workspace \ | |
| -v "${WORK_DIR}:/out" \ | |
| -w /workspace \ | |
| "$DEV_IMAGE" \ | |
| "${VERIFIER}" "${TEST_LL}" \ | |
| --target-function "${TARGET_FN}" \ | |
| -S -o /out/harness.ll 2>&1; then | |
| echo "::warning::${TEST_NAME}: harness generation failed" | |
| FAILED=$((FAILED + 1)) | |
| continue | |
| fi | |
| # Verify harness has main() | |
| if ! grep -q 'define i32 @main()' "${WORK_DIR}/harness.ll"; then | |
| echo "::warning::${TEST_NAME}: harness missing main()" | |
| FAILED=$((FAILED + 1)) | |
| continue | |
| fi | |
| # Assemble to bitcode inside KLEE container | |
| if ! docker run --rm --platform linux/amd64 \ | |
| -v "${WORK_DIR}:/work" \ | |
| --entrypoint llvm-as \ | |
| "${KLEE_IMAGE}" \ | |
| /work/harness.ll -o /work/harness.bc 2>&1; then | |
| echo "::warning::${TEST_NAME}: llvm-as failed" | |
| FAILED=$((FAILED + 1)) | |
| continue | |
| fi | |
| # Run KLEE (30s per harness) | |
| if ! docker run --rm --platform linux/amd64 \ | |
| -v "${WORK_DIR}:/work" \ | |
| "${KLEE_IMAGE}" \ | |
| --run-bitcode \ | |
| --input /work/harness.bc \ | |
| --output /work/klee-out \ | |
| --max-time 30 2>&1; then | |
| echo "::warning::${TEST_NAME}: KLEE exited with error" | |
| fi | |
| # Check results | |
| KLEE_OUT="${WORK_DIR}/klee-out/klee-last" | |
| TESTS=$(find "${KLEE_OUT}" -name '*.ktest' 2>/dev/null | wc -l) | |
| ERRORS=$(find "${KLEE_OUT}" -name '*.err' 2>/dev/null | wc -l) | |
| echo " Tests: ${TESTS}, Errors: ${ERRORS}" | |
| if [ "${TESTS}" -gt 0 ]; then | |
| echo " PASS" | |
| PASSED=$((PASSED + 1)) | |
| else | |
| echo "::warning::${TEST_NAME}: KLEE generated no test cases" | |
| FAILED=$((FAILED + 1)) | |
| fi | |
| done | |
| echo "" | |
| echo "=== KLEE Summary ===" | |
| echo " Passed: ${PASSED}" | |
| echo " Failed: ${FAILED}" | |
| echo " Skipped: ${SKIPPED}" | |
| if [ "${FAILED}" -gt 0 ]; then | |
| echo "::warning::${FAILED} KLEE test(s) failed" | |
| exit 1 | |
| fi | |
| qemu-runtime-validation: | |
| strategy: | |
| matrix: | |
| llvm-version: [22] | |
| image-version: [22.04] | |
| runs-on: ubuntu-${{ matrix.image-version }} | |
| timeout-minutes: 45 | |
| container: | |
| image: | |
| ghcr.io/lifting-bits/patchestry-ubuntu-${{ matrix.image-version }}-llvm-${{ matrix.llvm-version }}-dev:latest | |
| env: | |
| CMAKE_PREFIX_PATH: "/usr/lib/llvm-${{ matrix.llvm-version }}/lib/cmake/mlir/;/usr/lib/llvm-${{ matrix.llvm-version }}/lib/cmake/clang/" | |
| LLVM_EXTERNAL_LIT: "/usr/local/bin/lit" | |
| CI: true | |
| steps: | |
| - name: Clone the Patchestry repository | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: true | |
| fetch-depth: 1 | |
| - name: Free up disk space | |
| run: | | |
| apt-get clean || true | |
| rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* | |
| rm -rf /gha-runner/usr/local/lib/android || true | |
| rm -rf /gha-runner/usr/local/share/boost || true | |
| df -h | |
| - name: Install QEMU and Python runtime prerequisites | |
| run: | | |
| apt-get update | |
| DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ | |
| ca-certificates \ | |
| curl \ | |
| python3 \ | |
| python3-pip \ | |
| qemu-system-arm | |
| curl -LsSf https://astral.sh/uv/install.sh | sh | |
| echo "${HOME}/.local/bin" >> "${GITHUB_PATH}" | |
| - name: Configure build | |
| run: cmake --preset ci -DPE_USE_VENDORED_Z3=OFF -DLLVM_EXTERNAL_LIT="$(which lit)" -DLLVM_Z3_INSTALL_DIR=/usr/local | |
| - name: Build Debug | |
| run: cmake --build --preset ci --config Debug -j"$(nproc)" | |
| - name: Run QEMU firmware runtime validation | |
| run: | | |
| export PATH="${HOME}/.local/bin:${PATH}" | |
| export PATCHESTRY_LLVM_PREFIX="/usr/local/bin" | |
| export PATCHESTRY_LD_LLD="/usr/bin/ld.lld" | |
| python_bin="$(command -v python3)" | |
| qemu_system_arm_bin="$(command -v qemu-system-arm)" | |
| export PATCHESTRY_PYTHON="${python_bin}" | |
| export QEMU_SYSTEM_ARM="${qemu_system_arm_bin}" | |
| scripts/test-qemu-firmware-runtime.sh \ | |
| --build-type Debug \ | |
| --output-dir "${{ github.workspace }}/builds/ci/qemu-firmware-runtime" | |
| - name: Upload QEMU runtime artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: qemu-firmware-runtime | |
| path: ./builds/ci/qemu-firmware-runtime | |
| if-no-files-found: warn |