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
2 changes: 1 addition & 1 deletion .github/actions/fetch-runtime/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ inputs:
description: Dependency bundle version.
required: true
platform:
description: Platform slug (linux-x64, linux-arm64, windows-x64, macos-arm64, macos-x64).
description: Platform slug (linux-x64, linux-arm64, windows-x64, windows-arm64, macos-arm64, macos-x64).
required: true
sdk-tag:
description: If set, also sparse-checkout the SDK headers at this tag and export IDA_SDK_DIR.
Expand Down
28 changes: 27 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ jobs:
- { runner: macos-15-intel, platform: macos-x64, version: 9.3.260421sp2, sdk-tag: v9.3.1-release }
- { runner: macos-14, platform: macos-arm64, version: 9.3.260421sp2, sdk-tag: v9.3.1-release }
- { runner: windows-2022, platform: windows-x64, version: 9.3.260421sp2, sdk-tag: v9.3.1-release }
- { runner: ubuntu-latest, platform: linux-x64, version: 9.4.260714, sdk-tag: v9.4.0-release }
- { runner: ubuntu-24.04-arm, platform: linux-arm64, version: 9.4.260714, sdk-tag: v9.4.0-release }
- { runner: macos-15-intel, platform: macos-x64, version: 9.4.260714, sdk-tag: v9.4.0-release }
- { runner: macos-14, platform: macos-arm64, version: 9.4.260714, sdk-tag: v9.4.0-release }
- { runner: windows-2022, platform: windows-x64, version: 9.4.260714, sdk-tag: v9.4.0-release }
- { runner: windows-11-arm, platform: windows-arm64, version: 9.4.260714, sdk-tag: v9.4.0-release }
# - { runner: ubuntu-latest, platform: linux-x64, version: 9.2.250908, sdk-tag: v9.2.0-release }
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
Expand All @@ -71,6 +77,10 @@ jobs:
AWS_REGION: auto
S3_ENDPOINT: ${{ secrets.S3_ENDPOINT }}
S3_BUCKET: ${{ secrets.S3_BUCKET }}
# clang-tidy publishes no win_arm64 wheel and its sdist won't build; the tidy and fmt
# steps skip Windows anyway. Job-level, not step-level: mise resolves tools lazily, so
# a later step re-attempts the install. `runner` is unavailable here, hence matrix.
MISE_DISABLE_TOOLS: ${{ startsWith(matrix.platform, 'windows') && 'pipx:clang-tidy,pipx:clang-format' || '' }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down Expand Up @@ -113,8 +123,24 @@ jobs:
CTCACHE_DIR: ${{ runner.temp }}/ctcache
CTCACHE_SAVE_OUTPUT: "1"
run: just tidy
# IDALOG is the runtime's own message log, and it survives a fatal exit -- the only channel
# that says *why* bring-up died, since the facade's exit trap does not cover init and stdout
# capture is a no-op on Windows. Explicit bash (pwsh mangles `$?`) and no `-q`, so cargo's
# own diagnostics and the real exit code both survive.
- name: Accept IDA EULA
run: cargo run -q -p idakit --example accept_eula
shell: bash
env:
IDALOG: ida-init.log
IDALOG_TIMESTAMPS: "1"
run: |
set +e
cargo run -p idakit --example accept_eula
rc=$?
set -e
echo "accept_eula exit=$rc"
echo "--- IDALOG ---"
cat ida-init.log || echo "(no IDALOG produced)"
exit $rc
# Fixtures are host-independent, so every platform fetches the same corpus tier; the
# exported manifest drives both the corpus matrix and the dedicated tests below.
- name: Fetch test corpus
Expand Down
2 changes: 2 additions & 0 deletions crates/idakit-sys/facade/cfg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
// construct it below); gen_cfg.h only forward-declares it.
#include "gen_bridge.h"

#include <allow_deprecated.hpp>

namespace gen {

// Builds the control-flow graph of the function at addr, owned by the returned handle; throws if
Expand Down
2 changes: 2 additions & 0 deletions crates/idakit-sys/facade/cfunc_shims.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

#include "cfunc_shims.h"

#include <allow_deprecated.hpp>

namespace {

// The decompiler is a plugin; init it once (idempotent) before any decompile_func call.
Expand Down
2 changes: 2 additions & 0 deletions crates/idakit-sys/facade/function.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

#include "gen_function.h"

#include <allow_deprecated.hpp>

namespace gen {

// Start address of the nth function in the functions list, or BADADDR when n is out of range.
Expand Down
2 changes: 2 additions & 0 deletions crates/idakit-sys/facade/hexrays.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
// below); gen_hexrays.h only forward-declares them.
#include "gen_bridge.h"

#include <allow_deprecated.hpp>

using namespace facade;

namespace gen {
Expand Down
2 changes: 2 additions & 0 deletions crates/idakit-sys/facade/qvec_bridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

#include "qvec_bridge.h"

#include <allow_deprecated.hpp>

// The recipe's "explicit instantiation to force the symbols" step (template class
// qvector<range_t>;) does NOT compile here: it instantiates *every* member, and
// qvector<T>::resize_noinit carries CASSERT(is_trivially_constructible<T>), which range_t
Expand Down
2 changes: 2 additions & 0 deletions crates/idakit-sys/facade/range.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
// it) and instantiates rust::Vec<range_t>; gen_range.h only forward-declares ChunkInfo.
#include "gen_bridge.h"

#include <allow_deprecated.hpp>

namespace gen {

// The entry (main) chunk of the function at addr; throws if there's no function there or it has
Expand Down
2 changes: 2 additions & 0 deletions crates/idakit-sys/facade/reference.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
// instantiates rust::Vec<XrefRec>; gen_reference.h only forward-declares XrefRec.
#include "gen_bridge.h"

#include <allow_deprecated.hpp>

namespace gen {

// Every cross-reference edge to (is_to) or from addr, collected into an owned Vec; empty if there
Expand Down
2 changes: 2 additions & 0 deletions crates/idakit-sys/facade/segment.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

#include "gen_seg.h"

#include <allow_deprecated.hpp>

namespace gen {

// The segment's comment (repeatable or regular) at index n; throws when n is out of range or that
Expand Down
2 changes: 2 additions & 0 deletions crates/idakit-sys/facade/typewalk_bridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
#include "gen_facade_consts.h" // gen::NONE
#include "gen_visitors.h"

#include <allow_deprecated.hpp>

namespace bridge {

namespace {
Expand Down
117 changes: 95 additions & 22 deletions crates/idakit/src/claim.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
//!
//! `g_main` is private, so we recover its address by decoding the PC-relative load
//! in `is_main_thread`'s prologue instead of hardcoding an offset: `mov register,
//! [rip+disp32]` on x86-64, an `adrp`+`ldr` pair on aarch64.
//! [rip+disp32]` on x86-64, an `adrp` plus a load on aarch64.

use std::ffi::c_void;
use std::ptr;
Expand Down Expand Up @@ -100,40 +100,99 @@ unsafe fn follow_jmp_thunk(entry: *const u8) -> *const u8 {
entry
}

/// aarch64: decodes `is_main_thread`'s `&g_main` materialization, an `adrp Xd, page` +
/// `ldr Xt, [Xd, off]` pair.
/// aarch64: decodes `is_main_thread`'s `&g_main` materialization, an `adrp Xd, page` followed by
/// a load through it.
///
/// A stack-save prologue precedes it, so this scans a short window for the first such pair:
/// `g_main = (adrp_pc & !0xfff) + (page << 12) + off`.
/// Clang loads straight through the `adrp` destination; MSVC splits the address across
/// `adrp`+`add` and loads through the sum, so a forwarding `add` is followed before the load. A
/// stack-save prologue precedes either, hence the scan. On Windows the function's address is an
/// import thunk; [`follow_import_thunk`] resolves it to the body.
#[cfg(target_arch = "aarch64")]
fn decode_g_main(entry: *const u8) -> Result<*const u8, String> {
const WINDOW: usize = 8;

// SAFETY: `entry` is a mapped code pointer; a thunk and its slot are mapped (see fn doc).
let entry = unsafe { follow_import_thunk(entry) };

// SAFETY: `entry` is a mapped executable function; its first WINDOW 4-byte
// instructions lie within the body.
let insns: [u32; WINDOW] = unsafe { ptr::read(entry.cast()) };

for (i, &adrp) in insns.iter().enumerate() {
// ADRP: bit31=1, bits28..24=10000 (mask 0x9f00_0000 -> 0x9000_0000).
if adrp & 0x9f00_0000 != 0x9000_0000 {
let Some((mut reg, mut base)) = adrp_page(entry.wrapping_add(i * 4) as u64, adrp) else {
continue;
}
let rd = adrp & 0x1f;
let imm = i64::from((((adrp >> 5) & 0x7_ffff) << 2) | ((adrp >> 29) & 0x3));
let page = (imm ^ 0x10_0000) - 0x10_0000; // sign-extend the 21-bit page count
let adrp_pc = entry.wrapping_add(i * 4) as u64;
let base = ((adrp_pc & !0xfff) as i64 + (page << 12)) as u64;

// First following `ldr Xt, [Xd, off]` (64-bit unsigned offset, mask
// 0xffc0_0000 -> 0xf940_0000) that dereferences Xd carries the offset.
for &ldr in &insns[i + 1..] {
if ldr & 0xffc0_0000 != 0xf940_0000 || (ldr >> 5) & 0x1f != rd {
continue;
};
for &insn in &insns[i + 1..] {
if let Some(off) = ldr_offset(insn, reg) {
return Ok(base.wrapping_add(off) as *const u8);
}
if let Some((dst, imm)) = add_imm(insn, reg) {
reg = dst;
base = base.wrapping_add(imm);
}
let off = u64::from((ldr >> 10) & 0xfff) * 8; // imm12, scaled by access size
return Ok(base.wrapping_add(off) as *const u8);
}
}
Err("is_main_thread has no adrp+ldr g_main load in its prologue".to_owned())
Err(format!(
"no g_main load in is_main_thread prologue {insns:08x?}"
))
}

/// The destination register and page address of an `adrp Xd, page` sited at `pc`.
#[cfg(target_arch = "aarch64")]
fn adrp_page(pc: u64, insn: u32) -> Option<(u32, u64)> {
if insn & 0x9f00_0000 != 0x9000_0000 {
return None;
}
let imm = i64::from((((insn >> 5) & 0x7_ffff) << 2) | ((insn >> 29) & 0x3));
let page = (imm ^ 0x10_0000) - 0x10_0000; // sign-extend the 21-bit page count
Some((insn & 0x1f, ((pc & !0xfff) as i64 + (page << 12)) as u64))
}

/// The destination register and immediate of a 64-bit `add Xd, Xn, #imm` based on `rn`.
#[cfg(target_arch = "aarch64")]
fn add_imm(insn: u32, rn: u32) -> Option<(u32, u64)> {
if insn & 0xff80_0000 != 0x9100_0000 || (insn >> 5) & 0x1f != rn {
return None;
}
let shift = if insn & 0x0040_0000 == 0 { 0 } else { 12 }; // `sh` scales the immediate by a page
Some((insn & 0x1f, u64::from((insn >> 10) & 0xfff) << shift))
}

/// The byte offset of a 64-bit `ldr Xt, [Xn, #off]` based on `rn`.
#[cfg(target_arch = "aarch64")]
fn ldr_offset(insn: u32, rn: u32) -> Option<u64> {
if insn & 0xffc0_0000 != 0xf940_0000 || (insn >> 5) & 0x1f != rn {
return None;
}
Some(u64::from((insn >> 10) & 0xfff) * 8) // imm12, scaled by access size
}

/// Follows an ARM64 import thunk (`adrp Xd, page`; `ldr Xd, [Xd, off]`; `br Xd`) to its target.
///
/// On Windows the address of an imported function is such a thunk rather than the body; the real
/// address lives in the slot the `ldr` reads. Elsewhere the address is already the body, whose
/// prologue does not tail-jump, so `entry` is returned unchanged. Requiring the `br` through the
/// adrp's own register keeps an ordinary prologue, or an ELF PLT entry (which stages through x16
/// then x17), from matching.
///
/// # Safety
/// `entry` must be a mapped code pointer. When it is a thunk, its three instructions and the slot
/// it references (both in the same image) must be mapped, which holds for a real thunk.
#[cfg(target_arch = "aarch64")]
unsafe fn follow_import_thunk(entry: *const u8) -> *const u8 {
// SAFETY: caller guarantees `entry` is a mapped code pointer, so its first 12 bytes read.
let [adrp, ldr, br]: [u32; 3] = unsafe { ptr::read(entry.cast()) };
let Some((rd, base)) = adrp_page(entry as u64, adrp) else {
return entry;
};
if ldr & 0x1f != rd || br != (0xd61f_0000 | (rd << 5)) {
return entry;
}
match ldr_offset(ldr, rd) {
// SAFETY: a real thunk's slot is a mapped, loader-bound pointer in the same image.
Some(off) => unsafe { *(base.wrapping_add(off) as *const *const u8) },
None => entry,
}
}

#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
Expand Down Expand Up @@ -277,4 +336,18 @@ mod aarch64_tests {
let w = [0u32; 8];
assert!(decode_g_main(w.as_ptr().cast::<u8>()).is_err());
}

/// The real Windows ARM64 prologue: `adrp x8` / `add x20, x8, #0x620` / `ldr x19, [x20, #8]`.
/// The load's base is the `add`'s destination, not the `adrp`'s, so the pair alone misses it.
#[test]
fn decodes_the_forwarding_add_form() {
const ADRP_X8: u32 = 0xB001_46E8;
const ADD_X20_X8: u32 = 0x9118_8114;
const LDR_X19_X20_8: u32 = 0xF940_0693;

let (reg, base) = adrp_page(0x1_006b_5c58, ADRP_X8).expect("adrp x8");
let (reg, imm) = add_imm(ADD_X20_X8, reg).expect("add x20, x8");
let off = ldr_offset(LDR_X19_X20_8, reg).expect("ldr x19, [x20]");
assert!(base + imm + off == 0x1_02f9_2628);
}
}
2 changes: 2 additions & 0 deletions crates/idakit/src/kernel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ impl IdaConfig {
// pump too so no stray panic can unwind and kill the kernel.
let _ = catch_unwind(AssertUnwindSafe(|| job(&mut idb)));
}
// A database left open at exit deadlocks 9.4's atexit flush.
idb.close(false);
})
.expect("spawn kernel thread");

Expand Down
11 changes: 11 additions & 0 deletions crates/idakit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,8 @@ use crate::kernel::KernelClaim;
pub struct Database {
/// Interior mutability lets `decompile(&self)` init Hex-Rays lazily.
hexrays_ready: Cell<bool>,
/// Whether a database is currently open.
is_open: Cell<bool>,
/// `Some` for an in-place `Database`; `None` for the actor's, whose claim `run` holds.
_claim: Option<KernelClaim>,
/// Suppresses the auto `Send`/`Sync` impls. `Send` is re-granted below by a guarded
Expand Down Expand Up @@ -369,6 +371,8 @@ impl Database {
reason: reason.unwrap_or_else(|| format!("open failed (status {rc})")),
});
}
// Set before auto_wait: a trap there still leaves the database open.
self.is_open.set(true);
// run_auto only enables the analysis queue; block until it drains so callers
// observe a fully analyzed database. Analysis runs kernel code, so it can trap too.
if run_auto && self.auto_wait() == sys::EXIT_TRAPPED {
Expand All @@ -383,6 +387,7 @@ impl Database {
pub(crate) fn new() -> Self {
Self {
hexrays_ready: Cell::new(false),
is_open: Cell::new(false),
_claim: None,
_not_sync: PhantomData,
}
Expand All @@ -392,6 +397,7 @@ impl Database {
pub(crate) fn owned(claim: KernelClaim) -> Self {
Self {
hexrays_ready: Cell::new(false),
is_open: Cell::new(false),
_claim: Some(claim),
_not_sync: PhantomData,
}
Expand All @@ -400,12 +406,17 @@ impl Database {
/// Close the current database, optionally saving analysis back to the `.i64`.
#[doc(alias("close_database"))]
pub fn close(&mut self, save: bool) {
// Closing with no database open aborts on 9.4.
if !self.is_open.get() {
return;
}
// Both are per-database, so the next open must not inherit them: a stale `hexrays_ready`
// skips `hexrays_init` for a database that never had it, and the cfunc cache is keyed by
// address, which means nothing across two different programs.
self.clear_decompilation_cache();
self.close_database(save);
self.hexrays_ready.set(false);
self.is_open.set(false);
}

/// Opens `path`, runs `f` against the open database, and closes it (without saving) on
Expand Down