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]
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"

View File

@ -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.