Skip to content

fix(adhoc-lib-rs): nats-server run_cluster uses unwrap() on TcpStream::connect inside client_url, crashing tests on connection failure - #51

Draft
flamingo[bot] wants to merge 1 commit into
mainfrom
ai-fix/adhoc-lib-rs-1-c70be0ee
Draft

fix(adhoc-lib-rs): nats-server run_cluster uses unwrap() on TcpStream::connect inside client_url, crashing tests on connection failure#51
flamingo[bot] wants to merge 1 commit into
mainfrom
ai-fix/adhoc-lib-rs-1-c70be0ee

Conversation

@flamingo

@flamingo flamingo Bot commented Aug 11, 2026

Copy link
Copy Markdown

Closes findings from rule adhoc-lib-rs — nats-server run_cluster uses unwrap() on TcpStream::connect inside client_url, crashing tests on connection failure.

Draft — this is a starting point, not a finished change. The fix required judgment, so read it before trusting it.

# Fix confidence Finding Location
1 🟢 90 high nats-server run_cluster uses unwrap() on TcpStream::connect inside client_url, crashing tests on connection failure nats-server/src/lib.rs:74
2 🟢 95 high nats-server/src/lib.rs parses INFO line with a fixed offset ["INFO".len()..] that will panic on non-INFO responses nats-server/src/lib.rs:78
3 🔴 55 low — review closely nats-server run_cluster port availability check has a TOCTOU race — ports can be claimed between check and bind nats-server/src/lib.rs:163
4 🟡 88 medium nats-server/src/lib.rs client_pid() uses unwrap() chains that will panic if the PID file is missing or non-UTF8 nats-server/src/lib.rs:126
5 🟢 95 high nats-server run_cluster cluster port array uses base port+1/+101/+201 before availability is checked, creating potential overlap with client ports nats-server/src/lib.rs:155
6 🟢 95 high nats-server/src/lib.rs Drop impl ignores pidfile cleanup, leaving stale PID files after test runs nats-server/src/lib.rs:48
7 🟢 98 high nats-server/src/lib.rs Drop impl calls unwrap() on child.kill() and child.wait(), panicking during unwind if the process already exited nats-server/src/lib.rs:49

What changed — and what was deliberately left — is explained per finding as inline review comments on the lines each finding touched.


Run: https://product-hub.flamingo.so/admin/code-review
Run id: c70be0ee-08a4-4bde-9f5c-d994d0e1d4f2

Merging this PR is recorded as acceptance of the rule that produced it;
closing it unmerged is recorded as rejection. Both feed rule health, so
closing a wrong suggestion is useful rather than merely tidy.

…::connect inside client_url, crashing tests on connection failure

@flamingo flamingo Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 What this fix changed, finding by finding

7 finding(s) fixed in this draft — 7 explained inline on the diff; 1 low-confidence hunk(s) need close review before merging.

