Skip to content

7992: io: AioSource now employs IO Safety#91

Open
martin-augment wants to merge 4 commits intomasterfrom
pr-7992-2026-04-04-10-58-30
Open

7992: io: AioSource now employs IO Safety#91
martin-augment wants to merge 4 commits intomasterfrom
pr-7992-2026-04-04-10-58-30

Conversation

@martin-augment
Copy link
Copy Markdown
Owner

7992: To review by AI

asomers added 4 commits March 27, 2026 17:57
implementors

Older implementors will continue to work without IO Safety.  But newer
implementors should implement AioSource::register_borrowed instead of
AioSource::register.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

Walkthrough

This pull request updates the tokio crate to support borrowed file descriptor registration for POSIX AIO sources. The main changes include: (1) bumping dependency versions in tokio/Cargo.toml for mio (to 1.2.0), nix (to 0.31.0), and mio-aio (to 2); (2) introducing a new register_borrowed method in the AioSource trait that accepts BorrowedFd instead of RawFd, while deprecating the original register method; (3) updating the internal reactor registration to use the borrowed file descriptor API; and (4) updating test implementations and helper functions across multiple files to use the borrowed file descriptor trait (AsFd, BorrowedFd) instead of raw file descriptors.

✨ 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-7992-2026-04-04-10-58-30

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 4, 2026

TEST

@claude
Copy link
Copy Markdown

claude bot commented Apr 4, 2026

PR Review: IO Safety for AioSource

Overall this is a well-structured change that correctly adopts IO safety idioms and updates dependencies in sync. The backwards-compatibility design (each default calling the other, with one panicking to break the cycle) is a known Rust pattern and is correctly implemented here. A few issues worth addressing:


Bugs / Correctness

The # Safety heading on a non-unsafe fn (poll_aio.rs, line 22-27): Using the # Safety rustdoc section is a Rust convention reserved for unsafe fn. Since register is a safe function, this heading will mislead readers into thinking the function requires an unsafe call site. Consider renaming it to # IO Safety or # Caution, which is what the standard library uses for IO-safety caveats on safe functions.

Typo in doc comment (poll_aio.rs, line 26): The text says "It's memory-safe safe, but not I/O safe." — double "safe" — should be "memory-safe".

Misleading comment calls register "unsafe" (poll_aio.rs, line 38): The text says "implementors written before 1.51.0 that only implemented the unsafe register method." — register is not unsafe fn; it's merely not IO-safe. Calling it "unsafe" is confusing in a Rust context where that word has a precise meaning.


Design / API

Runtime-only enforcement of the "implement at least one" contract (poll_aio.rs, line 19-20): The doc says "Implementors must implement at least one of AioSource::register and AioSource::register_borrowed." But if a downstream implementor overrides neither, they get a runtime panic rather than a compile error. This is unavoidable given the backwards-compat constraint, but worth noting explicitly in the documentation, e.g.: "Failure to implement at least one will result in a runtime panic when the source is registered."


Test Coverage

No test for the backwards-compatibility path: The tests in io_poll_aio.rs now all use register_borrowed. There is no test verifying the backwards-compat path: an AioSource that only overrides the deprecated register method should still work through the default register_borrowed. A small test fixture that implements only register would catch any future regression.


Minor

AsRawFd import kept for internal use only: In poll_aio.rs, AsRawFd is imported and used inside the default register_borrowed body via kq.as_raw_fd(). This is correct and intentional, but a brief comment would help future readers who might otherwise flag it as dead code.

Error message typo fix is a nice catch: The fcntl(F_GETFD) / fcntl(F_SETFD) to fcntl(F_GETFL) / fcntl(F_SETFL) fixes in the test error strings are correct and appreciated.


