Fix port allocation Groundhog Day loop, expand pool to 128 ports
Some checks are pending
Build Ghidra Plugin / build (push) Waiting to run

Port allocator would spin on the same Docker-occupied ports because
releasing a flock and re-calling allocate() restarts from port 8192.
Now holds flocks on occupied ports during the scan so allocate()
advances past them.

Also expands default pool from 32 to 128 ports (8192-8319), and
makes range configurable via MCGHIDRA_PORT_START/MCGHIDRA_PORT_END
environment variables.
This commit is contained in:
Ryan Malloy 2026-03-02 05:06:27 -07:00
parent 112c1969c8
commit f4cf1cef9e
2 changed files with 39 additions and 31 deletions

View File

@ -1,6 +1,6 @@
[project] [project]
name = "mcghidra" name = "mcghidra"
version = "2026.2.11" version = "2026.3.2"
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"

View File

@ -24,10 +24,10 @@ from fastmcp.contrib.mcp_mixin import mcp_tool
from mcghidra.core.logging import logger from mcghidra.core.logging import logger
from mcghidra.mixins.base import MCGhidraMixinBase from mcghidra.mixins.base import MCGhidraMixinBase
# Port pool configuration (32 ports should handle many concurrent sessions) # Port pool configuration — 128 ports by default, configurable via env vars
PORT_POOL_START = 8192 PORT_POOL_START = int(os.environ.get("MCGHIDRA_PORT_START", "8192"))
PORT_POOL_END = 8223 PORT_POOL_END = int(os.environ.get("MCGHIDRA_PORT_END", "8319"))
PORT_LOCK_DIR = Path("/tmp/mcghidra-ports") PORT_LOCK_DIR = Path(os.environ.get("MCGHIDRA_PORT_LOCK_DIR", "/tmp/mcghidra-ports"))
class PortPool: class PortPool:
@ -216,7 +216,7 @@ class DockerMixin(MCGhidraMixinBase):
with the MCGhidra plugin pre-installed. with the MCGhidra plugin pre-installed.
Supports multi-process environments with: Supports multi-process environments with:
- Dynamic port allocation from a pool (8192-8223) - Dynamic port allocation from a pool ({PORT_POOL_START}-{PORT_POOL_END})
- Session-scoped container naming with UUIDs - Session-scoped container naming with UUIDs
- Docker label-based tracking for cross-process visibility - Docker label-based tracking for cross-process visibility
- Automatic cleanup of orphaned containers - Automatic cleanup of orphaned containers
@ -561,7 +561,7 @@ class DockerMixin(MCGhidraMixinBase):
plugin pre-installed. The binary will be imported and analyzed, plugin pre-installed. The binary will be imported and analyzed,
then the HTTP API will be available. then the HTTP API will be available.
Ports are automatically allocated from the pool (8192-8223) to Ports are automatically allocated from the pool ({PORT_POOL_START}-{PORT_POOL_END}) to
prevent conflicts between concurrent sessions. Container names prevent conflicts between concurrent sessions. Container names
are auto-generated with the session ID to ensure uniqueness. are auto-generated with the session ID to ensure uniqueness.
@ -588,6 +588,7 @@ class DockerMixin(MCGhidraMixinBase):
# Clean up invalid characters in container name # Clean up invalid characters in container name
name = "".join(c if c.isalnum() or c in "-_" else "-" for c in name) name = "".join(c if c.isalnum() or c in "-_" else "-" for c in name)
port = None
try: try:
# Check if container with this name already exists # Check if container with this name already exists
check_result = await self._run_docker_cmd( check_result = await self._run_docker_cmd(
@ -598,10 +599,11 @@ class DockerMixin(MCGhidraMixinBase):
"error": f"Container '{name}' already exists. Stop it first with docker_stop." "error": f"Container '{name}' already exists. Stop it first with docker_stop."
} }
# Allocate a port that's both lockable AND not in use by Docker # Allocate a port that's both lockable AND not in use by Docker.
# This handles external containers (not managed by MCGhidra) using ports in our range # We HOLD flocks on Docker-occupied ports while searching, so
port = None # allocate() advances past them. Release held ports after.
ports_tried = [] held_ports = [] # Ports we locked but can't use (Docker-occupied)
try:
for _ in range(PORT_POOL_END - PORT_POOL_START + 1): for _ in range(PORT_POOL_END - PORT_POOL_START + 1):
candidate_port = self.port_pool.allocate(self.session_id) candidate_port = self.port_pool.allocate(self.session_id)
if candidate_port is None: if candidate_port is None:
@ -612,19 +614,24 @@ class DockerMixin(MCGhidraMixinBase):
["ps", "-q", "-f", f"publish={candidate_port}"], check=False ["ps", "-q", "-f", f"publish={candidate_port}"], check=False
) )
if port_check.stdout.strip(): if port_check.stdout.strip():
# Port is in use by Docker - release and try next # Port is Docker-occupied — hold the flock so allocate()
ports_tried.append(candidate_port) # skips it on the next iteration, then release after loop
self.port_pool.release(candidate_port) held_ports.append(candidate_port)
continue continue
# Found a usable port! # Found a usable port!
port = candidate_port port = candidate_port
break break
finally:
# Release all the Docker-occupied ports we held during the scan
for held in held_ports:
self.port_pool.release(held)
if port is None: if port is None:
return { return {
"error": "Port pool exhausted (8192-8223). All ports are in use by Docker containers.", "error": f"Port pool exhausted ({PORT_POOL_START}-{PORT_POOL_END}). All ports are in use.",
"ports_checked": ports_tried if ports_tried else "all ports locked by other MCGhidra sessions", "docker_occupied": held_ports if held_ports else [],
"hint": "Stop some containers with docker_stop or docker_cleanup.",
"allocated_ports": self.port_pool.get_allocated_ports(), "allocated_ports": self.port_pool.get_allocated_ports(),
} }
@ -679,6 +686,7 @@ class DockerMixin(MCGhidraMixinBase):
} }
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
if port is not None:
self.port_pool.release(port) self.port_pool.release(port)
return {"error": f"Failed to start container: {e.stderr or e.stdout}"} return {"error": f"Failed to start container: {e.stderr or e.stdout}"}
@ -945,7 +953,7 @@ class DockerMixin(MCGhidraMixinBase):
2. If not, allocates a port from the pool and starts a new container 2. If not, allocates a port from the pool and starts a new container
3. Returns connection info immediately 3. Returns connection info immediately
Ports are auto-allocated from the pool (8192-8223) to prevent Ports are auto-allocated from the pool ({PORT_POOL_START}-{PORT_POOL_END}) to prevent
conflicts between concurrent sessions. conflicts between concurrent sessions.
After starting, poll docker_health(port) in a loop to check readiness. After starting, poll docker_health(port) in a loop to check readiness.