Fix five allocation and reference leaks, and make the cap that measures them work - #36
Open
sfc-gh-okalaci wants to merge 6 commits into
Open
Fix five allocation and reference leaks, and make the cap that measures them work#36sfc-gh-okalaci wants to merge 6 commits into
sfc-gh-okalaci wants to merge 6 commits into
Conversation
sfc-gh-okalaci
force-pushed
the
up/05-leaks
branch
from
August 18, 2026 06:58
fc25e43 to
9c0080d
Compare
sfc-gh-okalaci
force-pushed
the
up/04-lifetimes
branch
from
August 18, 2026 09:15
3d17657 to
8d18f4b
Compare
sfc-gh-okalaci
force-pushed
the
up/05-leaks
branch
from
August 18, 2026 09:15
9c0080d to
10aa254
Compare
sfc-gh-okalaci
force-pushed
the
up/04-lifetimes
branch
from
August 18, 2026 09:24
8d18f4b to
22f096b
Compare
sfc-gh-okalaci
force-pushed
the
up/05-leaks
branch
from
August 18, 2026 09:24
10aa254 to
c8028d0
Compare
pljs.memory_limit was read when the runtime was created and never again, so SET pljs.memory_limit changed the GUC without reaching the interpreter. Raising it did not allow more allocation and lowering it did not constrain any. An assign hook now applies the value to the live runtime. Lowering it below current usage does not reclaim anything -- the next allocation fails with a JavaScript "out of memory" instead, which is QuickJS's behaviour and worth knowing rather than hiding. Adds sql/pg_memory_limit_set.sql, which lowers the limit mid-session and asserts the new value takes effect, including that a rolled-back SET leaves the previous limit in force.
pljs_storage_for_context() and store_storage_in_context() each took references to the pljs namespace object and the global object and never released them. Both run on every context setup, so each one leaked two QuickJS references for the life of the context -- which in turn is what stops JS_FreeContext() from ever freeing that context. The global object keeps the storage alive across the release, so reading the storage afterwards is still safe; the comment at the site says so, because it is the obvious worry when reading the change. No regression test accompanies this. Each leak is a single reference per call on paths that a regression test would have to run tens of thousands of times to make visible against pljs.memory_limit, which is what the soak measurement is for rather than the suite.
pljs_execute() and plan.execute() build a one-shot SPI plan and a ParamListInfo per call and freed both only after a successful execute. Anything that raised in between -- a constraint violation, a type mismatch, a cancelled query -- leaked both, for every failing call, without bound. Both are now allocated in a short-lived child context that is deleted unconditionally, with SPI_freeplan() in a PG_CATCH. That is less code than freeing on each path and it cannot be got wrong later by adding a new early return. Note the plan's parserSetupArg points at a stack-local pljs_param_state, so the plan has to be freed before this frame exits; a comment at the site records that, because it looks like an ordering that could safely be relaxed.
JS_GetOwnPropertyNames() returns a table the caller owns, and each entry holds an atom reference that must be released as well. Object key enumeration dropped both on the floor, so every conversion of a JavaScript object to a record leaked the table and one atom per key. The leak is on the QuickJS heap, which pg_backend_memory_contexts cannot see at all, so it does not show up in the usual place one would look. Adds sql/pg_object_keys_leak.sql. It measures against a low pljs.memory_limit rather than PostgreSQL's memory accounting, because that is the only SQL-visible instrument that counts QuickJS allocations; with the release sites removed the file fails, so it discriminates.
pljs_jsvalue_to_datums() and the column loop in pljs_jsvalue_to_record() fetch each column's value with JS_GetPropertyStr() and never release it -- on the null path or the conversion path. That is one leaked QuickJS reference per column, per row. For a RETURNS TABLE function this is the hottest path in the extension, so it is also the leak most likely to matter: a query returning ten columns over a hundred thousand rows leaks a million references. Adds sql/pg_record_column_leak.sql, which returns enough rows under a low pljs.memory_limit that the unfixed code fails with "out of memory" rather than merely growing.
pljs_execute_params() freed its plan and parameter arrays after the execute returned, so a raising query leaked them. The arrays are palloc'd and the plan is a saved SPI plan, so the leak was per failing call and unbounded in a long-lived backend. The allocation now lives in a child context deleted on both paths, with SPI_freeplan() in a PG_CATCH. Adds sql/pg_param_plan_error_leak.sql, which runs a parameterised execute that raises many times and asserts memory does not trend upwards.
sfc-gh-okalaci
force-pushed
the
up/04-lifetimes
branch
from
August 18, 2026 09:29
22f096b to
2340189
Compare
sfc-gh-okalaci
force-pushed
the
up/05-leaks
branch
from
August 18, 2026 09:29
c8028d0 to
8dcf76a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
None of these is visible in a single call; all of them accumulate in a long-lived
backend.
The first commit is what makes the rest testable.
pljs.memory_limitwas read once, atlibrary load, so
SET pljs.memory_limit = 64in a session changed the GUC without everreaching the interpreter — the runtime kept the 512MB default. A leak test that sets a
low cap and expects
out of memorytherefore passed whether or not the leak was fixed.Applying the value to the live runtime on
SETis a fix in its own right, and it isplaced first here because the two heap-leak tests below assert against it.
The one that matters most is a leaked QuickJS reference per column, per row, in the
composite conversion loops:
JS_GetPropertyStr()'s result was never released on eitherthe null path or the conversion path. That is the hottest path in the extension for a
RETURNS TABLEfunction — its test fails without of memorywithout the fix, underthe 64MB cap the first commit makes effective. It asserts against
pljs.memory_limitrather than
pg_backend_memory_contextsdeliberately: QuickJS allocates on the libcheap, so that view cannot see a leaked JavaScript reference at all, and a test built on
it would pass either way.
The rest: the SPI plan and parameter list allocated per iteration of
pljs.execute()/plan.execute()were freed only on success, so a raising query leakedboth — a short-lived child context deleted unconditionally replaces the per-path frees;
the property-name table from
JS_GetOwnPropertyNames()and its atoms were dropped onthe floor during object key enumeration; two JavaScript references were taken and never
released in the storage helpers.
Commits
Re-apply pljs.memory_limit to the live runtime on SETDrop leaked JavaScript references in the storage helpersFree the per-iteration SPI plan and parameter listFree the property-name table from JS_GetOwnPropertyNamesRelease the per-column JSValue in the composite conversion loopsFree the SPI plan and parameters when a parameterised execute raisesEvery commit in this series builds from clean and passes the full ordered suite on its
own, verified per commit on PostgreSQL 17. The tip is additionally green on PostgreSQL
16, 17, 18 and 19beta3 — the versions this repository's CI matrix builds — with
pljs.memory_limit=64, and under AddressSanitizer with no reports.