Commit 62742f1870

62742f187088804e31f93d036984ba84a251d4ee

parent: b172654262

Verified · cmc ci/build: success ci/test: success ci/vuln: success

cmc <hello@cleberg.net> · 2026-09-01 07:03 UTC

e2e: reserve every port before releasing any of them

freePort closed its listener before returning, so the kernel was free to
give the next call the same port. An instance asks for three in a row, then
fails to bind its second listener and the test that started it times out on
"gitbayd did not start listening" — attributed to whichever test drew the
collision, so it moved around and never looked like the same bug twice.

Measured on the runner host, 200 rounds of three picks: 1 collision closing
each listener before the next, 0 holding them all open until every port is
chosen. Roughly half a percent per instance, across many instances a run.

Caught in build 157:

    INFO git-daemon listening addr=[::]:33767
    INFO http listening addr=127.0.0.1:33767 tls=off
    gitbayd: listen tcp 127.0.0.1:33767: bind: address already in use

TestACMEServe and the backup restore allocate in pairs and had the same bug.

Distinct from #67, where a dropped log stream failed builds whose tests had
passed; that one truncates the log and names nothing.

Closes #68
Ref #62, #67

Layout: unified · split

e2e/acme_test.go +2 −2
@@ -22,8 +22,8 @@ func TestACMEServe(t *testing.T) {
2222 inst.proc.Process.Kill()
2323 inst.proc.Wait()
2424
25 httpsPort := freePort(t)
26 acmeHTTPPort := freePort(t)
25 ports := freePorts(t, 2)
26 httpsPort, acmeHTTPPort := ports[0], ports[1]
2727 cfg := fmt.Sprintf(`
2828[server]
2929root = %q
e2e/backup_test.go +2 −2
@@ -69,8 +69,8 @@ func TestAdminBackup(t *testing.T) {
6969 if outB, err := exec.Command("tar", "-xzf", archive, "-C", root2).CombinedOutput(); err != nil {
7070 t.Fatalf("extract: %v\n%s", err, outB)
7171 }
72 port2 := freePort(t)
73 httpPort2 := freePort(t)
72 ports := freePorts(t, 2)
73 port2, httpPort2 := ports[0], ports[1]
7474 config2 := filepath.Join(root2, "config.toml")
7575 cfg := fmt.Sprintf(`
7676[server]
e2e/ssh_test.go +48 −9
@@ -36,14 +36,36 @@ func buildGitbayd(t *testing.T) string {
3636 return bin
3737}
3838
39func freePort(t *testing.T) int {
39// freePorts reserves n distinct ports. A port is chosen by binding :0 and
40// reading back what the kernel assigned, so every listener has to stay open
41// until all of them are picked — closing one before picking the next lets
42// the kernel hand out the same port again, and the instance that asked for
43// three then fails to bind its second listener.
44//
45// Still a narrowing rather than a guarantee: another process can take a port
46// between the close here and the bind in gitbayd. Distinctness within one
47// instance is the part that is ours.
48func freePorts(t *testing.T, n int) []int {
4049 t.Helper()
41 ln, err := net.Listen("tcp", "127.0.0.1:0")
42 if err != nil {
43 t.Fatal(err)
50 lns := make([]net.Listener, 0, n)
51 ports := make([]int, 0, n)
52 for i := 0; i < n; i++ {
53 ln, err := net.Listen("tcp", "127.0.0.1:0")
54 if err != nil {
55 t.Fatal(err)
56 }
57 lns = append(lns, ln)
58 ports = append(ports, ln.Addr().(*net.TCPAddr).Port)
4459 }
45 defer ln.Close()
46 return ln.Addr().(*net.TCPAddr).Port
60 for _, ln := range lns {
61 ln.Close()
62 }
63 return ports
64}
65
66func freePort(t *testing.T) int {
67 t.Helper()
68 return freePorts(t, 1)[0]
4769}
4870
4971func startInstance(t *testing.T) *instance {
@@ -53,12 +75,13 @@ func startInstance(t *testing.T) *instance {
5375// startInstanceWith appends extra TOML to the instance config.
5476func startInstanceWith(t *testing.T, extra string) *instance {
5577 t.Helper()
78 ports := freePorts(t, 3)
5679 inst := &instance{
5780 gitbayd: buildGitbayd(t),
5881 root: t.TempDir(),
59 port: freePort(t),
60 httpPort: freePort(t),
61 gitPort: freePort(t),
82 port: ports[0],
83 httpPort: ports[1],
84 gitPort: ports[2],
6285 sshDir: t.TempDir(),
6386 }
6487 inst.config = filepath.Join(inst.root, "config.toml")
@@ -244,3 +267,19 @@ func TestControlPlaneOverBareSSH(t *testing.T) {
244267 t.Fatalf("keys remove with spaced arg: exit %d (want 3), stderr %q", code, errOut)
245268 }
246269}
270
271// freePort used to close its listener before returning, so the kernel was
272// free to hand the same port to the next call. An instance asks for three in
273// a row and then fails to bind its second listener, which surfaces as an
274// unrelated test timing out on "gitbayd did not start listening".
275func TestFreePortsAreDistinct(t *testing.T) {
276 for round := 0; round < 50; round++ {
277 seen := map[int]bool{}
278 for _, p := range freePorts(t, 8) {
279 if seen[p] {
280 t.Fatalf("round %d: port %d issued twice in one request", round, p)
281 }
282 seen[p] = true
283 }
284 }
285}