Fix port allocation to skip ports used by external Docker containers
Some checks failed
Build Ghidra Plugin / build (push) Has been cancelled

When port 8192 was already in use by a non-MCGhidra container (e.g.,
LTspice), docker_start would fail instead of trying the next port.
Now loops through the pool, checking each candidate against Docker's
published ports before using it.

Also includes Docker build retry improvements from earlier session.
This commit is contained in:
Ryan Malloy 2026-02-11 05:37:40 -07:00
parent 57f042a802
commit 112c1969c8
4 changed files with 42 additions and 23 deletions

View File

@ -25,8 +25,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
# Download and extract Ghidra # Download and extract Ghidra
WORKDIR /opt WORKDIR /opt
RUN curl -fsSL "https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${GHIDRA_VERSION}_build/ghidra_${GHIDRA_VERSION}_PUBLIC_${GHIDRA_DATE}.zip" \ # Download with retries and resume support for unreliable connections
-o ghidra.zip \ RUN for i in 1 2 3 4 5; do \
curl -fSL --http1.1 -C - \
"https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${GHIDRA_VERSION}_build/ghidra_${GHIDRA_VERSION}_PUBLIC_${GHIDRA_DATE}.zip" \
-o ghidra.zip && break || sleep 30; \
done \
&& unzip -q ghidra.zip \ && unzip -q ghidra.zip \
&& rm ghidra.zip \ && rm ghidra.zip \
&& mv ghidra_${GHIDRA_VERSION}_PUBLIC ghidra && mv ghidra_${GHIDRA_VERSION}_PUBLIC ghidra
@ -89,8 +93,12 @@ 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) # Download and extract Ghidra (in runtime stage for cleaner image)
WORKDIR /opt WORKDIR /opt
RUN curl -fsSL "https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${GHIDRA_VERSION}_build/ghidra_${GHIDRA_VERSION}_PUBLIC_${GHIDRA_DATE}.zip" \ # Download with retries and resume support for unreliable connections
-o ghidra.zip \ RUN for i in 1 2 3 4 5; do \
curl -fSL --http1.1 -C - \
"https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${GHIDRA_VERSION}_build/ghidra_${GHIDRA_VERSION}_PUBLIC_${GHIDRA_DATE}.zip" \
-o ghidra.zip && break || sleep 30; \
done \
&& unzip -q ghidra.zip \ && unzip -q ghidra.zip \
&& rm ghidra.zip \ && rm ghidra.zip \
&& mv ghidra_${GHIDRA_VERSION}_PUBLIC ghidra \ && mv ghidra_${GHIDRA_VERSION}_PUBLIC ghidra \

View File

@ -1,6 +1,6 @@
[project] [project]
name = "mcghidra" name = "mcghidra"
version = "2025.12.3" version = "2026.2.11"
description = "AI-assisted reverse engineering bridge: a multi-instance Ghidra plugin exposed via a HATEOAS REST API plus an MCP Python bridge for decompilation, analysis & binary manipulation" description = "AI-assisted reverse engineering bridge: a multi-instance Ghidra plugin exposed via a HATEOAS REST API plus an MCP Python bridge for decompilation, analysis & binary manipulation"
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
@ -22,7 +22,7 @@ requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = ["src/mcghidra"] packages = ["mcghidra"]
[tool.hatch.build] [tool.hatch.build]
sources = ["src"] sources = ["src"]

View File

@ -581,14 +581,6 @@ class DockerMixin(MCGhidraMixinBase):
if not binary_file.exists(): if not binary_file.exists():
return {"error": f"Binary not found: {binary_path}"} return {"error": f"Binary not found: {binary_path}"}
# Always allocate from pool to prevent conflicts between sessions
port = self.port_pool.allocate(self.session_id)
if port is None:
return {
"error": "Port pool exhausted (8192-8223). Stop some containers first.",
"allocated_ports": self.port_pool.get_allocated_ports(),
}
# Generate container name if not specified # Generate container name if not specified
if name is None: if name is None:
name = self._generate_container_name(binary_file.name) name = self._generate_container_name(binary_file.name)
@ -602,19 +594,38 @@ class DockerMixin(MCGhidraMixinBase):
["ps", "-a", "-q", "-f", f"name=^{name}$"], check=False ["ps", "-a", "-q", "-f", f"name=^{name}$"], check=False
) )
if check_result.stdout.strip(): if check_result.stdout.strip():
self.port_pool.release(port)
return { return {
"error": f"Container '{name}' already exists. Stop it first with docker_stop." "error": f"Container '{name}' already exists. Stop it first with docker_stop."
} }
# Check if port is already in use by a non-pool container # Allocate a port that's both lockable AND not in use by Docker
port_check = await self._run_docker_cmd( # This handles external containers (not managed by MCGhidra) using ports in our range
["ps", "-q", "-f", f"publish={port}"], check=False port = None
) ports_tried = []
if port_check.stdout.strip(): for _ in range(PORT_POOL_END - PORT_POOL_START + 1):
self.port_pool.release(port) candidate_port = self.port_pool.allocate(self.session_id)
if candidate_port is None:
break # Pool exhausted
# Check if this port is already in use by a Docker container
port_check = await self._run_docker_cmd(
["ps", "-q", "-f", f"publish={candidate_port}"], check=False
)
if port_check.stdout.strip():
# Port is in use by Docker - release and try next
ports_tried.append(candidate_port)
self.port_pool.release(candidate_port)
continue
# Found a usable port!
port = candidate_port
break
if port is None:
return { return {
"error": f"Port {port} is already in use by another container" "error": "Port pool exhausted (8192-8223). All ports are in use by Docker containers.",
"ports_checked": ports_tried if ports_tried else "all ports locked by other MCGhidra sessions",
"allocated_ports": self.port_pool.get_allocated_ports(),
} }
# Build label arguments # Build label arguments

2
uv.lock generated
View File

@ -572,7 +572,7 @@ wheels = [
[[package]] [[package]]
name = "mcghidra" name = "mcghidra"
version = "2025.12.3" version = "2026.2.11"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "fastmcp" }, { name = "fastmcp" },