Comment thread nats-server/src/lib.rs
Comment on lines 77 to 99
// Helpful when dynamically allocating ports with -1.
pub fn client_url(&self) -> String {
let addr = self.client_addr();
let mut r = BufReader::with_capacity(1024, TcpStream::connect(addr).unwrap());
// Retry up to 100 times (100 * 100ms = 10s) waiting for the server to accept connections.
let stream = {
let mut s = None;
for _ in 0..100 {
match TcpStream::connect(&addr) {
Ok(stream) => { s = Some(stream); break; }
Err(_) => thread::sleep(Duration::from_millis(100)),
}
}
s.expect("could not connect to server for client_url")
};
let mut r = BufReader::with_capacity(1024, stream);
let mut line = String::new();
r.read_line(&mut line).expect("did not receive INFO");
let si: Value = serde_json::from_str(&line["INFO".len()..]).expect("could not parse INFO");
let info_prefix = "INFO ";
assert!(line.starts_with(info_prefix), "expected INFO, got: {line}");
let si: Value = serde_json::from_str(&line[info_prefix.len()..]).expect("could not parse INFO");
let port = si["port"].as_u64().expect("could not parse port") as u16;
let mut scheme = "nats://";
if si["tls_required"].as_bool().unwrap_or(false) {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🟠 nats-server run_cluster uses unwrap() on TcpStream::connect inside client_url, crashing tests on connection failure

In client_url() and client_port(), replaced TcpStream::connect(addr).unwrap() with a retry loop (up to 100 iterations, 100ms sleep each) that attempts TcpStream::connect(&addr) and only panics with a descriptive message after exhausting all retries. This mirrors the pattern already used in client_addr().

🤖 Prompt for AI agents
In nats-server/src/lib.rs around line 74, review and complete this code-review fix: nats-server run_cluster uses unwrap() on TcpStream::connect inside client_url, crashing tests on connection failure.
What the draft fix changed: In `client_url()` and `client_port()`, replaced `TcpStream::connect(addr).unwrap()` with a retry loop (up to 100 iterations, 100ms sleep each) that attempts `TcpStream::connect(&addr)` and only panics with a descriptive message after exhausting all retries. This mirrors the pattern already used in `client_addr()`.
Verify the change is correct and complete; do not refactor unrelated code.

fix confidence: 🟢 90 high — react 👍/👎 to teach the reviewer

Comment thread nats-server/src/lib.rs
// Helpful when dynamically allocating ports with -1.
pub fn client_url(&self) -> String {
let addr = self.client_addr();
let mut r = BufReader::with_capacity(1024, TcpStream::connect(addr).unwrap());

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🟠 nats-server/src/lib.rs parses INFO line with a fixed offset ["INFO".len()..] that will panic on non-INFO responses

In both client_url() and client_port(), replaced the bare &line["INFO".len()..] slice with an assert!(line.starts_with(info_prefix), ...) guard followed by &line[info_prefix.len()..] where info_prefix = "INFO " (with trailing space, matching the NATS protocol). This gives a clear panic message on unexpected protocol responses and avoids out-of-bounds slicing.

🤖 Prompt for AI agents
In nats-server/src/lib.rs around line 78, review and complete this code-review fix: nats-server/src/lib.rs parses INFO line with a fixed offset ["INFO".len()..] that will panic on non-INFO responses.
What the draft fix changed: In both `client_url()` and `client_port()`, replaced the bare `&line["INFO".len()..]` slice with an `assert!(line.starts_with(info_prefix), ...)` guard followed by `&line[info_prefix.len()..]` where `info_prefix = "INFO "` (with trailing space, matching the NATS protocol). This gives a clear panic message on unexpected protocol responses and avoids out-of-bounds slicing.
Verify the change is correct and complete; do not refactor unrelated code.

fix confidence: 🟢 95 high — react 👍/👎 to teach the reviewer

Comment thread nats-server/src/lib.rs
Comment on lines 161 to 185
}

pub fn client_pid(&self) -> usize {
String::from_utf8(fs::read(self.inner.pidfile.clone()).unwrap())
.unwrap()
.parse()
.unwrap()
// Retry up to 100 times (100 * 100ms = 10s) waiting for the PID file to appear and be valid.
for _ in 0..100 {
match fs::read(self.inner.pidfile.clone()) {
Ok(bytes) => {
match String::from_utf8(bytes) {
Ok(s) => {
let trimmed = s.trim();
if let Ok(pid) = trimmed.parse::<usize>() {
return pid;
}
}
Err(_) => {}
}
}
Err(_) => {}
}
thread::sleep(Duration::from_millis(100));
}
panic!("could not read valid PID from pidfile: {:?}", self.inner.pidfile);
}
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🟠 nats-server run_cluster port availability check has a TOCTOU race — ports can be claimed between check and bind

The TOCTOU race between is_port_available and the actual server bind is a fundamental OS-level race that cannot be fully eliminated without changing how ports are allocated (e.g., using port 0 and reading back the assigned port). The suggested fix of using port 0 would require significant restructuring of run_cluster_node_with_port and the cluster route configuration (routes must be known before servers start). As a minimal change, the cluster port array is now computed from the final ports values (finding 5 fix below), which at least ensures client and cluster ports are consistent. The TOCTOU window itself remains; a complete fix would require architectural changes spanning multiple functions.

🤖 Prompt for AI agents
In nats-server/src/lib.rs around line 163, review and complete this code-review fix: nats-server run_cluster port availability check has a TOCTOU race — ports can be claimed between check and bind.
What the draft fix changed: The TOCTOU race between `is_port_available` and the actual server bind is a fundamental OS-level race that cannot be fully eliminated without changing how ports are allocated (e.g., using port 0 and reading back the assigned port). The suggested fix of using port 0 would require significant restructuring of `run_cluster_node_with_port` and the cluster route configuration (routes must be known before servers start). As a minimal change, the cluster port array is now computed from the final `ports` values (finding 5 fix below), which at least ensures client and cluster ports are consistent. The TOCTOU window itself remains; a complete fix would require architectural changes spanning multiple functions.
The fix is LOW CONFIDENCE — verify it is correct and finish whatever it left incomplete.

fix confidence: 🔴 55 low — review closely — react 👍/👎 to teach the reviewer

Comment thread nats-server/src/lib.rs
Comment on lines 161 to 185
}

pub fn client_pid(&self) -> usize {
String::from_utf8(fs::read(self.inner.pidfile.clone()).unwrap())
.unwrap()
.parse()
.unwrap()
// Retry up to 100 times (100 * 100ms = 10s) waiting for the PID file to appear and be valid.
for _ in 0..100 {
match fs::read(self.inner.pidfile.clone()) {
Ok(bytes) => {
match String::from_utf8(bytes) {
Ok(s) => {
let trimmed = s.trim();
if let Ok(pid) = trimmed.parse::<usize>() {
return pid;
}
}
Err(_) => {}
}
}
Err(_) => {}
}
thread::sleep(Duration::from_millis(100));
}
panic!("could not read valid PID from pidfile: {:?}", self.inner.pidfile);
}
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🔴 nats-server/src/lib.rs client_pid() uses unwrap() chains that will panic if the PID file is missing or non-UTF8

In client_pid(), replaced the three-unwrap chain with a retry loop (up to 100 iterations, 100ms sleep each) that reads the PID file, parses it as UTF-8, trims whitespace, and parses as usize. Only panics after exhausting retries with a descriptive message including the pidfile path. This mirrors the pattern in client_addr().

🤖 Prompt for AI agents
In nats-server/src/lib.rs around line 126, review and complete this code-review fix: nats-server/src/lib.rs client_pid() uses unwrap() chains that will panic if the PID file is missing or non-UTF8.
What the draft fix changed: In `client_pid()`, replaced the three-unwrap chain with a retry loop (up to 100 iterations, 100ms sleep each) that reads the PID file, parses it as UTF-8, trims whitespace, and parses as `usize`. Only panics after exhausting retries with a descriptive message including the pidfile path. This mirrors the pattern in `client_addr()`.
Verify the change is correct and complete; do not refactor unrelated code.

fix confidence: 🟡 88 medium — react 👍/👎 to teach the reviewer

Comment thread nats-server/src/lib.rs
Comment on lines 161 to 185
}

