Support for setjmp / longjmp
Motivation
https://github.com/rust-lang/libc/pull/1216 proposes adding setjmp and longjmp to libc. These functions are required to interface with some C libraries, but sadly these functions are currently impossible to use correctly. Users deserve a solution that works and warns or prevents common pitfalls.
Issues with current solution
The first issue is that libc cannot add LLVM returns_twice attribute to setjmp:
This attribute indicates that this function can return twice. The C
setjmpis an example of such a function. The compiler disables some optimizations (like tail calls) in the caller of these functions.
Basically, LLVM assumes that setjmp can return only once without this attribute, potentially leading to miscompilations (playground):
unsafe fn foo() -> i32 {
let mut buf: jmp_buf = [0; 8];
let mut x = 42;
if setjmp(&mut buf) != 0 { // Step 0: setjmp returns 0
// Step 3: when setjmp returns 1 x has always been
// modified to be == 13 so this should always return 13:
return x;
}
x = 13; // Step 1: x is modified
longjmp(&mut buf, 1); // Step 2: jumps to Step 0 returning 1
x // this will never be reached
}
// In debug builds foo returns 13 correctly, but
// in release builds foo returns 42
assert_eq!(unsafe { foo() }, 13); // FAILs in release
Because setjmp is not returns_twice, LLVM assumes that the return x; will only be reached before x = 13, so it will always return 42. However, setjmp returns 0 the first time, and returns 1 when jumped into it from the longjmp.
Using a volatile load instead works around this issue (e.g. playground). Basically, if stack variables are modified after a setjmp, all reads and writes until all possible longjmps will probably need to be volatile.
One way to improve this could be to add an stable attribute #[returns_twice] that users can use to mark that extern "C" { ... } functions can return multiple times and for Rust to handle these functions specially (e.g. by emitting the corresponding LLVM attribute). An alternative would be for Rust to provide these functions as part of the language.
Modulo LLVM-level misoptimizations, C only allows setjmp in specific "contexts" [0], and these features interact badly with languages with destructors. Rust does not guarantee that destructors will run (e.g. mem::forget is safe), so skipping destructors using longjmp is not unsound [1]. However, unsafe code will need to take into account that code outside it can use longjmp to skip destructors when creating safe abstractions (e.g. see Observational equivalency and unsafe code).
More worrying is how longjmp subverts the borrow checker to, e.g., produce undefined behavior of the form use-after-move without triggering a type error (playground):
fn bar(_a: A) { println!("a moved") }
fn foo() {
let mut buf: jmp_buf = [0; 8];
let a = A;
if unsafe { setjmp(&mut buf) } != 0 { // Step 0: setjmp returns 0
bar(a); // Step 3: a is moved _again_ (UB: use-after-move)
return;
}
bar(a); // Step 1: a is moved here
unsafe { longjmp(&mut buf, 1) }; // Step 2: jumps to Step 0 returning 1
}
This prints "a moved" twice, which means that the variable a was moved from twice, so the second time an use-after-move happened which type-checked. Obviously, longjmp is not the only way to achieve this in Rust, e.g. it is trivial to use the pointer methods to do so as well, but longjmp combined with Drop types makes this happen with no effort (playground):
struct A; impl Drop for A { ... }
fn bar(_a: A) {
// _a is dropped here
}
fn foo() {
let mut buf: jmp_buf = [0; 8];
let a = A;
if unsafe { setjmp(&mut buf) } != 0 {
// use-after-move: a has been moved (and dropped) below
// but a is used (and dropped) in this branch again
// => double-drop => UB
return;
}
bar(a); // moves a
unsafe { longjmp(&mut buf, 1) };
}
That is, using setjmp+longjmp to create double-drops (undefined behavior) is trivial.
Finally, there are problems with creating wrappers around these functions (playground):
fn foo(buf: &mut jmp_buf) {
let mut a: i32 = 42;
if unsafe { setjmp(buf) } != 0 {
dbg!(a); // use-after-free
panic!("done");
}
a = 13;
}
fn main() {
let mut buf: jmp_buf = [0; 8];
foo(&mut buf);
let b: i32 = 666;
dbg!(b);
unsafe { longjmp(&mut buf, 1); }
}
Prints b = 666 and a = 0. The problem here is that this code saves the stack pointer inside foo, but then foo returns, and afterwards the longjmp jumps to a stack frame that is no longer live, so dbg!(a) reads a after it has been free'd (use-after-free).
There are probably many other problems with these two functions, that does not mean that they are impossible to use correctly. Still, it would be good to have a solution here that at least warns about potentially incorrect usages since reasoning about these and the surrounding unsafe code is often very tricky.
It would also be good to have a way to soundly model these in miri and detect when they are used incorrectly.
At a minimum we should be able to write down documentation for these functions in Rust. Where exactly can they be used, what does the unsafe code surrounding them need to uphold to be correct, etc.
cc @nikomatsakis (wrote blog post about observational equivalence and unsafe code), @rkruppe , @RalfJung @ubsan - the unsafe code guidelines should probably say whether extern "C" functions are allowed to modify the stack pointer etc. like setjmp/longjmp do.
- [0] C11 7.13.1.1p4 states the following about
setjmp:
An invocation of the setjmp macro shall appear only in one of the following contexts:
- the entire controlling expression of a selection or iteration statement;
- one operand of a relational or equality operator with the other operand an integer constant expression, with the resulting expression being the entire controlling expression of a selection or iteration statement;
- the operand of a unary ! operator with the resulting expression being the entire controlling expression of a selection or iteration statement; or
- the entire expression of an expression statement (possibly cast to void).
While C11 7.13.1.1p5 states:
If the invocation appears in any other context, the behavior is undefined.
That is, let x = setjmp(...); would be UB in C. In Rust having a result of a function be usable only in some contexts would be weird.
- [1] In C++, skipping destructors with
longjmpis undefined behavior, e.g., [support.runtime] states:
If any automatic objects would be destroyed by a thrown exception transferring control to another (destination) point in the program, then a call to longjmp(jbuf, val) at the throw point that transfers control to the same (destination) point has undefined behavior.
Something like this would add zero checks over what C offers, moving the responsibility for solving all these issues to the user. Particularly, Drop values moved in one branch need to be manually mem::forget'ed in all others to prevent them from being used in a double-drop.
MVP
Add setjmp and longjmp with the following semantics:
pub type JmpBuf = <implementation-defined>;
/// Saves the current execution context into `env`. If `longjmp(env, val)` is used
/// to restore execution at this call site, this function returns `val`. Otherwise,
/// it returns `0`.
#[rustc_returns_twice]
pub unsafe fn setjmp(env: &mut JmpBuf) -> c_int;
/// Restores execution at the `setjmp` call-site that stored `env`, returning
/// there the `value` (if `value == 0`, then `setjmp` returns `1`).
///
/// If the function that called `setjmp` exits before the call to `longjmp`, the behavior is
/// undefined.
///
/// All variables for which `drop` would be called if `longjmp` was to be replaced with
/// `panic!` and `setjmp` with `catch_unwind` are not dropped.
pub unsafe fn longjmp(env: &JmpBuf, value: c_int);
The invocation of setjmp can appear only in the following contexts:
- the entire controlling expression of
match, e.g.match setjmp(env) { ... }. if setjmp(env) $integer_relational_operator $integer_constant_expression { ... }- the entire expression of an expression statement:
setjmp(env);
If setjmp appears in any other context, the behavior is undefined.
Upon return to the scope of setjmp, all accessible objects, floating-point status flags, and other components of the abstract machine have the same values as they had when longjmp was executed, except for the local variables in setjmp's scope whose values have been changed since the setjmp invocation - the behavior of reading / writing to these variables via non-volatile reads/write is undefined.
Ultimately, what I am hoping to achieve is something like: longjmp_panic!(my_ffi_function(x,y,z))
Which would call my_ffi_function() and return normally, or panic if my_ffi_function() longjmps.
It would also be nice to have: longjmp_catch!(my_ffi_function(x,y,z)) which would return a Result (Ok if normal return, Err if longjmp).
The MVP suggested above allows such a macro to be written, so I am in favor of that approach (at least until we find something better).
There may be opportunity for something higher-level than the MVP that can be more safely exposed to the user and more easily documented (and leave rust more free to change the internals). The challenge is that, even if this is the only use case to support, we need some knowledge about how the FFI function might call longjmp().
My use case is postgres, to do something like PG_TRY():
#define PG_TRY() \
do { \
sigjmp_buf *save_exception_stack = PG_exception_stack; \
ErrorContextCallback *save_context_stack = error_context_stack; \
sigjmp_buf local_sigjmp_buf; \
if (sigsetjmp(local_sigjmp_buf, 0) == 0) \
{ \
PG_exception_stack = &local_sigjmp_buf
#define PG_CATCH() \
} \
else \
{ \
PG_exception_stack = save_exception_stack; \
error_context_stack = save_context_stack
#define PG_END_TRY() \
} \
PG_exception_stack = save_exception_stack; \
error_context_stack = save_context_stack; \
} while (0)
All of this depends on longjmp() being called with the argument *PG_exception_stack. If it's called with anything else, our previous setjmp() is useless.
So, if we want to make something higher-level in rust, it would need a way to specify what this magic global variable is.
Modulo LLVM-level misoptimizations, C only allows setjmp in specific "contexts" [0], and these features interact badly with languages with destructors. Rust does not guarantee that destructors will run (e.g. mem::forget is safe), so skipping destructors using longjmp is not unsound [1].
It's not that easy. For example, setjmp/longjmp are incompatible with closure-based APIs that expect "well-bracketed control flow", such as crossbeam::scope: you could use longjmp to skip the part of the code that is otherwise guaranteed to run. This has nothing to do with dropping.
Moreover, mem::forget leaks memory and does not call the destructor. setjmp/longjmp deallocates memory without calling its destructor, and that's unsound. For pinned memory we guarantee that once some memory is pinned, if this memory every gets invalidated (e.g., deallocated), then the destructor will first get called. Any mechanism that pops stack frames without calling destructors breaks this guarantee (well, at least it makes stack-pinning impossible, and that is intended to be allowed).
In other words, the "observational equivalence" argument does not apply in this case, setjmp/longjmp increases the observational power of the context. (This is a well-known result about continuations in PL theory, it is not a particular quirk of how they got implemented in C.) I don't think there is any way setjmp/longjmp can ever be used to "jump across" unknown code. This can only be sound if you can carefully control the stack frames that get popped by the longjmp, and make sure that they contain no types where skipping drop is a problem.
is not unsound
To clarify, what I meant by "is not unsound" is that performing a longjmp does not instantaneously invoke undefined behavior because it could make a lot of live objects instantaneously invalid - AFAICT longjmp does not do that.
I did not meant to say that it is not unsound in the sense that safe abstractions around unsafe code written without considering setjmp/longjmp remain safe - as you mention, they don't.
Moreover, mem::forget leaks memory and does not call the destructor. setjmp/longjmp deallocates memory without calling its destructor, and that's unsound.
That's a good point.
@RalfJung I think C++'s semantic might be what we need:
If replacing of
longjmpwithpanic!()andsetjmpwithpanic::catch_unwindwould execute a non-trivial destructor for any automatic object whenpanic=unwind, the behavior of suchlongjmpis undefined.
We might just need to replace "non-trivial destructor" with something else, and also setjmp cannot just be replaced with catch_unwind, but basically all the code from the setjmp to the longjmp would need to be inside a catch_unwind.
C's setjmp/longjmp is essentially a C-styled implementation of continuations, and this C flavor definitely shows in how it works. More refined continuation mechanisms are present in higher-level langauges, especially functional languages, and the same #[returns_twice] needed to support setjmp without causing undefined behavior would also help make it possible to implement continuations reasonably safely in Rust.
As for setjmp/longjmp itself, setjmp can be wrapped up to be safe in the same sense that a raw pointer that is never dereferenced is safe. longjmp, on the other hand, is crazy unsafe. That thing can punch holes straight down the call stack, which not only allows for memory leaks as large as dam breaks, it completely subverts any mechanism that a library could use to ensure that code which must run after user code to hold up partial correctness of the API actually does run. Even in C, I never use it, because I find that keeping straight what happened between a landing point I might want to use for setjmp and wherever it might be useful to longjmp back to it is nearly impossible without garbage collection or dedicated bookkeeping, neither of which I am inclined to take the time to write and debug. However, others use setjmp/longjmp, so we may as well figure out how to make it happen for C FFI purposes.
From what I can tell, this is a (probably horribly incomplete) list of what we must make undefined to permit all of Rust's existing safety invariants to continue to hold up:
longjmping to ajmp_bufwhich does not correspond to a stack frame, to an uninitializedjmp_buf, to ajmp_bufcorresponding to the stack frame of a function which has already returned, or to ajmp_bufcorresponding to a stack frame on a stack which is still being used by another threadlongjmping ahead over initialization of memory, and subsequently using this uninitialized memorylongjmping such that deallocated or leaked memory on the heap, or memory in stack frames above thejmp_bufstack frame, is accessible by reference, and subsequently dereferencing such a referencelongjmping to ajump_bufpoint between the initialization of an!Unpintype value and its pinning, and subsequently moving the value- In general, using
longjmpto jump back to a point before a safety invariant needs to hold while something else expects the safety invariant to still hold and subsequently violating the safety invariant, or performing an operation which would violate a safety invariant at the pointsetjmpbut is permitted because it is relaxed at a later point, thenlongjmping back to thejmp_bufwithout restoring the safety invariant longjmping over the destructor of a pinned value that has live references to itlongjmping after modification of values held at the point ofsetjmp(either by borrowing them or owning them without letting them be mutably borrowed), unless these values had interior mutability at the timelongjmping overcatch_unwind
Presumably a #[returns_twice] attribute would also come with borrow checker additions that account for the multiple return property of functions annotated with it, so I didn't include it in the above list. ~~I'm still looking for a concrete example where longjmping over the destructor of a stack-pinned object can cause undefined behavior in otherwise safe code, but so far, I'm stuck.~~ Turns out the requirement that stack destructors of Pinned values run before deallocation is needed to ensure you can safely have intrusive collections.
FWIW, here is the macro I am currently using, which seems like a valid use of sigsetjmp(), and the MVP above would satisfy my requirements.
#[macro_export]
macro_rules! longjmp_panic {
($e:expr) => {
let retval;
unsafe {
use postgres_extension::utils::elog
::{PG_exception_stack,
error_context_stack,
PgError};
use postgres_extension::setjmp::{sigsetjmp,sigjmp_buf};
let save_exception_stack: *mut sigjmp_buf = PG_exception_stack;
let save_context_stack: *mut ErrorContextCallback = error_context_stack;
let mut local_sigjmp_buf: sigjmp_buf = std::mem::uninitialized();
if sigsetjmp(&mut local_sigjmp_buf, 0) == 0 {
PG_exception_stack = &mut local_sigjmp_buf;
retval = $e;
} else {
PG_exception_stack = save_exception_stack;
error_context_stack = save_context_stack;
panic!(PgError);
}
PG_exception_stack = save_exception_stack;
error_context_stack = save_context_stack;
}
retval
}
}
It is intended to wrap a FFI function call which, when evaluated, may call siglongjmp() in accordance with the postgresql convention for throwing an exception. It can be used in a function like this:
#[inline(never)]
fn wrap_elog() {
longjmp_panic! { elog_internal(file!(), line!(), ERROR, "test error") }
}
(In the above example, elog_internal() will always throw an exception and longjmp(). It's not a direct FFI call in this particular example, but it's close enough for illustration.)
I looked at the optimized IR, and it looks fine to me, but someone who understands this better might see a problem:
; udf_error::wrap_elog
; Function Attrs: noinline nonlazybind uwtable
define internal fastcc void @_ZN9udf_error9wrap_elog17he7efb2354edb7c3eE() unnamed_addr #\
8 {
start:
%local_sigjmp_buf = alloca %"postgres_extension::setjmp::sigjmp_buf", align 8
%0 = load i64, i64* bitcast (%"postgres_extension::setjmp::sigjmp_buf"** @PG_exception_\
stack to i64*), align 8
%1 = load i64, i64* bitcast (%"postgres_extension::utils::elog::ErrorContextCallback"**\
@error_context_stack to i64*), align 8
%2 = bitcast %"postgres_extension::setjmp::sigjmp_buf"* %local_sigjmp_buf to i8*
call void @llvm.lifetime.start.p0i8(i64 208, i8* nonnull %2)
%3 = call i32 @__sigsetjmp(%"postgres_extension::setjmp::sigjmp_buf"* nonnull %local_si\
gjmp_buf, i32 0)
%4 = icmp eq i32 %3, 0
br i1 %4, label %bb3, label %bb4
bb3: ; preds = %start
store %"postgres_extension::setjmp::sigjmp_buf"* %local_sigjmp_buf, %"postgres_extensio\
n::setjmp::sigjmp_buf"** @PG_exception_stack, align 8
; call postgres_extension::utils::elog::elog_internal
call void @_ZN18postgres_extension5utils4elog13elog_internal17h0f179afe9978ab59E([0 x i\
8]* noalias nonnull readonly bitcast (<{ [10 x i8] }>* @1 to [0 x i8]*), i64 10, i32 14, \
i32 20, [0 x i8]* noalias nonnull readonly bitcast (<{ [10 x i8] }>* @2 to [0 x i8]*), i6\
4 10)
store i64 %0, i64* bitcast (%"postgres_extension::setjmp::sigjmp_buf"** @PG_exception_s\
tack to i64*), align 8
store i64 %1, i64* bitcast (%"postgres_extension::utils::elog::ErrorContextCallback"** \
@error_context_stack to i64*), align 8
call void @llvm.lifetime.end.p0i8(i64 208, i8* nonnull %2)
ret void
bb4: ; preds = %start
store i64 %0, i64* bitcast (%"postgres_extension::setjmp::sigjmp_buf"** @PG_exception_s\
tack to i64*), align 8
store i64 %1, i64* bitcast (%"postgres_extension::utils::elog::ErrorContextCallback"** \
@error_context_stack to i64*), align 8
; call std::panicking::begin_panic
call fastcc void @_ZN3std9panicking11begin_panic17hc642c894ac6ffd69E()
unreachable
}
This usage is quite constrained, but it serves my purposes. Because the wrapper function is always almost identical (just the FFI call expression changes), and because it's marked #[inline(never)], there are unlikely to be any more optimizations applied that could cause a problem.
Note that sigsetjmp() is declared as:
; Function Attrs: nounwind nonlazybind uwtable
declare i32 @__sigsetjmp(%"postgres_extension::setjmp::sigjmp_buf"*, i32) unnamed_addr #4
but it should have the returns_twice attribute as well. This means that it may be undefined behavior, but in such a constrained use I don't see an obvious way that it would misoptimize it.
This usage is quite constrained, but it serves my purposes.
I think this should work as long as we use the returns_twice attribute for sigsetjmp, and as long as the longjmp from the C code does not introduce undefined behavior in the C code (e.g. because it skips C++ or Rust destructors).
In practice, returns_twice in LLVM only affects a few things, but the things it does affect are important:
- Disables stack slot coloring, aka reusing the same stack slot for unrelated variables with disjoint lifetimes (and ditto for WebAssembly locals);
- When AddressSanitizer is on, disables allocating locals on the heap (for somewhat dubious reasons);
- Disables tail call optimization;
- Ensures that functions that call
returns_twicefunctions cannot themselves be inlined into other functions (i.e. what @jeff-davis replicated manually with#[inline(never)]); - On SPARC, prevents assuming that the register window mechanism will automatically restore registers on return;
- When X86SpeculativeLoadHardening is enabled (Spectre paranoia mode), change a gadget added after roughly every call, which normally tries to read from the callee's return address stack slot to ensure the return to the current IP wasn't mispredicted.
At least the AddressSanitizer, X86SpeculativeLoadHardening, and SPARC ones seem potentially relevant even to a simple function like wrap_elog, so I would recommend ensuring that the returns_twice attribute is added.
At least the AddressSanitizer, X86SpeculativeLoadHardening, and SPARC ones seem potentially relevant even to a simple function like
wrap_elog, so I would recommend ensuring that thereturns_twiceattribute is added.
The path of least resistance would be to add a #[rustc_returns_twice] attribute to rustc, and add these functions to libc, exposing them only for libc's that are built as part of the Rust standard library, using that attribute in that case. We then would need to expose those functions through standard somehow.
#[returns_twice] would also alert the borrow checker to the multiple-return nature of functions annotated with it, so it should be able to figure out that the example given is invalid:
fn bar(_a: A) { println!("a moved") }
fn foo() {
let mut buf: jmp_buf = [0; 8];
let a = A;
if unsafe { setjmp(&mut buf) } != 0 {
bar(a); // error[EO382]: use of moved value: `a`
return;
}
bar(a); // Note: `a` was previously moved here
// Note: `setjmp` may return multiple times
unsafe { longjmp(&mut buf, 1) };
}
@eaglgenes101 I don't think it can work like that, at least if the lint is only based on #[returns_twice]. This is a different way of writing that example:
fn bar(_a: A) { println!("a moved") }
fn foo() {
let mut buf: jmp_buf = [0; 8];
let a = A;
if unsafe { setjmp(&mut buf) } == 1 {
bar(a);
return;
} else {
bar(a);
}
}
At best we could error stating that different branches depending on a #[return_twice] "value" move the same value (if the value is not Copy), but I doubt the error message can get better than that without actually specifying how setjmp and friends work (e.g. returning 0 on first call, and non-zero when being jumped into). We'd probably need a #[setjmp_like] attribute for that.
Why is it important for Rust to support setjmp/longjmp at all? The motivation says "Users deserve a solution that works and warns or prevents common pitfalls." However, that is not motivation as far as an RFC is concerned; it is just a statement of opinion with no supporting evidence.
Strawman: Document in the libc crate that they are intentionally omitted. Document that using them at all is undefined behavior. Motivation: Avoiding this functionality saves a lot of complexity and time, and it is unlikely that any attempt to implement it would be correct any time soon.
@briansmith some C libraries emulate exceptions using setjmp/longjmp and use that as their error reporting mechanism (e.g. Postgres).
some C libraries emulate exceptions using
setjmp/longjmpand use that as their error reporting mechanism (e.g. Postgres).
Some C++ libraries use C++ exceptions too, but Rust doesn't support them either, even though they're much more reasonable than setjmp/longjmp.
Of course it's nice to support Postgres but I think we should try to find ways of doing it that don't involve adding setjmp/longjmp to Rust.
Some C++ libraries use C++ exceptions too,
Rust has C FFI, AFAIK it does not have C++ FFI (yet).
These are C functions available on all systems with a C standard library and are easily callable via C FFI. If we want Rust to be able to interoperate with all correct and potentially legacy C code out there via its C FFI, we need to support them in some form. There is precedence of supporting bad and dangerous C APIs in Rust C FFI for the purpose of being able to interface with C code that uses them (e.g. va_args).
Of course it's nice to support Postgres but I think we should try to find ways of doing it that don't involve adding
setjmp/longjmpto Rust.
I would be interested in better ways of interfacing with Postgres, and I think @jeff-davis would be interested too. My first suggestion was for @jeff-davis to write a C wrapper that performs the setjmp/longjmp without propagating it to Rust, so that Rust code calling the wrapper does not have to use these functions (in a similar way how one would wrap C++ code with a C api, catching all exceptions, before interfacing with Rust code).
If we want Rust to be able to interoperate with all correct and potentially legacy C code out there via its C FFI, we need to support them in some form.
I don't think that is a goal, and I don't think it should be, especially in cases where one can work around the issues by writing a little bit of C that wraps the other C code and resolves the issue. I don't know about va_args issue and the decision making in it. I think setjmp/longjmp has a lot broader negative impact on Rust than va_args. (I am no fan of va_args either, BTW.)
I would argue it really doesn't have a lot of impact. One might add some extra cases to the borrow checker, but other than that it's fairly similar to simple exception handling, something that Rust already has.
On the other hand, would it be possible to make it #[rustc::returns_twice]? That seems like the best way to do namespacing here. Although it seem better to call it #[returns_twice] or something along those lines, for FFI?
On the other hand, would it be possible to make it
#[rustc::returns_twice]?
So currently all rustc internal attributes start with #[rustc_...], if this were to be changed, it would make sense to do that for all internal attributes at once.
Although it seem better to call it
#[returns_twice]or something along those lines, for FFI?
That's an open question. Is #[returns_twice] as an attribute useful enough to warrant going through the stabilization process ? Also, I worry that doing this might be stabilizing an LLVM-ism. Would this attribute be useful for Cranelift (cc @sunfishcode ) ?
Here's what compcert's documentation has to say about setjmp/longjmp:
http://compcert.inria.fr/man/manual005.html:
§7.13 Non-local jumps <setjmp.h> The CompCert C compiler has no special knowledge of the setjmp and longjmp functions, treating them as ordinary functions that respect control flow. It is therefore not advised to use these two functions in CompCert-compiled code. To prevent misoptimisation, it is crucial that all local variables that are live across an invocation of setjmp be declared with volatile modifier.
GCC, for one, supports a returns_twice attribute.
@briansmith A lot of languages work well with C in the easy cases. I love rust because it works well with C in the hard cases.
I would really like to allow pure rust extensions to be written for postgres. Introducing C creates a lot of headaches that I'd rather not deal with.
@comex rust-lang/libc does not expose vfork, but that comment suggest that #[returns_twice] would also be required there. The blog post vfork considered dangerous by Rich Felker (main author/developer of musl), is a great read, it explains:
[
vfork] works likefork, but without creating a new virtual address space; the parent and child run in the same address space. Unlike withpthread_create, where the new thread runs on its own stack,vforkbehaves likeforkand “returns twice”, once in the child and once in the parent. This seems impossible, since the parent and child would clobber one another’s stacks, but a clever trick saves the day: the parent process is suspended until the child performsexecor_exit, breaking the shared-memory-space relation between the two processes.
vforkwas omitted from POSIX and modern standards because it’s difficult to use [...] However, many systems (including Linux) still provide a similar or identical interface at the kernel level, and new interest in its use has arisen again due to the fact that huge processes cannotforkon systems with strict commit charge accounting due to lack of memory, and the fact that copying the virtual memory layout of a process can be expensive if the process has a huge number of maps created bymmap. As such, bothmuslandglibcuse vfork to implementposix_spawn, the modern interface for executing external programs as a new process.
If it were only setjmp, then a hack in the form of a #[rustc_returns_twice] private attribute might have been ok, but it appears that there are multiple low-level platform APIs that can invalidate the optimizers assumptions by being able to return multiple times and people might want to create bindings to these in their own code instead of only accessing them via the standard library or libc.So i'm leaning towards having a real #[returns_twice] attribute for this.
Does anybody know why it is called returns_twice in GCC? AFAICT vfork can only return twice, but setjmp can return multiple times. I don't know if this distinction matters. GCC describes returns_twice as:
The returns_twice attribute tells the compiler that a function may return more than one time.
But we probably want to keep the same name as C since that is what people using these interfaces will probably search for.
Unrelated question, the GCC docs for returns_twice also mention:
The
longjmp-like counterpart of such function, if any, might need to be marked with thenoreturnattribute.
Since C++17, the signature of longjmp is:
[[noreturn]] void longjmp( std::jmp_buf env, int status );
so in Rust longjmp should return -> !, right?
Does anybody know why it is called
returns_twicein GCC? AFAICTvforkcan only return twice, butsetjmpcan return multiple times.
Yes, it's legal in C to longjmp to the same jmp_buf multiple times, which would cause setjmp to return more than twice. I think "twice" is used just because it's shorter than, say, "multiple_times" would be.
so in Rust
longjmpshould return-> !, right?
I believe so.
So I wrote an RFC for a #[returns_twice] attribute: https://github.com/gnzlbg/rfcs/commit/e9ac14eabf58a40c1cd1b28db36bd1d2230d2a9f
Early feedback (e.g. as a comment on that commit) would be really appreciated.
@jeff-davis Here's how I dealt with postgres errors in rust: https://github.com/main--/rustgres/blob/master/src/error.rs
Basically since postgres errors already emulate unwinding I simply catch them, then panic to unwind through the Rust world, then on the lower end catch the panic and translate it back into a postgres error. Works just fine with virtually no performance overhead.
An implementation of my RFC has a rust-lang/rust PR: https://github.com/rust-lang/rust/pull/58315
From the RFC:
Well, currently it returns
13in debug mode, and42in release. This is because Rust assumes thatsetjmpcan only return once. Under this assumption, if it returns a value that's not zero, then the value ofxat that point must be42because thex = 13assignment will never be reached.The
#[returns_twice]attribute specifies that a function might returns multiple times, inhibiting optimizations that assume that this is never the case.
What you wrote accurately describes what returns_twice does in GCC, where it "ensures that all registers are dead before calling such a function", i.e. saves all variables across the call on the stack rather than in callee-saved registers. (When longjmp is called, it doesn't affect the stack, but it resets registers to their state as of the call to setjmp.) But LLVM does not seem to implement this behavior, based on the search I did through its codebase to find checks for returns_twice.
For concrete evidence, look at this:
https://gcc.godbolt.org/z/E4ZvMm
In GCC, changing setjmp to notsetjmp (i.e. a function not marked with returns_twice) changes the generated code: x is saved in the register r12d instead of on the stack at [rsp+12]. But if you switch the compiler to clang, the only difference in the assembly from changing setjmp to notsetjmp is which function is called, and in both cases x is saved in the register ebx.
In short, while GCC tries to provide consistent behavior for all variables, Clang/LLVM is relying on users manually marking variables as volatile to prevent them going back in time, as prescribed in the C standard:
All accessible objects have values, and all other components of the abstract machine have state, as of the time the longjmp function was called, except that the values of objects of automatic storage duration that are local to the function containing the invocation of the corresponding setjmp macro that do not have volatile-qualified type and have been changed between the setjmp invocation and longjmp call are indeterminate.
What returns_twice does in LLVM is inhibit optimizations which could break code even if it uses volatile as prescribed, such as tail call optimization (which clobbers the caller function's stack frame: no good if the callee might longjmp to earlier in the caller).
Interestingly, the GCC behavior is buggy anyway, as it doesn't properly inhibit constant folding: https://gcc.godbolt.org/z/g9IK_4
Anyway, I suggest changing the RFC text to enforce the same requirement as the C standard. Rust does have an equivalent of volatile, but it's worth noting that in practice, most users of setjmp don't actually need to use volatile; they can just avoid the dangerous situation, i.e. setting variables before setjmp and then mutating them after, altogether.
@clobber The example in the RFC has UB even if #[returns_twice] is used because, as you point out, there should be a read_volatile in the return x statement. We need a better example, and the RFC should make it clear that it is not an RFC about specifying the semantics of setjump and longjump.