Skip to content

Sample code with race condition introduced by thread::spawn can be compiled & run #140326

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
zhongxinghong opened this issue Apr 26, 2025 · 2 comments

Comments

@zhongxinghong
Copy link

I tried this code (playground):

struct MyStruct {
    name: String,
}

pub fn main() {
    let mut s = MyStruct {
        name: String::from(""),
    };
    let _h = std::thread::spawn(move || {
        s.name = String::from("hello"); // line 10
        println!("{:p} {:?}", &s.name, s.name); // line 11
    });
    // let _ = _h.join().unwrap(); // line 13
    // println!("{:p}", &s.name); // error[E0382]: borrow of moved value: `s.name`
    s.name = String::from("world");
    println!("{:p} {:?}", &s.name, s.name); // line 16
}

I expected to see this happen:
Cannot be compiled. String doesn't implement Copy trait so the s.name should be point to the same address at line 11 and line 16. This can cause concurrently reading & writing on s.name by (1) partially write s.name at line 10 (e.g. only write the ptr field); (2) read at line 16.

Instead, this happened:
Output:

0x7fff673b1ed0 "world"

Uncomment the line 13:

0x744cf7eaba20 "hello"
0x7fff38051b40 "world"

It seems that implicitly Copy happened to s or s.name, but it's illegal because nether String nor MyStruct implement the Copy trait.

Meta

Could be reproduced in the following versions with debug or release build:

Stable version: 1.86.0

Beta version: 1.87.0-beta.5
(2025-04-19 386abeb)

Nightly version: 1.88.0-nightly
(2025-04-25 b4c8b0c)

@zhongxinghong zhongxinghong added the C-bug Category: This is a bug. label Apr 26, 2025
@rustbot rustbot added the needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. label Apr 26, 2025
@theemathas
Copy link
Contributor

theemathas commented Apr 26, 2025

This is working as intended. In editions 2021 and later, the closure captures only the s.name field. Furthermore, since you annotated the closure with move, the closure moves the s.name field out into the closure. This leaves s to be a struct with a temporarily uninitialized field, which is fine.

Your line 11 doesn't run in my testing. This is because the programs ends before the spawned thread gets a turn to executed.

Your code is roughly equivalent to the following, which doesn't have UB:

struct Closure {
    captured_name: String,
}

impl Closure {
    fn my_call(mut self) {
        self.captured_name = String::from("hello");
        println!("{:p} {:?}", &self.captured_name, self.captured_name);
    }
}

struct MyStruct {
    name: String,
}

fn my_spawn(f: Closure) {
    // stuff here that calls f.my_call()
}

pub fn main() {
    let mut s = MyStruct {
        name: String::from(""),
    };
    let _h = my_spawn(Closure { captured_name: s.name });
    s.name = String::from("world");
    println!("{:p} {:?}", &s.name, s.name);
}

@zhongxinghong
Copy link
Author

This leaves s to be a struct with a temporarily uninitialized field, which is fine.

I got it. Thanks for your answer!

@jieyouxu jieyouxu removed needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. C-bug Category: This is a bug. labels Apr 28, 2025
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

No branches or pull requests

4 participants