Skip to content

decomp: add ARM interrupt handler detection and exception return intrinsic support #1527

decomp: add ARM interrupt handler detection and exception return intrinsic support

decomp: add ARM interrupt handler detection and exception return intrinsic support #1527

Workflow file for this run

#
# 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