Skip to content

[Bug] search_strings misses some Rust string literals and does not say so #1659

Closed

Description

@CassieFox23

TL;DR: REA's string search only sees text that Ghidra has marked as a string. In stripped Rust files, many messages are missed. The list tool says so in its limits, but the search tool does not.

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Native analysis or decompilation

Observed behavior

I searched stripped Rust libraries for text I knew was inside them. The search found none of it, or a small part.

The message "projection cache ignored" is in amethyst_filegraph.abi3.so once. REA's search_strings returns an empty list for it. The file is the Rust module of Amethyst Mod Manager 2.5.3 (GPL-3.0-or-later). It is built with PyO3 0.27.2 and is stripped.

Here are more searches. Each count is case-insensitive, which is the default of search_strings. The file column comes from grep -a -o -i -F.

file       text                        in the file  REA search_strings
filegraph  projection cache ignored    1            0
filegraph  projection                  9            1
filegraph  filegraph                   32           4
loot       masterlist                  23           15
loot       libloot                     170          163

Plain Ghidra gives the same picture, so the partial list comes from Ghidra. I ran analyzeHeadless with default analysis and no REA script on the filegraph file. It defines 2,551 strings, and none of them contains "projection cache ignored". REA's list_strings returns 2,551 for the same file.

In these files, the text sits back to back with no end byte, and the length is in the code. For example, the panic message has its length of 0x1e loaded in the code. The bytes are all there.

A way in does exist if you already hold the address. A read of 48 bytes at 0x13f787 returns the message. The xrefs call at 0x13f787 returns two code sites, 0x31ab97 and 0x31b8f7. The disassembly has an lea to that spot at file offsets 0x21ab97 and 0x21b8f7. But nothing in REA finds 0x13f787 from the text.

Reproduction

The smallest case is a short Rust library of about 45 lines. No crates are needed.

  1. Save the source from the fold below as repro.rs.
  2. Build it: rustc --edition 2021 -C opt-level=3 --crate-type cdylib -C strip=symbols -o libreprolib.so repro.rs. I used rustc 1.97.0 (2d8144b78 2026-07-07). The bytes can differ on other versions.
  3. Check the text is there: strings -a libreprolib.so | grep -c "projection cache ignored" prints 2.
  4. Run rea search /abs/path/libreprolib.so "projection cache ignored" --kind strings --provider ghidra --format json. Over MCP, call open_binary with provider_id set to ghidra, then search_strings.
  5. The result is an empty list.

The same search for "reprolib" finds 2 strings. Both are export names, reprolib_describe and reprolib_enter. The file holds 9 copies of that word, and the rest sit in messages.

On the real file, get Amethyst Mod Manager 2.5.3 from its v2.5.3 release. The Flatpak is attached as AmethystModManager.flatpak. The file is /app/lib/python3.13/site-packages/amethyst_filegraph.abi3.so inside it. Its SHA-256 should be 288f8035cfd75c0262be2eb4735809b5a65797eb99642fbc56bf39222c99d919. I took my copy from an installed Flatpak and did not compare the release asset byte for byte (unverified).

The first import took about 90 seconds for the small library, and 4 to 5 minutes for the real file. After open_binary, later calls on the same MCP session answered in under a second.

Expected behavior

A caller who gets an empty list should know the list may be short. The list tool already carries the line Only Ghidra-defined string Data is observed. The search tool should carry it too.

The docs and the tool text should say what "complete" means here. It means complete over the strings Ghidra defined. It does not mean complete over the bytes in the file.

Impact and scope

  • Affected: search_strings and the CLI search --kind strings. A caller reads an empty list as "this text is not in the file".
  • Not affected: strings Ghidra does define. Most of the 170 "libloot" paths in the loot file were found. read_bytes and xrefs work when you hold an address.
  • Rust files are the case I saw. Other languages that pack text, such as Go, may behave the same. I have not tried them.

REA version

