From db40811ac45146b3317c8a3af6c60c91aa464ae7 Mon Sep 17 00:00:00 2001 From: haochen <3033940900@qq.com> Date: Wed, 7 Oct 2026 17:26:57 +0800 Subject: [PATCH] fix(sandbox): stop port allocation at the TCP port limit (#6424) Co-authored-by: Zhangchenhao-111 <285911435+Zhangchenhao-111@users.noreply.github.com> --- README.md | 4 ++ .../packages/harness/deerflow/utils/AGENTS.md | 8 +++ .../harness/deerflow/utils/network.py | 7 ++- backend/tests/test_port_allocator_bounds.py | 57 +++++++++++++++++++ 4 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 backend/tests/test_port_allocator_bounds.py diff --git a/README.md b/README.md index 01f443b81d..a92f975468 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/backend/packages/harness/deerflow/utils/AGENTS.md b/backend/packages/harness/deerflow/utils/AGENTS.md index 98df8ca010..5a736942a1 100644 --- a/backend/packages/harness/deerflow/utils/AGENTS.md +++ b/backend/packages/harness/deerflow/utils/AGENTS.md @@ -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 diff --git a/backend/packages/harness/deerflow/utils/network.py b/backend/packages/harness/deerflow/utils/network.py index 4f98b92f2d..6de4a70e6b 100644 --- a/backend/packages/harness/deerflow/utils/network.py +++ b/backend/packages/harness/deerflow/utils/network.py @@ -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. diff --git a/backend/tests/test_port_allocator_bounds.py b/backend/tests/test_port_allocator_bounds.py new file mode 100644 index 0000000000..c6b5d78277 --- /dev/null +++ b/backend/tests/test_port_allocator_bounds.py @@ -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]