Skip to content

Features

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:

  1. ExecutorStart hook fires → context_walker walks the entire MemoryContext tree and records a snapshot: each context's name, parent pointer, and allocation stats.
  2. Extension code runs.
  3. ExecutorEnd hook fires → context_walker takes a second snapshot and diffs it against the first.
  4. Any context present in the post-query snapshot but absent from the pre-query snapshot is logged as a context_leak violation — provided its name matches the active ext_context_pattern (if set).
/* Example: buggy extension that leaks a context */
Datum
my_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_leak
context_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();

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), if totalAllocated grew since the pre-query snapshot, a wrong_ctx_alloc WARNING 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_alloc WARNING 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 */
Datum
my_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_alloc
severity: WARNING
detail: "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"]}'
);

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 +1 byte 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 by CACHELINEALIGN(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 scenario
SELECT ext_memcheck.run_scenario('shmem_sentinel_probe', 10, 'SELECT 1');
-- Check for overrun violations
SELECT * FROM ext_memcheck.violation_log WHERE check_type = 'shmem_overrun';
-- Reset the registry between test runs
SELECT ext_memcheck.clear_shmem_registry();

pg_ext_memcheck will report:

violation_type: shmem_overrun
severity: ERROR
detail: "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.


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 tracking
SELECT ext_memcheck.track_dsm_handle(1234);
-- Inspect currently tracked segments
SELECT * FROM ext_memcheck.dsm_tracking();
-- Run your test workload here, then check for leaks
SELECT * from ext_memcheck.end() WHERE check_type = 'dsm_leak';
-- Clear the tracking table between runs
SELECT ext_memcheck.clear_dsm_tracking();

DSM tracking table columns:

ColumnTypeDescription
segidbigintDSM handle (OS-level identifier)
backend_pidintPID of the backend that attached the segment
attach_attimestamptzWhen the segment was recorded
size_bytesbigintSegment size in bytes
detachedbooleanWhether a matching detach was observed

pg_ext_memcheck will report:

violation_type: dsm_leak
severity: WARNING
detail: "DSM segment handle ... was attached but not detached (size=... bytes)"

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 code
SELECT 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 code
SELECT ext_memcheck.run_scenario('oom_simulation', 1, 'SELECT 1');
SELECT ext_memcheck.flush_violations();

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:

  1. Checkpoint iterations are computed as powers of 10 up to iterations (e.g., 1 → 10 → 100 → 1000), capped at 8 checkpoints.
  2. At each checkpoint, context_walker snapshots every context in the MemoryContext tree and records used bytes (allocated − freed), keyed by (name, depth, parent hash).
  3. After all iterations finish, each context is tested: monotonic growth across ≥ 2 checkpoints, total growth ≥ bloat_min_bytes (default 8 KiB).
  4. 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:

LevelBase conditionEscalation
ERRORTotal growth > 1 MiBor WARNING + superlinear
WARNINGTotal growth > 64 KiBor INFO + superlinear
INFOTotal 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_bloat
severity: WARNING
detail: "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.