1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
//! Executable demonstration of the `spawn_local` soundness hazard.
//!
//! # The bug
//!
//! `affinitypool::spawn_local` (and `Threadpool::spawn_local`) accept a
//! closure that may **borrow** non-`'static` data from the caller's
//! stack. To make that lifetime erasure work, the returned future runs a
//! blocking cancellation in its `Drop` impl: dropping the future parks
//! the current thread until the worker has stopped running the closure,
//! so the closure's borrows are guaranteed dead before they expire.
//!
//! That guarantee relies entirely on the destructor running, and Rust
//! does **not** guarantee destructors run. Safe code can skip one with
//! `std::mem::forget`, `Box::leak`, a `ManuallyDrop`, an `Rc`/`Arc`
//! cycle, or by nesting the future inside another future that is itself
//! leaked. If the future is leaked while it still borrows caller data,
//! the worker goes on to read that data after it has gone out of scope:
//! a data race and a use-after-free. This is the classic
//! "leak ⇒ unsoundness" hole — the same one that sank the pre-1.0
//! `std::thread::scoped` API.
//!
//! This test reproduces exactly that. `PollAndLeak` polls the spawn
//! future once (so the closure is scheduled and the worker parks on a
//! channel) and then `mem::forget`s it instead of dropping it. The
//! borrow region for `v` then ends, `v` is mutated, and only afterwards
//! is the worker told to read `&v`.
//!
//! # Why there is no fix in async Rust
//!
//! There is no sound, fully safe `spawn_local` in today's async Rust. A
//! future is a *value*, and safe code may always leak a value, so a
//! future's destructor can never be a load-bearing safety barrier. The
//! only construct whose completion safe code cannot skip is a returning
//! stack frame — which is why the one sound design is a *synchronous*
//! scoped API (`std::thread::scope` / `rayon::scope`), where the join
//! happens as the scope call returns. That shape cannot be expressed
//! over `.await`: awaiting hands control to an executor that is never
//! obliged to poll the future again.
//!
//! # How to use `spawn_local` safely
//!
//! Because the hazard cannot be designed out, `spawn_local` is `unsafe`.
//! The way to ensure safety is to use it correctly: **never leak the
//! returned future while it borrows non-`'static` data** — always let it
//! drop, or drive it to completion, before the borrows end. The ordinary
//! patterns (`pool.spawn_local(..).await`, or just dropping the future)
//! all uphold this; only a deliberate leak like the one below breaks it.
//! If the closure captures only `'static` data, use the safe
//! `affinitypool::spawn` instead.
//!
//! This test is run in CI but is **not** allowed to block it: it
//! exercises undefined behaviour, whose observable result is not
//! guaranteed (it usually "passes" because the freed stack slot still
//! reads back as a valid integer). It exists as an executable record of
//! the hazard, not as a correctness check — see the non-blocking
//! `unsound` job in `.github/workflows/ci.yml`.
use ;
/// A future adapter that polls its inner future exactly once and then,
/// if it is still `Pending`, **leaks** it via [`std::mem::forget`]
/// instead of dropping it — deliberately skipping the `SpawnFuture`
/// destructor that `spawn_local`'s soundness depends on.
;
/// Drives `spawn_local` into a use-after-free by leaking its future
/// mid-flight. See the module docs for the full explanation.
///
/// `#[ignore]`d so the default `cargo test` run never executes it; CI
/// runs it explicitly in a dedicated, non-blocking job.