diff --git a/pyproject.toml b/pyproject.toml index 4f06f42..7d43512 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] 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" readme = "README.md" requires-python = ">=3.11" diff --git a/src/mcghidra/mixins/docker.py b/src/mcghidra/mixins/docker.py index 54cdec6..f21e310 100644 --- a/src/mcghidra/mixins/docker.py +++ b/src/mcghidra/mixins/docker.py @@ -24,10 +24,10 @@ from fastmcp.contrib.mcp_mixin import mcp_tool from mcghidra.core.logging import logger from mcghidra.mixins.base import MCGhidraMixinBase -# Port pool configuration (32 ports should handle many concurrent sessions) -PORT_POOL_START = 8192 -PORT_POOL_END = 8223 -PORT_LOCK_DIR = Path("/tmp/mcghidra-ports") +# Port pool configuration — 128 ports by default, configurable via env vars +PORT_POOL_START = int(os.environ.get("MCGHIDRA_PORT_START", "8192")) +PORT_POOL_END = int(os.environ.get("MCGHIDRA_PORT_END", "8319")) +PORT_LOCK_DIR = Path(os.environ.get("MCGHIDRA_PORT_LOCK_DIR", "/tmp/mcghidra-ports")) class PortPool: @@ -216,7 +216,7 @@ class DockerMixin(MCGhidraMixinBase): with the MCGhidra plugin pre-installed. 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 - Docker label-based tracking for cross-process visibility - Automatic cleanup of orphaned containers @@ -561,7 +561,7 @@ class DockerMixin(MCGhidraMixinBase): plugin pre-installed. The binary will be imported and analyzed, 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 are auto-generated with the session ID to ensure uniqueness. @@ -588,6 +588,7 @@ class DockerMixin(MCGhidraMixinBase): # Clean up invalid characters in container name name = "".join(c if c.isalnum() or c in "-_" else "-" for c in name) + port = None try: # Check if container with this name already exists check_result = await self._run_docker_cmd( @@ -598,33 +599,39 @@ class DockerMixin(MCGhidraMixinBase): "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 - # This handles external containers (not managed by MCGhidra) using ports in our range - port = None - ports_tried = [] - for _ in range(PORT_POOL_END - PORT_POOL_START + 1): - candidate_port = self.port_pool.allocate(self.session_id) - if candidate_port is None: - break # Pool exhausted + # Allocate a port that's both lockable AND not in use by Docker. + # We HOLD flocks on Docker-occupied ports while searching, so + # allocate() advances past them. Release held ports after. + held_ports = [] # Ports we locked but can't use (Docker-occupied) + try: + for _ in range(PORT_POOL_END - PORT_POOL_START + 1): + 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 + # 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 Docker-occupied — hold the flock so allocate() + # skips it on the next iteration, then release after loop + held_ports.append(candidate_port) + continue - # Found a usable port! - port = candidate_port - break + # Found a usable port! + port = candidate_port + 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: return { - "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", + "error": f"Port pool exhausted ({PORT_POOL_START}-{PORT_POOL_END}). All ports are in use.", + "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(), } @@ -679,7 +686,8 @@ class DockerMixin(MCGhidraMixinBase): } except subprocess.CalledProcessError as e: - self.port_pool.release(port) + if port is not None: + self.port_pool.release(port) return {"error": f"Failed to start container: {e.stderr or e.stdout}"} @mcp_tool( @@ -945,7 +953,7 @@ class DockerMixin(MCGhidraMixinBase): 2. If not, allocates a port from the pool and starts a new container 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. After starting, poll docker_health(port) in a loop to check readiness.