#9647·ghidra

Headless: analysis reports success when an analyzer throws, with no machine-readable signal of the failure

Author: Panchal-SahilCreated Sep 17, 2026Updated Sep 17, 2026
LabelsFeature: AnalysisStatus: Prioritize

Describe the bug

In a headless run (analyzeHeadless), when an auto-analyzer throws an unchecked RuntimeException, AutoAnalysisManager catches it, prints an ERROR + stack trace via the global Msg logger, and continues. The catch is reasonable; aborting a long run for one bad analyzer would be worse. The problem is that every machine-readable signal a headless or CI consumer checks then reports a successful analysis, and none records that an analyzer died.

One run, one injected analyzer whose added(...) throws:

Signal a headless consumer trusts Value
analyzeHeadless process exit code 0
GhidraProgramUtilities.isAnalyzed(program) true
REPORT: Analysis succeeded / REPORT: Import succeeded logged
AutoAnalysisManager.getMessageLog() no record of the analyzer failure

The console ERROR is the only trace, and a script cannot act on it: it carries no REPORT: line (the machine-consumable headless contract) and never reaches getMessageLog(), the per-program log handed to the failed analyzer's own added(...) and queried by a script afterward (the analyzer held that exact log; the swallow added nothing to it). So a pipeline gating on the exit code, REPORT:, isAnalyzed(), or getMessageLog() treats the program as fully analyzed, even if the dead analyzer was, say, a function-identification or reference analyzer.

To Reproduce

Confirmed on the prebuilt 12.1.3 release, no source build. Two scripts and one headless invocation.

  1. -preScript schedules a one-shot analyzer whose added(...) throws before touching the MessageLog:

    Analyzer throwing = new AbstractAnalyzer("ThrowingAnalyzer", "throws", AnalyzerType.BYTE_ANALYZER) {
        { setPriority(AnalysisPriority.LOW_PRIORITY); setDefaultEnablement(true); setSupportsOneTimeAnalysis(); }
        @Override public boolean canAnalyze(Program p) { return true; }
        @Override public boolean added(Program p, AddressSetView s, TaskMonitor m, MessageLog log)
                throws CancelledException {
            throw new RuntimeException("injected analyzer failure");
        }
    };
    AutoAnalysisManager.getAnalysisManager(currentProgram)
        .scheduleOneTimeAnalysis(throwing, currentProgram.getMemory());
    
  2. -postScript reads back the signals:

    println("isAnalyzed() = " + GhidraProgramUtilities.isAnalyzed(currentProgram));
    MessageLog log = AutoAnalysisManager.getAnalysisManager(currentProgram).getMessageLog();
    // the swallowed failure appears nowhere in the per-program analysis log
    println("getMessageLog() records the failure = " + log.toString().contains("injected analyzer failure"));
    
  3. Run on any small binary and check the exit code:

    analyzeHeadless <proj> p -import <binary> \
        -preScript ThrowingAnalyzerInjectorScript.java -postScript AnalysisIntegrityProbeScript.java
    echo "exit=$?"
    

Output (abridged): the injected analyzer logs ERROR Analysis Task: ThrowingAnalyzer ... (AnalysisTaskWrapper) java.lang.RuntimeException: injected analyzer failure, then REPORT: Analysis succeeded, isAnalyzed() = true, getMessageLog() records the failure = false, REPORT: Import succeeded, exit=0.

Expected behavior

At least one machine-readable signal should reflect that an analyzer failed, so a scripted consumer can detect the incomplete analysis without scraping console text: e.g. record the failure in the per-program getMessageLog() (alongside the global Msg.showError), and/or expose an "analysis completed with errors" status / a distinct REPORT: line / a non-zero exit code. I have not assumed which layer the fix belongs in and am happy to open a PR once you point at one.

Screenshots

N/A (headless CLI; relevant console output is under To Reproduce).

Attachments

None. The To Reproduce steps are self-contained. I can share the two helper scripts (a throwing-analyzer injector and an integrity probe) or a full run log if that would help.

Environment (please complete the following information):

  • OS: Fedora Linux 44
  • Java Version: OpenJDK 25 (runtime)
  • Ghidra Version: 12.1.3 (confirmed); code path unchanged on master HEAD 263160cf57
  • Ghidra Origin: official GitHub distro (ghidra_12.1.3_PUBLIC)

Additional context

Root cause on master HEAD 263160cf57:

  1. AutoAnalysisManager.AnalysisTaskWrapper.run: catch (RuntimeException th) (:663) routes to Msg.showError(...) (:672), returns normally, sets no failure flag, and never appends to getMessageLog().
  2. HeadlessAnalyzer.analyzeProgram: after mgr.startAnalysis(...) returns, logs REPORT: Analysis succeeded and calls markProgramAnalyzed(program) unconditionally (:1067-1068).
  3. GhidraProgramUtilities.isAnalyzed returns true (:115); the exit code stays 0 because the swallowed exception never reaches AnalyzeHeadless's top-level catch (Throwable).

The swallow is shared infrastructure, not one-shot-specific: the registered-analyzer path (AnalysisTask.applyTo) and the one-shot path (OneShotAnalysisCommand.applyTo) both catch only CancelledException, so an unchecked exception from either hits the same :663 catch. A registered analyzer that throws during the normal auto-analysis loop produces the same result; I can provide that variant if useful.

Source: NationalSecurityAgency/ghidra