Skip to main content

pgrx_pg_sys/submodules/
panic.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#![allow(non_snake_case)]
12
13use core::ffi::CStr;
14use std::any::Any;
15use std::borrow::Cow;
16use std::cell::Cell;
17use std::fmt::{Display, Formatter};
18use std::hint::unreachable_unchecked;
19use std::panic::{
20    AssertUnwindSafe, Location, PanicHookInfo, UnwindSafe, catch_unwind, panic_any, resume_unwind,
21};
22
23use crate::elog::PgLogLevel;
24use crate::errcodes::PgSqlErrorCode;
25use crate::{AsPgCStr, MemoryContextSwitchTo, pfree};
26
27/// Indicates that something can be reported as a Postgres ERROR, if that's what it might represent.
28pub trait ErrorReportable {
29    type Inner;
30
31    /// Raise a Postgres ERROR if appropriate, otherwise return a value
32    fn unwrap_or_report(self) -> Self::Inner;
33}
34
35impl<T, E> ErrorReportable for Result<T, E>
36where
37    E: Any + Display,
38{
39    type Inner = T;
40
41    /// If this [`Result`] represents the `Ok` variant, that value is returned.
42    ///
43    /// If this [`Result`] represents the `Err` variant, raise it as an error.  If it happens to
44    /// be an [`ErrorReport`], then that is specifically raised.  Otherwise it's just a general
45    /// [`ereport!`] as a [`PgLogLevel::ERROR`].
46    fn unwrap_or_report(self) -> Self::Inner {
47        self.unwrap_or_else(|e| {
48            let any: Box<&dyn Any> = Box::new(&e);
49            if any.downcast_ref::<ErrorReport>().is_some() {
50                let any: Box<dyn Any> = Box::new(e);
51                any.downcast::<ErrorReport>().unwrap().report(PgLogLevel::ERROR);
52                unreachable!();
53            } else {
54                ereport!(ERROR, PgSqlErrorCode::ERRCODE_DATA_EXCEPTION, format!("{e}"));
55            }
56        })
57    }
58}
59
60#[derive(Debug)]
61pub struct ErrorReportLocation {
62    pub(crate) file: String,
63    pub(crate) funcname: Option<String>,
64    pub(crate) line: u32,
65    pub(crate) col: u32,
66    pub(crate) backtrace: Option<std::backtrace::Backtrace>,
67}
68
69impl Default for ErrorReportLocation {
70    fn default() -> Self {
71        Self {
72            file: std::string::String::from("<unknown>"),
73            funcname: None,
74            line: 0,
75            col: 0,
76            backtrace: None,
77        }
78    }
79}
80
81impl Display for ErrorReportLocation {
82    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
83        match &self.funcname {
84            Some(funcname) => {
85                // mimic's Postgres' output for this, but includes a column number
86                write!(f, "{}, {}:{}:{}", funcname, self.file, self.line, self.col)?;
87            }
88
89            None => {
90                write!(f, "{}:{}:{}", self.file, self.line, self.col)?;
91            }
92        }
93
94        if let Some(backtrace) = &self.backtrace
95            && backtrace.status() == std::backtrace::BacktraceStatus::Captured
96        {
97            write!(f, "\n{backtrace}")?;
98        }
99
100        Ok(())
101    }
102}
103
104impl From<&Location<'_>> for ErrorReportLocation {
105    fn from(location: &Location<'_>) -> Self {
106        Self {
107            file: location.file().to_string(),
108            funcname: None,
109            line: location.line(),
110            col: location.column(),
111            backtrace: None,
112        }
113    }
114}
115
116impl From<&PanicHookInfo<'_>> for ErrorReportLocation {
117    fn from(pi: &PanicHookInfo<'_>) -> Self {
118        pi.location().map(|l| l.into()).unwrap_or_default()
119    }
120}
121
122/// Represents the set of information necessary for pgrx to promote a Rust `panic!()` to a Postgres
123/// `ERROR` (or any [`PgLogLevel`] level)
124#[derive(Debug)]
125pub struct ErrorReport {
126    pub(crate) sqlerrcode: PgSqlErrorCode,
127    pub(crate) message: Cow<'static, str>,
128    pub(crate) hint: Option<String>,
129    pub(crate) detail: Option<String>,
130    pub(crate) domain: Option<String>,
131    pub(crate) location: ErrorReportLocation,
132}
133
134impl Display for ErrorReport {
135    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
136        write!(f, "{}: {}", self.sqlerrcode, self.message)?;
137        if let Some(hint) = &self.hint {
138            write!(f, "\nHINT: {hint}")?;
139        }
140        if let Some(detail) = &self.detail {
141            write!(f, "\nDETAIL: {detail}")?;
142        }
143        write!(f, "\nLOCATION: {}", self.location)
144    }
145}
146
147#[derive(Debug)]
148pub struct ErrorReportWithLevel {
149    pub(crate) level: PgLogLevel,
150    pub(crate) inner: ErrorReport,
151}
152
153impl ErrorReportWithLevel {
154    fn report(self) {
155        match self.level {
156            // ERRORs get converted into panics so they can perform proper stack unwinding
157            PgLogLevel::ERROR => panic_any(self),
158
159            // FATAL and PANIC are reported directly to Postgres -- they abort the process
160            PgLogLevel::FATAL | PgLogLevel::PANIC => {
161                do_ereport(self);
162                unreachable!()
163            }
164
165            // Everything else (INFO, WARN, LOG, DEBUG, etc) are reported to Postgres too but they only emit messages
166            _ => do_ereport(self),
167        }
168    }
169
170    /// Returns the logging level of this error report
171    pub fn level(&self) -> PgLogLevel {
172        self.level
173    }
174
175    /// Returns the sql error code of this error report
176    pub fn sql_error_code(&self) -> PgSqlErrorCode {
177        self.inner.sqlerrcode
178    }
179
180    /// Returns the error message of this error report
181    pub fn message(&self) -> &str {
182        self.inner.message()
183    }
184
185    /// Returns the detail line of this error report, if there is one
186    pub fn detail(&self) -> Option<&str> {
187        self.inner.detail()
188    }
189
190    /// Get the detail line with backtrace. If backtrace is not available, it will just return the detail.
191    pub fn detail_with_backtrace(&self) -> Option<String> {
192        match (self.detail(), self.backtrace()) {
193            (Some(detail), Some(bt))
194                if bt.status() == std::backtrace::BacktraceStatus::Captured =>
195            {
196                Some(format!("{detail}\n{bt}"))
197            }
198            (Some(d), _) => Some(d.to_string()),
199            (None, Some(bt)) if bt.status() == std::backtrace::BacktraceStatus::Captured => {
200                Some(format!("\n{bt}"))
201            }
202            (None, _) => None,
203        }
204    }
205
206    /// Returns the hint line of this error report, if there is one
207    pub fn hint(&self) -> Option<&str> {
208        self.inner.hint()
209    }
210
211    /// Returns the domain of this error report, if set
212    pub fn domain(&self) -> Option<&str> {
213        self.inner.domain()
214    }
215
216    /// Returns the name of the source file that generated this error report
217    pub fn file(&self) -> &str {
218        &self.inner.location.file
219    }
220
221    /// Returns the line number of the source file that generated this error report
222    pub fn line_number(&self) -> u32 {
223        self.inner.location.line
224    }
225
226    /// Returns the backtrace when the error is reported
227    pub fn backtrace(&self) -> Option<&std::backtrace::Backtrace> {
228        self.inner.location.backtrace.as_ref()
229    }
230
231    /// Returns the name of the function that generated this error report, if we were able to figure it out
232    pub fn function_name(&self) -> Option<&str> {
233        self.inner.location.funcname.as_deref()
234    }
235
236    /// Returns the context message of this error report, if any
237    fn context_message(&self) -> Option<String> {
238        // NB:  holding this here for future use
239        None
240    }
241}
242
243impl ErrorReport {
244    /// Create an [ErrorReport] which can be raised via Rust's [std::panic::panic_any()] or as
245    /// a specific Postgres "ereport()` level via [ErrorReport::unwrap_or_report(self, PgLogLevel)]
246    ///
247    /// Embedded "file:line:col" location information is taken from the caller's location
248    #[track_caller]
249    pub fn new<S: Into<Cow<'static, str>>>(
250        sqlerrcode: PgSqlErrorCode,
251        message: S,
252        funcname: &'static str,
253    ) -> Self {
254        let mut location: ErrorReportLocation = Location::caller().into();
255        location.funcname = Some(funcname.to_string());
256
257        Self {
258            sqlerrcode,
259            message: message.into(),
260            hint: None,
261            detail: None,
262            domain: None,
263            location,
264        }
265    }
266
267    /// Create an [ErrorReport] which can be raised via Rust's [std::panic::panic_any()] or as
268    /// a specific Postgres "ereport()` level via [ErrorReport::unwrap_or_report(self, PgLogLevel)].
269    ///
270    /// For internal use only
271    fn with_location<S: Into<Cow<'static, str>>>(
272        sqlerrcode: PgSqlErrorCode,
273        message: S,
274        location: ErrorReportLocation,
275    ) -> Self {
276        Self {
277            sqlerrcode,
278            message: message.into(),
279            hint: None,
280            detail: None,
281            domain: None,
282            location,
283        }
284    }
285
286    /// Set the `detail` property, whose default is `None`
287    pub fn set_detail<S: Into<String>>(mut self, detail: S) -> Self {
288        self.detail = Some(detail.into());
289        self
290    }
291
292    /// Set the `hint` property, whose default is `None`
293    pub fn set_hint<S: Into<String>>(mut self, hint: S) -> Self {
294        self.hint = Some(hint.into());
295        self
296    }
297
298    /// Set the `domain` property for message translation/internationalization, whose default is `None`.
299    ///
300    /// This corresponds to the `domain` argument in Postgres' `ereport_domain` C macro.
301    /// When set, the domain is passed to `errstart()` to indicate the gettext text domain
302    /// for message translation.
303    pub fn set_domain<S: Into<String>>(mut self, domain: S) -> Self {
304        self.domain = Some(domain.into());
305        self
306    }
307
308    /// Returns the error message of this error report
309    pub fn message(&self) -> &str {
310        &self.message
311    }
312
313    /// Returns the detail message of this error report
314    pub fn detail(&self) -> Option<&str> {
315        self.detail.as_deref()
316    }
317
318    /// Returns the hint message of this error report
319    pub fn hint(&self) -> Option<&str> {
320        self.hint.as_deref()
321    }
322
323    /// Returns the domain of this error report, if set
324    pub fn domain(&self) -> Option<&str> {
325        self.domain.as_deref()
326    }
327
328    /// Report this [ErrorReport], which will ultimately be reported by Postgres at the specified [PgLogLevel]
329    ///
330    /// If the provided `level` is >= [`PgLogLevel::ERROR`] this function will not return.
331    pub fn report(self, level: PgLogLevel) {
332        ErrorReportWithLevel { level, inner: self }.report()
333    }
334}
335
336thread_local! { static PANIC_LOCATION: Cell<Option<ErrorReportLocation>> = const { Cell::new(None) }}
337
338fn take_panic_location() -> ErrorReportLocation {
339    PANIC_LOCATION.with(|p| p.take().unwrap_or_default())
340}
341
342pub fn register_pg_guard_panic_hook() {
343    use super::thread_check::is_os_main_thread;
344
345    let default_hook = std::panic::take_hook();
346    std::panic::set_hook(Box::new(move |info: _| {
347        // Always capture the backtrace into PANIC_LOCATION so that
348        // downcast_panic_payload can attach it to CaughtError::PostgresError.
349        // This is a thread-local, so it's harmless on non-main threads.
350        PANIC_LOCATION.with(|thread_local| {
351            thread_local.replace({
352                let mut info: ErrorReportLocation = info.into();
353                info.backtrace = Some(std::backtrace::Backtrace::capture());
354                Some(info)
355            })
356        });
357
358        if is_os_main_thread() == Some(false) {
359            // definitely not the main thread -- we don't know which connection
360            // to associate the panic with, so also call the default hook
361            default_hook(info)
362        }
363    }))
364}
365
366/// What kind of error was caught?
367#[derive(Debug)]
368pub enum CaughtError {
369    /// An error raised from within Postgres
370    PostgresError(ErrorReportWithLevel),
371
372    /// A `pgrx::error!()` or `pgrx::ereport!(ERROR, ...)` raised from within Rust
373    ErrorReport(ErrorReportWithLevel),
374
375    /// A Rust `panic!()` or `std::panic::panic_any()`
376    RustPanic { ereport: ErrorReportWithLevel, payload: Box<dyn Any + Send> },
377}
378
379impl CaughtError {
380    /// Rethrow this [CaughtError].  
381    ///
382    /// This is the same as [std::panic::resume_unwind()] and has the same semantics.
383    pub fn rethrow(self) -> ! {
384        // we resume_unwind here as [CaughtError] represents a previously caught panic, not a new
385        // one to be thrown
386        resume_unwind(Box::new(self))
387    }
388}
389
390#[derive(Debug)]
391enum GuardAction<R> {
392    Return(R),
393    ReThrow,
394    Report(ErrorReportWithLevel),
395}
396
397/// Guard a closure such that Rust Panics are properly converted into Postgres ERRORs.
398///
399/// Note that any Postgres ERRORs raised within the supplied closure are transparently converted
400/// to Rust panics.
401///
402/// Generally, this function won't need to be used directly, as it's also the implementation
403/// behind the `#[pg_guard]` and `#[pg_extern]` macros.  Which means the function you'd like to guard
404/// is likely already guarded.
405///
406/// Where it does need to be used is as a wrapper around Rust `extern "C-unwind"` function pointers
407/// given to Postgres, and the `#[pg_guard]` macro takes care of this for you.
408///
409/// In other words, this isn't the function you're looking for.
410///
411/// You're probably looking for the `#[pg_guard]` macro.
412///
413/// Alternatively, if you're trying to mimic Postgres' C `PG_TRY/PG_CATCH` API, then you instead
414/// want [`crate::pg_try::PgTryBuilder`].
415///
416/// # Safety
417/// The function needs to only have [trivially-deallocated stack frames]
418/// above it. That is, the caller (and their caller, etc) cannot have
419/// objects with pending destructors in their stack frames, unless those
420/// objects have already been dropped.
421///
422/// In practice, this should only ever be called at the top level of an
423/// `extern "C-unwind" fn` implemented in Rust.
424///
425/// [trivially-deallocated stack frames](https://github.com/rust-lang/rfcs/blob/master/text/2945-c-unwind-abi.md#plain-old-frames)
426#[doc(hidden)]
427// FIXME: previously, R was bounded on Copy, but this prevents using move-only POD types
428// what we really want is a bound of R: !Drop, but negative bounds don't exist yet
429pub unsafe fn pgrx_extern_c_guard<Func, R>(f: Func) -> R
430where
431    Func: FnOnce() -> R,
432{
433    match unsafe { run_guarded(AssertUnwindSafe(f)) } {
434        GuardAction::Return(r) => r,
435        GuardAction::ReThrow => {
436            #[cfg_attr(target_os = "windows", link(name = "postgres"))]
437            unsafe extern "C-unwind" {
438                fn pg_re_throw() -> !;
439            }
440            unsafe {
441                crate::CurrentMemoryContext = crate::ErrorContext;
442                pg_re_throw()
443            }
444        }
445        GuardAction::Report(ereport) => {
446            do_ereport(ereport);
447            unreachable!("pgrx reported a CaughtError that wasn't raised at ERROR or above");
448        }
449    }
450}
451
452// SAFETY: similar constraints as pgrx_extern_c_guard
453#[inline(never)]
454unsafe fn run_guarded<F, R>(f: F) -> GuardAction<R>
455where
456    F: FnOnce() -> R + UnwindSafe,
457{
458    match catch_unwind(f) {
459        Ok(v) => GuardAction::Return(v),
460        Err(e) => match downcast_panic_payload(e) {
461            CaughtError::PostgresError(_) => {
462                // Return to the caller to rethrow the original Postgres ErrorData unchanged.  We
463                // can't do it here because this function has non-POF frames.
464                GuardAction::ReThrow
465            }
466            CaughtError::ErrorReport(ereport) | CaughtError::RustPanic { ereport, .. } => {
467                GuardAction::Report(ereport)
468            }
469        },
470    }
471}
472
473/// convert types of `e` that we understand/expect into the representative [CaughtError]
474pub(crate) fn downcast_panic_payload(e: Box<dyn Any + Send>) -> CaughtError {
475    if e.downcast_ref::<CaughtError>().is_some() {
476        // caught a previously caught CaughtError that is being rethrown
477        let mut caught = *e.downcast::<CaughtError>().unwrap();
478
479        // For PostgresErrors (originating from a pg_sys FFI longjmp caught by
480        // pg_guard_ffi_boundary), the panic hook captured a Rust backtrace into
481        // PANIC_LOCATION.  Attach it now so PgTryBuilder catch handlers can inspect it without
482        // changing the original Postgres error that will be rethrown.
483        if let CaughtError::PostgresError(ref mut ereport) = caught {
484            if ereport.inner.location.backtrace.is_none() {
485                let panic_location = take_panic_location();
486                ereport.inner.location.backtrace = panic_location.backtrace;
487            }
488        }
489
490        caught
491    } else if e.downcast_ref::<ErrorReportWithLevel>().is_some() {
492        // someone called `panic_any(ErrorReportWithLevel)`
493        CaughtError::ErrorReport(*e.downcast().unwrap())
494    } else if e.downcast_ref::<ErrorReport>().is_some() {
495        // someone called `panic_any(ErrorReport)` so we convert it to be PgLogLevel::ERROR
496        CaughtError::ErrorReport(ErrorReportWithLevel {
497            level: PgLogLevel::ERROR,
498            inner: *e.downcast().unwrap(),
499        })
500    } else if let Some(message) = e.downcast_ref::<&str>() {
501        // something panic'd with a &str, so it gets raised as an INTERNAL_ERROR at the ERROR level
502        CaughtError::RustPanic {
503            ereport: ErrorReportWithLevel {
504                level: PgLogLevel::ERROR,
505                inner: ErrorReport::with_location(
506                    PgSqlErrorCode::ERRCODE_INTERNAL_ERROR,
507                    message.to_string(),
508                    take_panic_location(),
509                ),
510            },
511            payload: e,
512        }
513    } else if let Some(message) = e.downcast_ref::<String>() {
514        // something panic'd with a String, so it gets raised as an INTERNAL_ERROR at the ERROR level
515        CaughtError::RustPanic {
516            ereport: ErrorReportWithLevel {
517                level: PgLogLevel::ERROR,
518                inner: ErrorReport::with_location(
519                    PgSqlErrorCode::ERRCODE_INTERNAL_ERROR,
520                    message.clone(),
521                    take_panic_location(),
522                ),
523            },
524            payload: e,
525        }
526    } else {
527        // not a type we understand, so it gets raised as an INTERNAL_ERROR at the ERROR level
528        CaughtError::RustPanic {
529            ereport: ErrorReportWithLevel {
530                level: PgLogLevel::ERROR,
531                inner: ErrorReport::with_location(
532                    PgSqlErrorCode::ERRCODE_INTERNAL_ERROR,
533                    "Box<Any>",
534                    take_panic_location(),
535                ),
536            },
537            payload: e,
538        }
539    }
540}
541
542/// This is a (as faithful as possible) Rust unrolling of Postgres' `#define ereport(...)` macro.
543///
544/// Different implementations are provided for different postgres version ranges to ensure
545/// best performance. Care is taken to avoid work if `errstart` signals we can finish early.
546///
547/// We localize the definition of the various `err*()` functions involved in reporting a Postgres
548/// error (and purposely exclude them from `build.rs`) to ensure users can't get into trouble
549/// trying to roll their own error handling.
550fn do_ereport(ereport: ErrorReportWithLevel) {
551    const PERCENT_S: &CStr = c"%s";
552    const DEFAULT_DOMAIN: *const ::std::os::raw::c_char = std::ptr::null_mut();
553
554    // the following code is definitely thread-unsafe -- not-the-main-thread can't be creating Postgres
555    // ereports.  Our secret `extern "C"` definitions aren't wrapped by #[pg_guard] so we need to
556    // manually do the active thread check
557    crate::thread_check::check_active_thread();
558
559    //
560    // only declare these functions here.  They're explicitly excluded from bindings generation in
561    // `build.rs` and we'd prefer pgrx users not have access to them at all
562    //
563
564    #[cfg_attr(target_os = "windows", link(name = "postgres"))]
565    unsafe extern "C-unwind" {
566        fn errcode(sqlerrcode: ::std::os::raw::c_int) -> ::std::os::raw::c_int;
567        fn errmsg(fmt: *const ::std::os::raw::c_char, ...) -> ::std::os::raw::c_int;
568        fn errdetail(fmt: *const ::std::os::raw::c_char, ...) -> ::std::os::raw::c_int;
569        fn errhint(fmt: *const ::std::os::raw::c_char, ...) -> ::std::os::raw::c_int;
570        fn errcontext_msg(fmt: *const ::std::os::raw::c_char, ...) -> ::std::os::raw::c_int;
571    }
572
573    // we only allocate file, lineno and funcname if `errstart` returns true
574    #[cfg_attr(target_os = "windows", link(name = "postgres"))]
575    unsafe extern "C-unwind" {
576        fn errstart(elevel: ::std::os::raw::c_int, domain: *const ::std::os::raw::c_char) -> bool;
577        fn errfinish(
578            filename: *const ::std::os::raw::c_char,
579            lineno: ::std::os::raw::c_int,
580            funcname: *const ::std::os::raw::c_char,
581        );
582    }
583
584    let level = ereport.level();
585
586    // Use the domain from the ErrorReport if one was set, otherwise use the null default
587    let domain_cstring = ereport.inner.domain.as_ref().map(|d| {
588        std::ffi::CString::new(d.as_str()).expect("domain must not contain interior NUL bytes")
589    });
590    let domain_ptr = domain_cstring.as_ref().map_or(DEFAULT_DOMAIN, |c| c.as_ptr());
591
592    unsafe {
593        if errstart(level as _, domain_ptr) {
594            let sqlerrcode = ereport.sql_error_code();
595            let message = ereport.message().as_pg_cstr();
596            let detail = ereport.detail_with_backtrace().as_pg_cstr();
597            let hint = ereport.hint().as_pg_cstr();
598            let context = ereport.context_message().as_pg_cstr();
599            let lineno = ereport.line_number();
600
601            // SAFETY:  We know that `crate::ErrorContext` is a valid memory context pointer and one
602            // that Postgres will clean up for us in the event of an ERROR, and we know it'll live long
603            // enough for Postgres to use `file` and `funcname`, which it expects to be `const char *`s
604
605            let prev_cxt = MemoryContextSwitchTo(crate::ErrorContext);
606            let file = ereport.file().as_pg_cstr();
607            let funcname = ereport.function_name().as_pg_cstr();
608            MemoryContextSwitchTo(prev_cxt);
609
610            // do not leak the Rust `ErrorReportWithLocation` instance
611            drop(ereport);
612
613            // SAFETY
614            //
615            // The following functions are all FFI into Postgres, so they're inherently unsafe.
616            //
617            // The various pointers used as arguments to these functions might have been allocated above
618            // or they might be the null pointer, so we guard against that possibility for each usage.
619            errcode(sqlerrcode as _);
620            if !message.is_null() {
621                errmsg(PERCENT_S.as_ptr(), message);
622                pfree(message.cast());
623            }
624            if !detail.is_null() {
625                errdetail(PERCENT_S.as_ptr(), detail);
626                pfree(detail.cast());
627            }
628            if !hint.is_null() {
629                errhint(PERCENT_S.as_ptr(), hint);
630                pfree(hint.cast());
631            }
632            if !context.is_null() {
633                errcontext_msg(PERCENT_S.as_ptr(), context);
634                pfree(context.cast());
635            }
636
637            if level >= PgLogLevel::ERROR {
638                crate::submodules::thread_check::active_thread::clear();
639            }
640            errfinish(file, lineno as _, funcname);
641
642            if level >= PgLogLevel::ERROR {
643                // SAFETY:  `crate::errstart() is guaranteed to have returned true if >=ERROR and
644                // `crate::errfinish()` is guaranteed to not have not returned at all if >= ERROR, which
645                // means we won't either
646                unreachable_unchecked()
647            } else {
648                // if it wasn't an ERROR we need to free up the things that Postgres wouldn't have
649                //
650                // ... except on Postgres 19, whose `errfinish()` resets `ErrorContext` (where we
651                // allocated `file` and `funcname`) on its way out, making a `pfree()` here a
652                // use-after-free.  In the nested-error case where pg19 doesn't reset the context,
653                // these allocations just live in `ErrorContext` until it's eventually reset
654                #[cfg(not(feature = "pg19"))]
655                {
656                    if !file.is_null() {
657                        pfree(file.cast());
658                    }
659                    if !funcname.is_null() {
660                        pfree(funcname.cast());
661                    }
662                }
663            }
664        }
665    }
666}