Skip to content

8001: Surface errors from SO_ERROR on recv for UDP sockets on Linux#90

Open
martin-augment wants to merge 3 commits intomasterfrom
pr-8001-2026-04-02-07-53-39
Open

8001: Surface errors from SO_ERROR on recv for UDP sockets on Linux#90
martin-augment wants to merge 3 commits intomasterfrom
pr-8001-2026-04-02-07-53-39

Conversation

@martin-augment
Copy link
Copy Markdown
Owner

8001: To review by AI

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 2, 2026

Walkthrough

This pull request updates AI agent configuration rules across three files (.cursor/rules.md, AGENTS.md, and CLAUDE.md) to restrict linking to GitHub issues or pull requests and to prevent reviewing other AI agent configuration files. Additionally, it modifies the Tokio UDP implementation by changing UdpSocket::recv to register for both read and error readiness events, and adds a corresponding test that verifies the socket returns a ConnectionRefused error when attempting to receive on a closed UDP address.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pr-8001-2026-04-02-07-53-39

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude
Copy link
Copy Markdown

claude bot commented Apr 2, 2026

Code Review

Summary

This PR fixes a real behavior issue on Linux where UdpSocket::recv on a connected socket would hang instead of returning ConnectionRefused when an ICMP Port Unreachable error was received. The fix is correct in principle — adding Interest::ERROR causes epoll to wake up on EPOLLERR, which is set when SO_ERROR has a pending error.


Issues

1. Fix is incomplete — other async recv methods are not updated

The change only covers recv (line 788), but the same problem affects these methods which still use only Interest::READABLE:

  • recv_buf (line 989) — async, same pattern
  • recv_from (line 1318) — async, same pattern

The try_recv (line 887) and try_recv_buf family are synchronous callers that return WouldBlock when nothing is ready, so they would not hang; however they also would not surface the SO_ERROR until the next readable event, which could be surprising behavior. It's worth considering whether consistency is valuable there too.

2. Test is not platform-gated

The test send_to_recv_closed_err_kind_refused relies on the OS delivering an ICMP Port Unreachable error back to the sender, which is Linux-specific behavior (controlled by IP_RECVERR / IPV6_RECVERR or the default connected-UDP error surfacing). On macOS this may or may not behave the same way; on Windows it is unlikely to work, and the test would hang for the full 5-second timeout before panicking.

The test should be gated:

#[cfg(target_os = "linux")]
#[tokio::test]
async fn send_to_recv_closed_err_kind_refused() ...

If the behavior is known to also apply on other Unix platforms, #[cfg(any(target_os = "linux", ...))] can be broadened later.

3. The source change itself may warrant a platform gate

The Interest::ERROR addition in udp.rs should ideally be #[cfg(target_os = "linux")] (or broader Unix) to avoid silently changing readiness semantics on platforms where ERROR readiness may not mean SO_ERROR is populated. At minimum a comment explaining the Linux-specific motivation would improve maintainability.


Minor Observations

  • The 5-second timeout in the test is generous for a loopback ICMP round-trip. A shorter value (e.g. 1s) would keep CI fast, but this is a minor nit.
  • The PR title and description accurately describe the intent; the implementation just needs to be completed for the other async recv methods and properly gated.

Verdict

The core idea and fix are correct. Please address:

  1. Apply Interest::ERROR to recv_from and recv_buf as well.
  2. Gate the new test with #[cfg(target_os = "linux")].
  3. Consider a cfg guard or at minimum a comment on the udp.rs change explaining the Linux-specific rationale.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request modifies UdpSocket::recv to include Interest::ERROR for surfacing socket errors and adds a test case for connection refusal. It also updates AI agent configuration rules. Feedback highlights a critical bug where the bitwise combination of interests may cause async_io to hang due to limitations in Interest::mask. Furthermore, it is recommended to apply these changes consistently across all receive methods and expand test coverage accordingly.

self.io
.registration()
.async_io(Interest::READABLE, || self.io.recv(buf))
.async_io(Interest::READABLE | Interest::ERROR, || self.io.recv(buf))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

While adding Interest::ERROR is intended to surface SO_ERROR events (such as ICMP unreachable) on connected UDP sockets, this change as implemented has two significant issues:

  1. Broken Readiness Logic: In tokio/src/io/interest.rs, the Interest::mask method uses a match statement that does not handle bitwise combinations. Combining Interest::READABLE | Interest::ERROR will result in Ready::EMPTY being returned by mask(). This will cause async_io to hang indefinitely because the underlying readiness future will be waiting for an empty set of events. A fix for Interest::mask in interest.rs is required to support combined interests.

  2. Inconsistency: This change only updates the recv method. Other receive methods such as recv_from, recv_buf, recv_buf_from, and their try_* counterparts (e.g., try_recv, try_recv_from) still use Interest::READABLE only. This leads to inconsistent behavior across the API where some methods surface errors while others do not.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

value:useful; category:bug; feedback: The Gemini AI reviewer is correct! The issue is fixed for only one of the recv_** methods. There are few more similar methods which will need the same fix. Prevents fixing an issue only partially

