CommitFest 7254 — Allow bare library names for non-superuser LOAD

code-review · generated from canonical Markdown

Type
code-review
Source
postgresql-master
Slug
commitfest-7254-review
Tags
code-reviewcommitfestcommitfest-7254coveragepatch-reviewpostgresqlsecurity
Updated
2026-09-07T15:33:23.759096+00:00
Verdict / SummaryReview of patch moving $libdir/plugins/ bare-name expansion from load_libraries() into load_file(). Code correct and memory-safe; security preserved. SEC-1 (pre-existing traversal) refuted. MAJOR test gap (TEST-1) for headline behavior has validated proposed test patch. MINOR DEBUG1 log change (LOG-1). NIT doc wording (DOC-1). Verdict: ready-for-committer (test follow-up required).
Front matter
schema: review-report-v2
source: postgresql-master
slug: commitfest-7254-review
target: CommitFest 7254 — Allow bare library names for non-superuser LOAD
head: 6168c65ddcac4494fb292418cc774897791e0667
base: 6168c65ddcac4494fb292418cc774897791e0667
diff_sha256: e2e3d1b254040736f2ba40199ee5c4a54d2ed9482788b926166ad9a31629cea8
verdict: ready-for-committer (test follow-up required)
blocker_counts:
  verified: 0
  unverified: 0
model: glm-5.2:cloud

CommitFest 7254 — Review Report

Allow bare library names for non-superuser LOAD

Patch summary

This patch moves the $libdir/plugins/ bare-library-name auto-expansion from load_libraries() (src/backend/utils/init/miscinit.c) into load_file() (src/backend/utils/fmgr/dfmgr.c), so that the SQL LOAD command for non-superusers (utility.c:854, restricted = !superuser()) gains the same bare-name convenience that local_preload_libraries already had. After the patch, a non-superuser can write LOAD 'mylib'; and have it interpreted as LOAD '$libdir/plugins/mylib';. The security model is preserved: the expansion only fires for names with no directory separator, and the expanded result is still validated by check_restricted_library_name().

Three files changed (+19/-15): doc/src/sgml/ref/load.sgml (documentation update), src/backend/utils/fmgr/dfmgr.c (expansion moved here), src/backend/utils/init/miscinit.c (expansion removed here).

Verdict

ready-for-committer (test follow-up required)

Per the deterministic verdict: no surviving BLOCKER; no surviving correctness/security MAJOR (SEC-1 refuted); one test-only MAJOR (TEST-1) ⇒ ready-for-committer (test follow-up required).

Findings

TEST-1

— MAJOR / tests — verified

Location: src/backend/utils/fmgr/dfmgr.c:155-166 (reached via src/backend/tcop/utility.c:854)

Mechanism: No regression test exercises the patch's headline behavior: non-superuser LOAD of a bare library name now auto-expands to $libdir/plugins/<name> via the new restricted-path branch in load_file(). Existing tests run LOAD as superuser (restricted=false), so the new branch is never reached.

Failure: A future regression dropping the expansion guard or reverting to check_restricted_library_name-first behavior would not be caught. The error message for non-superuser LOAD of a non-existent bare name changed from access to library "foo" is not allowed to could not access file "$libdir/plugins/foo": No such file or directory and this is unpinned.

Verification: Static inspection confirmed no existing test sets session authorization to a non-superuser and issues LOAD. Live repro on disposable server: SET session authorization regress_loaduser; LOAD 'no_such_plugin_lib' → ERROR: could not access file "$libdir/plugins/no_such_plugin_lib": No such file or directory (expansion happened; pre-patch this was access to library "no_such_plugin_lib" is not allowed). pg_regress --temp-instance create_function_c with the proposed test patch: All 1 tests passed.

Repair: Append a non-superuser LOAD test section to src/test/regress/sql/create_function_c.sql and src/test/regress/expected/create_function_c.out covering: (a) bare name → expansion → file-not-found, (b) name with separator + wrong prefix → privilege denied, (c) explicit $libdir/plugins/ prefix → passes check → file-not-found.

  • Patch SHA-256: 85c9267abb10a5b6df9b0deca1b33ee24d8b837f458bc039f600a5149e71cfd8
  • Validation: passed (apply-check, diff-check, targeted-test via

pg_regress --temp-instance). All three LOAD statements produced the expected error messages.

SEC-1

— MAJOR / security — refuted (pre-existing, not widened)

Location: src/backend/utils/fmgr/dfmgr.c:159-166 + check_restricted_library_name at dfmgr.c:533-542

Mechanism: check_restricted_library_name passes tails that are exactly . or .. (no separator in the tail), so LOAD '..' / LOAD '$libdir/plugins/..' both reach expand_dynamic_library_name as $libdir/plugins/.. which resolves to a directory; dlopen/stat on the unexpanded literal $libdir path fails.