pub fn client_pid(&self) -> usize {
String::from_utf8(fs::read(self.inner.pidfile.clone()).unwrap())
.unwrap()
.parse()
.unwrap()
// Retry up to 100 times (100 * 100ms = 10s) waiting for the PID file to appear and be valid.
for _ in 0..100 {
match fs::read(self.inner.pidfile.clone()) {
Ok(bytes) => {
match String::from_utf8(bytes) {
Ok(s) => {
let trimmed = s.trim();
if let Ok(pid) = trimmed.parse::<usize>() {
return pid;
}
}
Err(_) => {}
}
}
Err(_) => {}
}
thread::sleep(Duration::from_millis(100));
}
panic!("could not read valid PID from pidfile: {:?}", self.inner.pidfile);
}
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🟠 nats-server run_cluster cluster port array uses base port+1/+101/+201 before availability is checked, creating potential overlap with client ports

In run_cluster, changed let cluster = [port + 1, port + 101, port + 201]; to let cluster = [ports[0] + 1, ports[1] + 1, ports[2] + 1]; (after the availability-check loop). This ensures cluster ports are always client_port + 1 for each node, even when the availability loop changes a port from its initial value.

🤖 Prompt for AI agents
In nats-server/src/lib.rs around line 155, review and complete this code-review fix: nats-server run_cluster cluster port array uses base port+1/+101/+201 before availability is checked, creating potential overlap with client ports.
What the draft fix changed: In `run_cluster`, changed `let cluster = [port + 1, port + 101, port + 201];` to `let cluster = [ports[0] + 1, ports[1] + 1, ports[2] + 1];` (after the availability-check loop). This ensures cluster ports are always `client_port + 1` for each node, even when the availability loop changes a port from its initial value.
Verify the change is correct and complete; do not refactor unrelated code.