Comment on lines +51 to +68
async fn send_to_recv_closed_err_kind_refused() -> std::io::Result<()> {
let sender = UdpSocket::bind("127.0.0.1:0").await?;
let receiver = UdpSocket::bind("127.0.0.1:0").await?;

let receiver_addr = receiver.local_addr()?;
drop(receiver);
sender.connect(receiver_addr).await?;
sender.send(MSG).await?;

let mut recv_buf = [0u8; 32];
let err = time::timeout(Duration::from_secs(5), sender.recv(&mut recv_buf))
.await
.expect("timed out instead of returning error")
.unwrap_err();

assert_eq!(err.kind(), io::ErrorKind::ConnectionRefused);
Ok(())
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The new test case send_to_recv_closed_err_kind_refused is valuable for verifying that errors are surfaced. However, to ensure consistency and prevent regressions, you should add similar test cases for other receive methods, such as recv_from, try_recv, and recv_buf. This is especially important given that these methods currently lack the Interest::ERROR flag in the implementation and would likely fail this test or time out.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

value:useful; category:bug; feedback: The Gemini AI reviewer is correct! The issue is fixed for only one of the recv_** methods. There are few more similar methods which will need the same fix. Prevents fixing an issue only partially

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tokio/tests/udp.rs`:
- Around line 50-68: The test send_to_recv_closed_err_kind_refused exhibits
Linux-specific behavior and must be gated to Linux; add a platform cfg attribute
(e.g. #[cfg(target_os = "linux")] or #[cfg(all(unix, target_os = "linux"))])
directly above the #[tokio::test] for the send_to_recv_closed_err_kind_refused
function so the test only runs on Linux targets, leaving the test body and
assertions unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 09d17b8b-487f-4b26-a41c-178660e0897f

📥 Commits

Reviewing files that changed from the base of the PR and between df10978 and 4f3040c.

📒 Files selected for processing (5)
  • .cursor/rules.md
  • AGENTS.md
  • CLAUDE.md
  • tokio/src/net/udp.rs
  • tokio/tests/udp.rs

Comment on lines +50 to +68
#[tokio::test]
async fn send_to_recv_closed_err_kind_refused() -> std::io::Result<()> {
let sender = UdpSocket::bind("127.0.0.1:0").await?;
let receiver = UdpSocket::bind("127.0.0.1:0").await?;

let receiver_addr = receiver.local_addr()?;
drop(receiver);
sender.connect(receiver_addr).await?;
sender.send(MSG).await?;

let mut recv_buf = [0u8; 32];
let err = time::timeout(Duration::from_secs(5), sender.recv(&mut recv_buf))
.await
.expect("timed out instead of returning error")
.unwrap_err();

assert_eq!(err.kind(), io::ErrorKind::ConnectionRefused);
Ok(())
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Gate this regression test to Linux to avoid cross-platform failures.

Line [50] currently enables this test everywhere, but the asserted behavior (Line [66], ConnectionRefused surfaced on recv) is Linux-specific per the PR intent. On non-Linux targets, this can fail or be flaky.

Suggested fix
-#[tokio::test]
+#[cfg(target_os = "linux")]
+#[tokio::test]
 async fn send_to_recv_closed_err_kind_refused() -> std::io::Result<()> {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[tokio::test]
async fn send_to_recv_closed_err_kind_refused() -> std::io::Result<()> {
let sender = UdpSocket::bind("127.0.0.1:0").await?;
let receiver = UdpSocket::bind("127.0.0.1:0").await?;
let receiver_addr = receiver.local_addr()?;
drop(receiver);
sender.connect(receiver_addr).await?;
sender.send(MSG).await?;
let mut recv_buf = [0u8; 32];
let err = time::timeout(Duration::from_secs(5), sender.recv(&mut recv_buf))
.await
.expect("timed out instead of returning error")
.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::ConnectionRefused);
Ok(())
}
#[cfg(target_os = "linux")]
#[tokio::test]
async fn send_to_recv_closed_err_kind_refused() -> std::io::Result<()> {
let sender = UdpSocket::bind("127.0.0.1:0").await?;
let receiver = UdpSocket::bind("127.0.0.1:0").await?;
let receiver_addr = receiver.local_addr()?;
drop(receiver);
sender.connect(receiver_addr).await?;
sender.send(MSG).await?;
let mut recv_buf = [0u8; 32];
let err = time::timeout(Duration::from_secs(5), sender.recv(&mut recv_buf))
.await
.expect("timed out instead of returning error")
.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::ConnectionRefused);
Ok(())
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tokio/tests/udp.rs` around lines 50 - 68, The test
send_to_recv_closed_err_kind_refused exhibits Linux-specific behavior and must
be gated to Linux; add a platform cfg attribute (e.g. #[cfg(target_os =
"linux")] or #[cfg(all(unix, target_os = "linux"))]) directly above the
#[tokio::test] for the send_to_recv_closed_err_kind_refused function so the test
only runs on Linux targets, leaving the test body and assertions unchanged.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

