Skip to content

fix: raise PostgreSQL ERROR on OOM instead of unwinding out of the allocator - #976

Open
nateships wants to merge 1 commit into
mainfrom
oom-error-no-unwind
Open

nateships wants to merge 1 commit into
mainfrom
oom-error-no-unwind

Conversation

@nateships

Copy link
Copy Markdown
Member

Problem

palloc.rs turns a failed allocation into a Rust panic! from inside GlobalAlloc. On rustc 1.93 and later, this design aborts the whole backend: the first OOM panic unwinds and becomes a clean transaction abort, but the second one in the same backend fails with fatal runtime error: failed to initiate panic, and PostgreSQL restarts. A toolchain bisect shows 1.92 unwinds correctly and 1.93 aborts (the suspected trigger is the allocator/TLS rework in rust-lang/rust#144465). This repo pins rust 1.96.0 (tools/dependencies.sh), so the abort is reachable today.

There is no stable panic-based replacement: an unwind out of GlobalAlloc is documented undefined behavior, -Zoom=panic was removed in Rust 1.94, and set_alloc_error_hook (rust-lang/rust#51245) is still nightly-only.

Change

On the backend main thread (recorded in _PG_init), the allocator now reports OOM the way PostgreSQL C code does. It calls errstart/errcode/errmsg/errfinish, which longjmp to the active PostgreSQL error handler and abort the transaction. No Rust frames unwind, so the rustc limitation does not apply. PostgreSQL keeps a preallocated reserve in its ErrorContext, so the report also works under genuine memory exhaustion.

Trade-offs and edges:

  • The longjmp skips Rust frames without running their destructors, so allocations owned by those frames leak. On the OOM path this is acceptable, and it is strictly better than a backend abort plus a PostgreSQL restart.
  • Threads other than the backend main thread keep the previous panic! fallback.
  • The err* symbols are declared locally because pgrx does not expose them in pg_sys (pgrx routes errors through its panic-based ereport! macro, which is exactly the mechanism that cannot be used inside the allocator).

…locator

On rustc >= 1.93, the second out-of-memory panic that unwinds out of
the global allocator in one backend aborts the whole backend with
'fatal runtime error: failed to initiate panic' (bisected: 1.92
unwinds correctly, 1.93 aborts). An unwind out of GlobalAlloc is
documented undefined behavior, -Zoom=panic was removed in Rust 1.94,
and set_alloc_error_hook is still nightly-only, so no stable
panic-based design exists.

On the backend main thread (recorded in _PG_init), report OOM the way
PostgreSQL C code does: errstart/errfinish longjmp to the error
handler and abort the transaction without unwinding Rust frames.
Skipped frames leak their allocations on this path, which is
acceptable. Other threads keep the panic fallback. The err* symbols
are declared locally because pgrx does not expose them in pg_sys.
@nateships
nateships force-pushed the oom-error-no-unwind branch from 6a7c49f to fe96672 Compare August 26, 2026 01:56
@surister
surister self-requested a review September 1, 2026 09:30
Comment thread extension/src/palloc.rs
Comment on lines +163 to +166
type Errstart = unsafe extern "C" fn(::core::ffi::c_int, *const ::core::ffi::c_char) -> bool;
type Errcode = unsafe extern "C" fn(::core::ffi::c_int) -> ::core::ffi::c_int;
type Errmsg = unsafe extern "C" fn(*const ::core::ffi::c_char, ...) -> ::core::ffi::c_int;
type Errfinish = unsafe extern "C" fn(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it make more sense for these to be C-unwind instead?

Comment thread extension/src/palloc.rs
/// it is safe to call from inside the global allocator.
unsafe fn pg_oom_error() -> ! {
unsafe {
let errstart = dlsym(RTLD_DEFAULT, c"errstart".as_ptr());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like dlsym does not allocate memory under normal fast paths, but digging out implementations we can see that glib for example can reach malloc/calloc calls for unique symbols, https://raw.githubusercontent.com/bminor/glibc/master/elf/dl-lookup.c in this particular malloc call it seems safe that it was not able to allocate.

This alone makes me wonder whether trusting the allocation-less contract with dlsym across versions/implementations/platforms is correct, what would happen if an implementation calls the same allocator as Rusts's and cannot allocate, can it recurse back to our own handler? I see this as a very edgy edgy case but something similar happened many years ago in RealmDB in some specific Android/Platform version and it took them weeks to debug, I'd like to avoid this.

can we alternatively load the symbols in _PG_init and have them cached? This seems perfectly safe as an allocation there would never fail.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants