fix: Add Gson dependency for headless script support

GhydraMCPServer.java imports Gson but headless scripts run in a
separate OSGi classloader that can't access extension lib JARs.

Fix: Download gson-2.13.1.jar to Framework/Generic/lib/ where it's
available to all scripts regardless of execution mode.

Closes issue documented in BUG_REPORT_HEADLESS_GSON.md
This commit is contained in:
Ryan Malloy 2026-01-26 03:28:33 -07:00
parent f640df70ca
commit ac06111288
2 changed files with 246 additions and 0 deletions

115
BUG_REPORT_HEADLESS_GSON.md Normal file
View File

@ -0,0 +1,115 @@
# Bug Report: Docker Headless Mode Fails - Missing Gson Dependency
## Summary
The GhydraMCP Docker container fails to start the HTTP API server because `GhydraMCPServer.java` imports Gson, but Gson is not available in Ghidra's headless script classpath.
## Environment
- GhydraMCP Docker image: `ghydramcp:latest`
- Ghidra Version: 11.4.2
- Build Date: 2025-08-26
## Steps to Reproduce
1. Build the Docker image:
```bash
docker build -t ghydramcp:latest -f docker/Dockerfile .
```
2. Run with a binary:
```bash
docker run -p 8192:8192 -v /path/to/binary:/binaries/test ghydramcp:latest /binaries/test
```
3. Check logs:
```bash
docker logs <container_id>
```
## Expected Behavior
Container should start and expose HTTP API on port 8192.
## Actual Behavior
Analysis completes but the script fails to load:
```
INFO REPORT: Analysis succeeded for file: file:///binaries/cardv (HeadlessAnalyzer)
ERROR REPORT SCRIPT ERROR: GhydraMCPServer.java : The class could not be found.
It must be the public class of the .java file: Failed to get OSGi bundle containing script:
/opt/ghidra/scripts/GhydraMCPServer.java (HeadlessAnalyzer)
```
The health check fails because the HTTP server never starts:
```json
{"healthy":false,"port":8192,"error":"[Errno 111] Connection refused"}
```
## Root Cause Analysis
`GhydraMCPServer.java` (lines 22-24) imports Gson:
```java
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
```
However:
1. Gson is **not** bundled with Ghidra
2. The GhydraMCP extension JAR includes Gson, but headless scripts run in a **separate OSGi classloader** without access to extension lib dependencies
3. The Dockerfile doesn't copy Gson to Ghidra's script classpath
## Verification
```bash
# Check if Gson is in the built extension
unzip -l target/GhydraMCP-*.zip | grep -i gson
# Result: No matches
# Check Ghidra's lib directories
ls /opt/ghidra/Ghidra/Framework/*/lib/ | grep -i gson
# Result: No matches
```
## Proposed Solutions
### Option 1: Bundle Gson JAR with Scripts (Recommended)
Add Gson JAR to Ghidra's script classpath in Dockerfile:
```dockerfile
# Download Gson and add to Ghidra lib
RUN curl -fsSL "https://repo1.maven.org/maven2/com/google/gson/gson/2.10.1/gson-2.10.1.jar" \
-o /opt/ghidra/Ghidra/Framework/Generic/lib/gson-2.10.1.jar
```
### Option 2: Use Built-in JSON (No External Dependencies)
Rewrite `GhydraMCPServer.java` to use only JDK classes:
- Replace Gson with `javax.json` or manual JSON string building
- This ensures the script works without any external dependencies
### Option 3: Pre-compiled Script JAR
Compile `GhydraMCPServer.java` with Gson into a JAR and place it in the extension, then reference it differently in headless mode.
## Impact
- **Severity**: High - Docker deployment is completely broken
- **Affected**: All users attempting to use Docker/headless mode
- **Workaround**: None currently (must use GUI mode)
## Additional Context
The main GhydraMCP plugin works fine in GUI mode because the extension's lib dependencies are loaded. This only affects the headless Docker workflow where scripts are loaded separately from the extension.
---
**Reported by**: Firmware analysis session
**Date**: 2026-01-26
**Binary being analyzed**: WOLFBOX G850 dashcam `cardv` (ARM 32-bit)

131
docker/Dockerfile Normal file
View File

@ -0,0 +1,131 @@
# GhydraMCP Docker Image
# Ghidra + GhydraMCP Plugin pre-installed for headless binary analysis
#
# Build: docker build -t ghydramcp:latest -f docker/Dockerfile .
# Run: docker run -p 8192:8192 -v /path/to/binaries:/binaries ghydramcp:latest
ARG GHIDRA_VERSION=11.4.2
ARG GHIDRA_DATE=20250826
# =============================================================================
# Stage 1: Build the GhydraMCP plugin
# =============================================================================
FROM eclipse-temurin:21-jdk-jammy AS builder
ARG GHIDRA_VERSION
ARG GHIDRA_DATE
# Install build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
unzip \
maven \
git \
&& rm -rf /var/lib/apt/lists/*
# Download and extract Ghidra
WORKDIR /opt
RUN curl -fsSL "https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${GHIDRA_VERSION}_build/ghidra_${GHIDRA_VERSION}_PUBLIC_${GHIDRA_DATE}.zip" \
-o ghidra.zip \
&& unzip -q ghidra.zip \
&& rm ghidra.zip \
&& mv ghidra_${GHIDRA_VERSION}_PUBLIC ghidra
ENV GHIDRA_HOME=/opt/ghidra
# Copy GhydraMCP source and build
WORKDIR /build
COPY pom.xml .
COPY src ./src
# Build the plugin (skip git-commit-id plugin since .git isn't in Docker context)
RUN mvn clean package -P plugin-only -DskipTests \
-Dmaven.gitcommitid.skip=true \
-Dghidra.generic.jar=${GHIDRA_HOME}/Ghidra/Framework/Generic/lib/Generic.jar \
-Dghidra.softwaremodeling.jar=${GHIDRA_HOME}/Ghidra/Framework/SoftwareModeling/lib/SoftwareModeling.jar \
-Dghidra.project.jar=${GHIDRA_HOME}/Ghidra/Framework/Project/lib/Project.jar \
-Dghidra.docking.jar=${GHIDRA_HOME}/Ghidra/Framework/Docking/lib/Docking.jar \
-Dghidra.decompiler.jar=${GHIDRA_HOME}/Ghidra/Features/Decompiler/lib/Decompiler.jar \
-Dghidra.utility.jar=${GHIDRA_HOME}/Ghidra/Framework/Utility/lib/Utility.jar \
-Dghidra.base.jar=${GHIDRA_HOME}/Ghidra/Features/Base/lib/Base.jar
# =============================================================================
# Stage 2: Runtime image with Ghidra + GhydraMCP
# =============================================================================
# NOTE: Ghidra requires JDK (not JRE) - it checks for javac in LaunchSupport
FROM eclipse-temurin:21-jdk-jammy AS runtime
ARG GHIDRA_VERSION
ARG GHIDRA_DATE
LABEL org.opencontainers.image.title="ghydramcp" \
org.opencontainers.image.description="Ghidra + GhydraMCP Plugin for AI-assisted reverse engineering" \
org.opencontainers.image.source="https://github.com/starsong-consulting/GhydraMCP" \
org.opencontainers.image.licenses="Apache-2.0"
# Install runtime dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
unzip \
fontconfig \
libfreetype6 \
&& rm -rf /var/lib/apt/lists/*
# Create non-root user
RUN groupadd -g 1001 ghidra && useradd -u 1001 -g ghidra -m -s /bin/bash ghidra
# Download and extract Ghidra (in runtime stage for cleaner image)
WORKDIR /opt
RUN curl -fsSL "https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${GHIDRA_VERSION}_build/ghidra_${GHIDRA_VERSION}_PUBLIC_${GHIDRA_DATE}.zip" \
-o ghidra.zip \
&& unzip -q ghidra.zip \
&& rm ghidra.zip \
&& mv ghidra_${GHIDRA_VERSION}_PUBLIC ghidra \
&& chown -R ghidra:ghidra /opt/ghidra
ENV GHIDRA_HOME=/opt/ghidra
ENV PATH="${GHIDRA_HOME}:${PATH}"
# Install the GhydraMCP plugin
COPY --from=builder /build/target/GhydraMCP-*.zip /tmp/
RUN mkdir -p /opt/ghidra/Ghidra/Extensions \
&& unzip -q /tmp/GhydraMCP-*.zip -d /opt/ghidra/Ghidra/Extensions/ \
&& rm /tmp/GhydraMCP-*.zip \
&& chown -R ghidra:ghidra /opt/ghidra/Ghidra/Extensions/
# Create directories for projects, binaries, and scripts
RUN mkdir -p /projects /binaries /home/ghidra/.ghidra /opt/ghidra/scripts \
&& chown -R ghidra:ghidra /projects /binaries /home/ghidra /opt/ghidra/scripts
# Download Gson JAR for headless script support
# (GhydraMCPServer.java requires Gson but headless scripts can't access extension libs)
RUN curl -fsSL "https://repo1.maven.org/maven2/com/google/code/gson/gson/2.13.1/gson-2.13.1.jar" \
-o /opt/ghidra/Ghidra/Framework/Generic/lib/gson-2.13.1.jar \
&& chown ghidra:ghidra /opt/ghidra/Ghidra/Framework/Generic/lib/gson-2.13.1.jar
# Copy the GhydraMCP scripts
COPY docker/GhydraMCPServer.java /opt/ghidra/scripts/
COPY docker/ImportRawARM.java /opt/ghidra/scripts/
RUN chown -R ghidra:ghidra /opt/ghidra/scripts/ && chmod 644 /opt/ghidra/scripts/*.java
# Copy entrypoint script (755 so ghidra user can read and execute)
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod 755 /entrypoint.sh
# Switch to non-root user
USER ghidra
WORKDIR /home/ghidra
# Expose the GhydraMCP HTTP API port (and additional ports for multiple instances)
EXPOSE 8192 8193 8194 8195
# Default environment
ENV GHYDRA_MODE=headless
ENV GHYDRA_PORT=8192
ENV GHYDRA_MAXMEM=2G
# Healthcheck
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:${GHYDRA_PORT}/ || exit 1
ENTRYPOINT ["/entrypoint.sh"]