neon/sys/
no_panic.rs

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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
//! Utilities that _will_ not panic for use in contexts where unwinding would be
//! undefined behavior.
//!
//! The following helpers do not panic and instead use `napi_fatal_error`
//! to crash the process in a controlled way, making them safe for use in FFI
//! callbacks.
//!
//! `#[track_caller]` is used on these helpers to ensure `fatal_error` reports
//! the calling location instead of the helpers defined here.

use std::{
    any::Any,
    ffi::c_void,
    mem::MaybeUninit,
    panic::{catch_unwind, AssertUnwindSafe},
    ptr,
};

use super::{
    bindings as napi,
    debug_send_wrapper::DebugSendWrapper,
    error::fatal_error,
    raw::{Env, Local},
};

type Panic = Box<dyn Any + Send + 'static>;

const UNKNOWN_PANIC_MESSAGE: &str = "Unknown panic";

/// `FailureBoundary`] acts as boundary between Rust and FFI code, protecting
/// a critical section of code from unhandled failure. It will catch both Rust
/// panics and JavaScript exceptions. Attempts to handle failures are executed
/// in order of ascending severity:
///
/// 1. Reject a `Promise` if a `Deferred` was provided
/// 2. Emit a fatal exception
/// 3. Abort the process with a message and location
///
/// This process will be aborted if any step unrecoverably fails. For example,
/// if a `napi::Env` is unavailable, it is impossible to reject a `Promise` or
/// emit a fatal exception.
pub struct FailureBoundary {
    pub both: &'static str,
    pub exception: &'static str,
    pub panic: &'static str,
}

impl FailureBoundary {
    #[track_caller]
    pub unsafe fn catch_failure<F>(&self, env: Env, deferred: Option<napi::Deferred>, f: F)
    where
        F: FnOnce(Option<Env>) -> Local,
    {
        // Make `env = None` if unable to call into JS
        #[allow(clippy::unnecessary_lazy_evaluations)]
        let env = can_call_into_js(env).then(|| env);

        // Run the user supplied callback, catching panics
        // This is unwind safe because control is never yielded back to the caller
        let panic = catch_unwind(AssertUnwindSafe(move || f(env)));

        // Unwrap the `Env`
        let env = if let Some(env) = env {
            env
        } else {
            // If there was a panic and we don't have an `Env`, crash the process
            if let Err(panic) = panic {
                let msg = panic_msg(&panic).unwrap_or(UNKNOWN_PANIC_MESSAGE);

                fatal_error(msg);
            }

            // If we don't have an `Env`, we can't catch an exception, nothing more to try
            return;
        };

        // Check and catch a thrown exception
        let exception = catch_exception(env);

        // Create an error message or return if there wasn't a panic or exception
        let msg = match (exception, panic.as_ref()) {
            // Exception and a panic
            (Some(_), Err(_)) => self.both,

            // Exception, but not a panic
            (Some(err), Ok(_)) => {
                // Reject the promise without wrapping
                if let Some(deferred) = deferred {
                    reject_deferred(env, deferred, err);

                    return;
                }

                self.exception
            }

            // Panic, but not an exception
            (None, Err(_)) => self.panic,

            // No errors occurred! We're done!
            (None, Ok(value)) => {
                if let Some(deferred) = deferred {
                    resolve_deferred(env, deferred, *value);
                }

                return;
            }
        };

        // Reject the promise
        if let Some(deferred) = deferred {
            let error = create_error(env, msg, exception, panic.err());

            reject_deferred(env, deferred, error);

            return;
        }

        let error = create_error(env, msg, exception, panic.err());

        // Trigger a fatal exception
        fatal_exception(env, error);
    }
}

// HACK: Force `NAPI_PREAMBLE` to run without executing any JavaScript to tell if it's
// possible to call into JS.
//
// `NAPI_PREAMBLE` is a macro that checks if it is possible to call into JS.
// https://github.com/nodejs/node/blob/5fad0b93667ffc6e4def52996b9529ac99b26319/src/js_native_api_v8.h#L211-L218
//
//  `napi_throw` starts by using `NAPI_PREAMBLE` and then a `CHECK_ARGS` on the `napi_value`. Since
// we already know `env` is non-null, we expect the `null` value to cause a `napi_invalid_arg` error.
// https://github.com/nodejs/node/blob/5fad0b93667ffc6e4def52996b9529ac99b26319/src/js_native_api_v8.cc#L1925-L1926
fn can_call_into_js(env: Env) -> bool {
    !env.is_null() && unsafe { napi::throw(env, ptr::null_mut()) == Err(napi::Status::InvalidArg) }
}

// We cannot use `napi_fatal_exception` because of this bug; instead, cause an
// unhandled rejection which has similar behavior on recent versions of Node.
// https://github.com/nodejs/node/issues/33771
unsafe fn fatal_exception(env: Env, error: Local) {
    let mut deferred = MaybeUninit::uninit();
    let mut promise = MaybeUninit::uninit();

    let deferred = match napi::create_promise(env, deferred.as_mut_ptr(), promise.as_mut_ptr()) {
        Ok(()) => deferred.assume_init(),
        _ => fatal_error("Failed to create a promise"),
    };

    if napi::reject_deferred(env, deferred, error) != Ok(()) {
        fatal_error("Failed to reject a promise");
    }
}

#[track_caller]
unsafe fn create_error(
    env: Env,
    msg: &str,
    exception: Option<Local>,
    panic: Option<Panic>,
) -> Local {
    // Construct the `uncaughtException` Error object
    let error = error_from_message(env, msg);

    // Add the exception to the error
    if let Some(exception) = exception {
        set_property(env, error, "cause", exception);
    };

    // Add the panic to the error
    if let Some(panic) = panic {
        set_property(env, error, "panic", error_from_panic(env, panic));
    }

    error
}

#[track_caller]
unsafe fn resolve_deferred(env: Env, deferred: napi::Deferred, value: Local) {
    if napi::resolve_deferred(env, deferred, value) != Ok(()) {
        fatal_error("Failed to resolve promise");
    }
}

#[track_caller]
unsafe fn reject_deferred(env: Env, deferred: napi::Deferred, value: Local) {
    if napi::reject_deferred(env, deferred, value) != Ok(()) {
        fatal_error("Failed to reject promise");
    }
}

#[track_caller]
unsafe fn catch_exception(env: Env) -> Option<Local> {
    if !is_exception_pending(env) {
        return None;
    }

    let mut error = MaybeUninit::uninit();

    if napi::get_and_clear_last_exception(env, error.as_mut_ptr()) != Ok(()) {
        fatal_error("Failed to get and clear the last exception");
    }

    Some(error.assume_init())
}

#[track_caller]
unsafe fn error_from_message(env: Env, msg: &str) -> Local {
    let msg = create_string(env, msg);
    let mut err = MaybeUninit::uninit();

    let status = napi::create_error(env, ptr::null_mut(), msg, err.as_mut_ptr());

    match status {
        Ok(()) => err.assume_init(),
        Err(_) => fatal_error("Failed to create an Error"),
    }
}

#[track_caller]
unsafe fn error_from_panic(env: Env, panic: Panic) -> Local {
    if let Some(msg) = panic_msg(&panic) {
        error_from_message(env, msg)
    } else {
        let error = error_from_message(env, UNKNOWN_PANIC_MESSAGE);
        let panic = external_from_panic(env, panic);

        set_property(env, error, "cause", panic);
        error
    }
}

#[track_caller]
unsafe fn set_property(env: Env, object: Local, key: &str, value: Local) {
    let key = create_string(env, key);

    if napi::set_property(env, object, key, value).is_err() {
        fatal_error("Failed to set an object property");
    }
}

#[track_caller]
unsafe fn panic_msg(panic: &Panic) -> Option<&str> {
    if let Some(msg) = panic.downcast_ref::<&str>() {
        Some(msg)
    } else if let Some(msg) = panic.downcast_ref::<String>() {
        Some(msg)
    } else {
        None
    }
}

unsafe fn external_from_panic(env: Env, panic: Panic) -> Local {
    let fail = || fatal_error("Failed to create a neon::types::JsBox from a panic");
    let mut result = MaybeUninit::uninit();

    if napi::create_external(
        env,
        Box::into_raw(Box::new(DebugSendWrapper::new(panic))).cast(),
        Some(finalize_panic),
        ptr::null_mut(),
        result.as_mut_ptr(),
    )
    .is_err()
    {
        fail();
    }

    let external = result.assume_init();

    #[cfg(feature = "napi-8")]
    if napi::type_tag_object(env, external, &*crate::MODULE_TAG).is_err() {
        fail();
    }

    external
}

extern "C" fn finalize_panic(_env: Env, data: *mut c_void, _hint: *mut c_void) {
    unsafe {
        drop(Box::from_raw(data.cast::<Panic>()));
    }
}

#[track_caller]
unsafe fn create_string(env: Env, msg: &str) -> Local {
    let mut string = MaybeUninit::uninit();

    if napi::create_string_utf8(env, msg.as_ptr().cast(), msg.len(), string.as_mut_ptr()).is_err() {
        fatal_error("Failed to create a String");
    }

    string.assume_init()
}

unsafe fn is_exception_pending(env: Env) -> bool {
    let mut throwing = false;

    if napi::is_exception_pending(env, &mut throwing).is_err() {
        fatal_error("Failed to check if an exception is pending");
    }

    throwing
}