Coding & Refactoringmedium risk

debugging

Debug a running PostgreSQL backend with lldb / gdb — covers attaching to the right forked backend via pg_backend_pid, single-user mode for postmaster / InitPostgres startup paths, breakpoints in ExecInitNode / heap_update / LWLockAcquire, elog(LOG, ...) printf-debugging, macOS / Linux core-dump configuration, and SQL-level inspection via pg_buffercache / pageinspect / pg_visibility. Use whenever the user wants to step through backend C code, attach lldb / gdb to a specific dev-cluster backend, chase a SIGSEGV / hang / loop / assertion-failure in PG, instrument with elog, inspect shmem / buffer / lock state at runtime, or read a PG core dump. Skip for app-level debugging in Node.js / Python / Ruby / Go / Rust (use language-specific debuggers), JVM hprof / jstack, browser DevTools, Chrome Tracing, application performance profiling, and production PG tuning (autovacuum / shared_buffers — that's DBA work).

matejformanek/postgres-claude·.claude/skills/debugging/SKILL.md
85/ 100Quality

Install this skill

Choose your coding agent and copy a project or personal installation command.

Pinned to the indexed commit
Project installation.agents/skills/debugging
npx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/debugging -a codex -y
Personal installation~/.agents/skills/debugging
npx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/debugging -a codex -g -y
Manual folder.agents/skills/debuggingOfficial docs ↗
Project installation.claude/skills/debugging
npx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/debugging -a claude-code -y
Personal installation~/.claude/skills/debugging
npx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/debugging -a claude-code -g -y
Manual folder.claude/skills/debuggingOfficial docs ↗
Project installation.agents/skills/debugging
npx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/debugging -a github-copilot -y
Personal installation~/.copilot/skills/debugging
npx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/debugging -a github-copilot -g -y
Manual folder.agents/skills/debuggingOfficial docs ↗
Project installation.agents/skills/debugging
npx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/debugging -a cursor -y
Personal installation~/.cursor/skills/debugging
npx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/debugging -a cursor -g -y
Manual folder.agents/skills/debuggingOfficial docs ↗
Project installation.agents/skills/debugging
npx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/debugging -a gemini-cli -y
Personal installation~/.gemini/skills/debugging
npx skills add https://github.com/matejformanek/postgres-claude/tree/eb838af606a4c3c5a2efc4302e49d85a39c2fd44/.claude/skills/debugging -a gemini-cli -g -y
Native Gemini CLIgemini skills install https://github.com/matejformanek/postgres-claude.git --scope workspace --path .claude/skills/debugging
Manual folder.agents/skills/debuggingOfficial docs ↗
⚠ Installation uses the open-source skills CLI. Inspect the source and permissions before running the command.

Skill instructions

View source on GitHub ↗
# debugging

PostgreSQL is a **multi-process** server, not threaded: the postmaster
forks a fresh backend per connection. That single fact dictates the whole
debugging workflow. [verified-by-code; see `source/src/backend/postmaster/postmaster.c`,
and `.claude/skills/build-and-run/SKILL.md` line 59–63]

## 1. Build first

Debugging only makes sense against a debug build. See
`.claude/skills/build-and-run/SKILL.md` — `--buildtype=debug -Dcassert=true`.
`debug` (not `debugoptimized`) keeps locals and inline frames readable.
[verified-by-code; build-and-run/SKILL.md line 33]

The upstream FAQ recommends `CFLAGS="-ggdb -Og -g3 -fno-omit-frame-pointer"`
for autoconf builds — `-Og` keeps reasonable perf while preserving frames.
[from-wiki: Developer_FAQ]

## 2. The attach pattern (most common workflow)

**Project shortcuts.** In this repo, `/pg-attach` automates the held-PID
grab and prints the exact `lldb -p <pid>` line ready to paste; `/pg-tail-log`
follows `dev/data-debug/server.log` (where `pprint` and `elog` output land).
Use them.

The postmaster itself never executes your query. Attach to the **backend**
that handles your session. The key footgun: a one-shot
`psql -tAc "SELECT pg_backend_pid()"` closes the connection immediately,
so the backend you "got the PID of" is already gone by the time you read
the output. You need a **held** backend — one whose connection stays open
long enough for you to attach.

### The held-PID pattern (works from a script)

Open a backend that holds its connection for N seconds via `pg_sleep`, tag
it with a recognizable `application_name`, then look the PID up via
`pg_stat_activity`:

```bash
# Hold a backend open for 60s, tagged so we can find it.
PGAPPNAME=hold psql -h /tmp -d postgres -X -c 'SELECT pg_sleep(60);' &

# Look up its backend PID (give it a moment to register).
sleep 0.5
PID=$(psql -h /tmp -d postgres -tAc \
  "SELECT pid FROM pg_stat_activity WHERE application_name='hold'")
echo "$PID"

# Attach in a separate terminal:
lldb -p "$PID"
```

`PGAPPNAME` is a libpq env var (NOT a psql `\set` variable — that won't
propagate to the server). Inside the lldb session, the backend will be
parked inside `pg_sleep`'s `WaitLatch` loop, ready for breakpoints.

### The interactive variant

When you actually want to drive queries through the attached session, hold
psql open interactively instead:

```sql
-- in psql, on the connection you want to debug:
SELECT pg_backend_pid();
 pg_backend_pid
----------------
          54321
```

Keep that psql session alive while you attach in another terminal:

Then in another terminal:

```bash
# Linux / where gdb works
gdb -p 54321

# macOS — lldb is the path of least resistance
lldb -p 54321
```

Why lldb on macOS: gdb on Darwin needs a code-signed binary with the
`com.apple.security.cs.debugger` entitlement; lldb ships with Xcode
Command Line Tools and works out of the box. [from-wiki: Developer_FAQ
implies gdb-or-lldb; macOS codesigning requirement is well-known toolchain
quirk — `[inferred]` for the entitlement specifics]

### Translating common gdb commands to lldb

| gdb                          | lldb                          |
| ---------------------------- | ----------------------------- |
| `b ExecutorRun`              | `b ExecutorRun`               |
| `p MyProcPid`                | `p MyProcPid`                 |
| `bt`                         | `bt`                          |
| `call pprint(node)`          | `expr pprint(node)`           |
| `handle SIGUSR1 noprint pass`| `pro hand -p true -s false SIGUSR1` |

### First command after every attach

PostgreSQL uses `SIGUSR1` heavily for latch wakeups. **Silence it before
you do anything else**, or every `continue` lands back in the signal
handler:

```
(lldb) pro hand -p true -s false SIGUSR1
(gdb)  handle SIGUSR1 noprint pass
```

[from-wiki: Developer_FAQ]

## 3. Stepping the startup path — single-user mode

**Decision rule — which tool for which startup phase:**

| Symptom                                                                | Tool                                  |
| ---------------------------------------------------------------------- | ------------------------------------- |
| Code runs *before* shared memory is attached / inside `InitPostgres`   | `postgres --single` (this section)    |
| Code runs in the forked-backend startup path (post-shmem, pre-query)   | `PGOPTIONS="-W N" psql ...` (§4)      |
| Code runs in a worker (autovacuum / parallel / bgworker)               | spin-loop waitpoint (§4)              |
| Code runs during a query you can drive from psql                       | normal `lldb -p` attach (§2)          |

Attaching to a forked backend can't help with code that runs *before* the
backend is ready to accept queries (auth, shmem init, startup hooks).
Use `postgres --single` for that. No postmaster, no fork — the debugger
sees the whole lifetime in one process.

```bash
gdb --args postgres --single -D "$PGDATA" postgres
# or
lldb -- postgres --single -D "$PGDATA" postgres
```

[verified-by-code; `source/doc/src/sgml/ref/postgres-ref.sgml` line 714+:
"To start a single-user mode server, use a command like
`postgres --single -D /usr/local/pgsql/data other-options my_database`"]

In single-user mode newline terminates a command (not semicolon).
Use `-j` to switch to `;\n\n` mode if you're pasting psql-style scripts.
[verified-by-code; postgres-ref.sgml around line 730]

## 4. Catching things that happen too fast

For code that runs before you can grab the PID (e.g. early backend
startup, parallel workers, autovacuum workers):

- `PGOPTIONS="-W 10" psql ...` — the backend sleeps 10 seconds at startup
  so you have time to `pg_backend_pid()` and attach. [from-wiki: Developer_FAQ]
- For short-lived background workers, insert a temporary spin-loop:
  ```c
  bool wait = true;
  elog(LOG, "waiting for debugger, pid=%d", MyProcPid);
  while (wait) pg_usleep(1000000L);
  ```
  Attach, then `set var wait = 0` to release. [from-wiki: Developer_FAQ
  — pattern shown there with `sleep()`; using `pg_usleep` is the
  PG-idiomatic equivalent, `[inferred]`]
- Some test paths already sleep deliberately. Example: WAL recovery has
  `pg_usleep(10000L); /* wait for 10 msec */` retry loops.
  [verified-by-code; `source/src/backend/access/transam/xlog.c:7707, 7724`]
- True waitpoint infrastructure exists for tests — see
  `source/src/test/authentication/t/007_pre_auth.pl` line 38: "Connect
  to the server and inject a waitpoint." Waitpoints let TAP tests pause
  the backend at a labeled location. [verified-by-code]

## 5. Useful breakpoints

Where to break depends on what you're chasing:

| Goal                           | Breakpoint                                          |
| ------------------------------ | --------------------------------------------------- |
| Any incoming SQL               | `exec_simple_query` (`src/backend/tcop/postgres.c`) |
| Plan execution                 | `ExecutorStart`, `ExecutorRun`, `ExecutorEnd`       |
| All errors (`ereport`/`elog`)  | `errfinish` (filter on `edata->elevel >= ERROR`)    |
| Buffer pin/lookup              | `BufferAlloc`, `ReadBuffer_common`                  |
| Lock acquisition               | `LockAcquire`, `LockAcquireExtended`                |
| Transaction boundaries         | `StartTransaction`, `CommitTransaction`             |
| Heap insert/update             | `heap_insert`, `heap_update`                        |
| WAL insert                     | `XLogInsert`                                        |

[`errfinish` for trapping errors: from-wiki Developer_FAQ.
The rest: verified-by-code, names exist as symbols in
`source/src/backend/{tcop,executor,storage/buffer,storage/lmgr,access/transam,access/heap}/`.]

### 5.1 Trapping every error (the universal ereport breakpoint)

For SIGSEGV chases or "I don't know which `ereport()` is firing", break on
`errfinish` — it's the single funnel every `ereport`/`elog` call goes
through. Filter on `elevel >= ERROR` (= 21) so you don't stop on every
LOG/NOTICE:

```
(lldb) b errfinish
(lldb) br mod -c 'edata->elevel >= 21'   # ERROR = 21
(gdb)  b errfinish
(gdb)  cond <n> edata->elevel >= 21
```

[`ERROR = 21` verified-by-code: `source/src/include/utils/elog.h:53`.]

## 6. Pretty-printing PG internals

Postgres ships `pprint()` for `Node *` trees (parse trees, plans).
Call it from the debugger:

```
(gdb) call pprint(parsetree)
(lldb) expr pprint(parsetree)
```

Output goes to the backend's stderr (the server log, typically).
[from-wiki: Developer_FAQ]

Memory inspection without stopping execution:

```
(gdb) p MemoryContextStats(TopMemoryContext)
```

Or, from SQL on PG 14+: `SELECT * FROM pg_backend_memory_contexts;`.
[from-wiki: Developer_FAQ; pg_backend_memory_contexts: verified-by-code,
view exists.]

There is **no `gdbpg.py`** in the current source tree (searched
`source/src/tools/` — not present). Community pretty-printers exist on
GitHub but are not bundled. [verified-by-code: `find` returned zero
matches under `source/`.]

## 7. `elog` / `ereport` instrumentation pattern

For "I want to know which path got taken" rather than stepping:

```c
elog(DEBUG1, "BufferAlloc: blkno=%u, hit=%d", blkno, found);
```

Then start the server with `log_min_messages = debug1` (or higher number
through `debug5`). See `.claude/skills/build-and-run/SKILL.md` line 78
for the GUC name. [verified-by-code; `ereport`/`elog` are the canonical
logging macros throughout the backend — see
`.claude/skills/error-handling/` for full conventions.]

For ad-hoc tracing, `fprintf(stderr, ...)` works in `--single` mode and
goes straight to the controlling terminal. Don't ship that.

## 8. Core dumps on macOS

### Prerequisites

```bash
# 1. The shell that launches postgres must allow cores.
ulimit -c unlimited
# expected (after setting): "unlimited"

# 2. /cores must exist and be writable.
ls -ld /cores
# expected: drwxrwxrwt  root  wheel   (sticky, world-writable)
sudo chmod 1777 /cores    # only if the perms above don't match

# 3. Verify kernel allows cores.
sysctl kern.coredump
# expected: kern.coredump: 1
```

Then crash the backend and open the core:

```bash
lldb /path/to/postgres /cores/core.<pid>
```

### Caveats (macOS-specific)

- **Apple-signed / hardened binaries suppress cores** regardless of
  `ulimit`. Our locally-built `postgres` is unsigned, so this isn't a
  concern here — but if you ever try to core-dump `/usr/bin/postgres` or a
  Homebrew bottle, this is why nothing lands in `/cores`.
- **No `kern.coredump` sysctl on your machine?** If `sysctl kern.coredump`
  returns `unknown oid`, your macOS version controls cores via `ulimit`
  alone — steps 1 and 2 are sufficient.

[`ulimit -c unlimited` and `/cores` location: standard Darwin defaults
documented in `man 5 core` on macOS. `kern.coredump` sysctl and
hardened-binary suppression: `[inferred]` from well-known Darwin behavior.]

## 9. SQL-level inspection extensions

When you want to look at shared state without a debugger attach, the
contrib extensions are the right tool:

- **`pg_buffercache`** — one row per shared buffer with relfilenode,
  block number, usage count, dirty flag, pin count. Now also exposes
  `pg_buffercache_evict()` and `pg_buffercache_numa` (NUMA-page mapping).
  [verified-by-code; `source/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql`,
  `pg_buffercache_pages.c`]
- **`pageinspect`** — raw page contents (heap tuples, btree pages, GIN,
  BRIN). For "why is this page corrupt" or "what's the actual lp_off of
  this line pointer."
- **`pg_visibility`** — visibility map state per page; functions like
  `pg_check_frozen(regclass)` and `pg_check_visible(regclass)` flag VM
  bits that disagree with the heap. [verified-by-code;
  `source/contrib/pg_visibility/pg_visibility--1.1--1.2.sql`]
- **`amcheck`** — structural integrity checks for btree and heap.
  Reproduce-suspected-corruption tool.

All four are in-tree under `source/contrib/`. [verified-by-code]

## 10. Reproducing locking bugs

The hard ones. Standard recipe:

```sql
-- Session A: get the PID, take the lock you want to study.
SELECT pg_backend_pid();
BEGIN;
LOCK TABLE t IN ACCESS EXCLUSIVE MODE;

-- Session B: from a third session (so you can still query),
-- watch who holds what and who is waiting on whom:
SELECT pid, locktype, mode, granted, relation::regclass, transactionid
FROM   pg_locks
WHERE  NOT granted
   OR  pid IN (<A pid>, <B pid>);

SELECT pid, wait_event_type, wait_event, state, query
FROM   pg_stat_activity
WHERE  pid IN (<A pid>, <B pid>);
```

The `wait_event_type` / `wait_event` columns name the exact wait point
inside the backend — usually enough to know which LWLock or heavyweight
lock is blocking.

For deadlocks, set `deadlock_timeout = '100ms'` (default 1s) to make the
detector fire quickly, then check the server log for the standard
`DETAIL: Process N waits for X on Y; blocked by process M.` block.
[verified-by-code: `deadlock_timeout` GUC; deadlock log format from
`source/src/backend/storage/lmgr/deadlock.c` and `proc.c`. The
`100ms` value: `[inferred]` reasonable shorter-than-default — adjust to
taste.]

To dive deeper into the locking model itself, see
`.claude/skills/locking/`.

## 11. Memory bugs and leak hunting

Three tools, in increasing order of "bring out the heavy machinery":

### 11.1 `pg_backend_memory_contexts` — observe before debugging

Cheapest first step for any "the backend's RSS keeps growing" suspicion.
Run on the suspect connection between iterations of a workload:

```sql
SELECT name, level, parent, total_bytes/1024 AS kb, used_bytes/1024 AS used_kb
FROM   pg_backend_memory_contexts
ORDER  BY total_bytes DESC
LIMIT  20;
```

Run the suspect workload N times via `\watch`, then re-check. A context
that grows monotonically across iterations is your leak signature. For
*another* backend's contexts, use `pg_log_backend_memory_contexts(<pid>)`
(PG 14+) — it dumps to `dev/data-debug/server.log`. [verified-by-code;
`pg_backend_memory_contexts` view and `pg_log_backend_memory_contexts`
function exist in `src/backend/utils/adt/mcxtfuncs.c`.]

### 11.2 AddressSanitizer + UndefinedBehaviorSanitizer

For use-after-free, heap-buffer-overflow, double-free, and undefined
behavior (signed overflow, alignment violations, bool-from-non-bool).
Use the sanitizer build profile — see
`.claude/skills/build-and-run/SKILL.md` "Sanitizer builds":

```bash
# One-time build
/setup-pg-asan
# Start the asan cluster (separate data dir from debug cluster)
/pg-start-asan
```

Important runtime knobs (the `/pg-start-asan` wrapper sets them):

```bash
export ASAN_OPTIONS="abort_on_error=1:detect_leaks=0:detect_stack_use_after_return=1:print_stacktrace=1"
export UBSAN_OPTIONS="print_stacktrace=1:halt_on_error=1"
```

On a hit, ASan writes a full stack to the server log AND the controlling
terminal, then aborts the backend (postmaster restarts a fresh one).
Check `dev/data-asan/server.log` for the report. The stack frames will
have file:line directly into the source you built — no addr2line dance.

**`detect_leaks=0` is required on macOS** — LeakSanitizer is not
supported on Darwin and setting `detect_leaks=1` errors out at startup.
For leaks specifically, use 11.3 instead, or rebuild this profile on
Linux. [verified: macOS ASan does NOT include LSan, per LLVM
sanitizer-platform compat matrix.]

### 11.3 macOS-native leak detection (`leaks` / `malloc_history`)

When LSan isn't an option (i.e., on this Mac), the Darwin tooling does
the job — no rebuild required, works against the existing `build-debug`:

```bash
# Before pg_ctl start, enable malloc backtraces.
export MallocStackLogging=1
/pg-restart

# Reproduce the leak workload, then:
PID=<the suspect backend pid>
leaks "$PID"           # one-shot report
leaks --atExit -- /path/to/cmd       # for short-lived subprocesses

# Get allocation backtrace for a specific address found by `leaks`:
malloc_history "$PID" <addr>
```

`MallocStackLogging` adds ~10-30% RSS overhead — fine for dev, don't
ship.

**When `pg_backend_memory_contexts` is the right tool vs `leaks`:**
PG-context allocations (`palloc`/`MemoryContextAlloc`) show up in the
context view, NOT in `leaks` — because palloc'd memory is freed via
`MemoryContextDelete()` and is not "leaked" from libc's perspective.
For raw `malloc()` leaks (which do exist — extension code, libpq,
third-party libs called from the backend), `leaks` is the answer.

### 11.4 Choosing the right tool

| Symptom                                           | Reach for     |
| ------------------------------------------------- | ------------- |
| RSS grows, suspect a palloc-context not freed     | 11.1 contexts |
| Use-after-free, heap-buffer-overflow, double-free | 11.2 ASan     |
| Signed overflow / alignment / undefined behavior  | 11.2 UBSan    |
| raw `malloc()` leak (often in extension code)     | 11.3 `leaks`  |
| Need real LSan-quality leak reports               | Linux + ASan  |

## 12. Quick checklist

When something is wrong, in order:

1. Reproduce against a **debug+cassert** build. Asserts catch a lot.
2. Check the server log — `log_min_messages = debug2` is usually enough
   to surface the path taken.
3. `SELECT pg_backend_pid()` → attach lldb/gdb → set the breakpoint
   you suspect.
4. If it's startup or auth, switch to `postgres --single`.
5. If it's shared state, ask `pg_buffercache` / `pg_locks` /
   `pg_stat_activity` before reaching for the debugger.
6. If it crashed, check `/cores/core.<pid>` and open it with lldb.
7. If it's a memory bug (UAF, OOB, leak), pick the right tool from §11.4.

## Cross-references

- `.claude/skills/build-and-run/SKILL.md` — get a debug+cassert build before debugging anything; ASan profile lives here.
- `.claude/skills/psql/SKILL.md` — held-PID handoff (`PGAPPNAME=hold` + `pg_sleep`), `pg_backend_memory_contexts`, `\errverbose` from the SQL side.
- `.claude/skills/locking/SKILL.md` — interpret `pg_locks` / `wait_event` output once §10 surfaces the contention.
- `.claude/skills/error-handling/SKILL.md` — `errfinish` + `elevel` numbering + `ereport` macro structure (§5.1, §7).
- `.claude/skills/memory-contexts/SKILL.md` — what `pg_backend_memory_contexts` shows and how to read it (§11.1).
- `.claude/commands/pg-attach.md`, `.claude/commands/pg-tail-log.md` — slash-command wrappers built on top of this skill.