Summary:

  • Low: # Safety heading on a safe fn — should be # IO Safety
  • Low: Typo: "memory-safe safe"
  • Low: Comment calls register "unsafe" incorrectly
  • Low: Doc should note runtime-panic consequence of implementing neither method
  • Suggestion: Add a backwards-compat test using the deprecated register path

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 updates the mio, nix, and mio-aio dependencies and refactors the AioSource trait to utilize I/O-safe types like BorrowedFd and OwnedFd instead of RawFd. The register method is deprecated in favor of register_borrowed, with corresponding updates applied to the test suite. Feedback is provided regarding a minor typo in the documentation of the AioSource trait.

///
/// # Safety
///
/// It's memory-safe safe, but not I/O safe. If the file referenced by `kq` gets dropped, then
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

There is a typo in the documentation: "memory-safe safe" should be "memory-safe".

Suggested change
/// It's memory-safe safe, but not I/O safe. If the file referenced by `kq` gets dropped, then
/// It's memory-safe, but not I/O safe. If the file referenced by `kq` gets dropped, then

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:good-to-have; category:documentation; feedback: The Gemini AI reviewer is correct! memory-safe safe sounds wrong. One of the occurrences of safe should be removed.

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.

🧹 Nitpick comments (2)
tokio/tests/io_async_fd.rs (1)

106-114: Dead code: flags < 0 check is unreachable.

In nix 0.31.0, fcntl returns Result<c_int, Errno>. The .expect() on line 106 already panics on error, so if execution reaches line 108, flags is guaranteed to be the successful return value (valid file status flags, which are non-negative). The if flags < 0 branch can never execute.

This is pre-existing dead code, but since you're touching this function, consider cleaning it up:

🧹 Suggested cleanup
 fn set_nonblocking(fd: &OwnedFd) {
     use nix::fcntl::{OFlag, F_GETFL, F_SETFL};
 
     let flags = nix::fcntl::fcntl(fd, F_GETFL).expect("fcntl(F_GETFL)");
 
-    if flags < 0 {
-        panic!(
-            "bad return value from fcntl(F_GETFL): {} ({:?})",
-            flags,
-            nix::Error::last()
-        );
-    }
-
     let flags = OFlag::from_bits_truncate(flags) | OFlag::O_NONBLOCK;
 
     nix::fcntl::fcntl(fd, F_SETFL(flags)).expect("fcntl(F_SETFL)");
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tokio/tests/io_async_fd.rs` around lines 106 - 114, The code performs
nix::fcntl::fcntl(fd, F_GETFL).expect("fcntl(F_GETFL)") and then checks if the
returned flags < 0, but expect() already panics on error and valid flags are
non-negative, so remove the unreachable if flags < 0 { panic!(...) } branch;
keep the single-line let flags = nix::fcntl::fcntl(fd,
F_GETFL).expect("fcntl(F_GETFL)"); and any subsequent uses of flags unchanged
(references: variable name flags and the fcntl call F_GETFL).
tokio/tests/io_poll_aio.rs (1)

24-29: Please keep one fixture on the deprecated registration hook.

Every AioSource impl in this file now overrides register_borrowed, so the compatibility fallback added in tokio/src/io/bsd/poll_aio.rs is no longer exercised. A tiny legacy fixture that only implements the deprecated register would protect that path against regressions.

Also applies to: 84-92, 225-233

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tokio/tests/io_poll_aio.rs` around lines 24 - 29, One of the tests removed
the last use of the deprecated registration hook so the compatibility fallback
in poll_aio.rs (the AioSource registration fallback path) is no longer
exercised; restore a tiny legacy fixture that implements only the old deprecated
register/deregister hook (instead of register_borrowed) so the fallback path is
covered. Concretely, in the AioSource test fixtures (where implementations
define register_borrowed and deregister), keep or add a minimal fixture that
implements the deprecated register (and corresponding deregister) method
signature only, ensuring the test suite hits the compatibility branch in
poll_aio.rs that handles the deprecated registration API.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tokio/tests/io_async_fd.rs`:
- Around line 106-114: The code performs nix::fcntl::fcntl(fd,
F_GETFL).expect("fcntl(F_GETFL)") and then checks if the returned flags < 0, but
expect() already panics on error and valid flags are non-negative, so remove the
unreachable if flags < 0 { panic!(...) } branch; keep the single-line let flags
= nix::fcntl::fcntl(fd, F_GETFL).expect("fcntl(F_GETFL)"); and any subsequent
uses of flags unchanged (references: variable name flags and the fcntl call
F_GETFL).

In `@tokio/tests/io_poll_aio.rs`:
- Around line 24-29: One of the tests removed the last use of the deprecated
registration hook so the compatibility fallback in poll_aio.rs (the AioSource
registration fallback path) is no longer exercised; restore a tiny legacy
fixture that implements only the old deprecated register/deregister hook
(instead of register_borrowed) so the fallback path is covered. Concretely, in
the AioSource test fixtures (where implementations define register_borrowed and
deregister), keep or add a minimal fixture that implements the deprecated
register (and corresponding deregister) method signature only, ensuring the test
suite hits the compatibility branch in poll_aio.rs that handles the deprecated
registration API.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4e1cb8a9-a970-43e5-863b-311485651dea

📥 Commits

Reviewing files that changed from the base of the PR and between 80a0b97 and 5c1df99.

📒 Files selected for processing (6)
  • tokio/Cargo.toml
  • tokio/src/io/bsd/poll_aio.rs
  • tokio/tests/io_async_fd.rs
  • tokio/tests/io_async_fd_memory_leak.rs
  • tokio/tests/io_poll_aio.rs
  • tokio/tests/net_unix_pipe.rs

@martin-augment
Copy link
Copy Markdown
Owner Author

24-29: Please keep one fixture on the deprecated registration hook.

Every AioSource impl in this file now overrides register_borrowed, so the compatibility fallback added in tokio/src/io/bsd/poll_aio.rs is no longer exercised. A tiny legacy fixture that only implements the deprecated register would protect that path against regressions.

value:valid-but-wont-fix; category:bug; feedback: The CodeRabbit AI reviewer is not correct! The idea is to deprecate the register() method and remove it completely in the next version. All implementors should implement the new register_borrowed() method. The default impl of register_borrowed() delegates to the deprecated register(), so that the existing implementors do not break.

@martin-augment
Copy link
Copy Markdown
Owner Author

106-114: Dead code: flags < 0 check is unreachable.

In nix 0.31.0, fcntl returns Result<c_int, Errno>. The .expect() on line 106 already panics on error, so if execution reaches line 108, flags is guaranteed to be the successful return value (valid file status flags, which are non-negative). The if flags < 0 branch can never execute.

value:good-but-wont-fix; category:bug; feedback: The CodeRabbit AI reviewer is correct! The method returns Result, so any kind of problems should be returned as Err. But this is in tests code and it just makes sure that the error is not returned in C-style, i.e. a negative number. I do not do any harm, so there is no need to remove it.

@martin-augment
Copy link
Copy Markdown
Owner Author

augment review

@martin-augment
Copy link
Copy Markdown
Owner Author

Typo in doc comment (poll_aio.rs, line 26): The text says "It's memory-safe safe, but not I/O safe." — double "safe" — should be "memory-safe".

value:good-to-have; category:documentation; feedback: The Claude AI reviewer is correct! memory-safe safe sounds wrong. One of the occurrences of safe should be removed.

@martin-augment
Copy link
Copy Markdown
Owner Author

Design / API

Runtime-only enforcement of the "implement at least one" contract (poll_aio.rs, line 19-20): The doc says "Implementors must implement at least one of AioSource::register and AioSource::register_borrowed." But if a downstream implementor overrides neither, they get a runtime panic rather than a compile error. This is unavoidable given the backwards-compat constraint, but worth noting explicitly in the documentation, e.g.: "Failure to implement at least one will result in a runtime panic when the source is registered."

value:valid-but-wont-fix; category:bug; feedback: The Claude AI reviewer is not correct! Changing the type of the argument of the old/existing register() method would be a backward incompatible change. This is the reason to add a new method to the trait and delegate to the old one by default. This way old implementations of this trait will still work once the changes in this PR are released.

@augmentcode
Copy link
Copy Markdown

augmentcode bot commented Apr 6, 2026

🤖 Augment PR Summary

Summary: This PR updates Tokio’s FreeBSD/POSIX AIO integration to use Rust’s I/O-safety APIs.

Changes:

  • Bumps mio to 1.2 and updates related dev-dependencies (nix 0.31, mio-aio 2) to match newer FD APIs.
  • Extends AioSource with register_borrowed(BorrowedFd) and switches the reactor registration path to use Registry::as_fd().
  • Updates FreeBSD AIO tests to implement the new borrowed-FD registration API.
  • Adjusts several tests to use OwnedFd/AsFd instead of raw FD integers when calling nix::fcntl.

Technical Notes: The main behavioral intent is to reduce accidental misuse of raw file descriptors by propagating BorrowedFd through registration APIs where possible.

🤖 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. 1 suggestion posted.

Fix All in Augment

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

/// Registers this AIO event source with Tokio's reactor.
fn register_borrowed(&mut self, kq: BorrowedFd<'_>, token: usize) {
// This default implementation serves to provide backwards compatibility with AioSource
// implementors written before 1.51.0 that only implemented the unsafe `register` method.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

register_borrowed’s compat comment says older implementors “only implemented the unsafe register method”, but register isn’t declared unsafe here (it’s deprecated, but still safe). Consider adjusting the wording so the docs don’t imply an unsafe contract that doesn’t exist.

Severity: low

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:incorrect-but-reasonable; category:bug; feedback: The Augment AI reviewer is not correct! Here unsafe is confused with Rust's unsafe keyword while it actually means that the usage of RawFD is more unsafe than using BorrowedFS/OwnedFD.

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. 3 suggestions posted.

Fix All in Augment

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

fn register(&mut self, _kq: RawFd, _token: usize) {
// This default implementation exists so new AioSource implementors that implement the
// register_borrowed method can compile without the need to implement register.
unimplemented!("Use AioSource::register_borrowed instead")
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/io/bsd/poll_aio.rs:32: Because both register and register_borrowed have default implementations, an AioSource impl that forgets to override either will compile but panic at runtime when Tokio registers it (via this unimplemented!). Since AioSource is public, this is a fairly easy footgun for downstream implementors.

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:incorrect-but-reasonable; category:bug; feedback: The Augment AI reviewer is not correct! The important function now is register_borrowed(). By default it just delegates to register(), so that all old implementors still work. They will see a deprecation warning but their build will still pass and the functionality will work at runtime. Any new implementors may not override neither of the functions because both of them have default implementations but they will quickly realize that they need to override register_borrowed() on the first attempt to use the function. Hopefully they will test the new implementation before using it in production.

/// Registers this AIO event source with Tokio's reactor.
fn register_borrowed(&mut self, kq: BorrowedFd<'_>, token: usize) {
// This default implementation serves to provide backwards compatibility with AioSource
// implementors written before 1.51.0 that only implemented the unsafe `register` method.
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/io/bsd/poll_aio.rs:38: This comment describes the deprecated register as “unsafe”, but the method isn’t actually unsafe (it’s just not I/O-safe), which could mislead implementors reading the compatibility note.

Severity: low

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:incorrect-but-reasonable; category:bug; feedback: The Augment AI reviewer is not correct! Here unsafe is confused with Rust's unsafe keyword while it actually means that the usage of RawFD is more unsafe than using BorrowedFS/OwnedFD.

///
/// # Safety
///
/// It's memory-safe safe, but not I/O safe. If the file referenced by `kq` gets dropped, then
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/io/bsd/poll_aio.rs:26: Doc typo: “memory-safe safe” reads like an accidental duplication in the # Safety text.

Severity: low

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:good-to-have; category:documentation; feedback: The Augment AI reviewer is correct! memory-safe safe sounds wrong. One of the occurrences of safe should be removed.

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.

2 participants