Skip to content

Commit 9e1e794

Browse files
authored
Merge pull request #31 from BatmanAoD/longjmp-tbd
Make all `longjmp` and `pthread_exit` related behavior TBD
2 parents 9c65b4a + 473aae0 commit 9e1e794

File tree

2 files changed

+140
-85
lines changed

2 files changed

+140
-85
lines changed

rfcs/0000-c-unwind-abi.md

+50-85
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ previously-undefined cases when an unwind operation reaches a Rust function
1717
boundary with a non-`"Rust"`, non-`"C unwind"` ABI.
1818

1919
As part of this specification, we introduce the term ["Plain Old Frame"
20-
(POF)][POF-definition]. These are frames that may be safely deallocated with
21-
`longjmp`.
20+
(POF)][POF-definition]. These are frames that have no pending destructors and
21+
can be trivially deallocated.
2222

2323
This RFC does not define the behavior of `catch_unwind` in a Rust frame being
2424
unwound by a foreign exception. This is something the [project
@@ -40,12 +40,6 @@ unwinding mechanism, the current `rustc` implementation assumes that `extern
4040
"C"` functions cannot unwind, which permits LLVM to optimize with the
4141
assumption that such unwinding constitutes undefined behavior.
4242

43-
Additionally, there are libraries such as `rlua` that rely on `longjmp` across
44-
Rust frames; on Windows, `longjmp` is implemented via [forced
45-
unwinding][forced-unwinding]. The current `rustc` implementation makes it safe
46-
to `longjmp` across Rust [POFs][POF-definition] (frames without `Drop` types),
47-
but this is not formally specified in an RFC or by the Reference.
48-
4943
The desire for this feature has been previously discussed on other RFCs,
5044
including [#2699][rfc-2699] and [#2753][rfc-2753].
5145

@@ -57,11 +51,12 @@ several requirements for any cross-language unwinding design.
5751
The ["Analysis of key design goals"][analysis-of-design-goals] section analyzes
5852
how well the current design satisfies these constraints.
5953

60-
* **Changing from panic=unwind to panic=abort cannot cause UB:** We
61-
wish to ensure that choosing `panic=abort` doesn't ever create
62-
undefined behavior (relate to `panic=unwind`), even if one is
63-
relying on a library that triggers a panic or a foreign exception.
64-
* **Optimization with panic=abort:** when using `-Cpanic=abort`, we
54+
* **Changing from `panic=unwind` to `panic=abort` cannot cause undefined
55+
behavior:** We wish to ensure that changing from `panic=unwind` to
56+
`panic=abort` never creates undefined behavior (relate to `panic=unwind`),
57+
even if one is relying on a library that triggers a panic or a foreign
58+
exception.
59+
* **Optimization with `panic=abort`:** when using `panic=abort`, we
6560
wish to enable as many code-size optimizations as possible. This
6661
means that we shouldn't have to generate unwinding tables or other
6762
such constructs, at least in most cases.
@@ -78,19 +73,24 @@ how well the current design satisfies these constraints.
7873
languages) to raise exceptions that will propagate through Rust
7974
frames "as if" they were Rust panics (i.e., running destrutors or,
8075
in the case of `unwind=abort`, aborting the program).
81-
* **Enable error handling with `longjmp`:** As mentioned above, some existing
82-
Rust libraries use `longjmp`. Despite the fact that `longjmp` on Windows is
83-
[technically a form of unwinding][forced-unwinding], using `longjmp` across
84-
Rust [POFs][POF-definition] [is safe][longjmp-pr] with the current
85-
implementation of `rustc`, and we want to specify that this will remain safe.
76+
* **Enable error handling with `longjmp`:**
77+
As mentioned above, some existing Rust libraries rely on the ability to
78+
`longjmp` across Rust frames to interoperate with Ruby, Lua, and other C
79+
APIs. The behavior of `longjmp` traversing Rust frames is not specified or
80+
guaranteed to be safe; in the current implementation of `rustc`,
81+
however, it [is safe][longjmp-pr]. On Windows, `longjmp` is implemented as a
82+
form of unwinding called ["forced unwinding"][forced-unwinding], so any
83+
specification of the behavior of forced unwinding across FFI boundaries
84+
should be forward-compatible with a [future RFC][unresolved-questions] that
85+
will provide a well-defined way to interoperate with longjmp-based APIs.
8686
* **Do not change the ABI of functions in the `libc` crate:** Some `libc`
8787
functions may invoke `pthread_exit`, which uses [a form of
8888
unwinding][forced-unwinding] in the GNU libc implementation. Such functions
8989
must be safe to use with the existing `"C"` ABI, because changing the types
9090
of these functions would be a breaking change.
9191

92-
[inside-rust-requirements]: https://blog.rust-lang.org/inside-rust/2020/02/27/ffi-unwind-design-meeting.html#requirements-for-any-cross-language-unwinding-specification
93-
[longjmp-pr]: https://github.com/rust-lang/rust/pull/48572
92+
[inside-rust-requirements]: https://blog.rust-lang.org/inside-rust/2020/02/27/ffi-unwind-design-meeting.html#requirements-for-any-cross-language-unwinding-specification
93+
[longjmp-pr]: https://github.com/rust-lang/rust/pull/48572
9494

9595
# Guide-level explanation
9696
[guide-level-explanation]: #guide-level-explanation
@@ -170,41 +170,10 @@ types). In other words, a forced unwind operation on one platform will simply
170170
deallocate Rust frames without true unwinding on other platforms.
171171

172172
This RFC specifies that, regardless of the platform or the ABI string (`"C"` or
173-
`"C unwind"`), any platform features that may rely on forced unwinding are:
174-
175-
* _undefined behavior_ if they cross non-[POFs][POF-definition]
176-
* _defined behavior_ when all unwound frames are POFs
177-
178-
As an example:
179-
180-
```rust
181-
fn foo<D: Drop>(c: bool, d: D) {
182-
if c {
183-
drop(d);
184-
}
185-
longjmp_if_true(c);
186-
}
187-
188-
/// Calls `longjmp` if `c` is true; otherwise returns normally.
189-
extern "C" fn longjmp_if_true(c: bool);
190-
```
191-
192-
If a `longjmp` occurs, it can safely traverse the `foo` frame, which will be a
193-
POF because `d` has already been dropped.
194-
195-
Since `longjmp_if_true` function is using the `"C"` rather than the `"C
196-
unwind"` ABI, the optimizer may assume that it cannot unwind; on LLVM, this is
197-
represented by the `nounwind` attribute. On most platforms, `longjmp` is not a
198-
form of unwinding: the `foo` frame is simply discarded. On Windows, `longjmp`
199-
is implemented as a forced unwind, which is permitted to traverse `nounwind`
200-
frames. Since `foo` contains a `Drop` type the forced unwind will include a
201-
call to the frame's cleanup logic, but that logic will not produce any
202-
observable effect; in particular, `D::drop()` will not be called again. The
203-
observable behavior should therefore be the same on all platforms.
204-
205-
Conversely, if, due to a bug, `longjmp` were called unconditionally, then this
206-
code would have undefined behavior on all platforms when `c` is false, because
207-
`foo` would not be a POF.
173+
`"C unwind"`), any platform features that may rely on forced unwinding are
174+
undefined behavior if they cross non-[POFs][POF-definition]. For now, however,
175+
we do not specify the conditions required to use forced unwinding safely; we
176+
will specify this in [a future RFC][unresolved-questions].
208177

209178
[inside-rust-forced]: https://blog.rust-lang.org/inside-rust/2020/02/27/ffi-unwind-design-meeting.html#forced-unwinding
210179

@@ -216,29 +185,20 @@ boundary, either from a `panic!` "escaping" from a Rust function defined with
216185
`extern "C"` or by entering Rust from another language via an entrypoint
217186
declared with `extern "C"`, caused undefined behavior.
218187

219-
This RFC retains most of that undefined behavior, with two exceptions:
220-
221-
* With the `panic=unwind` runtime, `panic!` will cause an `abort` if it would
222-
otherwise "escape" from a function defined with `extern "C"`.
223-
* Forced unwinding is safe with `extern "C"` as long as only
224-
* [POFs][POF-definition] are unwound. This is to keep behavior of
225-
`pthread_exit` and `longjmp` consistent across platforms.
188+
This RFC retains most of that undefined behavior, with one exception: with the
189+
`panic=unwind` runtime, `panic!` will cause an `abort` if it would otherwise
190+
"escape" from a function defined with `extern "C"`.
226191

227192
## Interaction with `panic=abort`
228193

229194
If a non-forced foreign unwind would enter a Rust frame via an `extern "C
230195
unwind"` ABI boundary, but the Rust code is compiled with `panic=abort`, the
231196
unwind will be caught and the process aborted.
232197

233-
There are some types of unwinding that are not guaranteed to cause the program
234-
to abort with `panic=abort`, though:
235-
236-
* Forced unwinding: Rust provides no mechanism to catch this type of unwinding.
237-
This is safe with either the `"C"` ABI or the new `"C unwind"` ABI, as long
238-
as only [POFs][POF-definition] are unwound.
239-
* Unwinding from another language into Rust if the entrypoint to that language
240-
is declared with `extern "C"` (contrary to the guidelines above): this is
241-
always undefined behavior.
198+
With the exception of the above case, however, unwinding from another language
199+
into Rust through an FFI entrypoint declared with `extern "C"` is always
200+
undefined behavior, and is not guaranteed to cause the program to abort under
201+
`panic=abort`.
242202

243203
# Reference-level explanation
244204
[reference-level-explanation]: #reference-level-explanation
@@ -255,13 +215,15 @@ behavior. `"C"`-like ABIs are `"C"` itself but also related ABIs such as
255215
| `panic=abort` | `"C unwind"` | `panic!` aborts | abort |
256216
| `panic=abort` | `"C"`-like | `panic!` aborts (no unwinding occurs) | UB |
257217

258-
Regardless of the panic runtime, ABI, or platform, the interaction of Rust
259-
frames with C functions that deallocate frames (i.e. functions that may use
260-
forced unwinding on specific platforms) is specified as follows:
218+
The interaction of Rust frames with C functions that deallocate frames (i.e.
219+
functions that may use forced unwinding on specific platforms) is independent
220+
of the panic runtime, ABI, or platform.
261221

262-
* **When deallocating Rust [POFs][POF-definition]:** frames are safely
263-
deallocated; no undefined behavior
264-
* **When deallocating Rust non-POFs:** undefined behavior
222+
* **When deallocating Rust non-POFs:** this is explicitly undefined behavior.
223+
* **When deallocating Rust [POFs][POF-definition]:** for now, this is not
224+
specified, and must be considered undefined behavior. However, we do plan to
225+
specify a safe way to deallocate POFs with `longjmp` or `pthread_exit` in [a
226+
future RFC][unresolved-questions].
265227

266228
No subtype relationship is defined between functions or function pointers using
267229
different ABIs. This RFC also does not define coercions between `"C"` and
@@ -283,7 +245,7 @@ This design imposes some burden on existing codebases (mentioned
283245
[above][motivation]) to change their `extern` annotations to use the new ABI.
284246

285247
Having separate ABIs for `"C"` and `"C unwind"` may make interface design more
286-
difficult, especially since this RFC [postpones][future-possibilities]
248+
difficult, especially since this RFC [postpones][unresolved-questions]
287249
introducing coercions between function types using different ABIs.
288250

