Skip to content

sync: fix panic in Chan::recv_many when called with non-empty vector on closed channel#7991

Merged
Darksonn merged 1 commit into
tokio-rs:tokio-1.47.xfrom
LeoniePhiline:fix/7990-recv-many-rx-closed-non-empty-buffer
Apr 2, 2026
Merged

sync: fix panic in Chan::recv_many when called with non-empty vector on closed channel#7991
Darksonn merged 1 commit into
tokio-rs:tokio-1.47.xfrom
LeoniePhiline:fix/7990-recv-many-rx-closed-non-empty-buffer

Conversation

@LeoniePhiline

@LeoniePhiline LeoniePhiline commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

Motivation

Chan::recv_many intends to assert that no slots
have been consumed when exiting with Ready via
the rx_closed code path.

Instead of asserting no items were added to the
buffer, it asserted buffer emptiness, incorrectly
making assumptions about the provided buffer.

When recv_many was called on an empty channel
with idle semaphore after the receiver was closed,
the method would panic.

Solution

The branch coverage had been previously missing.

This changeset corrects the assertion
and adds tests covering the code path.

Fixes #7990.

@github-actions github-actions Bot added the R-loom-sync Run loom sync tests on this PR label Mar 27, 2026
@mattiapitossi mattiapitossi added A-tokio Area: The main tokio crate M-sync Module: tokio/sync labels Mar 27, 2026

@Darksonn Darksonn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks!

Merge without squash is recommended.

If you add the test first, then I will need to merge with squash. Otherwise bisection will not work on the main branch if someone hits the first commit in a bisection.

Comment thread tokio/tests/sync_mpsc.rs Outdated
Comment on lines +369 to +370
// Does not panic, no longer expecting an empty buffer (#7990).
rx.recv_many(&mut buffer, 1).await;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think that if someone sees this comment in one year, they will be confused about what you mean by "no longer". They don't know the context of this bug. Instead of this comment, can you add a comment above this test saying that it is a regression test for <link to issue>?

@LeoniePhiline LeoniePhiline Mar 30, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If you add the test first, then I will need to merge with squash. Otherwise bisection will not work on the main branch if someone hits the first commit in a bisection.

Both commits are squashed and the commit message and PR description updated.

I think that if someone sees this comment in one year, they will be confused about what you mean by "no longer". They don't know the context of this bug. Instead of this comment, can you add a comment above this test saying that it is a regression test for <link to issue>?

Following up on #7991 (comment), I have extended the scope of the tests from panic-regression to behavior test. Therefore, the suggested comment seems no longer useful. The original comment is removed.

Comment thread tokio/tests/sync_mpsc.rs Outdated
@mattiapitossi mattiapitossi added the S-waiting-on-author Status: awaiting some action (such as code changes) from the PR or issue author. label Mar 27, 2026
Comment thread tokio/tests/sync_mpsc.rs Outdated
Comment thread tokio/tests/sync_mpsc.rs Outdated
Comment thread tokio/src/sync/mpsc/chan.rs Outdated

if rx_fields.rx_closed && self.inner.semaphore.is_idle() {
assert!(buffer.is_empty());
assert!(buffer.len() == initial_length);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
assert!(buffer.len() == initial_length);
assert_eq!(buffer.len(), initial_length);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I chose assert! because it is a compiler intrinsic while assert_eq! is much more involved.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Using assert_eq! is better because it prints the values when they are not equal. But if you have perf concerns, we can make it into a debug_assert_eq!.

@LeoniePhiline LeoniePhiline Mar 30, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks. I would leave the decision up to you.

Performance is not a huge concern, as this is not a hot path, but of course the panic unwinding machinery will be part of the generated assembly.

Is seems useful to keep consistent with https://github.com/LeoniePhiline/tokio/blob/2222621764ca3b6fd70803874f76ccdc59885ca7/tokio/src/sync/mpsc/chan.rs#L389 and https://github.com/LeoniePhiline/tokio/blob/2222621764ca3b6fd70803874f76ccdc59885ca7/tokio/src/sync/mpsc/chan.rs#L315 at the other exit of the pop loop – should these be changed to debug_assert?

I myself would prefer debug assertions, as these are checking for bugs, not UB. And, to my mind, they are meant to verify the implementation in tests, not production code.

@LeoniePhiline LeoniePhiline force-pushed the fix/7990-recv-many-rx-closed-non-empty-buffer branch from 2222621 to 7dd98a3 Compare March 30, 2026 11:15
Comment thread tokio/tests/sync_mpsc.rs
Comment on lines -383 to +385
// TODO: This check may not be required as it most
// likely can only return `true` at this point. A
// channel is closed when all tx handles are
// A channel is closed when all tx handles are
// dropped. Dropping a tx handle releases memory,
// which ensures that if dropping the tx handle is
// visible, then all messages sent are also visible.
assert!(self.inner.semaphore.is_idle());
debug_assert!(self.inner.semaphore.is_idle());

@LeoniePhiline LeoniePhiline Mar 30, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Following up with #7991 (comment), and after my review of the internals, I am proposing to demote these bug checks (not UB guards) to cfg(debug).

This aligns these pop loop exit assertions with the rx_closed exit assertion.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

IMO such changes should happen in separate PR.

@LeoniePhiline LeoniePhiline requested a review from Darksonn March 30, 2026 11:28
@Darksonn

Darksonn commented Apr 2, 2026

Copy link
Copy Markdown
Member

I'm going to backport this to Tokio 1.47, so I'm changing the base of this PR now.

@Darksonn Darksonn changed the base branch from master to tokio-1.47.x April 2, 2026 10:06
@Darksonn Darksonn force-pushed the fix/7990-recv-many-rx-closed-non-empty-buffer branch from 7dd98a3 to 41a87ad Compare April 2, 2026 10:06

@Darksonn Darksonn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks.

@Darksonn Darksonn changed the title fix: Chan::recv_many must not make assumptions about buffer contents sync: fix panic in Chan::recv_many when called with non-empty vector on closed channel Apr 2, 2026
Darksonn pushed a commit to LeoniePhiline/tokio that referenced this pull request Apr 2, 2026
…r on closed channel (tokio-rs#7991)

`Chan::recv_many` intends to assert that no slots
have been consumed when exiting with `Ready` via
the `rx_closed` code path.

Instead of asserting no items were added to the
buffer, it asserted buffer emptiness, incorrectly
making assumptions about the provided buffer.

When `recv_many` was called on an empty channel
with idle semaphore after the receiver was closed,
the method would panic.

The branch coverage had been previously missing.

This changeset corrects the assertion
and adds tests covering the code path.

Fixes tokio-rs#7990.
@Darksonn Darksonn force-pushed the fix/7990-recv-many-rx-closed-non-empty-buffer branch from 41a87ad to 351c4f5 Compare April 2, 2026 10:31
@mattiapitossi mattiapitossi removed the S-waiting-on-author Status: awaiting some action (such as code changes) from the PR or issue author. label Apr 2, 2026
…r on closed channel (tokio-rs#7991)

`Chan::recv_many` intends to assert that no slots
have been consumed when exiting with `Ready` via
the `rx_closed` code path.

Instead of asserting no items were added to the
buffer, it asserted buffer emptiness, incorrectly
making assumptions about the provided buffer.

When `recv_many` was called on an empty channel
with idle semaphore after the receiver was closed,
the method would panic.

The branch coverage had been previously missing.

This changeset corrects the assertion
and adds tests covering the code path.

Fixes tokio-rs#7990.
@Darksonn Darksonn force-pushed the fix/7990-recv-many-rx-closed-non-empty-buffer branch from 351c4f5 to bf18ed4 Compare April 2, 2026 11:16
@Darksonn Darksonn merged commit bf18ed4 into tokio-rs:tokio-1.47.x Apr 2, 2026
82 checks passed
@LeoniePhiline

Copy link
Copy Markdown
Contributor Author

Thank you!

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

Labels

A-tokio Area: The main tokio crate M-sync Module: tokio/sync R-loom-sync Run loom sync tests on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mpsc::Chan::recv_many panics due to faulty assertion when called with a non-empty buffer after receiver closed

4 participants