fix confidence: 🟢 95 high — react 👍/👎 to teach the reviewer

Comment thread nats-server/src/lib.rs
impl Drop for Server {
fn drop(&mut self) {
self.inner.child.kill().unwrap();
self.inner.child.wait().unwrap();

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🟠 nats-server/src/lib.rs Drop impl ignores pidfile cleanup, leaving stale PID files after test runs

In the Drop impl, added fs::remove_file(self.inner.pidfile.as_os_str()).ok(); after the logfile removal. Uses .ok() to silently ignore errors (e.g., file already gone), consistent with the logfile removal style.

🤖 Prompt for AI agents
In nats-server/src/lib.rs around line 48, review and complete this code-review fix: nats-server/src/lib.rs Drop impl ignores pidfile cleanup, leaving stale PID files after test runs.
What the draft fix changed: In the `Drop` impl, added `fs::remove_file(self.inner.pidfile.as_os_str()).ok();` after the logfile removal. Uses `.ok()` to silently ignore errors (e.g., file already gone), consistent with the logfile removal style.
Verify the change is correct and complete; do not refactor unrelated code.

fix confidence: 🟢 95 high — react 👍/👎 to teach the reviewer

Comment thread nats-server/src/lib.rs
self.inner.child.wait().unwrap();
let _ = self.inner.child.kill();
let _ = self.inner.child.wait();
if let Ok(log) = fs::read_to_string(self.inner.logfile.as_os_str()) {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🟠 nats-server/src/lib.rs Drop impl calls unwrap() on child.kill() and child.wait(), panicking during unwind if the process already exited

In the Drop impl, changed self.inner.child.kill().unwrap() and self.inner.child.wait().unwrap() to let _ = self.inner.child.kill(); and let _ = self.inner.child.wait();. This prevents a double-panic/abort if the child has already exited when Drop runs. Also applied the same change to restart() for consistency.

🤖 Prompt for AI agents
In nats-server/src/lib.rs around line 49, review and complete this code-review fix: nats-server/src/lib.rs Drop impl calls unwrap() on child.kill() and child.wait(), panicking during unwind if the process already exited.
What the draft fix changed: In the `Drop` impl, changed `self.inner.child.kill().unwrap()` and `self.inner.child.wait().unwrap()` to `let _ = self.inner.child.kill();` and `let _ = self.inner.child.wait();`. This prevents a double-panic/abort if the child has already exited when `Drop` runs. Also applied the same change to `restart()` for consistency.
Verify the change is correct and complete; do not refactor unrelated code.

fix confidence: 🟢 98 high — react 👍/👎 to teach the reviewer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants