Skip to main content

jmp_scape/
lib.rs

1//! The jmp-scape crate provides access to `setjmp` and `sigsetjmp`
2//! functionality, via an interface that ensures LLVM won't miscompile things.
3//!
4//! Forked from [`cee-scape`](https://github.com/pnkfelix/cee-scape); credit for
5//! the original design and implementation belongs to the upstream authors.
6//!
7//! # Example usage
8//!
9//! The main intention is for this interface to be used with C code that expects
10//! to longjmp via jump buffers established at Rust-to-C FFI boundaries.
11//!
12//! Here is an example, where we are using `extern "C"` functions as stand-ins
13//! for the code you would normally expect to find in an external C library.
14//!
15//! ```rust
16//! mod pretend_this_comes_from_c {
17//!     use jmp_scape::JmpBuf;
18//!
19//!     // Returns sum of a and b, but longjmps through `env` if either argument
20//!     // is negative (passing 1) or if the sum overflows (passing 2).
21//!     pub extern "C" fn careful_sum(env: JmpBuf, a: i32, b: i32) -> i32 {
22//!         check_values(env, a, b);
23//!         return a + b;
24//!     }
25//!
26//!     extern "C" fn check_values(env: JmpBuf, a: i32, b: i32) {
27//!         use jmp_scape::longjmp;
28//!         if a < 0 || b < 0 { unsafe { longjmp(env, -1); } }
29//!         if (i32::MAX - a) < b { unsafe { longjmp(env, -2); } }
30//!     }
31//! }
32//!
33//! use pretend_this_comes_from_c::careful_sum as sum;
34//! use jmp_scape::call_with_setjmp;
35//!
36//! assert_eq!(call_with_setjmp(|env| { sum(env, 10, 20) + 1000 }), 1030);
37//! assert_eq!(call_with_setjmp(|env| { sum(env, -10, 20) + 1000 }), -1);
38//! assert_eq!(call_with_setjmp(|env| { sum(env, 10, -20) + 1000 }), -1);
39//! assert_eq!(call_with_setjmp(|env| { sum(env, i32::MAX, 1) + 1000 }), -2);
40//! ```
41//!
42//! # Background on `setjmp` and `longjmp`.
43//!
44//! The `setjmp` and `longjmp` functions in C are used as the basis for
45//! "non-local jumps", also known as "escape continuations". It is a way to have
46//! a chain of calls "`entry` calls `middle_1` calls `middle_2` calls
47//! `innermost`", where the bodies of `middle_1` or `middle_2` or `innermost`
48//! might at some point decide that they want to jump all the way back to
49//! `entry` without having to pass through the remaining code that they would
50//! normally have to execute when returning via each of their respective
51//! callers.
52//!
53//! In C, this is done by having `entry` first call `setjmp` to initialize a
54//! jump enviroment (which would hold, for example, the current stack pointer
55//! and, if present, the current frame pointer), and then passing a pointer to
56//! that jump environment along during each of the child subroutines of A. If at
57//! any point a child subroutine wants to jump back to the point where `setjmp`
58//! had first returned, that child subroutine invoke `longjmp`, which reestablishes
59//! the stack to the position it had when `setjmp` had originally returned.
60//!
61//! # Safety (or lack thereof)
62//!
63//! This crate cannot ensure that the usual Rust control-flow rules are upheld,
64//! which means that the act of actually doing a longjmp/siglongjmp to a
65//! non-local jump environment (aka continuation) is *unsafe*.
66//!
67//! For example, several Rust API's rely on an assumption that they will always
68//! run some specific cleanup code after a callback is done. Such cleanup is
69//! sometimes encoded as a Rust destructor, but it can also just be directly
70//! encoded as straight-line code waiting to be run.
71//!
72//! Calls to `longjmp` blatantly break these assumptions. A `longjmp` invocation
73//! does not invoke any Rust destructors, and it does not "unwind the stack".
74//! All pending cleanup code between the `longjmp` invocation and the target
75//! jump environment (i.e. the place where the relevant `setjmp` first returned)
76//! is skipped.
77//!
78//! ```rust
79//! use std::cell::Cell;
80//! // This emulates a data structure that has an ongoing invariant:
81//! // the `depth` is incremented/decremented according to entry/exit
82//! // to a given callback (see `DepthTracker::enter` below).
83//! pub struct DepthTracker { depth: Cell<usize>, }
84//!
85//! let track = DepthTracker::new();
86//! jmp_scape::call_with_setjmp(|env| {
87//!     track.enter(|| {
88//!         // This is what we expect: depth is larger in context of
89//!         // DepthTracker::enter callback
90//!         assert_eq!(track.depth(), 1);
91//!         "normal case"
92//!     });
93//!     0
94//! });
95//!
96//! // Normal case: the tracked depth has returned to zero.
97//! assert_eq!(track.depth(), 0);
98//!
99//! assert_eq!(jmp_scape::call_with_setjmp(|env| {
100//!     track.enter(|| {
101//!         // This is what we expect: depth is larger in context of
102//!         // DepthTracker::enter callback
103//!         assert_eq!(track.depth(), 1);
104//!         // DIFFERENT: Now we bypass the DepthTracker's cleanup code.
105//!         unsafe { jmp_scape::longjmp(env, 4) }
106//!         "abnormal case"
107//!     });
108//!     0
109//! }), 4);
110//!
111//! // This is the "surprise" due to the DIFFERENT line: longjmp skipped
112//! // over the decrement from returning from the callback, and so the count
113//! // is not consistent with what the data structure expects.
114//! assert_eq!(track.depth(), 1 /* not 0 */);
115//!
116//! // (These are just support routines for the `DepthTracker` above.)
117//! impl DepthTracker {
118//!     pub fn depth(&self) -> usize {
119//!         self.depth.get()
120//!     }
121//!     pub fn enter<X>(&self, callback: impl FnOnce() -> X) -> X {
122//!         self.update(|x|x+1);
123//!         let ret = callback();
124//!         self.update(|x|x-1);
125//!         ret
126//!     }
127//!     fn update(&self, effect: impl Fn(usize) -> usize) {
128//!         self.depth.set(effect(self.depth.get()));
129//!     }
130//!     pub fn new() -> Self {
131//!         DepthTracker { depth: Cell::new(0) }
132//!     }
133//! }
134//! ```
135//!
136//! In short, the `longjmp` routine is a blunt instrument. When a `longjmp`
137//! invocation skips some cleanup code, the compiler cannot know whether
138//! skipping that cleanup code was exactly what the program author intended, or
139//! if it represents a programming error.
140//!
141//! Furthermore, much cleanup code of this form is enforcing *Rust safety
142//! invariants*. This is why `longjmp` is provided here as an *unsafe* method;
143//! that is a reminder that while one can invoke `call_with_setjmp` safely, the
144//! obligation remains to audit whether any invocations of `longjmp` on the
145//! provided jump environment are breaking those safety invariants by skipping
146//! over such cleanup code.
147//!
148//! # Some static checking
149//!
150//! While not all of Rust's safety rules are statically enforced, one important
151//! one is enforced: When invoking `call_with_setjmp`, the saved jump
152//! environment is not allowed to escape the scope of the callback that is fed
153//! to `call_with_setjmp`:
154//!
155//! ```compile_fail
156//! let mut escaped = None;
157//! jmp_scape::call_with_setjmp(|env| {
158//!     // If `env` were allowed to escape...
159//!     escaped = Some(env);
160//!     0
161//! });
162//! // ... it would be bad if we could then do this with it.
163//! unsafe { jmp_scape::longjmp(escaped.unwrap(), 1); }
164//! ```
165//!
166//! We also cannot share jump environments across threads, because it is
167//! undefined behavior to `longjmp` via a jump environments that was initialized
168//! by a call to `setjmp` in a different thread.
169//!
170//! ```compile_fail
171//! jmp_scape::call_with_setjmp(move |env| {
172//!     std::thread::scope(|s| {
173//!         s.spawn(move || {
174//!             unsafe { jmp_scape::longjmp(env, 1); }
175//!         });
176//!         0
177//!     })
178//! });
179//! ```
180#![cfg_attr(not(test), no_std)]
181
182use libc::c_int;
183
184#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
185mod glibc_compat;
186#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
187mod macos_compat;
188#[cfg(target_os = "linux")]
189use glibc_compat as struct_defs;
190#[cfg(target_os = "macos")]
191use macos_compat as struct_defs;
192
193pub use crate::struct_defs::{JmpBufFields, JmpBufStruct};
194pub use crate::struct_defs::{SigJmpBufFields, SigJmpBufStruct};
195
196/// This is the type of the first argument that is fed to longjmp.
197pub type JmpBuf = *const JmpBufFields;
198
199/// This is the type of the first argument that is fed to siglongjmp.
200pub type SigJmpBuf = *const SigJmpBufFields;
201
202unsafe extern "C" {
203    /// Given a calling environment `jbuf` (which one can acquire via
204    /// `call_with_setjmp`) and a non-zero value `val`, moves the stack and
205    /// program counters to match the return position of where `jbuf` was
206    /// established via a call to `setjmp`, and then returns `val` from that
207    /// spot.
208    ///
209    /// You should only provide non-zero values for `val`. A zero-value may or
210    /// may not be replaced with a non-zero value for the return to the
211    /// non-local jump environment, depending on the underlying C library that
212    /// is linked in. (It may be silently replaced with a non-zero value, as a
213    /// non-zero value is the only way for the internal machinery to distinguish
214    /// between the first return from the initial call versus a non-local
215    /// return).
216    ///
217    /// FIXME: include safety note here, including the issues with destructors
218    pub fn longjmp(jbuf: JmpBuf, val: c_int) -> !;
219
220    /// Given a calling environment `jbuf` (which one can acquire via
221    /// `call_with_sigsetjmp`) and a non-zero value `val`, moves the stack and
222    /// program counters to match the return position of where `jbuf` was
223    /// established via a call to `setjmp`, and then returns `val` from that
224    /// spot.
225    ///
226    /// You should only provide non-zero values for `val`. A zero-value may or
227    /// may not be replaced with a non-zero value for the return to the
228    /// non-local jump environment, depending on the underlying C library that
229    /// is linked in. (It may be silently replaced with a non-zero value, as a
230    /// non-zero value is the only way for the internal machinery to distinguish
231    /// between the first return from the initial call versus a non-local
232    /// return).
233    ///
234    /// FIXME: include safety note here, including the issues with destructors
235    pub fn siglongjmp(jbuf: SigJmpBuf, val: c_int) -> !;
236}
237
238// FIXME: figure out how to access feature cfg'ing. (And then, look into linting
239// against people trying to do "the obvious things".)
240
241#[cfg(not(feature = "use_c_to_interface_with_setjmp"))]
242mod asm_based;
243#[cfg(not(feature = "use_c_to_interface_with_setjmp"))]
244pub use asm_based::{call_with_setjmp, call_with_sigsetjmp};
245
246#[cfg(feature = "use_c_to_interface_with_setjmp")]
247mod cee_based;
248#[cfg(feature = "use_c_to_interface_with_setjmp")]
249pub use cee_based::{call_with_setjmp, call_with_sigsetjmp};
250
251#[cfg(test)]
252mod tests {
253    // longjmp never returns, and its signature reflects that. But its noisy to
254    // be warned about it in the tests below, where the whole point is to ensure
255    // that everything *is* skipped in the expected manner.
256    #![allow(unreachable_code)]
257
258    use super::*;
259    use expect_test::expect;
260
261    #[test]
262    fn setjmp_basically_works() {
263        assert_eq!(call_with_setjmp(|_env| { 0 }), 0);
264        assert_eq!(call_with_setjmp(|_env| { 3 }), 3);
265        assert_eq!(
266            call_with_setjmp(|env| {
267                unsafe {
268                    longjmp(env, 4);
269                }
270                3
271            }),
272            4
273        );
274    }
275
276    #[test]
277    fn sigsetjmp_basically_works() {
278        assert_eq!(call_with_sigsetjmp(true, |_env| { 0 }), 0);
279        assert_eq!(call_with_sigsetjmp(true, |_env| { 3 }), 3);
280        assert_eq!(
281            call_with_sigsetjmp(true, |env| {
282                unsafe {
283                    siglongjmp(env, 4);
284                }
285                3
286            }),
287            4
288        );
289    }
290
291    #[test]
292    fn check_control_flow_details_1() {
293        // The basic test template: record control flow points via record, and
294        // compare them in the test output.
295        let mut record = String::new();
296        let result = call_with_setjmp(|env| {
297            record.push('A');
298            unsafe {
299                longjmp(env, 4);
300            }
301            record.push_str(" B");
302            0
303        });
304        assert_eq!(result, 4);
305        expect![["A"]].assert_eq(&record);
306    }
307
308    #[test]
309    fn check_control_flow_details_2() {
310        let mut record = String::new();
311        let result = call_with_setjmp(|_env1| {
312            record.push('A');
313            let ret = call_with_setjmp(|env2| {
314                record.push_str(" B");
315                unsafe {
316                    longjmp(env2, 4);
317                }
318                record.push_str(" C");
319                0
320            });
321            record.push_str(" D");
322            ret + 1
323        });
324        assert_eq!(result, 5);
325        expect![["A B D"]].assert_eq(&record);
326    }
327
328    #[test]
329    fn check_control_flow_details_3() {
330        let mut record = String::new();
331        let result = call_with_setjmp(|env1| {
332            record.push('A');
333            let ret = call_with_setjmp(|_env2| {
334                record.push_str(" B");
335                unsafe {
336                    longjmp(env1, 4);
337                }
338                record.push_str(" C");
339                0
340            });
341            record.push_str(" D");
342            ret + 1
343        });
344        assert_eq!(result, 4);
345        expect![["A B"]].assert_eq(&record);
346    }
347
348    #[cfg(feature = "test_c_integration")]
349    #[test]
350    fn c_integration() {
351        unsafe extern "C" {
352            fn subtract_but_longjmp_if_underflow(env: JmpBuf, a: u32, b: u32) -> u32;
353        }
354        assert_eq!(
355            call_with_setjmp(|env| {
356                (unsafe { subtract_but_longjmp_if_underflow(env, 10, 3) }) as c_int
357            }),
358            7
359        );
360
361        assert_eq!(
362            call_with_setjmp(|env| {
363                unsafe {
364                    subtract_but_longjmp_if_underflow(env, 3, 10);
365                    panic!("should never get here.");
366                }
367            }),
368            7
369        );
370    }
371
372    #[cfg(feature = "test_c_integration")]
373    #[test]
374    fn check_c_layout() {
375        // This type is defined in test_c_integration
376        #[repr(C)]
377        #[derive(Copy, Clone, Default, Debug)]
378        struct LayoutOfJmpBufs {
379            jb_size: usize,
380            jb_align: usize,
381            sigjb_size: usize,
382            sigjb_align: usize,
383        }
384
385        unsafe extern "C" {
386            fn get_c_jmpbuf_layout() -> LayoutOfJmpBufs;
387        }
388
389        let cinfo = unsafe { get_c_jmpbuf_layout() };
390        // Dump the info so that if the test fails the right values are easy
391        // enough to find.
392        eprintln!("Note: C jmp_buf/sigjmp_buf layout info: {cinfo:?}");
393
394        assert_eq!(cinfo.jb_size, core::mem::size_of::<JmpBufStruct>());
395        assert_eq!(cinfo.jb_align, core::mem::align_of::<JmpBufStruct>());
396        assert_eq!(cinfo.sigjb_size, core::mem::size_of::<SigJmpBufStruct>());
397        assert_eq!(cinfo.sigjb_align, core::mem::align_of::<SigJmpBufStruct>());
398    }
399}
400
401#[cfg(test)]
402mod tests_of_drop_interaction {
403    use super::{call_with_setjmp, call_with_sigsetjmp};
404    use std::sync::atomic::{AtomicUsize, Ordering};
405    struct IncrementOnDrop(&'static str, &'static AtomicUsize);
406    impl IncrementOnDrop {
407        fn new(name: &'static str, state: &'static AtomicUsize) -> Self {
408            println!("called new for {name}");
409            IncrementOnDrop(name, state)
410        }
411    }
412    impl Drop for IncrementOnDrop {
413        fn drop(&mut self) {
414            println!("called drop on {}", self.0);
415            self.1.fetch_add(1, Ordering::Relaxed);
416        }
417    }
418
419    #[test]
420    fn does_ptr_read_cause_a_double_drop_for_setjmp() {
421        static STATE: AtomicUsize = AtomicUsize::new(0);
422        let iod = IncrementOnDrop::new("iod", &STATE);
423        call_with_setjmp(move |_env| {
424            println!("at callback 1 start: {}", iod.1.load(Ordering::Relaxed));
425            let _own_it = iod;
426            0
427        });
428        println!(
429            "callback done, drop counter: {}",
430            STATE.load(Ordering::Relaxed)
431        );
432        assert_eq!(STATE.load(Ordering::Relaxed), 1);
433        let iod = IncrementOnDrop::new("iod", &STATE);
434        call_with_setjmp(move |_env| {
435            println!("at callback 2 start: {}", iod.1.load(Ordering::Relaxed));
436            let _own_it = iod;
437            0
438        });
439        println!(
440            "callback done, drop counter: {}",
441            STATE.load(Ordering::Relaxed)
442        );
443        assert_eq!(STATE.load(Ordering::Relaxed), 2);
444    }
445
446    #[test]
447    fn does_ptr_read_cause_a_double_drop_for_sigsetjmp() {
448        static STATE: AtomicUsize = AtomicUsize::new(0);
449        let iod = IncrementOnDrop::new("iod", &STATE);
450        call_with_sigsetjmp(false, move |_env| {
451            println!("at callback 3 start: {}", iod.1.load(Ordering::Relaxed));
452            let _own_it = iod;
453            0
454        });
455        println!(
456            "callback done, drop counter: {}",
457            STATE.load(Ordering::Relaxed)
458        );
459        assert_eq!(STATE.load(Ordering::Relaxed), 1);
460        let iod = IncrementOnDrop::new("iod", &STATE);
461        call_with_sigsetjmp(true, move |_env| {
462            println!("at callback 4 start: {}", iod.1.load(Ordering::Relaxed));
463            let _own_it = iod;
464            0
465        });
466        println!(
467            "callback done, drop counter: {}",
468            STATE.load(Ordering::Relaxed)
469        );
470        assert_eq!(STATE.load(Ordering::Relaxed), 2);
471    }
472
473    // FIXME: This test probably shouldn't be written this way. The intended safety property
474    // for calling longjmp is that there *are no* destructors waiting to run between the
475    // longjmp and its associated setjmp (and that we otherwise have UB).
476    #[test]
477    fn mix_drop_with_longjmp() {
478        use crate::longjmp;
479
480        static STATE: AtomicUsize = AtomicUsize::new(0);
481        // The above cases were checking that "normal" control flow,
482        // with no longjmp's involved, would not cause a double-drop.
483        // But as soon as longjmp is in the mix, we can no lonbger
484        // guarantee that the closure passed into call_with_setjmp will be dropped
485        let iod = IncrementOnDrop::new("iod", &STATE);
486        call_with_setjmp(move |env1| {
487            println!("at callback 1 start: {}", iod.1.load(Ordering::Relaxed));
488            let _own_it = iod;
489            unsafe { longjmp(env1, 4) }
490        });
491        println!(
492            "callback done, drop counter: {}",
493            STATE.load(Ordering::Relaxed)
494        );
495        assert_eq!(STATE.load(Ordering::Relaxed), 0);
496    }
497}