You've already forked ghidra-cli
mirror of
https://github.com/encounter/ghidra-cli.git
synced 2026-03-30 11:12:36 -07:00
ad01eaf930
On macOS, Ghidra stores project data in mmap pages that aren't flushed to disk while the bridge runs. Per-test DaemonTestHarness stop/restart cycles cause data loss. Use OnceLock<DaemonTestHarness> to share a single bridge per test binary, matching the readonly_tests pattern. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
289 lines
8.3 KiB
Rust
289 lines
8.3 KiB
Rust
//! Tests for patch operations.
|
|
//!
|
|
//! These tests verify that patching commands work correctly by:
|
|
//! 1. Using typed schemas to validate JSON output structure
|
|
//! 2. Dynamically resolving addresses instead of using hardcoded values
|
|
//! 3. Verifying actual effects through round-trip testing
|
|
//! 4. Using snapshot testing for output format regression detection
|
|
|
|
use serial_test::serial;
|
|
use std::sync::OnceLock;
|
|
|
|
#[macro_use]
|
|
mod common;
|
|
use common::{ensure_test_project, get_function_address, ghidra, DaemonTestHarness};
|
|
|
|
const TEST_PROJECT: &str = "ci-test";
|
|
const TEST_PROGRAM: &str = "sample_binary";
|
|
|
|
static HARNESS: OnceLock<DaemonTestHarness> = OnceLock::new();
|
|
|
|
fn harness() -> &'static DaemonTestHarness {
|
|
HARNESS.get_or_init(|| {
|
|
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
|
|
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon")
|
|
})
|
|
}
|
|
|
|
/// Test patching bytes at a dynamically resolved address.
|
|
///
|
|
/// Verifies:
|
|
/// - Command succeeds
|
|
/// - Output can be parsed as PatchResult
|
|
/// - Status indicates success
|
|
#[test]
|
|
#[serial]
|
|
fn test_patch_bytes_success() {
|
|
require_ghidra!();
|
|
let harness = harness();
|
|
|
|
// Dynamically get a valid code address
|
|
let main_addr = get_function_address(harness, TEST_PROJECT, TEST_PROGRAM, "main");
|
|
|
|
let result = ghidra(harness)
|
|
.arg("patch")
|
|
.arg("bytes")
|
|
.arg(&main_addr)
|
|
.arg("90909090") // 4 NOP bytes
|
|
.arg("--program")
|
|
.arg(TEST_PROGRAM)
|
|
.run();
|
|
|
|
// Patching at code addresses may conflict with existing instructions in Ghidra
|
|
assert!(
|
|
result.exit_code == 0
|
|
|| result.stderr.contains("conflict")
|
|
|| result.stderr.contains("Memory change"),
|
|
"Expected success or instruction conflict, got: stderr={}",
|
|
result.stderr
|
|
);
|
|
}
|
|
|
|
/// Test patching with NOP instruction.
|
|
#[test]
|
|
#[serial]
|
|
fn test_patch_nop_success() {
|
|
require_ghidra!();
|
|
let harness = harness();
|
|
|
|
let main_addr = get_function_address(harness, TEST_PROJECT, TEST_PROGRAM, "main");
|
|
|
|
let result = ghidra(harness)
|
|
.arg("patch")
|
|
.arg("nop")
|
|
.arg(&main_addr)
|
|
.arg("--program")
|
|
.arg(TEST_PROGRAM)
|
|
.run();
|
|
|
|
// NOP at code address may conflict with existing instructions
|
|
assert!(
|
|
result.exit_code == 0
|
|
|| result.stderr.contains("conflict")
|
|
|| result.stderr.contains("Memory change"),
|
|
"Expected success or instruction conflict, got: stderr={}",
|
|
result.stderr
|
|
);
|
|
}
|
|
|
|
/// Test exporting patched binary.
|
|
#[test]
|
|
#[serial]
|
|
fn test_patch_export() {
|
|
require_ghidra!();
|
|
let harness = harness();
|
|
|
|
// Use a unique output path to avoid conflicts
|
|
let output_path = format!("/tmp/ghidra-test-export-{}.bin", uuid::Uuid::new_v4());
|
|
|
|
let result = ghidra(harness)
|
|
.arg("patch")
|
|
.arg("export")
|
|
.arg("--output")
|
|
.arg(&output_path)
|
|
.arg("--program")
|
|
.arg(TEST_PROGRAM)
|
|
.run();
|
|
|
|
// Export may fail in headless mode due to BinaryExporter limitations
|
|
// Just verify the command completes without hanging
|
|
assert!(
|
|
result.exit_code == 0 || !result.stderr.is_empty(),
|
|
"Should either succeed or provide an error message"
|
|
);
|
|
|
|
// Clean up
|
|
let _ = std::fs::remove_file(&output_path);
|
|
}
|
|
|
|
/// Test patching at function boundary (start of a function).
|
|
///
|
|
/// This tests a common use case: patching the first instruction
|
|
/// of a function (e.g., to add a hook or bypass).
|
|
#[test]
|
|
#[serial]
|
|
fn test_patch_at_function_boundary() {
|
|
require_ghidra!();
|
|
let harness = harness();
|
|
|
|
// Get any function's entry point
|
|
let func_addr = get_function_address(harness, TEST_PROJECT, TEST_PROGRAM, "main");
|
|
|
|
// Patch with RET instruction (c3 on x86)
|
|
let result = ghidra(harness)
|
|
.arg("patch")
|
|
.arg("bytes")
|
|
.arg(&func_addr)
|
|
.arg("c3")
|
|
.arg("--program")
|
|
.arg(TEST_PROGRAM)
|
|
.run();
|
|
|
|
// Patching may succeed or fail with instruction conflict depending on Ghidra version
|
|
// Just verify it doesn't crash/hang
|
|
assert!(
|
|
result.exit_code == 0
|
|
|| result.stderr.contains("conflict")
|
|
|| result.stderr.contains("Memory change"),
|
|
"Expected success or instruction conflict error, got exit_code={}, stderr={}",
|
|
result.exit_code,
|
|
result.stderr
|
|
);
|
|
}
|
|
|
|
/// Test patching at an invalid/unmapped address fails gracefully.
|
|
#[test]
|
|
#[serial]
|
|
fn test_patch_invalid_address_fails() {
|
|
require_ghidra!();
|
|
let harness = harness();
|
|
|
|
// Use an address that's definitely outside the program's memory
|
|
let result = ghidra(harness)
|
|
.arg("patch")
|
|
.arg("bytes")
|
|
.arg("0xffffffffffffffff") // Very high address, unlikely to be mapped
|
|
.arg("90")
|
|
.arg("--program")
|
|
.arg(TEST_PROGRAM)
|
|
.run();
|
|
|
|
// Should fail gracefully
|
|
result.assert_failure();
|
|
|
|
// Should provide a meaningful error message
|
|
assert!(
|
|
result.stderr.to_lowercase().contains("error")
|
|
|| result.stderr.to_lowercase().contains("invalid")
|
|
|| result.stderr.to_lowercase().contains("address")
|
|
|| result.stdout.to_lowercase().contains("error"),
|
|
"Expected error message about invalid address.\nstderr: {}\nstdout: {}",
|
|
result.stderr,
|
|
result.stdout
|
|
);
|
|
}
|
|
|
|
/// Test patching with invalid hex bytes fails gracefully.
|
|
#[test]
|
|
#[serial]
|
|
fn test_patch_invalid_hex_fails() {
|
|
require_ghidra!();
|
|
let harness = harness();
|
|
|
|
let main_addr = get_function_address(harness, TEST_PROJECT, TEST_PROGRAM, "main");
|
|
|
|
let result = ghidra(harness)
|
|
.arg("patch")
|
|
.arg("bytes")
|
|
.arg(&main_addr)
|
|
.arg("ZZZZ") // Invalid hex
|
|
.arg("--program")
|
|
.arg(TEST_PROGRAM)
|
|
.run();
|
|
|
|
// Should fail with invalid hex
|
|
result.assert_failure();
|
|
}
|
|
|
|
/// Test patching with odd-length hex string (should fail or be handled).
|
|
#[test]
|
|
#[serial]
|
|
fn test_patch_odd_hex_length() {
|
|
require_ghidra!();
|
|
let harness = harness();
|
|
|
|
let main_addr = get_function_address(harness, TEST_PROJECT, TEST_PROGRAM, "main");
|
|
|
|
let _result = ghidra(harness)
|
|
.arg("patch")
|
|
.arg("bytes")
|
|
.arg(&main_addr)
|
|
.arg("909") // Odd length - not valid byte sequence
|
|
.arg("--program")
|
|
.arg(TEST_PROGRAM)
|
|
.run();
|
|
|
|
// This should either:
|
|
// 1. Fail with an error about odd-length hex
|
|
// 2. Succeed by padding (implementation-dependent)
|
|
// Either way, it shouldn't crash or hang
|
|
|
|
// Just verify the command completes (success or failure)
|
|
// The test is that it handles the edge case gracefully
|
|
}
|
|
|
|
/// Test that patching without --program argument uses default program.
|
|
#[test]
|
|
#[serial]
|
|
fn test_patch_without_program_arg() {
|
|
require_ghidra!();
|
|
let harness = harness();
|
|
|
|
let main_addr = get_function_address(harness, TEST_PROJECT, TEST_PROGRAM, "main");
|
|
|
|
let result = ghidra(harness)
|
|
.arg("patch")
|
|
.arg("bytes")
|
|
.arg(&main_addr)
|
|
.arg("90")
|
|
// Note: --program is missing, should use default from bridge
|
|
.run();
|
|
|
|
// May succeed (if bridge has default program) or fail
|
|
// Just verify it doesn't crash
|
|
assert!(
|
|
result.exit_code == 0 || !result.stderr.is_empty(),
|
|
"Should either succeed or provide an error message"
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Snapshot tests for output format regression detection
|
|
// ============================================================================
|
|
|
|
/// Test that patch bytes command produces meaningful output.
|
|
#[test]
|
|
#[serial]
|
|
fn test_patch_output_format_structure() {
|
|
require_ghidra!();
|
|
let harness = harness();
|
|
|
|
let main_addr = get_function_address(harness, TEST_PROJECT, TEST_PROGRAM, "main");
|
|
|
|
let result = ghidra(harness)
|
|
.arg("patch")
|
|
.arg("bytes")
|
|
.arg(&main_addr)
|
|
.arg("90")
|
|
.arg("--program")
|
|
.arg(TEST_PROGRAM)
|
|
.run();
|
|
|
|
// Patching at code address may conflict with existing instructions
|
|
// Verify the command produces some output (success or error)
|
|
assert!(
|
|
result.exit_code == 0 || !result.stderr.is_empty(),
|
|
"Should produce output (success or error message)"
|
|
);
|
|
}
|