Summary
src/bifrost.ts uses a two-phase port allocation strategy: (1) bind a TCP server to port 0 to get a free OS-assigned port, (2) immediately close the server, (3) pass that port number to the spawned Bifrost Go binary via -port N. Between steps 2 and 3, the OS may reassign that port to another process. The Bifrost binary then fails to bind with EADDRINUSE, or silently connects to the wrong service.
Panel verdict: P2, VERIFIED, EXISTING_DEFECT, consensus.
Affected Files
| File |
Lines |
Issue |
src/bifrost.ts |
L23–L38 |
getFreePort() closes the server before spawning — TOCTOU window |
src/bifrost.ts |
L76–L89 |
Port passed to spawn() may be taken by the time Bifrost binds |
Root Cause — Code Evidence
getFreePort() closes before use (src/bifrost.ts, lines 23–38):
function getFreePort(): Promise<number> {
return new Promise((resolve, reject) => {
const srv = net.createServer();
srv.unref();
srv.on('error', reject);
srv.listen(0, '127.0.0.1', () => {
const addr = srv.address();
if (addr && typeof addr === 'object') {
const p = addr.port;
srv.close(() => resolve(p)); // ← port freed here
// ↑ TOCTOU: another process can grab this port
// before Bifrost binds to it
} else {
srv.close(() => reject(new Error('no port')));
}
});
});
}
Port passed to spawn (src/bifrost.ts, lines 78–89):
const child: ChildProcess = spawn(
process.execPath,
[launcher, '-port', String(port)], // ← port may be taken by now
{ ... }
);
Impact
- Startup failure: Bifrost fails to bind and the scan cannot proceed, producing a confusing error.
- Silent proxy misdirection: If another service grabbed the port, Bifrost may error while the Probus client connects to the wrong service — causing API key leakage to the unintended service.
- Reproducibility on loaded systems: More likely under CI/CD runners or developer machines with many concurrent services.
Remediation
Option A — Let Bifrost bind to port 0 (OS-assigned) and report back
If the Bifrost binary supports binding to port 0 and reporting the bound port via stdout, use that approach:
// Pass port=0 and read the actual bound port from stdout
const child = spawn(process.execPath, [launcher, '-port', '0'], { ... });
const actualPort = await readPortFromStdout(child);
Option B — Keep the listener socket open and pass the fd (Unix only)
// Keep the socket open and pass it as a pre-bound fd:
const srv = net.createServer();
await new Promise<void>((res, rej) => { srv.listen(0, '127.0.0.1', res); srv.on('error', rej); });
const port = (srv.address() as net.AddressInfo).port;
// Don't close srv — pass the socket fd to the child process
const child = spawn(process.execPath, [launcher, '-port', String(port)], {
stdio: ['ignore', 'pipe', 'pipe', srv._handle] // platform-specific
});
srv.close(); // Now safe to close since child inherited the fd
Option C — Retry with exponential backoff (simplest)
async function spawnBifrostWithRetry(port: number, maxRetries = 3): Promise<ChildProcess> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const p = attempt === 0 ? port : await getFreePort();
const child = spawnBifrost(p);
await waitForPort(p, 60_000);
return child;
} catch (err) {
if (attempt === maxRetries - 1) throw err;
await new Promise(r => setTimeout(r, 100 * Math.pow(2, attempt)));
}
}
throw new Error('Failed to spawn Bifrost after retries');
}
Summary
src/bifrost.tsuses a two-phase port allocation strategy: (1) bind a TCP server to port 0 to get a free OS-assigned port, (2) immediately close the server, (3) pass that port number to the spawned Bifrost Go binary via-port N. Between steps 2 and 3, the OS may reassign that port to another process. The Bifrost binary then fails to bind withEADDRINUSE, or silently connects to the wrong service.Panel verdict: P2, VERIFIED, EXISTING_DEFECT, consensus.
Affected Files
src/bifrost.tsgetFreePort()closes the server before spawning — TOCTOU windowsrc/bifrost.tsspawn()may be taken by the time Bifrost bindsRoot Cause — Code Evidence
getFreePort()closes before use (src/bifrost.ts, lines 23–38):Port passed to spawn (
src/bifrost.ts, lines 78–89):Impact
Remediation
Option A — Let Bifrost bind to port 0 (OS-assigned) and report back
If the Bifrost binary supports binding to port 0 and reporting the bound port via stdout, use that approach:
Option B — Keep the listener socket open and pass the fd (Unix only)
Option C — Retry with exponential backoff (simplest)