How ipatool's Runtime Patch System Dynamically Patches Unicorn Library Functions

The runtime_patch system in ipatool loads the Unicorn CPU-emulation library into memory, searches for known 64-bit mask patterns that were truncated by the Windows 32-bit unsigned long type, flips the register-width bit in those masks, and persists the patched binary to disk for subsequent runs.

The open-source tool ipatool (by majd/ipatool) includes a sophisticated runtime patching mechanism that fixes compatibility issues in the bundled Unicorn emulation library without requiring users to rebuild from source. This system specifically addresses a Windows ARM64 architecture bug where the Tiny Code Generator (TCG) backend was compiled with incorrect 32-bit type assumptions.

How the Runtime Patch System Works

The patching pipeline operates in four distinct phases, each implemented across dedicated source files in internal/sap/unicorn/.

Step 1: Load the Binary into Memory

The entry point prepareRuntimeLibrary in runtime_library_windows_arm64.go reads the original Unicorn DLL using os.ReadFile, loading the entire binary into a byte slice for in-memory manipulation.

// From internal/sap/unicorn/runtime_library_windows_arm64.go
func prepareRuntimeLibrary(origPath string) (string, error) {
    data, err := os.ReadFile(origPath)
    if err != nil {
        return "", fmt.Errorf("read library: %w", err)
    }
    // ... patching logic follows
}

This approach avoids modifying the original distributed file and enables checksum validation before and after patching.

Step 2: Locate the Mask Patterns with Pattern Matching

The core search logic lives in runtime_patch.go. The patternOffsets function scans the binary image for two specific 8-byte sequences using bytes.Index in a loop to find all occurrences:

  • tcgMaskFirstPattern — the first mask constant
  • tcgMaskSecondPattern — the second mask constant (must appear exactly 24 bytes after its paired first pattern)
// From internal/sap/unicorn/runtime_patch.go
func patternOffsets(image []byte, pattern []byte) []int {
    var offsets []int
    start := 0
    for {
        idx := bytes.Index(image[start:], pattern)
        if idx == -1 {
            break
        }
        offset := start + idx
        offsets = append(offsets, offset)
        start = offset + 1
    }
    return offsets
}

The function collects every offset where each pattern appears, returning a slice of positions for validation in the next step.

Step 3: Apply the Register-Width Patch

The patchWindowsARM64TCGMasks function performs the actual bytecode modification. For each valid pair of offsets (where second offset = first offset + 24), it sets the high-order bit (0x80) in specific byte positions:

Byte Position Operation Effect
first+3 image[first+3] |= 0x80 Flips register-width bit in first mask
first+7 image[first+7] |= 0x80 Completes first mask correction
second+3 image[second+3] |= 0x80 Flips register-width bit in second mask
second+7 image[second+7] |= 0x80 Completes second mask correction

This surgical modification changes only the "register-width" flag without altering surrounding instruction bytes, preserving the original code structure while correcting the 64-bit semantics.

The patch validates two invariants before proceeding:

  • Exactly windowsARM64TCGPatternCount (16) matches must exist for each pattern
  • Spacing must be exactly 24 bytes between paired first and second patterns

Violation of either condition causes immediate abort with an error, protecting against application to unknown or corrupted builds.

Step 4: Persist and Cache the Patched Library

After successful modification, prepareRuntimeLibrary writes the patched image to a temporary file, syncs and closes it, then renames to libunicorn-windows-arm64.dll. A checksum of the patched binary is cached to eliminate redundant patching on subsequent executions.

// Simplified flow from runtime_library_windows_arm64.go
patchedPath := filepath.Join(cacheDir, "libunicorn-windows-arm64.dll")
if checksumMatches(patchedPath, expectedChecksum) {
    return patchedPath, nil // Already patched, skip work
}

// ... patch application ...

if err := installLibrary(tempFile, patchedPath); err != nil {
    return "", err
}

Integrating Patched Libraries in Go Code

The patching happens transparently during engine initialization. Application code never calls the patch functions directly:

// Load the original Windows-ARM64 Unicorn DLL.
origPath := "/path/to/unicorn.dll"
patchedPath, err := prepareRuntimeLibrary(origPath)
if err != nil {
    log.Fatalf("failed to prepare Unicorn library: %v", err)
}

// `patchedPath` now points to libunicorn-windows-arm64.dll
// with the ARM64 TCG masks correctly patched.
engine, err := unicorn.NewEngine(patchedPath)

In practice, ipatool's library_windows.go invokes prepareRuntimeLibrary automatically when constructing the emulation engine on Windows ARM64 hosts.

Source File Architecture

File Responsibility
internal/sap/unicorn/runtime_patch.go Pattern search (patternOffsets) and mask patching logic (patchWindowsARM64TCGMasks)
internal/sap/unicorn/runtime_library_windows_arm64.go DLL loading, checksum validation, patched library installation
internal/sap/unicorn/runtime_patch_test.go Unit tests verifying patch success on valid images and proper failure on malformed data
internal/sap/unicorn/library_windows.go Platform-specific loader integrating the patch system

Why This Approach Matters

Binary patching at runtime solves a distribution problem: ipatool can ship a single pre-compiled Unicorn library while still functioning correctly across architectures with incompatible ABI assumptions. The Windows ARM64 case specifically stems from the TCG backend using unsigned long (32-bit on Windows LLP64, 64-bit on Linux LP64) for mask calculations, causing silent truncation of 64-bit constants.

Rather than maintaining separate build pipelines or requiring users to compile from source, ipatool's runtime_patch system:

  • Detects the specific buggy pattern signatures
  • Applies minimal, verified corrections
  • Caches results for performance
  • Fails safely on unexpected binaries

This pattern of dynamic library patching demonstrates how Go-based tools can adapt native dependencies to heterogeneous deployment environments without sacrificing user experience.

Summary

  • Runtime patching enables architecture adaptation without source rebuilds or manual intervention
  • Pattern-based search locates specific bytecode sequences using bytes.Index loops in patternOffsets
  • Surgical bit-flipping corrects register-width flags at calculated byte offsets without disturbing surrounding instructions
  • Validation guards ensure the patch only applies to known-good binaries with expected pattern counts and spacing
  • Checksum caching eliminates redundant work across process restarts
  • Transparent integration means callers use standard engine initialization while the patch system handles compatibility automatically

Frequently Asked Questions

How does ipatool verify the patch won't corrupt the Unicorn library?

The patch system enforces two strict invariants: exactly 16 matches for each pattern (windowsARM64TCGPatternCount) and exact 24-byte spacing between paired patterns. If either check fails, patchWindowsARM64TCGMasks returns an error and the original binary remains unmodified. See internal/sap/unicorn/runtime_patch_test.go for test coverage of these failure modes.

Can the runtime_patch system be used for other platforms or libraries?

The current implementation is specialized for Windows ARM64 TCG masks, but the pattern-matching architecture in runtime_patch.go is generic. The patternOffsets function accepts arbitrary byte slices and patterns, making it reusable for similar binary-correction scenarios requiring runtime adaptation of pre-compiled libraries.

Why modify bytes at offset +3 and +7 specifically?

These positions correspond to the high-order byte of 32-bit words within the 64-bit mask constants. Setting bit 7 (0x80) at these offsets flips the effective register-width flag in the TCG's internal representation, promoting the truncated 32-bit values back to proper 64-bit semantics without changing the instruction encoding structure.

Where does the patched library get stored on disk?

prepareRuntimeLibrary writes to a platform-appropriate cache directory as libunicorn-windows-arm64.dll. The function uses atomic rename operations (write to temp, sync, close, then move) to prevent corrupted partial files, and checksum comparison prevents unnecessary re-patching on subsequent runs.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →