Files

289 lines
8.3 KiB
Rust
Raw Permalink Normal View History

//! 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)
2026-01-26 16:04:00 -08:00
.arg("90909090") // 4 NOP bytes
.arg("--program")
.arg(TEST_PROGRAM)
.run();
// Patching at code addresses may conflict with existing instructions in Ghidra
assert!(
2026-02-05 17:14:19 -08:00
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!(
2026-02-05 17:14:19 -08:00
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!(
2026-02-05 17:14:19 -08:00
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")
2026-01-26 16:04:00 -08:00
.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")
2026-01-26 16:04:00 -08:00
|| 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)
2026-01-26 16:04:00 -08:00
.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)
2026-01-26 16:04:00 -08:00
.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)"
);
}