Skip to content

Fix toasted FlatArray argument borrowing#2355

Draft
eeeebbbbrrrr wants to merge 1 commit into
developfrom
fix/toasted-flat-array-borrow
Draft

Fix toasted FlatArray argument borrowing#2355
eeeebbbbrrrr wants to merge 1 commit into
developfrom
fix/toasted-flat-array-borrow

Conversation

@eeeebbbbrrrr

@eeeebbbbrrrr eeeebbbbrrrr commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

  • detoast FlatArray specifically while unboxing borrowed PostgreSQL function arguments
  • explicitly free fresh copies after scalar results are boxed, while preserving them in an SRF's multi-call context when a returned iterator borrows the array
  • keep BorrowDatum::point_from allocation-free for already-shaped owned pointers
  • add live-backend coverage for external TOAST input, full-data access, bounded memory use, and a borrowing set-returning function

Fixes #2350.

Root cause

FlatArray requires a contiguous, aligned ArrayType, but the blanket borrowed-argument ABI passed the raw Datum pointer directly to FlatArray::point_from. For an external TOAST pointer, that code interpreted the TOAST representation as an ArrayType and the backend segfaulted.

This fix deliberately does not detoast in point_from as suggested in the issue. Pointer shaping remains pure for PBox<FlatArray> and other already-valid allocations. Instead, a defaulted BorrowDatum argument hook lets FlatArray report fresh detoast copies to the call frame.

For ordinary scalar calls, FcInfo frees those copies after the Rust result has been converted to a PostgreSQL Datum. Set-returning functions are different: their RetAbi switches argument construction into multi_call_memory_ctx, and their iterators may borrow inputs across calls. The generated wrapper records the exact context selected by RetAbi before decoding arguments, and registration fails fast if detoasting occurred in any other context. SRF copies remain context-owned until PostgreSQL ends the SRF. Existing BorrowDatum implementations and #[pg_extern] signatures remain unchanged.

Cleanup uses exclusive ownership rather than interior mutability: FcInfo owns a plain Vec of scalar-call allocations, and its argument decoder borrows that call frame through &mut. As a result, the low-level call-convention API is intentionally tightened: FcInfo is no longer Clone, FcInfo::args requires &mut self, and Args is a lending-style decoder rather than a standard Iterator. Normal #[pg_extern] functions and downstream ArgAbi implementation signatures are unchanged. Keeping the old low-level shapes would require shared mutable cleanup ownership and would make it possible to free an allocation while a borrow from a cloned frame was still live.

Verification

Risk Evidence
External TOAST input crashes New test reproduced signal 11 on unmodified develop and passes with this commit
Fat-pointer metadata truncates the array Test iterates all 2,500 elements and checks the sum, not just the header
Fresh copies accumulate per scalar call 64 calls in one persistent context stay bounded; disabling cleanup accumulates 645,632 bytes and fails the test
Cleanup invalidates a borrowing SRF Borrowed iterator returns all 2,500 values; forcing early cleanup makes the test return a corrupt sum
Cleanup ownership is ambiguous Call frame and argument decoder now enforce one mutable owner; no Rc, RefCell, or other interior-mutability primitive is used
Ambient context changes select the wrong cleanup policy The wrapper passes the exact RetAbi context into the decoder and registration asserts it is current
Inline/nullable FlatArray behavior regresses Complete array_borrowed module: 22 passed
Broader backend behavior regresses PostgreSQL 18 pgrx-unit-tests library suite: 570 passed, 1 ignored
Core Rust/API behavior regresses pgrx tests: 19 unit tests and 72 doctests passed
PostgreSQL version compatibility pgrx compile checks passed with pg13 and pg19

Formatting and diff checks pass. A seven-technique deep review of the call graph, scalar/SRF traces, invariants, ownership, failure modes, and diff minimality has no open findings after tightening the context-selection and unsafe-pointer contracts.

The unfiltered workspace test command still has an unrelated trybuild snapshot mismatch in arrays-dont-leak.stderr under Rust 1.96, and strict Clippy is blocked by existing warnings in unchanged files. Neither failure points into this change.

@eeeebbbbrrrr
eeeebbbbrrrr marked this pull request as draft July 13, 2026 17:54
@eeeebbbbrrrr
eeeebbbbrrrr force-pushed the fix/toasted-flat-array-borrow branch from 6c391bd to 3703a43 Compare July 13, 2026 18:04
@eeeebbbbrrrr
eeeebbbbrrrr marked this pull request as ready for review July 13, 2026 18:07
@eeeebbbbrrrr
eeeebbbbrrrr force-pushed the fix/toasted-flat-array-borrow branch 2 times, most recently from e60a8e4 to 0a8732d Compare July 13, 2026 18:34
@eeeebbbbrrrr
eeeebbbbrrrr force-pushed the fix/toasted-flat-array-borrow branch from 0a8732d to c5281a6 Compare July 13, 2026 19:03
@workingjubilee

