mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 11:13:21 +00:00
* fix(ghidra): prevent loader relocations from constant folding Ghidra imports loader relocation destinations with their file bytes. A destination in a non-writable loaded block can be populated by the runtime loader instead, for example a copy-relocated musl `stderr` in RELRO, so the decompiler folded the on-disk zero and showed `fprintf(NULL, ...)`. Before initializing the decompiler, mark each loaded, initialized, non-writable block containing a relocation destination volatile in the ephemeral database, and add a limitation to procedures that read such a destination. ELF shared objects and PIEs are included; relocatable objects and core files are not. Permissions and source FileBytes are unchanged. Sessions without database-mutation authority (Windows P0 and existing caller projects) record the limitation but do not mark blocks volatile, and the analysis profile versions the two behaviors separately. A real Linux Ghidra ELF probe runs in the Ghidra bridge CI lane. Fixes #4177 Squashed from #4198 and rebased onto main. Rebase adaptations: the read-only gate, the verifier renamed to verify-ghidra-loader-relocation so it stays out of the manual-verifier glob, docs moved to docs/testing-lanes.md and docs/providers/ghidra-provider.md. Co-Authored-By: gensokyo-dev <gensokyo-dev@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ghidra): await installation inspection in the relocation verifier inspectGhidraInstallation became async on main after #4198's base, so the verifier asserted on a pending Promise. Also wait for provider output to close before reading it, like the sibling Ghidra verifiers, and keep the workflow reference inside the docs site. Co-Authored-By: gensokyo-dev <gensokyo-dev@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ghidra): address relocation review findings - Version the existing-project analysis profile with the read-only relocation semantics, since those sessions now add the limitation. - Inspect the installation through the verifier helper that owns the file-reader resources. - Build the fixture with -fPIC: with -fPIE, GCC 12 emits a copy relocation of rea_external_value into writable .bss, so the probe's GOT check could not hold. - Run probe cleanup through withVerifierCleanup so a cleanup failure does not hide the probe failure, and keep the workspace when owned Ghidra cleanup is incomplete. - Link the workflow with a full GitHub URL, per the docs guide. Co-Authored-By: gensokyo-dev <gensokyo-dev@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(ghidra): reach the external data through the GOT with a weak extern Building the whole fixture with -fPIC also turned the reader's direct load of rea_loader_pointer into a GOT-relaxed lea plus two loads, and the real-Ghidra lane then reported that the baseline no longer folds the file constant. Keep -fPIE and declare only rea_external_value weak: GCC and clang must reach a possibly absent symbol through a GOT entry, which lands as R_X86_64_GLOB_DAT inside GNU_RELRO (checked with readelf for GCC 12.2 and clang/lld). Co-Authored-By: gensokyo-dev <gensokyo-dev@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ghidra): protect only relocations Ghidra could not apply Running the real-Ghidra lane locally (Ghidra 12.1.4, JDK 21, GCC 12) showed that marking every non-writable block that holds a relocation volatile also marks full-RELRO .got. External calls then decompile as opaque pointer calls (`(*pcVar2)(param_1)` through `PTR_rea_external_...` instead of `rea_external(param_1)`), which the probe's GOT-stability check caught. Ghidra applies GLOB_DAT and RELATIVE relocations, so those bytes already hold the loader's value and folding them is correct. The #4177 case is a copy relocation, which Ghidra imports as UNSUPPORTED with the file's zero in place. Protect only destinations whose relocation status is not APPLIED or APPLIED_OTHER. The fixture now reads a provider `const` object that the linker copies into .data.rel.ro, the #4177 shape. The probe requires that relocation to be unapplied and requires applied GOT relocations to stay non-volatile. Ghidra 12.1.4 already shows this x86-64 read by name without protection, so the probe records whether the baseline folds and compares before and after only when it does. The profile value becomes volatile-unapplied-relocation-blocks-v1. Co-Authored-By: gensokyo-dev <gensokyo-dev@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Arslan Basharat <arslanbasharat414@gmail.com> Co-authored-by: gensokyo-dev <gensokyo-dev@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
145 lines
8.5 KiB
YAML
145 lines
8.5 KiB
YAML
name: Ghidra bridge source compilation
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
workflow_call:
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
env:
|
|
HUSKY: "0"
|
|
|
|
jobs:
|
|
ghidra-bridge-sources:
|
|
name: Compile packaged Ghidra bridge sources
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 20
|
|
env:
|
|
GHIDRA_RELEASE_URL: https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_12.1.4_build/ghidra_12.1.4_PUBLIC_20260921.zip
|
|
GHIDRA_RELEASE_SHA256: ddac49f903da9d5bac833e5cc79395098b9c33cfd3279be5f31bd00387d2d4db
|
|
steps:
|
|
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
with:
|
|
node-version-file: .nvmrc
|
|
- name: Validate the SDK pin against the provider's verified release
|
|
run: node scripts/verify-ghidra-bridge-sources.mjs --check-sdk-pin >> "$GITHUB_ENV"
|
|
# The bridge probe imports only source-owned ARM, Thumb and MIPS bytes and never
|
|
# executes target code. All SDK/project state stays in runner storage.
|
|
- name: Acquire checksum-verified Ghidra SDK in isolated runner storage
|
|
shell: bash
|
|
run: |
|
|
ghidra_root="${RUNNER_TEMP}/rea-ghidra"
|
|
mkdir -p "${ghidra_root}"
|
|
curl -fsSL --retry 3 --write-out 'ghidra_download_bytes=%{size_download} ghidra_download_seconds=%{time_total}\n' -o "${ghidra_root}/ghidra.zip" "${GHIDRA_RELEASE_URL}"
|
|
echo "${GHIDRA_RELEASE_SHA256} ${ghidra_root}/ghidra.zip" | sha256sum -c -
|
|
unzip -q "${ghidra_root}/ghidra.zip" -d "${ghidra_root}"
|
|
rm "${ghidra_root}/ghidra.zip"
|
|
properties="$(find "${ghidra_root}" -path '*/Ghidra/application.properties' -print -quit)"
|
|
test -n "${properties}"
|
|
echo "GHIDRA_INSTALL_DIR=$(dirname "$(dirname "${properties}")")" >> "${GITHUB_ENV}"
|
|
# JAVA_HOME_<major>_X64 comes from the runner image, not the env context.
|
|
- name: Compile bridge sources with the verified runner JDK
|
|
shell: bash
|
|
run: |
|
|
java_home_variable="JAVA_HOME_${GHIDRA_JAVA_MAJOR}_X64"
|
|
test -n "${!java_home_variable}"
|
|
JAVA_HOME="${!java_home_variable}" npm run verify:ghidra:bridge-sources -- --verified-sdk
|
|
- name: Install locked dependencies for loader relocation regression
|
|
run: npm ci
|
|
- name: Verify loader relocation handling with the native decompiler
|
|
shell: bash
|
|
run: |
|
|
java_home_variable="JAVA_HOME_${GHIDRA_JAVA_MAJOR}_X64"
|
|
export JAVA_HOME="${!java_home_variable}"
|
|
npm run verify:ghidra:loader-relocation
|
|
- name: Inspect real ARM, Thumb and MIPS instruction context through the bridge
|
|
shell: bash
|
|
run: |
|
|
java_home_variable="JAVA_HOME_${GHIDRA_JAVA_MAJOR}_X64"
|
|
export JAVA_HOME="${!java_home_variable}"
|
|
probe_root="${RUNNER_TEMP}/rea-arm-context"
|
|
mkdir -p "${probe_root}/project"
|
|
printf '\036\377\057\341\160\107\000\000' > "${probe_root}/arm-thumb.bin"
|
|
timeout 180s "${GHIDRA_INSTALL_DIR}/support/analyzeHeadless" "${probe_root}/project" ArmContext \
|
|
-import "${probe_root}/arm-thumb.bin" -loader BinaryLoader -processor ARM:LE:32:v7 \
|
|
-noanalysis -readOnly -deleteProject \
|
|
-scriptPath "${GITHUB_WORKSPACE}/tests/conformance/ghidra;${GITHUB_WORKSPACE}/bridge/ghidra" \
|
|
-postScript ReaInstructionContextProbe.java "${GITHUB_WORKSPACE}/bridge/ghidra" \
|
|
> "${probe_root}/output.log" 2>&1 || { cat "${probe_root}/output.log"; exit 1; }
|
|
cat "${probe_root}/output.log"
|
|
grep -F 'REA_ARM_CONTEXT_OK arm=0 thumb=1 unavailable=null (injected provider value)' "${probe_root}/output.log"
|
|
mkdir -p "${probe_root}/mips-project"
|
|
# MIPS32 addiu v0,a0,3; MIPS16 li v0,90. These bytes are never executed.
|
|
printf '\003\000\202\044\132\152\000\000' > "${probe_root}/mips.bin"
|
|
timeout 180s "${GHIDRA_INSTALL_DIR}/support/analyzeHeadless" "${probe_root}/mips-project" MipsContext \
|
|
-import "${probe_root}/mips.bin" -loader BinaryLoader -processor MIPS:LE:32:default \
|
|
-noanalysis -readOnly -deleteProject \
|
|
-scriptPath "${GITHUB_WORKSPACE}/tests/conformance/ghidra;${GITHUB_WORKSPACE}/bridge/ghidra" \
|
|
-postScript ReaInstructionContextProbe.java "${GITHUB_WORKSPACE}/bridge/ghidra" mips \
|
|
> "${probe_root}/mips-output.log" 2>&1 || { cat "${probe_root}/mips-output.log"; exit 1; }
|
|
cat "${probe_root}/mips-output.log"
|
|
grep -F 'REA_MIPS_CONTEXT_OK standard=0 mips16=1' "${probe_root}/mips-output.log"
|
|
# Each probe imports source-owned bytes at 0x1000, away from the Cortex-M
|
|
# vector symbols that ARMCortex.pspec defines at 0, and decodes them from
|
|
# the language's default context, so M-profile Thumb state must hold at
|
|
# the first byte. The bytes are never executed.
|
|
- name: Decode M-profile ARM and Xtensa bytes in their default context
|
|
shell: bash
|
|
run: |
|
|
java_home_variable="JAVA_HOME_${GHIDRA_JAVA_MAJOR}_X64"
|
|
export JAVA_HOME="${!java_home_variable}"
|
|
probe_root="${RUNNER_TEMP}/rea-decode"
|
|
mkdir -p "${probe_root}"
|
|
# mrs r0, msp; bx lr
|
|
printf '\357\363\010\200\160\107' > "${probe_root}/cortex.bin"
|
|
# mrs r0, msplim (ARMv8-M only); bx lr
|
|
printf '\357\363\012\200\160\107' > "${probe_root}/v8m.bin"
|
|
# entry a1, 32; retw.n
|
|
printf '\066\101\000\035\360' > "${probe_root}/xtensa.bin"
|
|
decode() {
|
|
local name="$1" language="$2"
|
|
shift 2
|
|
mkdir -p "${probe_root}/${name}-project"
|
|
timeout 180s "${GHIDRA_INSTALL_DIR}/support/analyzeHeadless" "${probe_root}/${name}-project" Decode \
|
|
-import "${probe_root}/${name}.bin" -loader BinaryLoader -loader-baseAddr 0x1000 \
|
|
-processor "${language}" -noanalysis -readOnly -deleteProject \
|
|
-scriptPath "${GITHUB_WORKSPACE}/tests/conformance/ghidra" \
|
|
-postScript ReaDecodeProbe.java "${language}" "$@" \
|
|
> "${probe_root}/${name}.log" 2>&1 || { cat "${probe_root}/${name}.log"; exit 1; }
|
|
cat "${probe_root}/${name}.log"
|
|
grep -F "REA_DECODE_OK ${language} $*" "${probe_root}/${name}.log"
|
|
}
|
|
decode cortex ARM:LE:32:Cortex mrs/4/msp bx/2/lr
|
|
decode v8m ARM:LE:32:v8-m mrs/4/msplim bx/2/lr
|
|
decode xtensa Xtensa:LE:32:default entry/3/a1 retw.n/2
|
|
# A call through a writable slot keeps Ghidra's recorded target as an
|
|
# observed callee while callee coverage stays partial; a direct call is
|
|
# the complete control. The source-owned x86-64 bytes are never executed.
|
|
- name: Keep computed-call targets non-exhaustive in callee coverage
|
|
shell: bash
|
|
run: |
|
|
java_home_variable="JAVA_HOME_${GHIDRA_JAVA_MAJOR}_X64"
|
|
export JAVA_HOME="${!java_home_variable}"
|
|
probe_root="${RUNNER_TEMP}/rea-computed-call"
|
|
mkdir -p "${probe_root}/project"
|
|
{
|
|
printf '\270\001\000\000\000\303\314\314' # 0x1000: mov eax, 1; ret
|
|
printf '\270\002\000\000\000\303\314\314' # 0x1008: mov eax, 2; ret
|
|
printf '\377\025\052\000\000\000\303\314' # 0x1010: call [rip + 0x2a]; ret
|
|
printf '\377\320\303\314\314\314\314\314' # 0x1018: call rax; ret
|
|
printf '\350\333\377\377\377\303\314\314' # 0x1020: call 0x1000; ret
|
|
printf '\314%.0s' $(seq 24) # 0x1028: int3 padding
|
|
printf '\000\020\000\000\000\000\000\000' # 0x1040: slot = 0x1000
|
|
} > "${probe_root}/computed-call.bin"
|
|
test "$(wc -c < "${probe_root}/computed-call.bin")" -eq 72
|
|
timeout 180s "${GHIDRA_INSTALL_DIR}/support/analyzeHeadless" "${probe_root}/project" ComputedCall \
|
|
-import "${probe_root}/computed-call.bin" -loader BinaryLoader -loader-baseAddr 0x1000 \
|
|
-processor x86:LE:64:default -noanalysis -readOnly -deleteProject \
|
|
-scriptPath "${GITHUB_WORKSPACE}/tests/conformance/ghidra;${GITHUB_WORKSPACE}/bridge/ghidra" \
|
|
-postScript ReaComputedCallCoverageProbe.java "${GITHUB_WORKSPACE}/bridge/ghidra" \
|
|
> "${probe_root}/output.log" 2>&1 || { cat "${probe_root}/output.log"; exit 1; }
|
|
cat "${probe_root}/output.log"
|
|
grep -F 'REA_COMPUTED_CALL_COVERAGE_OK slot=observed-complete-discovery-unknown register=partial direct=complete callers=discovery-unknown' "${probe_root}/output.log"
|