pgrx_pg_sys/submodules/ffi.rs
1//LICENSE Portions Copyright 2019-2021 ZomboDB, LLC.
2//LICENSE
3//LICENSE Portions Copyright 2021-2023 Technology Concepts & Design, Inc.
4//LICENSE
5//LICENSE Portions Copyright 2023-2023 PgCentral Foundation, Inc. <contact@pgcentral.org>
6//LICENSE
7//LICENSE All rights reserved.
8//LICENSE
9//LICENSE Use of this source code is governed by the MIT license that can be found in the LICENSE file.
10#![deny(unsafe_op_in_unsafe_fn)]
11
12#[cfg(not(all(
13 any(target_os = "linux", target_os = "macos"),
14 any(target_arch = "x86_64", target_arch = "aarch64")
15)))]
16mod cee_scape {
17 #[cfg(not(feature = "cshim"))]
18 compile_error!("target platform cannot work without feature cshim");
19
20 use libc::{c_int, c_void};
21 use std::marker::PhantomData;
22
23 #[repr(C)]
24 pub struct SigJmpBufFields {
25 _internal: [u8; 0],
26 _neither_send_nor_sync: PhantomData<*const u8>,
27 }
28
29 pub fn call_with_sigsetjmp<F>(savemask: bool, mut callback: F) -> c_int
30 where
31 F: for<'a> FnOnce(&'a SigJmpBufFields) -> c_int,
32 {
33 extern "C" {
34 fn call_closure_with_sigsetjmp(
35 savemask: c_int,
36 closure_env_ptr: *mut c_void,
37 closure_code: extern "C" fn(
38 jbuf: *const SigJmpBufFields,
39 env_ptr: *mut c_void,
40 ) -> c_int,
41 ) -> c_int;
42 }
43
44 extern "C" fn call_from_c_to_rust<F>(
45 jbuf: *const SigJmpBufFields,
46 closure_env_ptr: *mut c_void,
47 ) -> c_int
48 where
49 F: for<'a> FnOnce(&'a SigJmpBufFields) -> c_int,
50 {
51 let closure_env_ptr: *mut F = closure_env_ptr as *mut F;
52 unsafe { (closure_env_ptr.read())(&*jbuf) }
53 }
54
55 let savemask: libc::c_int = if savemask { 1 } else { 0 };
56
57 unsafe {
58 let closure_env_ptr = core::ptr::addr_of_mut!(callback);
59 core::mem::forget(callback);
60 call_closure_with_sigsetjmp(
61 savemask,
62 closure_env_ptr as *mut libc::c_void,
63 call_from_c_to_rust::<F>,
64 )
65 }
66 }
67}
68
69use cee_scape::{call_with_sigsetjmp, SigJmpBufFields};
70
71/**
72Given a closure that is assumed to be a wrapped Postgres `extern "C"` function, [pg_guard_ffi_boundary]
73works with the Postgres and C runtimes to create a "barrier" that allows Rust to catch Postgres errors
74(`elog(ERROR)`) while running the supplied closure. This is done for the sake of allowing Rust to run
75destructors before Postgres destroys the memory contexts that Rust-in-Postgres code may be enmeshed in.
76
77Wrapping the FFI into Postgres enables
78- memory safety
79- improving error logging
80- minimizing resource leaks
81
82But only the first of these is considered paramount.
83
84At all times PGRX reserves the right to choose an implementation that achieves memory safety.
85Currently, this function is used to protect **every** bindgen-generated Postgres `extern "C"` function.
86
87Generally, the only time *you'll* need to use this function is when calling a Postgres-provided
88function pointer.
89
90# Safety
91
92It is undefined behavior if the function passed to `pg_guard_ffi_boundary` have objects with
93destructors on the stack when postgres raises an `ERROR`. For example, the following is
94both a resource leak, and undefined behavior (as it needs to be a [trivially-deallocated
95stack frame]):
96
97```rust,ignore
98// This is UB!
99pgrx::pg_sys::ffi::pg_guard_ffi_boundary(|| {
100 let data = vec![1, 2, 3, 4, 5];
101 // call FFI function that raises an ERROR
102});
103```
104Instead, you should write it like
105```rust,ignore
106let data = vec![1, 2, 3, 4, 5];
107pgrx::pg_sys::ffi::pg_guard_ffi_boundary(|| {
108 // call FFI function that raises an ERROR
109});
110```
111
112Further, it is undefined behavior if the function passed into `pg_guard_ffi_boundary` panics. It
113is recommended that you keep the body of the `pg_guard_ffi_boundary` closure very small -- ideally
114*only* containing a call to some C function, rather than containing any logic or variables of its
115own.
116
117Furthermore, Postgres is a single-threaded runtime. As such, [`pg_guard_ffi_boundary`] should
118**only** be called from the main thread. In fact, [`pg_guard_ffi_boundary`] will detect this
119and immediately panic.
120
121More generally, Rust cannot guarantee destructors are always run, PGRX is written in Rust code, and
122the implementation of `pg_guard_ffi_boundary` relies on help from Postgres, the OS, and the C runtime;
123thus, relying on the FFI boundary catching an error and propagating it back into Rust to guarantee
124Rust's language-level memory safety when calling Postgres is unsound (i.e. there are no promises).
125Postgres can and does opt to erase exception and error context stacks in some situations.
126The C runtime is beholden to the operating system, which may do as it likes with a thread.
127PGRX has many magical powers, some of considerable size, but they are not infinite cosmic power.
128
129Thus, if Postgres gives you a pointer into the database's memory, and you corrupt that memory
130in a way technically permitted by Rust, intending to fix it before Postgres or Rust notices,
131then you may not call Postgres and expect Postgres to not notice the code crimes in progress.
132Postgres and Rust will see you. Whether they choose to ignore such misbehavior is up to them, not PGRX.
133If you are manipulating transient "pure Rust" data, however, it is unlikely this is of consequence.
134
135# Implementation Note
136
137The main implementation uses `sigsetjmp`, [`pg_sys::error_context_stack`], and [`pg_sys::PG_exception_stack`].
138which, when Postgres enters its exception handling in `elog.c`, will prompt a `siglongjmp` back to it.
139
140This caught error is then converted into a Rust `panic!()` and propagated up the stack, ultimately
141being converted into a transaction-aborting Postgres `ERROR` by PGRX.
142
143[trivially-deallocated stack frame]: https://github.com/rust-lang/rfcs/blob/master/text/2945-c-unwind-abi.md#plain-old-frames
144**/
145use crate as pg_sys;
146use crate::panic::{CaughtError, ErrorReport, ErrorReportLocation, ErrorReportWithLevel};
147use core::ffi::CStr;
148use std::mem::MaybeUninit;
149
150#[inline(always)]
151#[track_caller]
152pub unsafe fn pg_guard_ffi_boundary<T, F: FnOnce() -> T>(f: F) -> T {
153 // SAFETY: Caller promises not to call us from anything but the main thread.
154 unsafe { pg_guard_ffi_boundary_impl(f) }
155}
156
157#[inline(always)]
158#[track_caller]
159unsafe fn pg_guard_ffi_boundary_impl<T, F: FnOnce() -> T>(f: F) -> T {
160 //! This is the version that uses sigsetjmp and all that, for "normal" Rust/PGRX interfaces.
161
162 // The next code is definitely thread-unsafe (it manipulates statics in an
163 // unsynchronized manner), so we may as well check here.
164 super::thread_check::check_active_thread();
165
166 // SAFETY: This should really, really not be done in a multithreaded context as it
167 // accesses multiple `static mut`. The ultimate caller asserts this is the main thread.
168 unsafe {
169 let caller_memxct = pg_sys::CurrentMemoryContext;
170 let prev_exception_stack = pg_sys::PG_exception_stack;
171 let prev_error_context_stack = pg_sys::error_context_stack;
172 let mut result: std::mem::MaybeUninit<T> = MaybeUninit::uninit();
173 let jump_value = call_with_sigsetjmp(false, |jump_buffer| {
174 // Make Postgres' error-handling system aware of our new
175 // setjmp/longjmp restore point.
176 pg_sys::PG_exception_stack = std::mem::transmute(jump_buffer as *const SigJmpBufFields);
177
178 // execute the closure, which will be a wrapped internal Postgres function
179 result.write(f());
180 0
181 });
182
183 if jump_value == 0 {
184 // Flag is 0, so we've taken the successful return path. We're not
185 // here as the result of a longjmp.
186 // Restore Postgres' understanding of where its next longjmp should go
187 pg_sys::PG_exception_stack = prev_exception_stack;
188 pg_sys::error_context_stack = prev_error_context_stack;
189
190 result.assume_init()
191 } else {
192 // We've landed here b/c of a longjmp originating in Postgres
193
194 // the overhead to get the current [ErrorData] from Postgres and convert
195 // it into our [ErrorReportWithLevel] seems worth the user benefit
196 //
197 // Note that this only happens in the case of us trapping an error
198
199 // At this point, we're running within `pg_sys::ErrorContext`, but should be in the
200 // memory context the caller was in before we call [CopyErrorData()] and start using it
201 pg_sys::CurrentMemoryContext = caller_memxct;
202
203 // SAFETY: `pg_sys::CopyErrorData()` will always give us a valid pointer, so just assume so
204 let errdata_ptr = pg_sys::CopyErrorData();
205 let errdata = errdata_ptr.as_ref().unwrap_unchecked();
206
207 // copy out the fields we need to support pgrx' error handling
208 let level = errdata.elevel.into();
209 let sqlerrcode = errdata.sqlerrcode.into();
210 let message = errdata
211 .message
212 .is_null()
213 .then(|| String::from("<null error message>"))
214 .unwrap_or_else(|| CStr::from_ptr(errdata.message).to_string_lossy().to_string());
215 let detail = errdata.detail.is_null().then_some(None).unwrap_or_else(|| {
216 Some(CStr::from_ptr(errdata.detail).to_string_lossy().to_string())
217 });
218 let hint = errdata.hint.is_null().then_some(None).unwrap_or_else(|| {
219 Some(CStr::from_ptr(errdata.hint).to_string_lossy().to_string())
220 });
221 let funcname = errdata.funcname.is_null().then_some(None).unwrap_or_else(|| {
222 Some(CStr::from_ptr(errdata.funcname).to_string_lossy().to_string())
223 });
224 let file =
225 errdata.filename.is_null().then(|| String::from("<null filename>")).unwrap_or_else(
226 || CStr::from_ptr(errdata.filename).to_string_lossy().to_string(),
227 );
228 let line = errdata.lineno as _;
229
230 // clean up after ourselves by freeing the result of [CopyErrorData] and restoring
231 // Postgres' understanding of where its next longjmp should go
232 pg_sys::FreeErrorData(errdata_ptr);
233 pg_sys::PG_exception_stack = prev_exception_stack;
234 pg_sys::error_context_stack = prev_error_context_stack;
235
236 // finally, turn this Postgres error into a Rust panic so that we can ensure proper
237 // Rust stack unwinding and also defer handling until later
238 std::panic::panic_any(CaughtError::PostgresError(ErrorReportWithLevel {
239 level,
240 inner: ErrorReport {
241 sqlerrcode,
242 message,
243 detail,
244 hint,
245 location: ErrorReportLocation { file, funcname, line, col: 0, backtrace: None },
246 },
247 }))
248 }
249 }
250}