fix(sandbox): stop port allocation at the TCP port limit (#6424)

Co-authored-by: Zhangchenhao-111 <285911435+Zhangchenhao-111@users.noreply.github.com>
This commit is contained in:
haochen 2026-10-07 17:26:57 +08:00 committed by GitHub
parent a9e5b6a853
commit db40811ac4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 73 additions and 3 deletions

View File

@ -687,6 +687,10 @@ When host Bash is enabled for Local Execution, DeerFlow starts OS detection with
For Docker development, service startup follows `config.yaml` sandbox mode. In Local/Docker modes, `provisioner` is not started.
Local AIO sandbox port allocation stops at TCP port 65535. If the remaining
ports from the configured starting port are occupied, allocation reports
that no port is available instead of attempting an out-of-range bind.
See the [Sandbox Configuration Guide](backend/docs/CONFIGURATION.md#sandbox) to configure your preferred mode.
Remote directory listings report traversal failures (for example, unreadable

View File

@ -1,3 +1,11 @@
### Port Allocation Bounds
`network.py::PortAllocator.allocate` caps its exclusive search endpoint at
65536. A valid start near 65535 must report exhaustion with `RuntimeError`
when all remaining ports are reserved or occupied, without probing invalid
TCP ports. Keep the last valid port allocatable and preserve `max_range`.
Regression coverage lives in `tests/test_port_allocator_bounds.py`.
### Goal Objective Validation
`goal_objective.py` owns the dependency-free normalized 4000-character objective

View File

@ -63,7 +63,7 @@ class PortAllocator:
Args:
start_port: The port number to start searching from.
max_range: Maximum number of ports to search.
max_range: Maximum number of ports to search, stopping at port 65535.
Returns:
An available port number.
@ -72,12 +72,13 @@ class PortAllocator:
RuntimeError: If no available port is found in the specified range.
"""
with self._lock:
for port in range(start_port, start_port + max_range):
end_port = min(start_port + max_range, 65536)
for port in range(start_port, end_port):
if self._is_port_available(port):
self._reserved_ports.add(port)
return port
raise RuntimeError(f"No available port found in range {start_port}-{start_port + max_range}")
raise RuntimeError(f"No available port found in range {start_port}-{end_port}")
def release(self, port: int) -> None:
"""Release a previously allocated port.

View File

@ -0,0 +1,57 @@
"""Port searches must stop at the largest valid TCP port."""
from unittest.mock import patch
import pytest
from deerflow.utils.network import PortAllocator
@pytest.mark.parametrize("start_port", [65534, 65535])
def test_reserved_tail_reports_exhaustion(start_port):
allocator = PortAllocator()
allocator._reserved_ports.update(range(start_port, 65536))
with pytest.raises(RuntimeError, match="No available port"):
allocator.allocate(start_port=start_port)
def test_occupied_tail_never_probes_invalid_port():
allocator = PortAllocator()
def bind(address):
if address[1] > 65535:
raise OverflowError("port must be 0-65535")
raise OSError("Address already in use")
with patch("deerflow.utils.network.socket.socket") as socket_factory:
sock = socket_factory.return_value.__enter__.return_value
sock.bind.side_effect = bind
with pytest.raises(RuntimeError, match="No available port"):
allocator.allocate(start_port=65535)
sock.bind.assert_called_once_with(("0.0.0.0", 65535))
assert not allocator._reserved_ports
def test_last_valid_port_can_be_allocated_and_released():
allocator = PortAllocator()
allocator._reserved_ports.add(65534)
with patch("deerflow.utils.network.socket.socket") as socket_factory:
sock = socket_factory.return_value.__enter__.return_value
with allocator.allocate_context(start_port=65534) as port:
assert port == 65535
assert port in allocator._reserved_ports
sock.bind.assert_called_once_with(("0.0.0.0", 65535))
assert port not in allocator._reserved_ports
assert allocator._reserved_ports == {65534}
def test_search_still_respects_max_range():
allocator = PortAllocator()
allocator._reserved_ports.update({8080, 8081})
with patch.object(allocator, "_is_port_available", return_value=False) as probe:
with pytest.raises(RuntimeError, match="No available port"):
allocator.allocate(start_port=8080, max_range=2)
assert [call.args[0] for call in probe.call_args_list] == [8080, 8081]