Why refuted: This behavior is pre-existing — LOAD '$libdir/plugins/..' was already accepted pre-patch (the check is unchanged by the CF diff), and the traversal is fully blocked downstream by pg_file_exists (rejects directories) and stat on the unexpanded literal $libdir path. The set of loadable regular-file targets remains exactly $libdir/plugins/<file>. This patch does not widen the attack surface; it only adds a second syntactic route (LOAD '..') to the same already-dead- ended traversal.

Follow-up note (not a finding against this patch): As a defense-in-depth improvement independent of this patch, check_restricted_library_name could be hardened to reject all-dots tails (strspn(tail, ".") == strlen(tail)) so the documented invariant holds even if downstream dlopen/stat behavior ever changes. Suggested for a separate hardening commit.

LOG-1

— MINOR / correctness — verified

Location: src/backend/utils/init/miscinit.c:1831-1835

Mechanism: Before the patch, load_libraries reassigned filename = expanded before the ereport(DEBUG1), so the message printed $libdir/plugins/mylib. After the patch, filename is never reassigned, so DEBUG1 prints the bare mylib. Behavior change in DEBUG1 log output for local_preload_libraries bare names only.

Failure: Unannounced cosmetic change in DEBUG1 log output. No security or correctness impact.

Verification: Static diff inspection + live repro on disposable server. Post-patch: loaded library "testplug" (bare). With optional repair: loaded library "$libdir/plugins/testplug" (expanded, pre-patch behavior restored).

Recommended repair: Accept and document in the commit message (the bare name is arguably more readable). An optional validated code patch (SHA-256 e6af4ad2c9bba9f73ec2883b4dc7cc5cdb742d9af1e34ff0da4f9f56e8391c96) restores the expanded form by re-adding a logging-only expansion in load_libraries; maintainer's choice.

DOC-1

— NIT / docs — verified

Location: doc/src/sgml/ref/load.sgml:59-60 vs doc/src/sgml/config.sgml:11482-11484

Mechanism: load.sgml now says "Administrators must ensure…" while config.sgml still says "It is the database administrator's responsibility…". Wording divergence (both accurate).

Failure: Cosmetic; no behavioral impact.

Repair: Optional harmonization patch (SHA-256 6465406dc488f2c0be2299ec505a53ed24f9a84fd5460e30421524e55486873b) aligns config.sgml to load.sgml. Not blocking.

Verification Summary

findingseverityclassverification_statusrepair.validationpatch sha256
TEST-1MAJORtestsverifiedpassed85c9267abb10a5b6df9b0deca1b33ee24d8b837f458bc039f600a5149e71cfd8
SEC-1MAJORsecurityrefutednot-applicable-refutednot-applicable-refuted
LOG-1MINORcorrectnessverifiedpassed (optional)e6af4ad2c9bba9f73ec2883b4dc7cc5cdb742d9af1e34ff0da4f9f56e8391c96
DOC-1NITdocsverifiedpartial6465406dc488f2c0be2299ec505a53ed24f9a84fd5460e30421524e55486873b

Verified/unverified blocker split: 0 verified BLOCKER, 0 unverified BLOCKER.

Repair Validation Summary

and pg_regress --temp-instance create_function_c passes (All 1 tests passed). The three pinned ERROR lines confirm the expansion behavior is exercised. Methods: apply-check, diff-check, targeted-test.

(reject all-dots tails in check_restricted_library_name) is noted as a separate follow-up.

compiles clean, and restores the pre-patch DEBUG1 log content. Methods: apply-check, diff-check, compile, targeted-test. Recommended: accept and document instead.

--noout --nonet` confirms no new SGML errors (13 pre-existing unchanged). Full jade/openjade doc-build was not available. Methods: apply-check, diff-check, static-check.

Changed-Code Coverage

Coverage requirement: required (changed instrumentable implementation in src/backend/utils/fmgr/dfmgr.c).

Coverage status: measured (fresh disposable counter epoch at HEAD 6168c65ddca + CF patch applied, gcov instrumentation on the two changed backend files).

Execution lane

scratch/cov-build, configured with --enable-debug and gcov instrumentation (--coverage -fprofile-arcs -ftest-coverage in CFLAGS for the two changed backend files), built and installed. Fresh counter epoch: all .gcda deleted before test run. pg_regress --temp-instance create_function_c (with verifier's proposed test patch applied): ok 1 - create_function_c 48 ms / All 1 tests passed. gcov -b -l produced valid .gcov files.

Test commands

make -C src/test/regress regress.so
cp src/test/regress/regress.so <pkglibdir>/regress.so
pg_regress --inputdir=. --temp-instance=<tmp> --bindir=<bindir> --dlpath=<pkglibdir> --temp-config=/dev/null create_function_c

Raw metrics

metricvalue
changed_executable_lines6
covered_changed_lines5
compiler_uncovered_changed_lines1
uncovered_changed_lines1
structural_uncovered_changed_lines0
changed_lines_without_coverage_coordinates0
changed_branches4
covered_changed_branches3

Integrity: integrity_errors: [] (empty list, no corrupted counters).

Uncovered regions

src/backend/utils/fmgr/dfmgr.c:176 — load_file — pfree(expanded)

branch fired (restricted=true AND first_dir_separator(filename)==NULL) AND load_file returned successfully (no ereport ERROR from check_restricted_library_name, expand_dynamic_library_name, or internal_load_library). The test exercises the expansion but uses non-existent library names, so internal_load_library ereports ERROR before reaching pfree(fullname)/pfree(expanded). A positive test that successfully loads a library from $libdir/plugins/ as a non-superuser would cover this line.

$libdir/plugins/testplug.so, then as a non-superuser: SET session authorization regress_loaduser; LOAD 'testplug'; RESET session authorization; — this successfully loads $libdir/plugins/testplug.so, reaching pfree(expanded) at line 176.

Uncovered branches

src/backend/utils/fmgr/dfmgr.c:175 — load_file — if (expanded) true branch

pfree(expanded) at 176) is never taken because the only test reaching line 175 uses non-existent libraries that ereport ERROR before returning. The true branch requires a successful restricted bare-name load.

regress.so to $libdir/plugins/, LOAD 'testplug' as non-superuser).

Touched functions

pathfunctionspanenteredlines covered/totalbranches covered/total
src/backend/utils/fmgr/dfmgr.cload_file150–177yes5/63/4
src/backend/utils/init/miscinit.cload_libraries1803–1840yes0/0 (pure deletion, no added executable lines)0/0

Non-instrumented changes

implementation. No gcov coverage coordinates apply.

Residual limitations

The coverage measurement used the existing create_function_c regression test plus the verifier's proposed test patch (which exercises the restricted bare-name expansion with non-existent libraries). The one uncovered line (176) and one uncovered branch (line 175 true branch) require a positive restricted-load test — loading an actual library from $libdir/plugins/ as a non-superuser — which is not in the current test patch but is straightforward to add by copying regress.so into $libdir/plugins/. The miscinit.c change is a pure deletion (0 added executable lines), so no coverage gap exists there.

pgsql-hackers Thread Ledger

Status: reconciled Ledger report: commitfest-7254-review-thread-ledger

The thread ([PATCH] Allow bare library names for non-superuser LOAD, thread-id 657405) consists of 4 messages between the patch author (ChangAo Chen, cca5507@qq.com) and one reviewer (Japin Li, japinli@hotmail.com), all on 2026-09-03. Japin Li examined the patch and said "it looks good," noting the $libdir/plugins auto-expansion was added in commit abc3120e9b2 for local_preload_libraries but not LOAD, and it was unclear if that was intentional. No committer has replied. No objections. None of this review's findings (TEST-1, SEC-1, LOG-1, DOC-1) were previously raised on the thread; all are new.

Finding dispositions: all 4 findings → new (not previously raised).

Aggregated Test Plan

  1. TEST-1 (blocking test follow-up): Apply the proposed test patch

adding non-superuser LOAD bare-name expansion coverage to create_function_c.sql/.out. Vehicle: pg_regress create_function_c. Validated: All 1 tests passed.

  1. Coverage gap (line 176/branch 175): Optionally add a positive

restricted-load test (copy regress.so to $libdir/plugins/, LOAD as non-superuser) to cover the successful-load path through pfree(expanded).

  1. LOG-1: No test owed (cosmetic DEBUG1-only change; waived).
  2. DOC-1: No test owed (documentation prose; waived).
  3. SEC-1: Refuted; no test owed.

Verified-clean section

in CurrentMemoryContext; expand_dynamic_library_name returns its own fresh allocation independent of expanded; pfree(fullname) does not double-free; pfree(expanded) is guarded by if (expanded). On ERROR paths the enclosing context is reset. Clean.

load_file expands at most once. Replication callers pass restricted=false (unaffected).

first_dir_separator(filename)==NULL. The expanded string is validated by check_restricted_library_name. Names with any separator are not expanded and must already begin with $libdir/plugins/. Windows backslash tricks equally blocked (first_dir_separator treats both / and \). Clean.

utility.c:854 (SQL LOAD) and literal true for local_preload_libraries. No path sets restricted from user-controllable input. Clean.

Clean.

expansion, the post-expansion string contains a separator, so expand_dynamic_library_name takes the have_slash=true branch and calls substitute_path_macro directly — never find_in_path. The user-controlled dynamic_library_path cannot redirect a restricted LOAD. Clean.

Provenance / model routes

commitfest-7254-review-handoff-patch-reviewer

commitfest-7254-review-handoff-security-reviewer

commitfest-7254-review-handoff-verifier

epoch at HEAD 6168c65ddca + CF patch applied.

— exit 0, "ok: pg-review-artifact-v2 is structurally complete".

Residual risk

Low. The code change is small, correct, memory-safe, and security-preserving. The only blocking follow-up is the test coverage gap (TEST-1), which has a validated proposed test patch ready to apply. The optional LOG-1 and DOC-1 repairs are cosmetic. The SEC-1 pre-existing hardening opportunity is noted for a separate commit.