Headless: analysis reports success when an analyzer throws, with no machine-readable signal of the failure
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.
-preScriptschedules a one-shot analyzer whoseadded(...)throws before touching theMessageLog: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());-postScriptreads 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"));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
masterHEAD263160cf57 - Ghidra Origin: official GitHub distro (
ghidra_12.1.3_PUBLIC)
Additional context
Root cause on master HEAD 263160cf57:
AutoAnalysisManager.AnalysisTaskWrapper.run:catch (RuntimeException th)(:663) routes toMsg.showError(...)(:672), returns normally, sets no failure flag, and never appends togetMessageLog().HeadlessAnalyzer.analyzeProgram: aftermgr.startAnalysis(...)returns, logsREPORT: Analysis succeededand callsmarkProgramAnalyzed(program)unconditionally (:1067-1068).GhidraProgramUtilities.isAnalyzedreturnstrue(:115); the exit code stays0because the swallowed exception never reachesAnalyzeHeadless's top-levelcatch (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