289251
A single ABI that "just works" with C++ (or any other language that may throw
@@ -348,7 +310,7 @@ Our reasons for preferring the current proposal are:
348310
This section revisits the key design goals to assess how well they
349311
are met by the proposed design.
350312

351-
### Changing from panic=unwind to panic=abort cannot cause UB
313+
### Changing from `panic=unwind` to `panic=abort` cannot cause UB
352314

353315
This constraint is met:
354316

@@ -389,14 +351,11 @@ future work.
389351

390352
### Enable error handling with `longjmp`
391353

392-
This constraint is met: `longjmp` is treated the same across all platforms, and
393-
is safe as long as only [POFs][POF-definition] are deallocated.
354+
This constraint has been [deferred][unresolved-questions].
394355

395356
### Do not change the ABI of functions in the `libc` crate
396357

397-
This constraint is met: `libc` functions will continue to use the `"C"` ABI.
398-
`pthread_exit` will be treated the same across all platforms, and will be safe
399-
as long as only [POFs][POF-definition] are deallocated.
358+
This constraint has been [deferred][unresolved-questions].
400359

401360
# Prior art
402361
[prior-art]: #prior-art
@@ -485,9 +444,15 @@ provide a well-defined behavior for this case, which will probably be either to
485444
let the exception pass through uncaught or to catch some or all foreign
486445
exceptions.
487446

447+
We would also like to specify conditions under which `longjmp` and
448+
`pthread_exit` may safely deallocate Rust frames. This RFC specifies that
449+
frames deallocated in this way [must be POFs][reference-level-explanation].
450+
However, this condition is merely necessary rather than sufficient to ensure
451+
well-defined behavior.
452+
488453
Within the context of this RFC and in discussions among members of the
489454
[FFI-unwind project group][project-group], this class of formally-undefined
490-
behavior which we plan to define at later date is referred to as "TBD
455+
behavior which we plan to define in future RFCs is referred to as "TBD
491456
behavior".
492457

493458
# Future possibilities

rfcs/0000-longjmp-pof-annotation.md

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
- Feature Name: `annotation-for-safe-longjmp`
2+
- Start Date: 2019-06-11
3+
- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000)
4+
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000)
5+
- Project group: [FFI-unwind][project-group]
6+
7+
[project-group]: https://github.com/rust-lang/project-ffi-unwind
8+
9+
<!-- TODO for now, content is copied from prior drafts of the "C unwind" RFC. -->
10+
11+
# Motivation
12+
[motivation]: #motivation
13+
14+
Additionally, there are libraries such as `rlua` that rely on `longjmp` across
15+
Rust frames; on Windows, `longjmp` is implemented via [forced
16+
unwinding][forced-unwinding]. The current `rustc` implementation makes it safe
17+
to `longjmp` across Rust [POFs][POF-definition] (frames without `Drop` types),
18+
but this is not formally specified in an RFC or by the Reference.
19+
20+
# Guide-level explanation
21+
[guide-level-explanation]: #guide-level-explanation
22+
23+
This RFC specifies that, regardless of the platform or the ABI string (`"C"` or
24+
`"C unwind"`), any platform features that may rely on forced unwinding is
25+
defined behavior when all unwound frames are POFs
26+
27+
As an example:
28+
29+
```rust
30+
#[cancelable]
31+
fn foo<D: Drop>(c: bool, d: D) {
32+
if c {
33+
drop(d);
34+
}
35+
longjmp_if_true(c);
36+
}
37+
38+
/// Calls `longjmp` if `c` is true; otherwise returns normally.
39+
#[cancelable]
40+
extern "C" fn longjmp_if_true(c: bool);
41+
```
42+
43+
If a `longjmp` occurs, it can safely traverse the `foo` frame, which will be a
44+
POF because `d` has already been dropped.
45+
46+
Since `longjmp_if_true` function is using the `"C"` rather than the `"C
47+
unwind"` ABI, the optimizer may assume that it cannot unwind; on LLVM, this is
48+
represented by the `nounwind` attribute. On most platforms, `longjmp` is not a
49+
form of unwinding: the `foo` frame is simply discarded. On Windows, `longjmp`
50+
is implemented as a forced unwind, which is permitted to traverse `nounwind`
51+
frames. Since `foo` contains a `Drop` type the forced unwind will include a
52+
call to the frame's cleanup logic, but that logic will not produce any
53+
observable effect; in particular, `D::drop()` will not be called again. The
54+
observable behavior should therefore be the same on all platforms.
55+
56+
Conversely, if, due to a bug, `longjmp` were called unconditionally, then this
57+
code would have undefined behavior on all platforms when `c` is false, because
58+
`foo` would not be a POF.
59+
60+
<!-- TODO the above only talks about UB, but we want warnings to be more
61+
conservative: the compiler should warn in any scenario where a frame is not
62+
guaranteed to be a POF at the time it calls a "cancelable" function. -->
63+
64+
# Reference-level explanation
65+
[reference-level-explanation]: #reference-level-explanation
66+
67+
Regardless of the panic runtime, ABI, or platform, the interaction of Rust
68+
frames with C functions that deallocate frames (i.e. functions that may use
69+
forced unwinding on specific platforms) is specified as follows:
70+
71+
* **When deallocating Rust [POFs][POF-definition]:** frames are safely
72+
deallocated; no undefined behavior
73+
* **When deallocating Rust non-POFs:** undefined behavior
74+
75+
# Rationale and alternatives
76+
[rationale-and-alternatives]: #rationale-and-alternatives
77+
78+
## Analysis of key design goals
79+
[analysis-of-design-goals]: #analysis-of-design-goals
80+
81+
### Enable error handling with `longjmp`
82+
83+
This constraint is met: `longjmp` is treated the same across all platforms, and
84+
is safe as long as only [POFs][POF-definition] are deallocated.
85+
86+
### Do not change the ABI of functions in the `libc` crate
87+
88+
This constraint is met: `libc` functions will continue to use the `"C"` ABI.
89+
`pthread_exit` will be treated the same across all platforms, and will be safe
90+
as long as only [POFs][POF-definition] are deallocated.

0 commit comments

Comments
 (0)