Skip to content

Replace dynamic free-port probing with OS port-0 binding #194313

Open

Description

@mdebbar

Problem

Across flutter_tools, package:dwds, dev/bots, package:web_benchmarks, and flutter/devtools, several code paths dynamically locate a "free" TCP port by probing a port (ServerSocket.bind(..., 0) or RawSocket.connect), closing the socket, and passing that port number to an HTTP server or child process (Chrome, chromedriver) to bind later. Other test harnesses hardcode fixed ports (9999, 10000, 10001), pushing downstream callers to add their own port probes.

Probing for a free port before binding has two flaws that cause recurring CI flakes:

  1. TOCTOU race condition: Once the probe socket closes (or RawSocket.connect fails), the port is unreserved. Under parallel CI load, another test shard or process can claim the port before the target server or browser binds it. Worse, RawSocket.connect probes only detect sockets actively listening on localhost, missing sockets that are bound but not yet listening.
  2. Address-family and dual-stack mismatch: The probe socket rarely matches the target bind's address family or socket flags. On macOS and Linux, an IPv4 socket bound to 127.0.0.1:P or 0.0.0.0:P does not conflict with an IPv6-only (v6Only: true) probe on [::1]:P.
    • For example, dwds's findUnusedPort() probes [::1] with v6Only: true, then HttpMultiServer.bind('any', port) binds [::] dual-stack (v6Only: false). If the kernel hands out a port held by an IPv4 socket, the probe succeeds on [::1] and the subsequent [::] bind fails with EADDRINUSE (errno = 48 on macOS, 98 on Linux). Under congested ephemeral port ranges, all 5 retry attempts fail in ~500ms.

The Flutter Style Guide already prohibits this pattern ("Never check if a port is available before using it... Instead, have the code that opens the port pick an available port and return it"), and dev/bots/utils.dart even names its helper findAvailablePortAndPossiblyCauseFlakyTests().

Past Flakes & Failures

Date Subsystem Failure Mode & Impact Links
2026-10 flutter/packages DriveExamplesCommand Hardcoded chromedriver --port=4444 inside the per-example loop combined with chromedriver.kill() without awaiting exitCode. When a package has multiple web examples (google_maps_flutter_web's example/latest and example/3-64), the dying chromedriver from the first example still holds port 4444 when the second example spawns a new chromedriver --port=4444, so the second chromedriver fails to bind and exits immediately. flutter/packages#13355, build 1, build 2, build 3
2026-10 dwds startHttpServer findUnusedPort() ([::1] v6Only) -> HttpMultiServer.bind('any') ([::] dual-stack) fails with Address already in use (errno = 48). Caused 8 of 15 recent post-submit failures on Mac web_tool_tests. #185452, CL 563621, build 1, build 2, build 3
2026-08 flutter_tools chrome.dart os.findFreePort() (127.0.0.1) -> chrome --remote-debugging-port=<port>. When the port is taken before Chrome binds, Chrome logs Cannot start http server for devtools. and stays alive, hanging flutter test --platform chrome for 30m. #192013, #193461
2025-07 package:web_benchmarks & flutter/devtools Hardcoded ports 9999 and 10000 collide when a prior Chrome holds 127.0.0.1:10000; the new Chrome falls back to [::1]:10000 while the runner connects to the stale IPv4 tab. Worked around in DevTools by copying dev/bots's RawSocket.connect scanner. flutter/devtools#9314, flutter/devtools#9315
2022-08 dev/bots (run_web_tests.dart, service_worker_test.dart) findAvailablePortAndPossiblyCauseFlakyTests() scans upward from 8080 via RawSocket.connect, missing bound-but-not-listening sockets and racing across shards. #109474
2022-02 dwds TestServer.start Inter-process race on findUnusedPort() -> errno = 98, address = 127.0.0.1. dart-lang/webdev#1514, dart-lang/webdev#1515
2021-08 dwds DebugService.start findUnusedPort() -> HttpMultiServer.bind threw EADDRINUSE (errno = 98). #87659, dart-lang/webdev#1378, dart-lang/webdev#1379
2020-10 flutter_tools WebAssetServer Pre-probed port collided on HttpMultiServer bind (address = ::1). #69348, #69351

Plan

Instead of probing for a free port and closing it, pass 0 directly to the bind() call (or --remote-debugging-port=0 / --port=0 flag) so the OS allocates an ephemeral port atomically for the target address family, then read back the assigned port from server.port or child stdout/stderr. Keep a bounded retry loop around multi-socket binds (HttpMultiServer.loopback).

Out of Scope

  • External binaries that do not report an OS-allocated port: iproxy (ios/devices.dart) and user-defined custom-device forwarding commands (custom_devices.dart) require a concrete host port on the CLI and do not emit a dynamically bound port. They must retain findFreePort() with retry re-probing.
  • Explicit user-supplied port flags (--web-port, --dds-port, --host-vmservice-port): When omitted, flutter_tools already defaults all three to port 0. When a user or test explicitly passes a port number (e.g., observatory_port_test.dart testing that --dds-port=N is honored), the tool must bind N.

Activity

  1. added
    team-toolOwned by Flutter Tool team
    fyi-webFor the attention of Web platform team
    fyi-infraFor the attention of Infrastructure team
    on Oct 13, 2026
  2. added
    P2Important issues not at the top of the work list
    triaged-toolTriaged by Flutter Tool team
    on Oct 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

P2Important issues not at the top of the work listteam-toolOwned by Flutter Tool teamtriaged-toolTriaged by Flutter Tool team

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions