Memory safety for Postgres extensions in C/C++

clickhouse.com · 2026-10-03 · by Long Nguyen ·generic
How ClickHouse’s Postgres extensions handle the memory safety challenges of combining C and C++, from clean language boundaries to isolated helper processes.

Memory safety for Postgres extensions in C/C++

\->Scroll to top

<-Back

  • Blog
  • /
  • Engineering
  • Copy pageCopied!More actions

  • View as MarkdownView as Markdown Open this page in Markdown
  • Open in ChatGPTOpen in ChatGPT Ask questions about this page
  • Open in ClaudeOpen in Claude Ask questions about this page
  • Open in v0Open in v0 Ask questions about this page
  • Memory safety for Postgres extensions in C/C++

    image from slack 3

    Philip Dubé

    Sep 30, 2026 · 6 minutes read

    We’ve developed a few postgres extensions at ClickHouse:

  • pg\_clickhouse (clickhouse fdw)
  • pg\_re2 (integrates re2, the same regex engine used by ClickHouse, into postgres)
  • pg\_chdb (ClickHouse object storage capabilities for COPY in postgres)
  • pg\_stat\_ch (ships metrics & logs)
  • All of these involved bringing C++ into a Postgres extension. Postgres is C. Surely C & C++ play nice together, right?

    No. Postgres has its own memory allocation pattern built around MemoryContext. Instead of using malloc/free, one should use palloc/pfree. These functions associate allocations with a MemoryContext, which can work as an arena allocator freeing everything at the end of a transaction or aggregate or what have you. MemoryContexts can also have callbacks to act as destructors. Postgres handles failed memory allocations by raising an error, for which it has a whole PG\_TRY/PG\_CATCH/PG\_FINALLY macro package built on setjmp/longjmp. PG\_FINALLY is another method of building RAII\-like destructor logic.

    C++ code on the other hand tends to use new/delete, which invokes constructors/destructors, & raises std::bad\_alloc on failed allocation. These two systems do not interact well: setjmp/longjmp skips C++ cleanup; jumping past nontrivial destructors for automatic objects is undefined behavior, not merely a leak. PG\_FINALLY and MemoryContext callbacks do not make such a jump safe. Exceptions bypass PG\_CATCH/PG\_FINALLY. Worse, an uncaught exception causes the process to abort, which even in a background worker will lead to the Postgres postmaster process having to restart everything in fear that shared memory has been corrupted.

    Each of these extensions took a different strategy around memory safety.

    ##

    For pg\_clickhouse we have completely eradicated the use of C++. This involved replacing clickhouse-cpp with an entirely new C library, clickhouse-c, which we recently wrapped in another library, pg-clickhouse-c, in order to consolidate postgres logic while keeping clickhouse-c a general-purpose library. clickhouse-c was designed to be transport-agnostic, supporting Native format outside Native protocol, so now the HTTP driver shares much of its decoding/encoding logic with the binary driver by using Native format over HTTP. For HTTP we use libcurl, which offers a low level enough interface to play nice with Postgres's environment.

    For example, previously in `binary.cpp`, `make_datum` converted a ClickHouse String with:

    1auto s = std::string(col->AsStrict<ColumnString>()->At(row));
    2ret = PointerGetDatum(cstring_to_text_with_len(s.data(), s.size()));

    Copy command

    `cstring_to_text_with_len` allocates a Postgres text value with `palloc`. If that allocation fails, Postgres raises ERROR and jumps to its error handler, bypassing `s`'s destructor. Surrounding C++ try/catch cannot catch this jump.

    ##

    For pg\_re2 C++ is scoped to re2\_wrapper.cpp. All C++ allocations happen within C++ try/catch, & it doesn't call into postgres C code. Separation of concerns is sufficient for this encapsulation to draw a clean line between the two systems.

    ##

    pg\_chdb reuses pg-clickhouse-c for decoding/encoding native blocks. Originally it loaded libchdb into the background worker, but eventually moved libchdb into a separate helper executable. Isolating libchdb is the point: its C++ runtime, threads, allocations, and failures live outside a Postgres backend or managed background worker. The backend forks and execs this helper, so it remains a descendant of postmaster, but postmaster does not manage it as a background worker. This helper only needs enough information to run a ClickHouse query & exchange Native blocks, so it has no need for postgres headers. A libchdb crash can then fail the calling operation without itself triggering Postgres cluster crash recovery.

    ##

    pg\_stat\_ch relies on OpenTelemetry libraries & Arrow's C++ bindings, as nanoarrow lacks many IPC features. C++ code is isolated to a Postgres background worker which we then instrument with C++ exception handling & PG exception handling. A terminate handler routes uncaught exceptions / std::terminate through `ereport(FATAL)` instead of default `abort()` / SIGABRT. FATAL normally runs Postgres process-exit cleanup and exits with status 1. Postmaster accepts this as a non-crash exit if shared-memory detachment also completes cleanly; worker restart then follows `bgw_restart_time`. By contrast, abnormal worker termination can trigger cluster-wide crash recovery and disconnect other sessions.

    This is a mitigation, not a general crash-isolation guarantee. FATAL cannot repair inconsistent shared memory, and failed cleanup can still make postmaster treat exit as a crash. A process-wide terminate handler also needs scrutiny if invoked by a library thread: it does not make Postgres error handling and exit callbacks safe to call from arbitrary threads.

    Writing this blog exposed a concrete ownership problem, addressed in PR #126: an interrupted dequeue could leave a slot pointing to already-freed error text or already-released query text. Recovery retries the slot before tail advances, potentially freeing or releasing the same reference twice. Long term moving C++ dependencies into a helper executable, as done in pg\_chdb, will further harden pg\_stat\_ch. PR #129 is the first step: isolate C++ in `src/exporter` with no Postgres dependency and let `bgworker.c` handle the C/C++ boundary.

    Get started today

    Interested in seeing how ClickHouse works on your data? Get started with ClickHouse Cloud in minutes and receive $300 in free credits.

    Sign up

    ---

    Share this post

  • Copy URL
  • Y Combinator icon
  • X icon
  • Bluesky icon
  • Facebook icon
  • LinkedIn icon

Subscribe to our newsletter

Stay informed on feature releases, product roadmap, support, and cloud offerings!

Recent posts

View all Blogs

what is direct io and why does clickhouse managed postgres use it for backups

Engineering

What is direct I/O, and why does ClickHouse Managed Postgres use it for backups?

Kaushik Iska · Oct 2, 2026

linkedin customer story cover

User stories

How LinkedIn extended ClickHouse from distributed tracing to metric discovery and analytics

ClickHouse · Oct 1, 2026

introducing scim provisioning in clickhouse cloud

Product

Introducing SCIM Provisioning in ClickHouse Cloud

Raymond Lee · Sep 30, 2026

gulcin updated the title

Product

pg\_clickhouse & chdb updates: Encoding, nesting, and types

David Wheeler · Sep 30, 2026

View all Blogs

Follow us

XBlueskySlackGithubTelegramMeetupRSS

clickhouse.com · 30 images · 121 links

Comments (0)

No comments yet