self.io
.registration()
.async_io(Interest::READABLE, || self.io.recv(buf))
.async_io(Interest::READABLE | Interest::ERROR, || self.io.recv(buf))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Inconsistent Interest::ERROR across similar recv methods

Medium Severity

Interest::ERROR was added to recv but not to the other async receive methods — recv_buf, recv_from, and recv_buf_from — which all use async_io(Interest::READABLE, ...) with the same underlying recv syscalls. This means SO_ERROR (e.g. ConnectionRefused) will only be surfaced when callers use recv, but not when they use any of the other receive variants, creating inconsistent behavior across functionally equivalent APIs.

Additional Locations (2)
Fix in Cursor Fix in Web

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

value:useful; category:bug; feedback: The Bugbot AI reviewer is correct! The issue is fixed for only one of the recv_** methods. There are few more similar methods which will need the same fix. Prevents fixing an issue only partially

@augmentcode
Copy link
Copy Markdown

augmentcode bot commented Apr 2, 2026

🤖 Augment PR Summary

Summary: Prevents connected UDP recv from hanging by waking on error readiness and surfacing pending SO_ERROR.

Changes:

  • Wait for Interest::READABLE | Interest::ERROR in UdpSocket::recv instead of read-only readiness.
  • Add a regression test that sends to a closed UDP port and expects recv to return ConnectionRefused within a timeout.
Technical Notes: On Linux, ICMP “port unreachable” can arrive as error readiness without making the socket readable.

🤖 Was this summary useful? React with 👍 or 👎

Copy link
Copy Markdown

@augmentcode augmentcode bot left a comment

Choose a reason for hiding this comment

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

Review completed. 2 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

self.io
.registration()
.async_io(Interest::READABLE, || self.io.recv(buf))
.async_io(Interest::READABLE | Interest::ERROR, || self.io.recv(buf))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

tokio/src/net/udp.rs:788: This adds Interest::ERROR for recv, but other connected receive APIs like recv_buf (and the poll-based poll_recv / poll_recv_ready paths) still appear to wait only on readable readiness. If they can also observe an SO_ERROR-only readiness state, they may still hang or delay surfacing the error.

Severity: medium

Other Locations
  • tokio/src/net/udp.rs:989
  • tokio/src/net/udp.rs:744
  • tokio/src/net/udp.rs:815

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

value:useful; category:bug; feedback: The Augment AI reviewer is correct! The issue is fixed for only one of the recv_** methods. There are few more similar methods which will need the same fix. Prevents fixing an issue only partially

}

#[tokio::test]
async fn send_to_recv_closed_err_kind_refused() -> std::io::Result<()> {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

tokio/tests/udp.rs:51: This test asserts ErrorKind::ConnectionRefused, but UDP ICMP/error reporting (and the mapped ErrorKind) varies across platforms, and the PR description/title suggests the behavior is Linux-specific. Without a platform cfg, this may be flaky or fail on non-Linux CI.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

value:useful; category:bug; feedback: The Augment AI reviewer is correct! The "connection refused" error is Unix specific. Windows uses ConnectionReset instead. The test should improved to expect the correct error for the respective OS. Prevents failed CI due to test failures on Windows and WASM.

@martin-augment
Copy link
Copy Markdown
Owner Author

. Fix is incomplete — other async recv methods are not updated

The change only covers recv (line 788), but the same problem affects these methods which still use only Interest::READABLE:

  • recv_buf (line 989) — async, same pattern
  • recv_from (line 1318) — async, same pattern

The try_recv (line 887) and try_recv_buf family are synchronous callers that return WouldBlock when nothing is ready, so they would not hang; however they also would not surface the SO_ERROR until the next readable event, which could be surprising behavior. It's worth considering whether consistency is valuable there too.

value:useful; category:bug; feedback: The Claude AI reviewer is correct! The issue is fixed for only one of the recv_** methods. There are few more similar methods which will need the same fix. Prevents fixing an issue only partially

@martin-augment
Copy link
Copy Markdown
Owner Author

2. Test is not platform-gated

The test send_to_recv_closed_err_kind_refused relies on the OS delivering an ICMP Port Unreachable error back to the sender, which is Linux-specific behavior (controlled by IP_RECVERR / IPV6_RECVERR or the default connected-UDP error surfacing). On macOS this may or may not behave the same way; on Windows it is unlikely to work, and the test would hang for the full 5-second timeout before panicking.

The test should be gated:

#[cfg(target_os = "linux")]
#[tokio::test]
async fn send_to_recv_closed_err_kind_refused() ...

If the behavior is known to also apply on other Unix platforms, #[cfg(any(target_os = "linux", ...))] can be broadened later.

value:useful; category:bug; feedback: The Claude AI reviewer is correct! The "connection refused" error is Unix specific. Windows uses ConnectionReset instead. The test should improved to expect the correct error for the respective OS. Prevents failed CI due to test failures on Windows and WASM.

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.

3 participants