Main branch, at commit 4999e4b (#1651). Its package.json says 6.3.0. The released 6.3.0 package gave the same empty result on the filegraph file in an earlier run. I did not re-run it today.

Main has moved to 639b730 since then. The string code, docs and limits have not changed there. I checked with git diff.

Runtime and platform

Node 24.18.0; Nobara 44 (Fedora 44 base), Linux 7.2.9, x86-64; Ghidra 12.1.4 (Apache-2.0); JDK 21.0.12.1

Target and artifact context

ELF64 x86-64 shared objects, stripped, Rust with PyO3. One file is 4,642,008 bytes and one is 5,836,952 bytes. The third is the short library above.

Interface

MCP

Earliest failure stage

Transport or result projection

Relevant output or evidence

search_strings pattern="projection cache ignored" -> []
list_strings -> 2551 items, none contains the phrase
read_bytes 0x13f787, 48 bytes -> "&[filegraph] projection cache ignored: ..."
xrefs 0x13f787 -> ["0x31ab97", "0x31b8f7"]

Where the docs and code say this today:

  • The Strings row of the inventory table in docs/ghidra-provider.md says Only Ghidra-defined string Data is observed.
  • In src/ghidra/GhidraProviderCapabilities.ts, the list case carries that line. The search case carries only the regex line.
  • In src/contracts/officialToolContracts.ts, the search tool says its results are "complete over the native inventory".

Typed outcome or recovery expected

The search_strings result carries the same limit as list_strings. The limit could also name the way around it. That way is read_bytes plus xrefs, once the address is known.

Analysis or suspected cause

This part is read from source and from Ghidra's own counts. The REA bridge lists strings with getDefinedData(true) and keeps the ones with a string value. So the search can only see what Ghidra defined. I did not look at why Ghidra defines some Rust literals and not others. One idea is the packed text with no end byte. I have not checked it.

A tool that finds a byte pattern would help. That is a feature, and I can open it as its own issue if you prefer.

Fixture source
use std::cell::Cell;

thread_local! {
    static DEPTH: Cell<isize> = const { Cell::new(0) };
}

#[inline(never)]
fn work(name: &str) -> Result<usize, String> {
    if name.len() > 100 {
        Err(format!("[reprolib] projection cache ignored: {name}"))
    } else {
        Ok(name.len())
    }
}

#[no_mangle]
pub extern "C" fn reprolib_enter(name: *const u8, len: usize) -> isize {
    let s = unsafe { std::str::from_utf8_unchecked(std::slice::from_raw_parts(name, len)) };
    DEPTH.with(|d| {
        let n = d.get();
        if n < 0 {
            return -1;
        }
        d.set(n + 1);
        let r = match work(s) {
            Ok(v) => v as isize,
            Err(e) => {
                eprintln!("[reprolib] uncaught failure at ffi boundary: {e}");
                -2
            }
        };
        d.set(d.get() - 1);
        r
    })
}

#[no_mangle]
pub extern "C" fn reprolib_describe(kind: u32) -> *const u8 {
    match kind {
        0 => "[reprolib] cache directory is missing".as_ptr(),
        1 => "[reprolib] catalog is corrupt:".as_ptr(),
        2 => "[reprolib] database error:".as_ptr(),
        3 => "[reprolib] projection cache ignored: unreadable".as_ptr(),
        _ => "[reprolib] unknown".as_ptr(),
    }
}
Commands
export GHIDRA_INSTALL_DIR=/path/to/ghidra_12.1.4_PUBLIC JAVA_HOME=/path/to/jdk-21
export REA_GHIDRA_STARTUP_TIMEOUT_MS=600000

node scripts/rea.mjs search /abs/amethyst_filegraph.abi3.so "projection cache ignored" --kind strings --provider ghidra --format json

grep -a -o -i -F "projection cache ignored" /abs/amethyst_filegraph.abi3.so | wc -l
strings -a -t x /abs/amethyst_filegraph.abi3.so | grep -F "projection cache ignored"

The script P4Dump2.java counts Ghidra's defined strings and the raw byte matches for a list of terms. Run it with -postScript P4Dump2.java label PyInit_ "projection cache ignored,filegraph". I can share it if that helps.

Test setup

🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊

Activity

  1. added a commit that references this issue on Oct 11, 2026
    4733fc8
  2. CassieFox23 commented on Oct 11, 2026

    @CassieFox23
    ContributorAuthor

    TL;DR: The limit now shows on search_strings, as this issue asked, in 4733fc8. On Linux, the missing text still reproduces: Ghidra does not define it as a string, and the new limit says so.

    What I saw

    I built main at 4733fc8 and ran both the CLI and an MCP session (open_binary with provider_id set to ghidra, then search_strings and list_strings).

    The search result for "projection cache ignored" now carries this line, and so does the list_strings result:

    Only Ghidra-defined string Data is observed; packed or undefined string bytes may be absent. Completeness applies to that inventory, and an empty search does not establish that text is absent from the artifact. If an address is known, use read_bytes and xrefs to inspect bytes and references.
    

    Your test used a Mach-O file on macOS arm64. Mine are ELF files on Linux x86-64, so the two results can both be true. To repeat mine:

    rustc --version
    rustc 1.97.0 (2d8144b78 2026-07-07)
    rustc --edition 2021 -C opt-level=3 --crate-type cdylib -C strip=symbols -o libreprolib.so repro.rs
    

    The source is the "Fixture source" fold in the issue body. The bytes can differ on other rustc versions.

    File Phrase in the file Ghidra alone: defined strings that hold it REA search_strings Limit line on the result
    libreprolib.so (built as above) 2 0 empty list, CLI and MCP yes
    amethyst_filegraph.abi3.so 1 0 empty list, CLI and MCP yes

    "Phrase in the file" counts raw byte matches in the loaded image, found by a Ghidra memory search. For the fixture, strings -a agrees: 2 lines. "Ghidra alone" is analyzeHeadless with default analysis and a short script, with no REA code.

    The fixture has 214 defined strings. The word "reprolib" has 9 raw copies and 2 defined ones, both export names. REA finds those 2.

    Your new test file, tests/boundary/mcp/ghidraInventoryMcp.test.ts, passes on main: 2 tests, exit code 0. Main has since moved to 69a8b3a. That commit does not touch the string inventory code.

    Not run

    • Hopper and IDA providers.
    • A Mach-O build of the fixture on macOS.
    • Other rustc versions, or other Linux builds of the same code.
    • read_bytes and xrefs again on this commit. The issue body has those results.
    • The loot file, and the full test suite.

    Thanks, morluto, for the fix.

    Test setup
    • Box: Nobara 44 (Fedora 44 base), Linux 7.2.9
    • CPU: AMD Ryzen 9 5900X, 12 cores / 24 threads
    • RAM: 32 GB DDR4-3600
    • Node: 24.18.0
    • Ghidra: 12.1.4 (Apache-2.0) with JDK 21.0.12.1
    • Compiler: rustc 1.97.0
    • Head: 4733fc8

    🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊

  3. added
    bugSomething isn't working
    area: nativeNative analysis and decompilation, including Ghidra, IDA, and Hopper analysis behavior
    on Oct 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: nativeNative analysis and decompilation, including Ghidra, IDA, and Hopper analysis behaviorbugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions