Testing a Leaky Extension
This page shows how to use pg_ext_memcheck to test an extension with known memory leaks or wrong-context allocations. It also explains how to interpret the results and severity levels.
For this example, we will using a buggy extension that allocates memory but forgets to free it, and also allocates in the wrong context. You can find the source code for this extension at: buggy-pg-ext.
What buggy-pg-ext does:
- Allocates
8KBfor each query in a child context under 'MessageContext' in the Planner Hook but never frees it → should trigger acontext_leakviolation. - Allocates
8KBdirectly inTopMemoryContextin the Executor Hook for each query → should trigger awrong_ctx_allocviolation. - It only allocates and returns. No other logic is present, so it won't cause any functional test failures — just memory leaks.
Setting up the test
Section titled “Setting up the test”- Install the buggy extension in your PostgreSQL instance from buggy-pg-ext.
- Load pg_ext_memcheck in
shared_preload_librariesand restart PostgreSQL. - Enable the extension in your test session and set
pg_ext_memcheck.memcheck_modetoallto capture both planner and executor phase violations.
CREATE EXTENSION buggy_pg_ext;CREATE EXTENSION pg_ext_memcheck;SET pg_ext_memcheck.memcheck_mode = 'all';Running the test
Section titled “Running the test”Run a simple query that invokes the buggy extension's hooks:
-- Any SELECT will do, as the extension's hooks run for every querySELECT count(*) FROM companies;
SELECT * FROM ext_memcheck.flush_violations(); -- flushes the ring buffer into the violation_log tableSELECT * FROM ext_memcheck.violation_log; -- check the persisted violation logResults and interpretation
Section titled “Results and interpretation”After running the query, check the violation log: The columns:
- id — auto-incrementing violation ID
- ts — timestamp of the violation
- backend_pid — PID of the backend that detected the violation
- check_type — type of the check that detected the violation (e.g., context_leak, wrong_ctx_alloc)
- severity — severity of the violation (e.g., ERROR, WARNING, INFO)
- detail — human-readable description of the violation
- source_lib — the library that triggered the violation (e.g., buggy_pg_ext)
22 | 2026-05-15 21:35:00.437495+05:30 | 62651 | context_leak | INFO | context 'TopMemoryContext' (depth 0): used grew by 9392 bytes (before=381800 after=391192); allocated before=589216 after=589216 | buggy_pg_ext.dylib 23 | 2026-05-15 21:35:00.437497+05:30 | 62651 | context_leak | INFO | context 'MessageContext' (depth 1): used grew by 30168 bytes (before=8384 after=38552); allocated before=16384 after=55360 | buggy_pg_ext.dylib 24 | 2026-05-15 21:35:00.437498+05:30 | 62651 | context_leak | INFO | context 'BuggyPlannerLeakCtx' (depth 2): used grew by 8488 bytes (before=0 after=8488); allocated before=0 after=24576 | buggy_pg_ext.dylibIn this example, we see three context_leak violations:
TopMemoryContextgrew by9392 bytes→ this is thewrong_ctx_allocviolation from the Executor Hook, but since it's a leak inTopMemoryContext, it is reported as acontext_leakwith severityINFObecause the leak size is less than64KB.MessageContextgrew by30168 bytes→ this is the leak from the Planner Hook, reported ascontext_leakwith severityINFO.BuggyPlannerLeakCtxgrew by8488 bytes→ this is the child context underMessageContextwhere the leak actually happens, also reported ascontext_leakwith severityINFO.
Scenario - growth_benchmark
Section titled “Scenario - growth_benchmark”You can also run a more extensive scenario that executes the same query in a loop to see how the leaks accumulate over time:
SET pg_ext_memcheck.memcheck_mode = 'all';SELECT ext_memcheck.begin('');SELECT ext_memcheck.run_scenario(scenario_name := 'growth_benchmark', iterations := 1000, workload := 'SELECT count(*) FROM companies;');SELECT * FROM ext_memcheck.end();Output
Section titled “Output”This will run the same query 1000 times, and you should see the leak sizes grow across iterations in the violation log. The severity may escalate to WARNING or ERROR if the leak size crosses the defined thresholds. Subset of expected output:
context_leak | ERROR | context 'TopMemoryContext' (depth 0): used grew by 8208448 bytes (before=743888 after=8952336); allocated before=1113504 after=16842144 | 2026-05-15 21:45:45.147019+05:30 | buggy_pg_ext.dylib wrong_ctx_alloc | WARNING | context 'TopMemoryContext' (depth 0): allocated grew by 15728640 bytes in a known global context (before=1113504 after=16842144); free before=378864 after=7889808 | 2026-05-15 21:45:45.148388+05:30 | buggy_pg_ext.dylib context_leak | INFO | context 'BuggyPlannerLeakCtx' (depth 2): used grew by 8488 bytes (before=0 after=8488); allocated before=0 after=24576 | 2026-05-15 21:45:45.148386+05:30 | buggy_pg_ext.dylib context_leak | WARNING | context 'ExprContext' (depth 4): used grew by 198840 bytes (before=4480 after=203320); allocated before=8192 after=227456 | 2026-05-15 21:45:45.147417+05:30 | buggy_pg_ext.dylib context_leak | INFO | context 'MessageContext' (depth 1): used grew by 26632 bytes (before=8832 after=35464); allocated before=24576 after=63552 | 2026-05-15 21:45:45.147987+05:30 | buggy_pg_ext.dylibInterpreting the growth_benchmark output
Section titled “Interpreting the growth_benchmark output”After 1000 iterations the accumulated leaks cross multiple severity thresholds. Here is what each row tells you:
-
context_leak ERROR—TopMemoryContextgrew by ~8 MB
Bug 2 in action at scale. Each iteration'sExecutorStarthook palloced 8 KB directly intoTopMemoryContext. After 1000 iterations that is ~8 MB of permanently retained memory, pushing the severity toERROR(>1 MiB threshold). Thebefore=0for allocated in earlier single-query runs becomes significant accumulated growth here. -
wrong_ctx_alloc WARNING—TopMemoryContextblock growth ~15 MB
The sameTopMemoryContextleak is also caught by the wrong-context checker, which fires independently on any allocation growth inside a known global context. The largerallocateddelta (~15 MB vs ~8 MB used) reflects PostgreSQL requesting new OS-level blocks to accommodate the steady stream of pallocs. -
context_leak INFO—BuggyPlannerLeakCtxgrew by 8488 bytes
Bug 1 — the planner hook creates a child context underMessageContextand allocates 8 KB into it. This single entry represents the last iteration's context, which was not yet reset. Earlier iterations' contexts were freed whenMessageContextwas reset at the end of each client message cycle during the scenario loop. -
context_leak WARNING—ExprContextgrew by ~194 KB
An internal PostgreSQL expression evaluation context that accumulated state across the 1000 iterations ofSELECT count(*) FROM companies. This is not a bug in the extension — it is normal PostgreSQL behavior under repeated execution within a single session. It surfaces here because the before-snapshot was taken before the scenario loop started and the after-snapshot is taken atend(). -
context_leak INFO—MessageContextgrew by ~26 KB
The parent context ofBuggyPlannerLeakCtx. Its growth reflects the block overhead of hosting the child contexts created by Bug 1 across iterations, plus normal query message bookkeeping.
Severity thresholds recap
Section titled “Severity thresholds recap”| Threshold | Severity |
|---|---|
| > 1 MiB net used growth | ERROR |
| > 64 KiB net used growth | WARNING |
≥ min_leak_bytes (default 8 KiB) | INFO |
| Below threshold | silently skipped |
- The
TopMemoryContextleak has grown to8.2MBafter 1000 iterations, which is now anERROR. - The
wrong_ctx_allocviolation always aWARNINGto indicate a potential issue with the extension's allocation strategy, independent of the actual leak size. - The
BuggyPlannerLeakCtxremains anINFObecause it is still under64KB, but you can see how it grows across iterations.
More testing options
Section titled “More testing options”Now you can try with memcheck_mode set to executor to only capture executor-phase violations:
SET pg_ext_memcheck.memcheck_mode = 'executor';Or disable the automatic hooks entirely and use the manual begin() / end() API for fine-grained control over the test window. Pass a context-name pattern to scope reporting to your extension's own contexts and eliminate noise from PostgreSQL internals:
SET pg_ext_memcheck.memcheck_mode = 'all';-- Only report contexts whose names start with 'Buggy'SELECT ext_memcheck.begin('Buggy%');SELECT count(*) FROM companies;SELECT * FROM ext_memcheck.end();If your extension intentionally allocates in TopMemoryContext (for a cross-query cache, for example), suppress the resulting wrong_ctx_alloc violations with an allowlist:
SELECT ext_memcheck.begin( 'Buggy%', '{"allowed_contexts": ["TopMemoryContext"]}');You can also test with other workloads or scenarios like tx_abort_loop, and inspect the violation log to understand the memory behavior of your extension under different conditions.
The source_lib column is particularly useful to confirm that the violations are indeed coming from your extension (e.g., buggy_pg_ext.dylib) and not from PostgreSQL internals or other extensions.