rust-lang / rust-lang/rust

windows: `TcpStream::shutdown` does not wake up blocking reads

Open
#121,594 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

A-docs A-io C-bug O-windows T-libs
Dominant language
Rust
Stars
119k
Forks
16.1k
PR merge metrics
PR metrics pending

Description

Description

The docs for TcpStream::shutdown currently guarantee that a shutdown with Shutdown::Read must wake up pending operations immediately:

This function will cause all pending and future I/O on the specified portions to return immediately with an appropriate value (see the documentation of Shutdown).

The docs for Shutdown::Read explicitly says that currently blocked reads must return:

All currently blocked and future reads will return Ok(0).

However, that is currently not the case on *-pc-windows-* platforms. Instead, a currently blocked read will stay blocked forever when shutdown is called with Shutdown::Read in a different thread. The same also happens with Shutdown::Both.

This is causing the close_read_wakes_up test to fail spuriously due to a race condition, for example in https://github.com/rust-lang/rust/pull/121523#issuecomment-1962581666 and https://github.com/rust-lang/rust/pull/120543#issuecomment-1925748911. The test only passes if the shutdown happens before the read.

Repro

I tried this code:

use std::io::Read;
use std::net::{Ipv6Addr, Shutdown, TcpListener, TcpStream};
use std::thread;
use std::time::Duration;

fn main() {
    let listener = TcpListener::bind((Ipv6Addr::LOCALHOST, 0)).unwrap();
    let listener_addr = listener.local_addr().unwrap();
    let mut serverbound_stream = None;
    thread::scope(|scope| {
        // 1. `accept` and `connect` concurrently
        let _clientbound_stream = scope.spawn(|| {
            let (clientbound_stream, _) = listener.accept().unwrap();
            clientbound_stream
        });
        let serverbound_stream =
            serverbound_stream.insert(TcpStream::connect(listener_addr).unwrap());

        // 3. shutdown read during blocking read
        scope.spawn(|| {
            thread::sleep(Duration::from_secs(1)); // just to be sure
            serverbound_stream.shutdown(Shutdown::Read).unwrap();
        });

        // 2. blocking read
        let count = (&*serverbound_stream).read(&mut [0]).unwrap();
        assert_eq!(count, 0);
    });
}

I expected to see this happen: The .shutdown(Shutdown::Read) makes the blocking read return immediately with Ok(0).

Instead, this happened: The blocking read does not return, making the program hang indefinitely.

Meta

rustc --version --verbose:

rustc 1.78.0-nightly (381d69953 2024-02-24)
binary: rustc
commit-hash: 381d69953bb7c3390cec0fee200f24529cb6320f
commit-date: 2024-02-24
host: x86_64-unknown-linux-gnu
release: 1.78.0-nightly
LLVM version: 18.1.0

Tested on these platforms:

  • x86_64-pc-windows-msvc with Microsoft Windows Server 2019 (GitHub CI)
  • x86_64-pc-windows-gnu with wine 9.1 on Linux
Related

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with library/std/src/net/tcp/tests.rs, especially close_read_wakes_up, and run the supplied reproduction on a Windows target to confirm the blocked-read behavior. Trace the TcpStream shutdown and blocking-read paths used there. Done means the test no longer races or hangs on Windows and Shutdown::Read returns the documented Ok(0).

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
networking, operating-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.