Skip to content

JoinGuard::join returning an Err is **really** unsafe #20807

Closed
@tomaka

Description

@tomaka

Exception safety is a very well-known problem in C++.

Consider this:

struct Foo {
    int* ptr;
};

void modify(Foo& value) {
    delete foo.ptr;
    foo.ptr = new int;
}

void error_proof_modify(Foo& value) {
    try {
        modify(value);
    } catch(...) {
        cerr << "Error, but let's continue!";
    }
}

If the call to new throws an exception, the error gets caught and the execution continue. However the Foo object is now in an invalid state with its pointer pointing to memory that has been free'd.

Before #20615 and before the change in Send this is not possible in Rust, because when a thread panics all objects that are local to this thread are no longer accessible. It's also the reason why mutexes are poisoned in case of a panic.

But now that it's possible to send local variables to other threads, and ignore when this other thread panics, situations like this can totally happen.

Let's take this code for example:

pub struct Foo {
    val1: int,
    val2: int,
    val3: int,
    calculation_result: int,    // must always be equal to val1+val2+val3
}

impl Foo {
    pub fn set_val1(&mut self, value: int) {
        self.val1 = value;
        self.update_calculation();
    }
    pub fn set_val2(&mut self, value: int) {
        self.val2 = value;
        self.update_calculation();
    }
    pub fn set_val3(&mut self, value: int) {
        self.val3 = value;
        self.update_calculation();
    }

    fn update_calculation(&mut self) {
        if self.val1 == 127 { panic!("for the sake of this example, we panic") };
        self.calculation_result = self.val1 + self.val2 + self.val3;
    }
}

Looks safe, right?

But what if you use that "ignore panics" trick?

let mut foo: Foo = ...;  // whatever

Thread::scoped(|| {
    foo.set_val1(127);
}).join();   // ignoring the panic

use_foo(&foo);   // continue using `foo`

At the end of this code, you have a Foo object in an invalid state because calculation_result is not equal to val1+val2+val3 as it should be. Continuing to use the Foo object in this invalid state could lead to weird and hard to debug results (depending on the rest of the code).

With the latest reforms in thread, library writer really have to take exception safety (or "panic safety") into account by avoiding the kind of code written above. You may argue that it's just the fault of the person who wrote the Foo struct. The problem is that exception safety is hard. Too hard to get right.

In my opinion, JoinGuard::join should just panic if the underlying thread panicked, without leaving the choice to the user. The only situation in which a try_join function (or another name) is safe is for JoinGuard<'static>.

cc @aturon

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions