Skip to content

Build: Limit shared library exports to the documented public API - #332

Open
ng-galien wants to merge 1 commit into
pganalyze:17-latestfrom
ng-galien:17-limit-shared-library-exports
Open

Build: Limit shared library exports to the documented public API#332
ng-galien wants to merge 1 commit into
pganalyze:17-latestfrom
ng-galien:17-limit-shared-library-exports

Conversation

@ng-galien

Copy link
Copy Markdown

Summary

libpg_query.so / .dylib exports its bundled PostgreSQL internals
(palloc, pfree, lappend, MemoryContextSwitchTo,
CurrentMemoryContext, ...) and the protobuf-c machinery in addition to
the documented pg_query_* / deparseRawStmt API — 2044 dynamic
exports vs. 1692 of public surface on a clean 17-6.2.2 build.

Restrict the .so / .dylib exports to the public API declared in
pg_query.h and postgres_deparse.h, via a GNU ld version script
(pg_query.map) on ELF and a Mach-O exported_symbols_list
(pg_query.exp) on macOS. The static library (libpg_query.a) is
unchanged.

Why

Embedding libpg_query inside a PostgreSQL extension fails at link time:
the bundled CurrentMemoryContext / TopMemoryContext / ErrorContext
/ error_context_stack are __thread (so the parser is reentrant),
while the host PostgreSQL declares them non-TLS, and the linker rejects:

/usr/bin/ld: CurrentMemoryContext: TLS reference in libpg_query.so
  mismatches non-TLS reference in <ext>.o

Hiding the bundled globals lets each .so resolve its own copy at load
time. We hit this in a custom PG extension consuming libpg_query
(ng-galien/esac), where static
linking wasn't an option because the parser is shared across modules in
the same backend.

PostgreSQL core itself adopted the same approach for extension libraries
(commitfest #3396, "Default to hidden visibility for extension
libraries").

Mechanism

  • pg_query.map / pg_query.exp — whitelist pg_query_*,
    deparseRawStmt, deparseRawStmtOpts; everything else hidden.
  • Makefile — picks the right file via SO_EXPORTS / SO_EXPORTS_FILE
    based on uname -s, wired into the \$(SOLIB) link line.

The pg_query_* wildcard covers the protobuf-c-generated symbols
(pg_query__scan_result__unpack, pg_query__token__descriptor, ...).

Test plan

  • macOS (arm64) and Linux (debian:bookworm-slim, GNU ld 2.40):
    full make passes; nm -D / nm -gU on the resulting .so /
    .dylib shows only pg_query_* and deparseRawStmt[Opts].
  • Windows MSVC: N/A — Makefile.msvc has no shared-lib target.
  • Windows MSYS2: shared-lib path not exercised in CI today
    (pre-existing).

Note on AI tool use: Claude Code drafted the linker scripts, Makefile
wiring and commit message; I steered it back to this repo's conventions
on review.

libpg_query.so currently exports its bundled PostgreSQL internals
(palloc, pfree, lappend, MemoryContextSwitchTo, CurrentMemoryContext,
...) and the protobuf-c machinery in addition to the documented
pg_query_*/deparseRawStmt API.

This breaks at link time when libpg_query is embedded inside a
PostgreSQL extension: the bundled CurrentMemoryContext is declared
__thread (so the parser is reentrant), while the host PostgreSQL
declares it non-TLS, and the linker refuses with

    TLS reference in libpg_query.so mismatches non-TLS reference in <ext>.o

Hide every symbol that isn't part of the public API surface, via a GNU
ld version script (pg_query.map) on ELF and a Mach-O exported_symbols
list (pg_query.exp) on macOS. Both whitelist pg_query_* and the two
deparseRawStmt entry points; nm -D / nm -gU confirms the dynamic
symbol table now matches pg_query.h + postgres_deparse.h exactly on
both Linux (debian:bookworm-slim, GNU ld 2.40) and macOS (arm64,
ld-prerelease 1115.7.3).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lfittl

lfittl commented May 4, 2026

Copy link
Copy Markdown
Member

@ng-galien Thanks for opening this PR & disclosing AI use.

Overall I think this is reasonable, even though the libpg_query bindings typically don't go through the shared library. But for use cases like the one you describe, its reasonable to limit to avoid exporting any of Postgres' own symbols.

One question to help me understand this better: Is there a reason that you don't call the Postgres parser directly in your extension? Or do you need other functionality provided by libpg_query? (would be helpful to know what specifically you're looking to use)

@ng-galien

Copy link
Copy Markdown
Author

@lfittl
Thanks! I had already integrated tree-sitter but it wasn't enough for PL. libpg_query returning JSON directly was exactly what I needed… and to be honest it saved me a lot of trouble with Claude.
Quick win.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants