Features
MemoryContext leak detection
Section titled “MemoryContext leak detection”PostgreSQL allocates memory through a tree of MemoryContext nodes. When extension code creates a child context to hold query-duration data, it must free that context before returning. If it doesn't, the context persists — silently growing with each invocation.
How it works:
ExecutorStarthook fires →context_walkerwalks the entireMemoryContexttree and records a snapshot: each context's name, parent pointer, and allocation stats.- Extension code runs.
ExecutorEndhook fires →context_walkertakes a second snapshot and diffs it against the first.- Any context present in the post-query snapshot but absent from the pre-query snapshot is logged as a
context_leakviolation — provided its name matches the activeext_context_pattern(if set).
/* Example: buggy extension that leaks a context */Datummy_func(PG_FUNCTION_ARGS){ MemoryContext ctx = AllocSetContextCreate( CurrentMemoryContext, "MyWorkCtx", ALLOCSET_DEFAULT_SIZES ); /* ... work happens here ... */ /* BUG: MemoryContextDelete(ctx) is missing */ PG_RETURN_NULL();}pg_ext_memcheck will report:
violation_type: context_leakcontext_name: "MyWorkCtx"detail: "Context present in post-query snapshot but absent pre-query"Scoping to your extension: pass a SQL LIKE pattern to begin() so that only contexts whose names match are checked. This eliminates false positives from PostgreSQL core contexts:
SET pg_ext_memcheck.memcheck_mode = 'executor';SELECT ext_memcheck.begin('MyWorkCtx%');SELECT my_extension.some_function();SELECT * FROM ext_memcheck.end();Wrong-context allocation detection
Section titled “Wrong-context allocation detection”Allocating in TopMemoryContext or CacheMemoryContext from a query-handler is almost always a bug. These contexts live for the entire backend lifetime; any allocation that lands there will never be freed until the backend exits.
How it works: check_wrong_context_alloc() in memcheck_hooks.c runs two passes over the before/after context-tree snapshots:
- Pass 1 — growth in global contexts: For every context whose name matches a known global (
TopMemoryContext,CacheMemoryContext), iftotalAllocatedgrew since the pre-query snapshot, awrong_ctx_allocWARNING is emitted with the delta and absolute sizes. - Pass 2 — new child contexts under globals: If a context appears in the post-query snapshot but not the pre-query snapshot, and its parent is a known global context, a
wrong_ctx_allocWARNING is emitted identifying the newly created child.
Both passes respect the allowed_contexts allowlist: any context (or parent context) listed in the allowlist is skipped. This lets you suppress false positives for extensions that intentionally cache data in long-lived contexts across queries.
/* Example: buggy extension allocating in the wrong context */Datummy_func(PG_FUNCTION_ARGS){ MemoryContext old = MemoryContextSwitchTo(TopMemoryContext); char *buf = palloc(1024); /* BUG: lands in TopMemoryContext */ MemoryContextSwitchTo(old); PG_RETURN_NULL();}pg_ext_memcheck will report:
violation_type: wrong_ctx_allocseverity: WARNINGdetail: "context 'TopMemoryContext' (depth 0): allocated grew by 1024 bytes in a known global context (before=... after=...)"To silence this for a context your extension uses intentionally, add it to the allowlist:
SET pg_ext_memcheck.memcheck_mode = 'all';SELECT ext_memcheck.begin( 'MyExtCtx%', '{"allowed_contexts": ["TopMemoryContext"]}');Shmem boundary sentinel probing
Section titled “Shmem boundary sentinel probing”Extensions that use ShmemAlloc() are responsible for staying within their allocated bounds. Overruns corrupt adjacent shared memory structures silently — no segfault, just data corruption.
How it works: shmem_probe.c maintains a ProbeRegistry in shared memory. For each tracked segment, probe_register(seg_name, alloc_size, data_end) looks up the existing ShmemInitStruct allocation by its exact size and writes a sentinel byte (0xDE) at base_ptr[data_end]. After a workload runs, probe_check_all() re-reads each sentinel. A mismatch logs a shmem_overrun violation.
Two sentinel placement strategies are used:
- Own segments (allocated with
+1byte in_PG_init):data_end = sizeof(struct)— the sentinel is placed in the reserved guard byte. - External segments (no extra allocation):
data_end = alloc_size— the sentinel uses the alignment slack guaranteed byCACHELINEALIGN(alloc_size).
To probe your own extension's shared memory segment, call ext_memcheck.register_shmem_probe(seg_name, allocated_size) before running the scenario.
-- Run the built-in sentinel probe scenarioSELECT ext_memcheck.run_scenario('shmem_sentinel_probe', 10, 'SELECT 1');
-- Check for overrun violationsSELECT * FROM ext_memcheck.violation_log WHERE check_type = 'shmem_overrun';
-- Reset the registry between test runsSELECT ext_memcheck.clear_shmem_registry();pg_ext_memcheck will report:
violation_type: shmem_overrunseverity: ERRORdetail: "Sentinel byte overwritten past 'pg_ext_memcheck ViolationLog' (declared_size=...)"Registered segments: ViolationLog and DsmTrackerState are registered automatically by the shmem_sentinel_probe scenario. Their ShmemInitStruct allocations are made with +1 byte in _PG_init so the sentinel location is always safe to write.
DSM segment lifecycle tracking
Section titled “DSM segment lifecycle tracking”Dynamic Shared Memory (DSM) segments must be explicitly detached when no longer needed. Leaked segments persist until the postmaster exits, consuming shared memory pool capacity.
How it works: dsm_tracker.c maintains a DsmTrackerState in shared memory — a fixed-size ring of DsmSegmentRecord entries protected by an LWLock. DSM tracking is manual-only: ext_memcheck.track_dsm_handle() records a handle via dsm_tracker_record_handle_observe(), temporarily attaching to read the segment size and then detaching immediately. Live/detached status is determined at query time by probing the handle with dsm_attach. At the end of a test window (ext_memcheck.end()), dsm_tracker_check_leaks() scans all recorded segments and logs any that are still reachable as dsm_leak violations.
-- Manually register a DSM handle for lifecycle trackingSELECT ext_memcheck.track_dsm_handle(1234);
-- Inspect currently tracked segmentsSELECT * FROM ext_memcheck.dsm_tracking();
-- Run your test workload here, then check for leaksSELECT * from ext_memcheck.end() WHERE check_type = 'dsm_leak';
-- Clear the tracking table between runsSELECT ext_memcheck.clear_dsm_tracking();DSM tracking table columns:
| Column | Type | Description |
|---|---|---|
segid | bigint | DSM handle (OS-level identifier) |
backend_pid | int | PID of the backend that attached the segment |
attach_at | timestamptz | When the segment was recorded |
size_bytes | bigint | Segment size in bytes |
detached | boolean | Whether a matching detach was observed |
pg_ext_memcheck will report:
violation_type: dsm_leakseverity: WARNINGdetail: "DSM segment handle ... was attached but not detached (size=... bytes)"Use-after-reset / OOM simulation
Section titled “Use-after-reset / OOM simulation”These scenarios run in a crash-isolated BGWorker process so SIGSEGV or OOM cannot kill the calling session.
use_after_reset — Runs the crash scenario inside a BGWorker that calls elog(FATAL) to simulate a use-after-reset crash. The worker exits with a non-zero exit code; the calling backend detects this via WorkerSlot.exit_code and logs the result. Verifies that the crash-isolation infrastructure works correctly before you run your own crash-inducing code.
oom_simulation — Allocates 1 MiB chunks via palloc_extended(MCXT_ALLOC_NO_OOM) inside a BGWorker until the allocator returns NULL (or 256 MiB are consumed), then exits with elog(FATAL). Confirms that an extension's OOM behavior is crash-detected in isolation without affecting the test session.
-- Run use-after-reset in a BGWorker; detects crash via non-zero exit codeSELECT ext_memcheck.run_scenario('use_after_reset', 1, 'SELECT 1');SELECT ext_memcheck.flush_violations();
-- Allocate until OOM in a BGWorker; detects crash via non-zero exit codeSELECT ext_memcheck.run_scenario('oom_simulation', 1, 'SELECT 1');SELECT ext_memcheck.flush_violations();Context growth measurement (ctx_bloat)
Section titled “Context growth measurement (ctx_bloat)”Measures per-MemoryContext used bytes at log-spaced checkpoints across repeated invocations. A context that grows monotonically and exceeds the configured threshold is emitted as a ctx_bloat violation — distinct from the per-query context_leak violation type.
How it works:
- Checkpoint iterations are computed as powers of 10 up to
iterations(e.g., 1 → 10 → 100 → 1000), capped at 8 checkpoints. - At each checkpoint,
context_walkersnapshots every context in theMemoryContexttree and records used bytes (allocated − freed), keyed by (name, depth, parent hash). - After all iterations finish, each context is tested: monotonic growth across ≥ 2 checkpoints, total growth ≥
bloat_min_bytes(default 8 KiB). - Growth shape is classified as linear or superlinear (late-interval rate > 1.5× early-interval rate). Superlinear growth bumps the severity one level.
Severity levels:
| Level | Base condition | Escalation |
|---|---|---|
ERROR | Total growth > 1 MiB | or WARNING + superlinear |
WARNING | Total growth > 64 KiB | or INFO + superlinear |
INFO | Total growth ≥ bloat_min_bytes | — |
SELECT ext_memcheck.run_scenario('growth_benchmark', 500, 'SELECT your_ext.fn()');SELECT ext_memcheck.flush_violations();SELECT check_type, severity, detail FROM ext_memcheck.violation_log WHERE check_type = 'ctx_bloat';pg_ext_memcheck will report:
check_type: ctx_bloatseverity: WARNINGdetail: "context 'MyWorkCtx' (depth 3): linear bloat over 100 iters, used 0->73728 bytes (+73728); rate early=1.0 late=1.0 B/iter"Tuning: Adjust pg_ext_memcheck.bloat_min_bytes to control the noise floor. Set it lower for maximum sensitivity; raise it to suppress expected background growth.