rpctest: scope shared state to the current process
What changed, and why it matters
This commit fixes a test-only reliability bug, not a security vulnerability. When running btcd's integration tests in parallel, two test processes were accidentally sharing the same temporary file path and the same starting port number. That caused random test failures: one process could overwrite the other's freshly built test binary, or two processes could try to use the same network port. The fix gives each test process its own random file name and a random starting port range. It does not change anything in the live Bitcoin node software end users run.
No production security action required. Developers running parallel tests with `-tags=rpctest` should update to this commit to eliminate flaky test failures. If backporting, include only the test harness changes; no node runtime changes are needed.
Security signals we found
Race condition on shared temporary executable path
Race condition on shared ephemeral port allocation
Test-only code path; no production node behavior affected
Symptoms included TLS certificate errors and connection refused failures during parallel test runs
Fix uses process-scoped randomization rather than synchronization
Evidence from the diff
The patch addresses race conditions in the rpctest integration harness under go test ./... / make unit -tags=rpctest. Two pieces of package-global state were aliased across OS processes: (1) btcdExecutablePath() built the btcd binary to a fixed /tmp/btcd/rpctest/btcd path, so concurrent go build invocations raced on the output file, producing truncated/stale binaries and downstream TLS ‘unknown authority’ errors; (2) lastPort started at defaultNodePort in every process, and because NextAvailablePort() closes its probe listener before returning, two processes scanning upward from the same base frequently allocated the same port, causing ‘connection refused’ failures. The fix suffixes the executable with rand.Uint32() and seeds lastPort with defaultNodePort + rand.Uint32N(50000). The changes are confined to test helper code (integration/rpctest/btcd.go and integration/rpctest/rpc_harness.go).
Changed components
integration/rpctest/btcd.gointegration/rpctest/rpc_harness.gorpctest integration harness build pathrpctest integration harness port allocatorInspect captured patch +17 / −3
diff --git a/integration/rpctest/btcd.go b/integration/rpctest/btcd.go
index 29642c8..22717a5 100644
--- a/integration/rpctest/btcd.go
+++ b/integration/rpctest/btcd.go
@@ -6,6 +6,7 @@ package rpctest
import (
"fmt"
+ "math/rand/v2"
"os/exec"
"path/filepath"
"runtime"
@@ -43,8 +44,12 @@ func btcdExecutablePath() (string, error) {
return "", err
}
- // Build btcd and output an executable in a static temp path.
- outputPath := filepath.Join(testDir, "btcd")
+ // Build btcd to a random path so concurrent `go test` processes
+ // (e.g. when test packages run in parallel under `make unit`) do
+ // not race on the same output file. Each test process pays a
+ // one-time compile cost; within a process the compileMtx-guarded
+ // cache keeps it to one build.
+ outputPath := filepath.Join(testDir, fmt.Sprintf("btcd-%d", rand.Uint32()))
if runtime.GOOS == "windows" {
outputPath += ".exe"
}
diff --git a/integration/rpctest/rpc_harness.go b/integration/rpctest/rpc_harness.go
index 1d3d42d..9c9cb85 100644
--- a/integration/rpctest/rpc_harness.go
+++ b/integration/rpctest/rpc_harness.go
@@ -6,6 +6,7 @@ package rpctest
import (
"fmt"
+ "math/rand/v2"
"net"
"os"
"path/filepath"
@@ -76,7 +77,15 @@ var (
// lastPort is the last port determined to be free for use by a new
// node. It should be used atomically.
- lastPort uint32 = defaultNodePort
+ //
+ // Seed with a random offset so concurrent `go test` processes
+ // (e.g. when integration/ and integration/rpctest/ run in parallel
+ // under `make unit`) do not race on the same port range. The
+ // bind-test in NextAvailablePort closes the listener before
+ // returning, leaving a window where another process could grab the
+ // same port; staggering each process's starting point avoids the
+ // collision. The 50k-port window leaves headroom below 65535.
+ lastPort uint32 = defaultNodePort + rand.Uint32N(50000)
)
// HarnessTestCase represents a test-case which utilizes an instance of the
Why this scored 23/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.