Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions .github/workflows/function-identity.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
name: Function identity validation

on:
push:
branches: [fix/cpp-function-identity-clean]
pull_request:
paths:
- 'decbench/**'
- 'scripts/**'
- 'tests/**'
- '.github/workflows/function-identity.yml'
workflow_dispatch:

permissions:
contents: read

env:
DECBENCH_NO_CACHE: '1'
IDENTITY_MAIN: ae070b885b4e005ed37510a2c3445af6284dc2b9
RUST_JOERN_REV: 9fd97f50885202596b125ebd2504e64910874bcb

jobs:
regression:
runs-on: ubuntu-latest
timeout-minutes: 40
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Record exact checkout
run: |
mkdir -p evidence
git rev-parse HEAD HEAD^{tree} | tee evidence/checkout.txt
git status --porcelain | tee evidence/status.txt
- name: Install compilers and Python dependencies
run: |
sudo apt-get update
sudo apt-get install -y clang lld g++ gcc-arm-none-eabi g++-mingw-w64-x86-64
python -m pip install -e '.[dev]' r2pipe
- name: Build pinned Rust Joern
run: |
git clone https://github.com/Noelo-Lab/rust-joern "$RUNNER_TEMP/rust-joern"
git -C "$RUNNER_TEMP/rust-joern" checkout "$RUST_JOERN_REV"
cargo build --manifest-path "$RUNNER_TEMP/rust-joern/Cargo.toml" --release --locked
python -m pip install -e "$RUNNER_TEMP/rust-joern"
test -f "$RUNNER_TEMP/rust-joern/target/release/librust_joern.so"
echo "RUST_JOERN_LIBRARY=$RUNNER_TEMP/rust-joern/target/release/librust_joern.so" >> "$GITHUB_ENV"
- name: Record environment
run: |
python -m pip freeze > evidence/python-environment.txt
gcc --version > evidence/gcc.txt
arm-none-eabi-g++ --version > evidence/arm.txt
x86_64-w64-mingw32-g++ --version > evidence/mingw.txt
ld.lld --version > evidence/lld.txt
- name: Identity and consumer regressions
run: |
pytest -q --no-cov tests/test_cpp_identity*.py tests/test_cpp_support.py tests/test_pr91_review_boundaries.py tests/test_evalkit.py tests/test_materialized.py tests/test_report_extras.py tests/test_reeval_ged.py tests/test_arm_thumb_extraction.py --junitxml=evidence/focused.xml
- name: Lint review changes
run: |
ruff check decbench/utils/function_identity.py decbench/evalkit/export.py decbench/evalkit/ingest.py decbench/evalkit/resolve.py decbench/models/decompilation.py decbench/scoring/report_extras.py scripts/run_benchmark.py scripts/rebuild_function_data.py scripts/reeval_ged.py scripts/validate_function_identity.py tests/test_pr91_review_boundaries.py tests/test_cpp_identity_pe_probe.py
black --check decbench/utils/function_identity.py decbench/evalkit/export.py decbench/evalkit/ingest.py decbench/evalkit/resolve.py decbench/models/decompilation.py decbench/scoring/report_extras.py scripts/run_benchmark.py scripts/rebuild_function_data.py scripts/reeval_ged.py scripts/validate_function_identity.py tests/test_pr91_review_boundaries.py tests/test_cpp_identity_pe_probe.py
mypy --follow-imports=silent decbench/utils/function_identity.py decbench/evalkit/export.py decbench/evalkit/ingest.py decbench/evalkit/resolve.py scripts/validate_function_identity.py
- name: Full suite differential against pinned main
run: |
git worktree add --detach "$RUNNER_TEMP/identity-main" "$IDENTITY_MAIN"
set +e
(cd "$RUNNER_TEMP/identity-main" && PYTHONPATH="$RUNNER_TEMP/identity-main" python -m pytest -q --no-cov --junitxml="$GITHUB_WORKSPACE/evidence/main.xml") > evidence/main.log 2>&1
main_status=$?
PYTHONPATH="$GITHUB_WORKSPACE" python -m pytest -q --no-cov --junitxml=evidence/candidate.xml > evidence/candidate.log 2>&1
candidate_status=$?
set -e
printf 'main=%s\ncandidate=%s\n' "$main_status" "$candidate_status" | tee evidence/pytest-status.txt
test "$main_status" -le 1
test "$candidate_status" -le 1
python scripts/validate_function_identity.py compare evidence/main.xml evidence/candidate.xml --output evidence/differential.json
- uses: actions/upload-artifact@v4
if: always()
with:
name: function-identity-${{ github.sha }}
path: evidence/

leveldb:
runs-on: ubuntu-latest
timeout-minutes: 35
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- name: Install dependencies and Ghidra
run: |
python -m pip install -e . pyghidra
curl -L --fail -o "$RUNNER_TEMP/ghidra.zip" https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_12.0.4_build/ghidra_12.0.4_PUBLIC_20260303.zip
echo "c3b458661d69e26e203d739c0c82d143cc8a4a29d9e571f099c2cf4bda62a120 $RUNNER_TEMP/ghidra.zip" | sha256sum -c -
unzip -q "$RUNNER_TEMP/ghidra.zip" -d "$RUNNER_TEMP"
echo "GHIDRA_INSTALL_DIR=$RUNNER_TEMP/ghidra_12.0.4_PUBLIC" >> "$GITHUB_ENV"
- name: Build pinned LevelDB
run: |
git clone https://github.com/google/leveldb "$RUNNER_TEMP/leveldb"
git -C "$RUNNER_TEMP/leveldb" checkout 99b3c03b3284f5886f9ef9a4ef703d57373e61be
cmake -S "$RUNNER_TEMP/leveldb" -B "$RUNNER_TEMP/leveldb/build" -DCMAKE_BUILD_TYPE= -DCMAKE_CXX_FLAGS='-O0 -g -fno-builtin' -DLEVELDB_BUILD_TESTS=OFF -DLEVELDB_BUILD_BENCHMARKS=OFF -DBUILD_SHARED_LIBS=ON
cmake --build "$RUNNER_TEMP/leveldb/build" -j2
- name: Stripped cross-decompiler identity
env:
_JAVA_OPTIONS: '-Xmx4g -Djava.awt.headless=true'
run: |
mkdir -p evidence
git rev-parse HEAD HEAD^{tree} > evidence/checkout.txt
python scripts/validate_function_identity.py leveldb "$RUNNER_TEMP/leveldb/build/libleveldb.so.1.23.0" --output evidence/leveldb.json
- uses: actions/upload-artifact@v4
if: always()
with:
name: leveldb-identity-${{ github.sha }}
path: evidence/
3 changes: 2 additions & 1 deletion decbench/decompilers/raw/angr_raw.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
LineMapping,
VariableInfo,
)
from decbench.utils.function_identity import insert_function

_l = logging.getLogger(__name__)

Expand Down Expand Up @@ -141,7 +142,7 @@ def _dump() -> None:
_l.debug("angr-raw: failed to decompile %s: %s", func_name, e)

if func_result is not None:
decompiled_functions[func_name] = func_result
insert_function(decompiled_functions, func_result)
else:
failed_functions.append(func_name)
_dump()
Expand Down
7 changes: 4 additions & 3 deletions decbench/decompilers/raw/binja_raw.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
LineMapping,
VariableInfo,
)
from decbench.utils.function_identity import insert_function

_l = logging.getLogger(__name__)

Expand Down Expand Up @@ -149,8 +150,8 @@ def _dump() -> None:
bv = self._load(binary_path)
enumerated = self._enumerate(bv, elf_base, text_range, addr_targets)
if functions is not None:
requested = {n for (n, _a) in functions}
enumerated = [(n, a) for (n, a) in enumerated if n in requested]
requested_addrs = {a for (_n, a) in functions}
enumerated = [(n, a) for (n, a) in enumerated if a in requested_addrs]
enumerated = common.narrow_to_source(
enumerated,
function_names,
Expand All @@ -170,7 +171,7 @@ def _dump() -> None:
except Exception as e: # noqa: BLE001
_l.debug("binja-raw: failed to decompile %s: %s", func_name, e)
if func_result is not None:
decompiled_functions[func_name] = func_result
insert_function(decompiled_functions, func_result)
else:
failed_functions.append(func_name)
_dump()
Expand Down
3 changes: 2 additions & 1 deletion decbench/decompilers/raw/dewolf_raw.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
FunctionDecompilation,
VariableInfo,
)
from decbench.utils.function_identity import insert_function

_l = logging.getLogger(__name__)

Expand Down Expand Up @@ -291,7 +292,7 @@ def _consume(addrs_arg: str) -> None:
metadata=common.extract_metrics(code),
)
with lock:
decompiled_functions[name] = fd
insert_function(decompiled_functions, fd)
_dump()
elif kind == "fail":
with lock:
Expand Down
24 changes: 14 additions & 10 deletions decbench/decompilers/raw/ghidra_raw.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
LineMapping,
VariableInfo,
)
from decbench.utils.function_identity import insert_function

_l = logging.getLogger(__name__)

Expand Down Expand Up @@ -264,11 +265,11 @@ def _dump() -> None:
program = flat.getCurrentProgram()
image_base = int(program.getImageBase().getOffset())

requested = {n for (n, _a) in functions} if functions is not None else None
requested_addrs = {a for (_n, a) in functions} if functions is not None else None

enumerated = self._enumerate(program, elf_base, text_range, addr_targets)
if requested is not None:
enumerated = [(n, a) for (n, a) in enumerated if n in requested]
if requested_addrs is not None:
enumerated = [(n, a) for (n, a) in enumerated if a in requested_addrs]

enumerated = common.narrow_to_source(
enumerated,
Expand All @@ -281,11 +282,11 @@ def _dump() -> None:
ifc.openProgram(program)
monitor = ConsoleTaskMonitor()
timeout_s = int(self.config.function_timeout_seconds)
by_name = self._functions_by_name(program)
by_address = self._functions_by_address(program, elf_base)

for func_name, file_addr in enumerated:
func_result = None
g_func = by_name.get(func_name)
g_func = by_address.get(file_addr)
if g_func is not None:
try:
func_result = self._decompile_one(
Expand All @@ -305,7 +306,7 @@ def _dump() -> None:
e,
)
if func_result is not None:
decompiled_functions[func_name] = func_result
insert_function(decompiled_functions, func_result)
else:
failed_functions.append(func_name)
_dump()
Expand Down Expand Up @@ -370,12 +371,15 @@ def _enumerate(
return sorted(out, key=lambda x: x[1])

@staticmethod
def _functions_by_name(program: Any) -> dict[str, Any]:
"""Map (last-seen) function name -> Ghidra Function object."""
out: dict[str, Any] = {}
def _functions_by_address(program: Any, elf_base: int) -> dict[int, Any]:
"""Map binary-local function address -> Ghidra Function object."""
image_base = int(program.getImageBase().getOffset())
out: dict[int, Any] = {}
fm = program.getFunctionManager()
for g_func in fm.getFunctions(True):
out[g_func.getName()] = g_func
offset = int(g_func.getEntryPoint().getOffset())
file_addr = (offset - image_base) + elf_base
out[file_addr] = g_func
return out

def _decompile_one(
Expand Down
11 changes: 8 additions & 3 deletions decbench/decompilers/raw/ida_raw.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
LineMapping,
VariableInfo,
)
from decbench.utils.function_identity import insert_function

_l = logging.getLogger(__name__)

Expand Down Expand Up @@ -158,8 +159,12 @@ def _dump() -> None:
try:
enumerated = self._enumerate(elf_base, text_range, addr_targets)
if functions is not None:
requested = {n for (n, _a) in functions}
enumerated = [(n, a) for (n, a) in enumerated if n in requested]
requested_addrs = {address for (_name, address) in functions}
enumerated = [
(name, address)
for name, address in enumerated
if address in requested_addrs
]
enumerated = common.narrow_to_source(
enumerated,
function_names,
Expand All @@ -173,7 +178,7 @@ def _dump() -> None:
except Exception as e: # noqa: BLE001
_l.debug("ida-raw: failed to decompile %s: %s", func_name, e)
if func_result is not None:
decompiled_functions[func_name] = func_result
insert_function(decompiled_functions, func_result)
else:
failed_functions.append(func_name)
_dump()
Expand Down
38 changes: 29 additions & 9 deletions decbench/decompilers/raw/kuna_raw.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
FunctionDecompilation,
VariableInfo,
)
from decbench.utils.function_identity import insert_function

_l = logging.getLogger(__name__)

Expand Down Expand Up @@ -158,32 +159,36 @@ def _dump() -> None:
_l.error("kuna-raw failed on %s: %s", binary_path, e)
return self._error_result(binary_path, start, str(e))

records = {str(r.get("name") or ""): r for r in self._records(payload)}
records_by_address = self._records_by_address(payload)
enumerated = sorted(
(
(n, int(r.get("address") or 0))
for n, r in records.items()
(str(record.get("name") or ""), address)
for address, record in records_by_address.items()
if not common.should_skip_function(
n, int(r.get("address") or 0), text_range, addr_targets
str(record.get("name") or ""), address, text_range, addr_targets
)
),
key=lambda x: x[1],
)
if functions is not None:
requested = {n for (n, _a) in functions}
enumerated = [(n, a) for (n, a) in enumerated if n in requested]
requested_addrs = {address for (_name, address) in functions}
enumerated = [
(name, address)
for name, address in enumerated
if address in requested_addrs
]
enumerated = common.narrow_to_source(
enumerated, function_names, backend="kuna", binary_name=binary_path.name
)

for func_name, file_addr in enumerated:
fd = None
try:
fd = self._build_function(records[func_name], func_name, file_addr)
fd = self._build_function(records_by_address[file_addr], func_name, file_addr)
except Exception as e: # noqa: BLE001
_l.debug("kuna-raw: assembling %s failed: %s", func_name, e)
if fd is not None:
decompiled[func_name] = fd
insert_function(decompiled, fd)
else:
failed.append(func_name)
_dump()
Expand Down Expand Up @@ -285,6 +290,21 @@ def _records(payload: Any) -> list[dict[str, Any]]:
return list(payload.get("functions") or [])
return payload if isinstance(payload, list) else []

@classmethod
def _records_by_address(cls, payload: Any) -> dict[int, dict[str, Any]]:
"""Index kuna records by binary-local address without collapsing names."""
out: dict[int, dict[str, Any]] = {}
for record in cls._records(payload):
value = record.get("address")
if value is None:
continue
try:
address = int(value)
except (TypeError, ValueError):
continue
out[address] = record
return out

def _build_function(
self, rec: dict[str, Any], name: str, file_addr: int
) -> FunctionDecompilation | None:
Expand Down Expand Up @@ -399,4 +419,4 @@ def _error_result(
failed_functions=["all"],
extra={"error": err, "backend": "kuna", "via": "raw"},
),
)
)
8 changes: 5 additions & 3 deletions decbench/evalkit/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,15 @@ def _resolve_manifest(
failures.append(f"{project}/{opt}/{stem}: {e}")
continue
stems = resolve.source_stems(tree, opt, project)
addr_by_name = resolve.name_to_addr(path, names=names, stems=stems or None)
for fn in sorted(names - set(addr_by_name)):
from decbench.utils.function_identity import unresolved_manifest_keys

selected = resolve.manifest_functions(path, names=names, stems=stems or None)
for fn in sorted(unresolved_manifest_keys(selected, names)):
failures.append(
f"{project}/{opt}/{stem}::{fn}: no DWARF address "
f"(decl-file-filtered subprogram walk over {path.name})"
)
funcs = {fn: addr_by_name[fn] for fn in sorted(names & set(addr_by_name))}
funcs = {key: address for address, key in selected.items()}
if funcs:
bins.append(_KitBinary(project, opt, stem, path, funcs))
elif allow_unresolved:
Expand Down
Loading