Copy link
Copy Markdown
Member

I designed the type specifically to avoid this kind of memory overhead for creating a referent.

@workingjubilee workingjubilee left a comment

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.

No.

Comment thread pgrx/src/datum/borrow.rs
/// released with `pg_sys::pfree`. Passing it transfers ownership, so the implementation must
/// not free it or register the caller-owned `ptr`.
#[doc(hidden)]
unsafe fn borrow_arg_unchecked<'dat>(

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.

This is entirely unsuitably named.

}
}

unsafe fn borrow_arg_unchecked<'dat>(

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.

ditto

Comment thread pgrx/src/callconv.rs
Comment on lines +647 to +659
fn register_detoasted_arg(&mut self, ptr: NonNull<u8>) {
// Set-returning functions unbox their arguments in a multi-call context so an iterator
// may retain borrows across calls. That context owns its detoast allocations and releases
// them when the SRF finishes. Ordinary functions unbox in the entry context, where we can
// release fresh detoast copies as soon as their result has been boxed.
assert_eq!(
unsafe { pg_sys::CurrentMemoryContext },
self.argument_memory_context,
"detoasted argument was allocated outside its selected memory context"
);
if self.argument_memory_context != self.entry_memory_context {
return;
}

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.

This is do-nothing assertion code that does not improve correctness.

Comment thread pgrx/src/callconv.rs
/// not cloneable.
pub struct FcInfo<'fcx> {
ptr: pgrx_pg_sys::FunctionCallInfo,
detoasted_arg_allocations: Vec<NonNull<u8>>,

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.

No, this is not appropriate. This type is intended to be 1 to 1 with FunctionCallInfo, such that we can transmute if necessary, and this breaks that.

Comment thread pgrx/src/callconv.rs
Comment on lines +605 to +612
impl Drop for FcInfo<'_> {
fn drop(&mut self) {
for allocation in self.detoasted_arg_allocations.drain(..) {
// SAFETY: `BorrowDatum::borrow_arg_unchecked` requires registered pointers to be
// PostgreSQL allocations that can be released after the function result is boxed.
unsafe { pg_sys::pfree(allocation.as_ptr().cast()) }
}
}

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.

No, this is not an appropriate way to handle this. It WILL introduce problems, because it means that we are terminating something before the expiry of 'fcx, which strictly outlives FcInfo per se, and the model for our code depends on not doing that.

@workingjubilee

Copy link
Copy Markdown
Member

Remember that your machine pet cannot perform logic so it cannot give a correct answer to a question, just a likely one.

@workingjubilee

workingjubilee commented Jul 15, 2026

Copy link
Copy Markdown
Member

Speaking purely observationally, it is also loathe, unless given instructions otherwise, to rewrite or redesign code because most code it is interacting with is working code. But that, of course, is a merely probabilistic statement.

@workingjubilee

workingjubilee commented Jul 15, 2026

Copy link
Copy Markdown
Member

If &T cannot be made without potentially allocating then it should probably simply not be permitted as an argument. Maybe that's too extreme, but I do not think this is the correct alternative.

@eeeebbbbrrrr

Copy link
Copy Markdown
Contributor Author

@workingjubilee Yeah, there was basically nothing about this that I liked which is why I didn't merge it.

I would like to fix the crash reported in the bug but I didn't see an obvious solution and my boy Claude made it things worse.

I appreciate you looking at this.

If &T cannot be made without potentially allocating then it should probably simply not be permitted as an argument. Maybe that's too extreme, but I do not think this is the correct alternative.

I don't know how we'd even do that. Any of postgres' varlena-style types could be toasted.

Part of the problem, I think is this:

unsafe impl<T: ?Sized> BorrowDatum for FlatArray<'_, T> {
    const PASS: layout::PassBy = layout::PassBy::Ref;
    unsafe fn point_from(ptr: ptr::NonNull<u8>) -> ptr::NonNull<Self> {

We can pass in any old pointer here but I guess what we need is a way to enforce that it's a "detoasted pointer"?

OTOH, the original bug report was about SPI doing things wrong, so maybe we need to just make sure the Datum is detoasted before FlatArray::point_from gets called?

(I won't merge this PR).

@eeeebbbbrrrr
eeeebbbbrrrr marked this pull request as draft July 15, 2026 17:02
@workingjubilee

Copy link
Copy Markdown
Member

cool! I do understand putting up the PR. It certainly at least attracted my notice. :P

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.

FlatArray<T> will SEGFAULT when referencing a Toasted array

2 participants