You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: text/0000-forget-marker-trait.md
+10-6Lines changed: 10 additions & 6 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -328,7 +328,7 @@ Currently, channels are created via `let (tx, rx) = channel()`. This is not comp
328
328
329
329
There exists a way to exploit the old `thread::scoped` API without any memory leaks[^no_leaks_sidenode]. We can move `JoinHandle` inside the thread it is meant to protect, thereby creating a cyclic relationship:
330
330
331
-
[^no_leaks_sidenode]: This would become a memory leak if instead of spawning the thread will use another way to create the cycle. See later.
331
+
[^no_leaks_sidenode]: This would become a memory leak if instead of spawning the thread we will just return the closure as the handle, but without it's type mentioned to prevent cycle errors. See later.
332
332
333
333
```rust
334
334
usestd::{
@@ -374,7 +374,7 @@ fn main() {
374
374
}
375
375
```
376
376
377
-
This code is clearly unsound because we are aliasing a mutable reference, which permits potential data races and use-after-free issues. Furthermore, many types of channels - including rendezvous channels—can be vulnerable to this issue if their signatures allow an equivalent implementation using reference counting.
377
+
This code is clearly unsound because we are aliasing a mutable reference, which permits potential data races and use-after-free issues. Furthermore, many types of channels - including rendezvous channels - can be vulnerable to this issue if their signatures allow an equivalent implementation using reference counting.
378
378
379
379
```rust
380
380
fnmain() {
@@ -444,11 +444,15 @@ fn main() {
444
444
}
445
445
```
446
446
447
+
`JoinHandle` and `scoped` have exactly the same signature as before, but use basic language primitives under the hood. The signature of `scoped` can be summarized as `F + 'a -> 'a` - it erased the concrete type `F` and returned just `JoinHandle<'a>`. `Box<dyn Trait>` is a basic property of the language.
448
+
449
+
We will call APis that split ownership of the allocation between `tx` and `rx` and allow writes `Arc`-like. `Box<dyn Trait>` can cause `Arc`-like APIs to leak, because we can erase the type of `rx` and place it in the shared allocation using `tx`.
450
+
451
+
*Any*`Arc`-like API is capable of causing leaks, without any `unsafe` code on the `scoped` side. This demonstrates that approaches such as making `JoinHandle: !Send` are not feasible. How can we fix it?
452
+
447
453
### Solution for message passing of `!Forget` types.
Thus, there is no point in blaming `scoped`; its API can already be replicated safely in current code, and other `Arc`-like APIs can also cause leaks. This demonstrates that approaches such as making `JoinHandle: !Send` are not feasible. How can we fix it?
451
-
452
456
Looking at our first example with `Arc`, let's replace our `Arc` with a reference:
453
457
454
458
```rust
@@ -505,7 +509,7 @@ error[E0505]: cannot move out of `mutex` because it is borrowed
505
509
|borrowlaterusedhere
506
510
```
507
511
508
-
Here, a single action to replace `Arc<T>` with `&'a T` allowed code to become sound - `JoinHandle: !Forget` allowes to pass references into tasks and removes the need for reference counting in that case. The same approach applies to channels. This approach is not compatible with `JoinHandle: Forget` and cannot be used today with functions like `tokio::spawn` due to `'static` bound, but ecosystem has some examples:
512
+
Here, a single step to replace `Arc<T>` with `&'a T` allowed code to become sound - `JoinHandle: !Forget` allowes to pass references into tasks and removes the need for the reference counting in that case - the allocation is not *owned* by `tx` and `rx`, they are *borrowing* from it, making them not `Arc`-like. The same approach applies to channels. This approach is not compatible with `JoinHandle: Forget` and cannot be used today with functions like `tokio::spawn` due to `'static` bound, but ecosystem has some examples:
509
513
510
514
```rust
511
515
fnmain() {
@@ -530,7 +534,7 @@ fn main() {
530
534
}
531
535
```
532
536
533
-
This code fails to compile, which prevents the unsound behavior. The failure occurs because the borrow checker detects a self-reference: `handle` borrows `queue`, but moving `handle` into `queue` causes `queue` to indirectly borrow itself. Since the compiler inserts a call to `drop` on `queue`, this self-referential borrow is caught at compile time.
537
+
This code fails to compile, which prevents the unsound behavior. The failure occurs because the borrow checker detects a self-reference: `handle` borrows `queue`, but moving `handle` into `queue` causes `queue` to indirectly borrow itself. Since the compiler inserts a call to `drop` on `queue`, this self-referential borrow is caught at compile time. But `Arc` is *designed* to remove the lifetime, removing borrow-checkers ability to prevent loops, self-references and leaks.
534
538
535
539
Thus, to support message passing with `!Forget` types, API authors must rely more heavily on lifetimes. Since `Forget` types inherently involve lifetime management, using explicit lifetimes (for example, by replacing `Arc<T>` with `&'a T`, having `PhantomData` together with a pointer etc) prevents the formation of cycles that can lead to unsoundness. While this approach is not compatible with APIs that require a `'static` bound (such as `tokio::spawn`), it works in environments like `thread::scope` or async scopes, where the future itself can be `!Forget`. Notably, rendezvous channels can be soundly expressed using this API alongside `PhantomData`.
0 commit comments