Skip to content

perf: optimize SCRAM-SHA-256 auth with cached EVP_MD and verifier cache#5689

Merged
renecannao merged 5 commits into
v3.0from
perf/scram-cached-hmac
May 4, 2026
Merged

perf: optimize SCRAM-SHA-256 auth with cached EVP_MD and verifier cache#5689
renecannao merged 5 commits into
v3.0from
perf/scram-cached-hmac

Conversation

@renecannao

@renecannao renecannao commented May 1, 2026

Copy link
Copy Markdown
Contributor

Summary

On OpenSSL 3.x, EVP_sha256() triggers EVP_MD_fetch() on every call, involving global lock contention and provider property lookups. The vendored PostgreSQL hmac_openssl.c calls it on every HMAC_Init_ex(), which means 4096 redundant fetches per SCRAM PBKDF2 derivation alone. This was responsible for ~58% of CPU time during SCRAM-heavy workloads.

Two optimizations:

  1. Cached EVP_MD* for HMAC (scram_hmac_cached.c): New scram_fast_* API that fetches the SHA-256 EVP_MD once via pthread_once and reuses it across all HMAC operations. Replaces all pg_hmac_* calls in libscram with the cached versions. No changes to vendored PostgreSQL code.

  2. SCRAM verifier cache for plaintext passwords (scram.c): When ProxySQL stores plaintext passwords, every SCRAM exchange runs the full PBKDF2 (4096 HMAC iterations). This cache stores the generated SCRAM verifier after the first successful auth, so subsequent connections skip PBKDF2 entirely — reducing HMAC operations from ~4096 to ~4 per connection, matching PgBouncer's behavior. Thread-local, no locks needed.

Benchmark results

Serial connect/disconnect benchmark (10K iterations, C client via libpq, SCRAM-SHA-256 + TLS, 1 thread, no backend):

Metric Before After PgBouncer
Conns/sec ~137 229 226
Connect avg ~7200 us 4319 us 4378 us
Ratio vs PgBouncer 1.64x slower 1.01x (parity) baseline

perf profile shifts from pg_hmac_init / EVP_MD_fetch (58%) to do_ssl_handshake (22%) — the actual TLS work.

Files changed

  • deps/libscram/include/scram_hmac_cached.h — new header with scram_fast_* API
  • deps/libscram/src/scram_hmac_cached.c — cached EVP_MD HMAC implementation + fast SCRAM key derivation
  • deps/libscram/src/scram.c — switch to scram_fast_* functions, add SCRAM verifier cache

Test plan

  • Serial connect/disconnect benchmark shows parity with PgBouncer
  • perf record confirms HMAC/EVP_MD_fetch no longer dominates
  • Both scram_hmac_cached.c and modified scram.c compile cleanly (gcc, OpenSSL 3.0.13)
  • Existing PgSQL SCRAM auth TAP tests pass

Summary by CodeRabbit

  • Performance Improvements
    • Faster SCRAM-SHA-256 auth using optimized crypto paths and thread-local verifier caching.
  • New Features
    • Thread-local SCRAM cache invalidation so password changes take effect immediately on reload.
  • Tests
    • New automated test that verifies SCRAM cache invalidation on user reload after password change.
  • Tools
    • Added a multi-threaded PostgreSQL connection benchmark and corresponding build target.
  • Chores
    • Updated ignore list to exclude new tool artifacts.

@coderabbitai

coderabbitai Bot commented May 1, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a fast SCRAM-SHA-256 HMAC/PBKDF2 implementation and opaque incremental HMAC API, integrates them into SCRAM flows with a per-thread plaintext-password verifier cache and runtime invalidation, adds a TAP test to validate cache invalidation, and introduces a PostgreSQL connect benchmark tool.

Changes

SCRAM: fast HMAC/PBKDF2, verifier cache, and integration

Layer / File(s) Summary
Public API
deps/libscram/include/scram_hmac_cached.h
New header: SCRAM_FAST_KEY_LEN, scram_fast_* PBKDF2/H and Client/ServerKey functions, opaque scram_fast_hmac_ctx incremental HMAC API, and scram_cache_invalidate().
Crypto Implementation
deps/libscram/src/scram_hmac_cached.c
New implementation using OpenSSL HMAC/SHA‑256 (EVP_MD fetch on OpenSSL 3.x), incremental HMAC context (create/init/update/final/free/error), scram_fast_SaltedPassword (iterative HMAC rounds), scram_fast_H, scram_fast_ClientKey, scram_fast_ServerKey.
SCRAM Integration
deps/libscram/src/scram.c
Replaces prior HMAC/derivation calls with scram_fast_* APIs across client/server proof and verification paths; adds thread-local plaintext-password verifier cache (lookup/store/clear) and uses it in build_server_first_message.
Cache Invalidation Hook
deps/libscram/src/scram.c, deps/libscram/include/scram_hmac_cached.h, lib/ProxySQL_Admin.cpp
Adds global generation counter and scram_cache_invalidate(); ProxySQL_Admin.cpp calls scram_cache_invalidate() after PgSQL user refresh to force per-thread cache refresh.
Tests / Test Group
test/tap/tests/pgsql-scram_cache_invalidation-t.cpp, test/tap/groups/groups.json
New TAP test that populates the verifier cache via repeated SSL connects, updates pgsql_users, reloads users (invoking invalidation), and verifies authentication before/after password change; test group entry added.

Tools / Benchmark

Layer / File(s) Summary
New Tool Source
tools/bench_connect.c
Adds a multithreaded PostgreSQL connect/close benchmark that measures per-iteration connect/close times, computes percentiles, and emits a JSON summary.
Build Rules
tools/Makefile
Adds bench_connect Make target and extends clean to remove the binary.
VCS Ignore
tools/.gitignore
Adds entries to ignore the new tool/sample artifacts.

Sequence Diagram

sequenceDiagram
    participant Client
    participant SCRAM_Module
    participant Verifier_Cache
    participant Crypto_Layer

    Client->>SCRAM_Module: build_server_first_message(password)
    SCRAM_Module->>Verifier_Cache: lookup(password)
    alt cache hit
        Verifier_Cache-->>SCRAM_Module: cached verifier
        SCRAM_Module->>SCRAM_Module: parse verifier -> ScramState
    else cache miss
        Verifier_Cache-->>SCRAM_Module: miss
        SCRAM_Module->>Crypto_Layer: scram_fast_SaltedPassword(password, salt, iter)
        Crypto_Layer->>Crypto_Layer: iterative HMAC rounds -> salted_password
        Crypto_Layer-->>SCRAM_Module: salted_password
        SCRAM_Module->>Crypto_Layer: scram_fast_ClientKey(salted_password)
        Crypto_Layer-->>SCRAM_Module: client_key
        SCRAM_Module->>Crypto_Layer: scram_fast_ServerKey(salted_password)
        Crypto_Layer-->>SCRAM_Module: server_key
        SCRAM_Module->>Verifier_Cache: store(verifier for password)
        SCRAM_Module->>SCRAM_Module: parse generated verifier -> ScramState
    end
    SCRAM_Module-->>Client: server-first-message
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐇 I twitched my whiskers, HMAC in paw,

Cached seeds that hummed beneath the straw.
Iterations hopped and spun,
New logins warmed with morning sun,
Old keys sighed — the new ones saw.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately summarizes the main optimization changes: cached EVP_MD for SCRAM-SHA-256 and verifier cache improvements.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/scram-cached-hmac

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an optimized SCRAM-SHA-256 implementation designed to reduce overhead in OpenSSL 3.x by caching EVP_MD objects. It also implements a thread-local cache for SCRAM verifiers to avoid redundant PBKDF2 calculations when using plaintext passwords. Review feedback identifies a potential memory leak in the thread-local cache because allocated strings are not freed upon thread termination, and suggests implementing an eviction policy for the fixed-size cache to handle more than 64 unique users per thread.

Comment thread deps/libscram/src/scram.c
Comment on lines +58 to +59
static __thread struct scram_verifier_cache_entry scram_verifier_cache[SCRAM_VERIFIER_CACHE_SLOTS];
static __thread int scram_verifier_cache_count = 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The scram_verifier_cache uses strdup to allocate memory for password and verifier strings. Since this is a __thread variable, the allocated memory will be leaked when a thread terminates, as there is no mechanism provided to free these strings. In a long-running process like ProxySQL, this can lead to memory accumulation if threads are recycled or frequently created and destroyed.

Comment thread deps/libscram/src/scram.c
Comment on lines +72 to +73
if (scram_verifier_cache_count >= SCRAM_VERIFIER_CACHE_SLOTS)
return false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The SCRAM verifier cache has a fixed size of 64 slots and lacks an eviction policy (e.g., LRU). Once the cache is full, no new entries can be added, and stale entries (e.g., from users whose passwords have changed) will continue to occupy slots indefinitely. This may lead to sub-optimal performance for threads handling a large number of unique users. Consider implementing a simple eviction strategy.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
deps/libscram/src/scram.c (1)

51-84: 💤 Low value

Thread-local cache lacks cleanup on thread exit — bounded memory leak.

The cache uses strdup() to copy passwords and verifiers but provides no destructor to free entries when threads exit. With ProxySQL's fixed thread pool this is bounded (64 entries × ~200 bytes × N threads), but entries for changed passwords also persist indefinitely.

Consider documenting this trade-off, or adding a pthread_key_create() destructor for cleanup if thread churn becomes a concern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@deps/libscram/src/scram.c` around lines 51 - 84, The thread-local cache
(scram_verifier_cache, scram_verifier_cache_count) stores strdup'ed strings in
scram_cache_store but never frees them on thread exit; add a proper cleanup path
by registering a pthread key destructor (via pthread_key_create + pthread_once
for one-time init) that runs on thread teardown to iterate scram_verifier_cache
entries, free each password and verifier, and reset scram_verifier_cache_count
to 0; ensure the destructor is associated with the thread-local storage so
existing code using scram_cache_lookup and scram_cache_store needs no other
changes, or alternatively provide a public scram_cache_cleanup() function and
call it from the destructor/when threads are joined.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@deps/libscram/src/scram.c`:
- Around line 51-84: The thread-local cache (scram_verifier_cache,
scram_verifier_cache_count) stores strdup'ed strings in scram_cache_store but
never frees them on thread exit; add a proper cleanup path by registering a
pthread key destructor (via pthread_key_create + pthread_once for one-time init)
that runs on thread teardown to iterate scram_verifier_cache entries, free each
password and verifier, and reset scram_verifier_cache_count to 0; ensure the
destructor is associated with the thread-local storage so existing code using
scram_cache_lookup and scram_cache_store needs no other changes, or
alternatively provide a public scram_cache_cleanup() function and call it from
the destructor/when threads are joined.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0067b1c7-e810-4922-bfbe-0dd10e5308de

📥 Commits

Reviewing files that changed from the base of the PR and between 3f1027b and 0259a62.

📒 Files selected for processing (3)
  • deps/libscram/include/scram_hmac_cached.h
  • deps/libscram/src/scram.c
  • deps/libscram/src/scram_hmac_cached.c
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: CI-builds / builds (ubuntu24,-tap-genai-gcov)
  • GitHub Check: CI-builds / builds (debian12,-dbg)
  • GitHub Check: CI-builds / builds (ubuntu22,-tap)
  • GitHub Check: run / trigger
  • GitHub Check: claude-review
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{cpp,h,hpp}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{cpp,h,hpp}: Class names should use PascalCase with protocol prefixes (MySQL_, PgSQL_, ProxySQL_)
Member variables should use snake_case naming
Constants and macros should use UPPER_SNAKE_CASE naming
C++17 is required; use conditional compilation via #ifdef PROXYSQLGENAI, #ifdef PROXYSQL31, etc. for feature-gated code
Use jemalloc for memory allocation
Use pthread mutexes for synchronization and std::atomic<> for counters

Files:

  • deps/libscram/include/scram_hmac_cached.h
🪛 Clang (14.0.6)
deps/libscram/src/scram_hmac_cached.c

[error] 1-1: 'scram_hmac_cached.h' file not found

(clang-diagnostic-error)

🔇 Additional comments (14)
deps/libscram/src/scram.c (8)

86-118: LGTM!

The build_scram_verifier function has proper memory management with correct cleanup on all error paths.


656-729: LGTM!

The migration to scram_fast_* API is correct. The HMAC context functions handle NULL internally, so the deferred NULL check pattern is safe here.


731-772: LGTM!

Correct migration to scram_fast_* API with proper error handling.


989-1005: LGTM!

Correct usage of the new key derivation functions with preserved error handling.


1112-1134: LGTM!

The cache integration is well-structured with graceful degradation when the cache is full. Memory management for the temporary verifier string is correct.


1182-1226: LGTM!

compute_server_signature correctly migrated to the new API.


1277-1324: LGTM!

verify_client_proof correctly migrated with proper cleanup paths.


1374-1378: LGTM!

scram_verify_plain_password correctly migrated to use scram_fast_SaltedPassword and scram_fast_ServerKey.

deps/libscram/include/scram_hmac_cached.h (1)

1-37: LGTM!

Well-designed header with proper include guards, clear documentation, and a minimal API surface. The function signatures correctly use portable types (uint8_t, size_t).

deps/libscram/src/scram_hmac_cached.c (5)

17-36: LGTM!

The EVP_MD caching approach is sound. Using pthread_once ensures thread-safe one-time initialization. If EVP_MD_fetch fails, returning NULL is correctly propagated to callers which handle it appropriately.


44-132: LGTM!

Well-designed HMAC context API with consistent error handling. All functions are defensive against NULL inputs and properly propagate errors.


136-188: LGTM!

PBKDF2 implementation is correct per RFC 5802. The algorithm properly computes U1 = HMAC(password, salt || INT(1)) and subsequent Ui = HMAC(password, U(i-1)) with XOR accumulation.


190-261: LGTM!

Hash and key derivation functions correctly implement the RFC 5802 SCRAM primitives with proper error handling.


1-1: [Rewritten review comment]
[Classification tag]

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@deps/libscram/src/scram.c`:
- Around line 682-683: The call to scram_fast_hmac_create() must be NULL-checked
before any subsequent HMAC calls; update the sites in scram.c (including the one
at the shown diff and the similar sites around the 725–750 region) to verify the
returned scram_fast_hmac_ctx* is non-NULL immediately after
scram_fast_hmac_create(), and if NULL avoid calling scram_fast_hmac_init() or
scram_fast_hmac_error() — instead propagate an error/cleanup (return an error
code or goto the existing cleanup path) and log the failure using available
logging routines so the OOM/OpenSSL setup failure is handled safely.
- Around line 58-66: The cache currently stores recoverable plaintext passwords
in struct scram_verifier_cache_entry (password field) via strdup(password),
increasing secret retention; change the cache to key entries by a
non-recoverable fingerprint (e.g., HMAC/SHA256 of the password plus salt)
instead of storing the plaintext, stop calling strdup(password) into
scram_verifier_cache[].password, and ensure any existing plaintext or verifier
buffers are securely scrubbed (overwrite with zeros) before free; update logic
that uses scram_verifier_cache_count, scram_verifier_cache,
scram_cache_generation and scram_my_generation to index by the fingerprint key
and to invalidate/scrub entries on eviction or generation bump.

In `@tools/bench_connect.c`:
- Around line 37-39: The code performs multiple heap allocations (e.g.,
arg->connect_us, arg->close_us, arg->total_us and other mallocs at later sites)
without checking for NULL; update each allocation site to check the return
value, handle OOM by freeing any earlier successful allocations and returning an
error (or exiting cleanly) instead of dereferencing NULL, and ensure any caller
(e.g., the function that allocates these arrays in bench_connect.c) observes the
failure so resources are not used; reference the specific allocation targets
arg->connect_us, arg->close_us, arg->total_us and the other malloc results noted
in the review and add proper NULL checks and cleanup paths.
- Around line 171-176: The min/p50 for close_us are being read from the unsorted
close_us array (uses close_us[0] and close_us[n/2]) which yields incorrect
stats; follow the pattern used for connect_us/total_us by allocating a new array
(e.g., double *sorted_close), memcpy close_us into it, qsort(sorted_close, n,
sizeof(double), cmp_double), then use sorted_close[0] and sorted_close[n/2]
where the code currently reads close_us[0]/close_us[n/2]; remember to free
sorted_close when done and keep naming consistent with sorted_conn/sorted_total.
- Around line 98-115: Validate and clamp all numeric CLI inputs: ensure
iterations is between 1 and MAX_ITERS, threads >= 1, warmup >= 0 (and warmup <=
iterations if intended), and port is in a valid range; reject or error on
invalid values (e.g., "--threads 0" or negative numbers). Before computing any
product like total_ops = threads * iterations (used later to derive n and that
can overflow), perform the multiplication in an unsigned wider type (e.g.,
uint64_t/size_t) and explicitly check for overflow or that total_ops > 0; ensure
any derived n is > 0 before doing divisions to avoid divide-by-zero. Return a
non-zero error when validation fails and add clear stderr messages referencing
the offending flag (iterations, threads, warmup, port) so downstream code (uses
of n and indexing into per-thread arrays) never sees invalid or overflowing
sizes.
- Around line 136-139: Check and handle return values from pthread_create and
pthread_join around the loop that spawns thread_worker using tids, args and
threads: capture each pthread_create return value, and on non-zero log an error
(including errno via strerror if desired), clean up any allocated resources and
exit or return a failure code instead of proceeding; similarly capture each
pthread_join return value and handle join failures (log and abort/cleanup) to
avoid inconsistent benchmark state. Ensure the error handling occurs in the same
scope where tids and args are used so threads are not assumed to have started or
completed when they have not.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a190bae4-03a3-41a7-872f-5d9784ddde97

📥 Commits

Reviewing files that changed from the base of the PR and between 0259a62 and 6d8dd5f.

📒 Files selected for processing (8)
  • deps/libscram/include/scram_hmac_cached.h
  • deps/libscram/src/scram.c
  • lib/ProxySQL_Admin.cpp
  • test/tap/groups/groups.json
  • test/tap/tests/pgsql-scram_cache_invalidation-t.cpp
  • tools/.gitignore
  • tools/Makefile
  • tools/bench_connect.c
✅ Files skipped from review due to trivial changes (2)
  • tools/.gitignore
  • test/tap/groups/groups.json
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: CI-builds / builds (debian12,-dbg)
  • GitHub Check: CI-builds / builds (ubuntu24,-tap-genai-gcov)
  • GitHub Check: CI-builds / builds (ubuntu22,-tap)
  • GitHub Check: run / trigger
  • GitHub Check: claude-review
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{cpp,h,hpp}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{cpp,h,hpp}: Class names should use PascalCase with protocol prefixes (MySQL_, PgSQL_, ProxySQL_)
Member variables should use snake_case naming
Constants and macros should use UPPER_SNAKE_CASE naming
C++17 is required; use conditional compilation via #ifdef PROXYSQLGENAI, #ifdef PROXYSQL31, etc. for feature-gated code
Use jemalloc for memory allocation
Use pthread mutexes for synchronization and std::atomic<> for counters

Files:

  • test/tap/tests/pgsql-scram_cache_invalidation-t.cpp
  • lib/ProxySQL_Admin.cpp
  • deps/libscram/include/scram_hmac_cached.h
test/tap/tests/*-t.cpp

📄 CodeRabbit inference engine (CLAUDE.md)

Test files should follow naming pattern test_*.cpp or *-t.cpp in test/tap/tests/

Files:

  • test/tap/tests/pgsql-scram_cache_invalidation-t.cpp
lib/**/*.cpp

📄 CodeRabbit inference engine (CLAUDE.md)

One class per file typically in lib/ directory

Files:

  • lib/ProxySQL_Admin.cpp
🧠 Learnings (4)
📚 Learning: 2026-01-20T09:34:19.124Z
Learnt from: yuji-hatakeyama
Repo: sysown/proxysql PR: 5307
File: test/tap/tests/reg_test_5306-show_warnings_with_comment-t.cpp:39-48
Timestamp: 2026-01-20T09:34:19.124Z
Learning: In ProxySQL's TAP test suite, resource leaks (e.g., not calling mysql_close() on early return paths) are commonly tolerated because test processes are short-lived and OS frees resources on exit. This pattern applies to all C++ test files under test/tap/tests. When reviewing, recognize this as a project-wide test convention and focus on test correctness and isolation rather than insisting on fixing such leaks in these test files.

Applied to files:

  • test/tap/tests/pgsql-scram_cache_invalidation-t.cpp
📚 Learning: 2026-04-17T16:31:58.196Z
Learnt from: CR
Repo: sysown/proxysql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-17T16:31:58.196Z
Learning: Applies to **/*.{cpp,h,hpp} : C++17 is required; use conditional compilation via `#ifdef PROXYSQLGENAI`, `#ifdef PROXYSQL31`, etc. for feature-gated code

Applied to files:

  • lib/ProxySQL_Admin.cpp
📚 Learning: 2026-04-01T21:27:03.216Z
Learnt from: wazir-ahmed
Repo: sysown/proxysql PR: 5557
File: test/tap/tests/unit/gtid_set_unit-t.cpp:14-17
Timestamp: 2026-04-01T21:27:03.216Z
Learning: In ProxySQL's unit test directory (test/tap/tests/unit/), test_globals.h and test_init.h are only required for tests that depend on the ProxySQL runtime globals/initialization (i.e., tests that exercise components linked against libproxysql.a). Pure data-structure or utility tests (e.g., ezoption_parser_unit-t.cpp, gtid_set_unit-t.cpp, gtid_trxid_interval_unit-t.cpp) only need tap.h and the relevant project header — omitting test_globals.h and test_init.h is correct and intentional in these cases.

Applied to files:

  • lib/ProxySQL_Admin.cpp
📚 Learning: 2026-04-17T16:31:58.196Z
Learnt from: CR
Repo: sysown/proxysql PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-17T16:31:58.196Z
Learning: Applies to test/tap/tests/unit/**/*.cpp : Unit tests must use `test_globals.h` and `test_init.h` and link against `libproxysql.a` via the custom test harness

Applied to files:

  • lib/ProxySQL_Admin.cpp
🪛 checkmake (0.3.2)
tools/Makefile

[warning] 25-25: Required target "all" is missing from the Makefile.

(minphony)


[warning] 25-25: Required target "test" is missing from the Makefile.

(minphony)

🪛 Clang (14.0.6)
test/tap/tests/pgsql-scram_cache_invalidation-t.cpp

[error] 19-19: 'string' file not found

(clang-diagnostic-error)

tools/bench_connect.c

[error] 1-1: 'stdio.h' file not found

(clang-diagnostic-error)

🪛 Cppcheck (2.20.0)
tools/bench_connect.c

[warning] 20-20: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 132-132: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 133-133: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 134-134: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 135-135: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 139-139: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 152-152: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 153-153: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 154-154: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 155-155: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 156-156: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 157-157: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 158-158: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 168-168: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 173-173: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 173-173: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 174-174: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 174-174: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 175-175: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 176-176: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 180-180: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 181-181: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 182-182: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 200-200: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 204-204: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 206-206: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 207-207: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 209-209: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 213-213: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 224-224: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 228-228: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 230-230: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 230-230: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 232-232: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 236-236: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[error] 153-153: If memory allocation fails

(nullPointerArithmeticOutOfMemory)


[error] 154-154: If memory allocation fails

(nullPointerArithmeticOutOfMemory)


[error] 155-155: If memory allocation fails

(nullPointerArithmeticOutOfMemory)

🔇 Additional comments (3)
tools/Makefile (1)

21-23: Looks good for benchmark target integration.

Line 21 and Line 26 cleanly add build/cleanup coverage for bench_connect, which matches the PR objective.

Also applies to: 26-26

lib/ProxySQL_Admin.cpp (2)

41-41: Good dependency wiring for SCRAM cache invalidation.

Including scram_hmac_cached.h here is the right integration point for the new cache-invalidation hook.


6141-6143: Nice cache-coherency fix in the PgSQL users refresh path.

Hooking scram_cache_invalidate() into __refresh_pgsql_users ensures SCRAM verifier caches are refreshed when credentials are reloaded.

Comment thread deps/libscram/src/scram.c
Comment on lines +58 to +66
struct scram_verifier_cache_entry {
char *password; /* plaintext password (key) */
char *verifier; /* SCRAM-SHA-256$... verifier (value) */
};

static uint64_t scram_cache_generation = 0;
static __thread uint64_t scram_my_generation = 0;
static __thread struct scram_verifier_cache_entry scram_verifier_cache[SCRAM_VERIFIER_CACHE_SLOTS];
static __thread int scram_verifier_cache_count = 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Avoid caching recoverable plaintext passwords in TLS state.

strdup(password) makes each worker thread keep its own heap copy of user passwords until invalidation or thread teardown. That is a significant secret-retention increase for a perf-only optimization. Prefer keying the cache with a non-recoverable fingerprint and scrub both cached secrets before freeing them.

Also applies to: 68-75, 96-108

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@deps/libscram/src/scram.c` around lines 58 - 66, The cache currently stores
recoverable plaintext passwords in struct scram_verifier_cache_entry (password
field) via strdup(password), increasing secret retention; change the cache to
key entries by a non-recoverable fingerprint (e.g., HMAC/SHA256 of the password
plus salt) instead of storing the plaintext, stop calling strdup(password) into
scram_verifier_cache[].password, and ensure any existing plaintext or verifier
buffers are securely scrubbed (overwrite with zeros) before free; update logic
that uses scram_verifier_cache_count, scram_verifier_cache,
scram_cache_generation and scram_my_generation to index by the fingerprint key
and to invalidate/scrub entries on eviction or generation bump.

Comment thread deps/libscram/src/scram.c
Comment on lines +682 to 683
scram_fast_hmac_ctx* ctx = scram_fast_hmac_create();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard scram_fast_hmac_create() before the first HMAC operation.

deps/libscram/src/scram_hmac_cached.c returns NULL when the context allocation/OpenSSL setup fails. This path currently falls through to scram_fast_hmac_init() and then formats scram_fast_hmac_error(ctx) with a maybe-null context, which makes the OOM/failure path brittle. The same guard should be added to the other new HMAC call sites in this file as well.

Suggested fix
 	const char* errstr = NULL;
 	scram_fast_hmac_ctx* ctx = scram_fast_hmac_create();
+	if (!ctx) {
+		snprintf(errorBuffer, MAX_ERROR_LENGTH, "could not allocate HMAC context");
+		goto failed;
+	}

Also applies to: 725-750

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@deps/libscram/src/scram.c` around lines 682 - 683, The call to
scram_fast_hmac_create() must be NULL-checked before any subsequent HMAC calls;
update the sites in scram.c (including the one at the shown diff and the similar
sites around the 725–750 region) to verify the returned scram_fast_hmac_ctx* is
non-NULL immediately after scram_fast_hmac_create(), and if NULL avoid calling
scram_fast_hmac_init() or scram_fast_hmac_error() — instead propagate an
error/cleanup (return an error code or goto the existing cleanup path) and log
the failure using available logging routines so the OOM/OpenSSL setup failure is
handled safely.

Comment thread tools/bench_connect.c
Comment on lines +37 to +39
arg->connect_us = malloc(arg->iterations * sizeof(double));
arg->close_us = malloc(arg->iterations * sizeof(double));
arg->total_us = malloc(arg->iterations * sizeof(double));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Unchecked heap allocations can crash the benchmark on OOM.

Lines 37-39, 125-126, 146-148, and 171-172 dereference allocation results without checks.

💡 Suggested fix
+static void *xmalloc(size_t n) {
+    void *p = malloc(n);
+    if (!p) {
+        fprintf(stderr, "Out of memory\n");
+        exit(1);
+    }
+    return p;
+}
+
+static void *xcalloc(size_t n, size_t sz) {
+    void *p = calloc(n, sz);
+    if (!p) {
+        fprintf(stderr, "Out of memory\n");
+        exit(1);
+    }
+    return p;
+}
...
-    arg->connect_us = malloc(arg->iterations * sizeof(double));
-    arg->close_us   = malloc(arg->iterations * sizeof(double));
-    arg->total_us   = malloc(arg->iterations * sizeof(double));
+    arg->connect_us = xmalloc(arg->iterations * sizeof(double));
+    arg->close_us   = xmalloc(arg->iterations * sizeof(double));
+    arg->total_us   = xmalloc(arg->iterations * sizeof(double));
...
-    pthread_t *tids = calloc(threads, sizeof(pthread_t));
-    struct thread_arg *args = calloc(threads, sizeof(struct thread_arg));
+    pthread_t *tids = xcalloc(threads, sizeof(pthread_t));
+    struct thread_arg *args = xcalloc(threads, sizeof(struct thread_arg));
...
-    double *connect_us = malloc(total_n * sizeof(double));
-    double *close_us   = malloc(total_n * sizeof(double));
-    double *total_us   = malloc(total_n * sizeof(double));
+    double *connect_us = xmalloc(total_n * sizeof(double));
+    double *close_us   = xmalloc(total_n * sizeof(double));
+    double *total_us   = xmalloc(total_n * sizeof(double));
...
-    double *sorted_conn = malloc(n * sizeof(double));
-    double *sorted_total = malloc(n * sizeof(double));
+    double *sorted_conn = xmalloc(n * sizeof(double));
+    double *sorted_total = xmalloc(n * sizeof(double));

Also applies to: 125-126, 146-148, 171-172

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/bench_connect.c` around lines 37 - 39, The code performs multiple heap
allocations (e.g., arg->connect_us, arg->close_us, arg->total_us and other
mallocs at later sites) without checking for NULL; update each allocation site
to check the return value, handle OOM by freeing any earlier successful
allocations and returning an error (or exiting cleanly) instead of dereferencing
NULL, and ensure any caller (e.g., the function that allocates these arrays in
bench_connect.c) observes the failure so resources are not used; reference the
specific allocation targets arg->connect_us, arg->close_us, arg->total_us and
the other malloc results noted in the review and add proper NULL checks and
cleanup paths.

Comment thread tools/bench_connect.c
Comment on lines +98 to +115
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--host") == 0 && i+1 < argc) host = argv[++i];
else if (strcmp(argv[i], "--port") == 0 && i+1 < argc) port = atoi(argv[++i]);
else if (strcmp(argv[i], "--user") == 0 && i+1 < argc) user = argv[++i];
else if (strcmp(argv[i], "--password") == 0 && i+1 < argc) password = argv[++i];
else if (strcmp(argv[i], "--dbname") == 0 && i+1 < argc) dbname = argv[++i];
else if (strcmp(argv[i], "--ssl-ca") == 0 && i+1 < argc) ssl_ca = argv[++i];
else if (strcmp(argv[i], "--iterations") == 0 && i+1 < argc) iterations = atoi(argv[++i]);
else if (strcmp(argv[i], "--warmup") == 0 && i+1 < argc) warmup = atoi(argv[++i]);
else if (strcmp(argv[i], "--label") == 0 && i+1 < argc) label = argv[++i];
else if (strcmp(argv[i], "--threads") == 0 && i+1 < argc) threads = atoi(argv[++i]);
else { fprintf(stderr, "Unknown arg: %s\n", argv[i]); return 1; }
}

if (iterations > MAX_ITERS) {
fprintf(stderr, "Too many iterations (max %d)\n", MAX_ITERS);
return 1;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Validate CLI numeric inputs and guard threads * iterations overflow.

Line 112 only caps the upper bound of iterations. Invalid values (e.g., --threads 0, negative values, or overflow at Line 145) can produce n<=0, divide-by-zero at Line 184, and invalid indexing at Lines 200-213.

💡 Suggested fix
+#include <limits.h>
...
-    if (iterations > MAX_ITERS) {
-        fprintf(stderr, "Too many iterations (max %d)\n", MAX_ITERS);
+    if (iterations <= 0 || iterations > MAX_ITERS || threads <= 0 || warmup < 0) {
+        fprintf(stderr, "Invalid args: iterations must be 1..%d, threads > 0, warmup >= 0\n", MAX_ITERS);
         return 1;
     }
+    if (threads > INT_MAX / iterations) {
+        fprintf(stderr, "threads * iterations overflow\n");
+        return 1;
+    }
...
-    int total_n = threads * iterations;
+    int total_n = threads * iterations;

Also applies to: 145-146, 184-184, 200-213

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/bench_connect.c` around lines 98 - 115, Validate and clamp all numeric
CLI inputs: ensure iterations is between 1 and MAX_ITERS, threads >= 1, warmup
>= 0 (and warmup <= iterations if intended), and port is in a valid range;
reject or error on invalid values (e.g., "--threads 0" or negative numbers).
Before computing any product like total_ops = threads * iterations (used later
to derive n and that can overflow), perform the multiplication in an unsigned
wider type (e.g., uint64_t/size_t) and explicitly check for overflow or that
total_ops > 0; ensure any derived n is > 0 before doing divisions to avoid
divide-by-zero. Return a non-zero error when validation fails and add clear
stderr messages referencing the offending flag (iterations, threads, warmup,
port) so downstream code (uses of n and indexing into per-thread arrays) never
sees invalid or overflowing sizes.

Comment thread tools/bench_connect.c
Comment on lines +136 to +139
pthread_create(&tids[t], NULL, thread_worker, &args[t]);
}
for (int t = 0; t < threads; t++)
pthread_join(tids[t], NULL);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Check pthread_create / pthread_join return codes.

Line 136 and Line 139 ignore failures; a failed create/join can leave invalid benchmark state and misleading results.

💡 Suggested fix
     for (int t = 0; t < threads; t++) {
         args[t].thread_id = t;
         strncpy(args[t].conninfo, conninfo, sizeof(args[t].conninfo) - 1);
         args[t].iterations = iterations;
         args[t].warmup = warmup;
-        pthread_create(&tids[t], NULL, thread_worker, &args[t]);
+        int rc = pthread_create(&tids[t], NULL, thread_worker, &args[t]);
+        if (rc != 0) {
+            fprintf(stderr, "pthread_create failed for thread %d (rc=%d)\n", t, rc);
+            return 1;
+        }
     }
-    for (int t = 0; t < threads; t++)
-        pthread_join(tids[t], NULL);
+    for (int t = 0; t < threads; t++) {
+        int rc = pthread_join(tids[t], NULL);
+        if (rc != 0) {
+            fprintf(stderr, "pthread_join failed for thread %d (rc=%d)\n", t, rc);
+            return 1;
+        }
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pthread_create(&tids[t], NULL, thread_worker, &args[t]);
}
for (int t = 0; t < threads; t++)
pthread_join(tids[t], NULL);
for (int t = 0; t < threads; t++) {
args[t].thread_id = t;
strncpy(args[t].conninfo, conninfo, sizeof(args[t].conninfo) - 1);
args[t].iterations = iterations;
args[t].warmup = warmup;
int rc = pthread_create(&tids[t], NULL, thread_worker, &args[t]);
if (rc != 0) {
fprintf(stderr, "pthread_create failed for thread %d (rc=%d)\n", t, rc);
return 1;
}
}
for (int t = 0; t < threads; t++) {
int rc = pthread_join(tids[t], NULL);
if (rc != 0) {
fprintf(stderr, "pthread_join failed for thread %d (rc=%d)\n", t, rc);
return 1;
}
}
🧰 Tools
🪛 Cppcheck (2.20.0)

[warning] 139-139: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/bench_connect.c` around lines 136 - 139, Check and handle return values
from pthread_create and pthread_join around the loop that spawns thread_worker
using tids, args and threads: capture each pthread_create return value, and on
non-zero log an error (including errno via strerror if desired), clean up any
allocated resources and exit or return a failure code instead of proceeding;
similarly capture each pthread_join return value and handle join failures (log
and abort/cleanup) to avoid inconsistent benchmark state. Ensure the error
handling occurs in the same scope where tids and args are used so threads are
not assumed to have started or completed when they have not.

Comment thread tools/bench_connect.c
Comment on lines +171 to +176
double *sorted_conn = malloc(n * sizeof(double));
double *sorted_total = malloc(n * sizeof(double));
memcpy(sorted_conn, connect_us, n * sizeof(double));
memcpy(sorted_total, total_us, n * sizeof(double));
qsort(sorted_conn, n, sizeof(double), cmp_double);
qsort(sorted_total, n, sizeof(double), cmp_double);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

close_us min/p50 are computed from unsorted data.

Line 206/207 and Line 230 read close_us[0]/close_us[n/2] without sorting, so reported close min/p50 are incorrect.

💡 Suggested fix
-    double *sorted_conn = malloc(n * sizeof(double));
-    double *sorted_total = malloc(n * sizeof(double));
+    double *sorted_conn = malloc(n * sizeof(double));
+    double *sorted_close = malloc(n * sizeof(double));
+    double *sorted_total = malloc(n * sizeof(double));
     memcpy(sorted_conn, connect_us, n * sizeof(double));
+    memcpy(sorted_close, close_us, n * sizeof(double));
     memcpy(sorted_total, total_us, n * sizeof(double));
     qsort(sorted_conn, n, sizeof(double), cmp_double);
+    qsort(sorted_close, n, sizeof(double), cmp_double);
     qsort(sorted_total, n, sizeof(double), cmp_double);
...
-    printf("  %-12s %10.1f %10.1f %10.1f\n",
-           "Close", close_us[0], avg_close,
-           close_us[n/2]);
+    printf("  %-12s %10.1f %10.1f %10.1f\n",
+           "Close", sorted_close[0], avg_close,
+           percentile(sorted_close, n, 0.50));
...
-    fprintf(stderr, "\"close_us\":{\"min\":%.1f,\"avg\":%.1f,\"p50\":%.1f},",
-            close_us[0], avg_close, close_us[n/2]);
+    fprintf(stderr, "\"close_us\":{\"min\":%.1f,\"avg\":%.1f,\"p50\":%.1f},",
+            sorted_close[0], avg_close, percentile(sorted_close, n, 0.50));
...
-    free(sorted_conn); free(sorted_total);
+    free(sorted_conn); free(sorted_close); free(sorted_total);

Also applies to: 205-207, 229-230, 241-241

🧰 Tools
🪛 Cppcheck (2.20.0)

[warning] 173-173: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 173-173: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 174-174: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 174-174: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 175-175: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 176-176: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/bench_connect.c` around lines 171 - 176, The min/p50 for close_us are
being read from the unsorted close_us array (uses close_us[0] and close_us[n/2])
which yields incorrect stats; follow the pattern used for connect_us/total_us by
allocating a new array (e.g., double *sorted_close), memcpy close_us into it,
qsort(sorted_close, n, sizeof(double), cmp_double), then use sorted_close[0] and
sorted_close[n/2] where the code currently reads close_us[0]/close_us[n/2];
remember to free sorted_close when done and keep naming consistent with
sorted_conn/sorted_total.

@renecannao
renecannao changed the base branch from feature/pgbouncer-compat to v3.0 May 4, 2026 08:03
renecannao added 2 commits May 4, 2026 15:09
On OpenSSL 3.x, EVP_sha256() triggers EVP_MD_fetch() on every call,
involving global lock contention and provider property lookups. The
vendored PostgreSQL hmac_openssl.c calls it on every HMAC_Init_ex(),
which means 4096 redundant fetches per SCRAM PBKDF2 derivation alone.

This was responsible for ~58% of CPU time during SCRAM-heavy workloads,
making ProxySQL 1.64x slower than PgBouncer at connect/disconnect.

Two optimizations:

1. Cached EVP_MD* for HMAC (scram_hmac_cached.c):
   New scram_fast_* API that fetches the SHA-256 EVP_MD once via
   pthread_once and reuses it across all HMAC operations. Replaces
   all pg_hmac_* calls in libscram with the cached versions.
   No changes to vendored PostgreSQL code.

2. SCRAM verifier cache for plaintext passwords (scram.c):
   When ProxySQL stores plaintext passwords, every SCRAM exchange
   runs the full PBKDF2 (4096 HMAC iterations). This cache stores
   the generated SCRAM verifier after the first successful auth,
   so subsequent connections skip PBKDF2 entirely -- reducing
   HMAC operations from ~4096 to ~4 per connection, matching
   PgBouncer's behavior when a SCRAM verifier is stored directly.
   Thread-local, no locks needed.

Result: ProxySQL connect/disconnect performance goes from 1.64x
slower to parity with PgBouncer. Perf profile shifts from
pg_hmac_init/EVP_MD_fetch (58%) to do_ssl_handshake (22%) --
the actual TLS work.
Generation counter for SCRAM verifier cache:
- Global atomic counter incremented on pgsql_users reload
- Thread-local caches check generation on lookup; stale entries are
  cleared and rebuilt from scratch
- scram_cache_invalidate() called from __refresh_pgsql_users()
- extern "C" guards added to scram_hmac_cached.h for C++ callers

TAP test (pgsql-scram_cache_invalidation-t):
- Creates user with pass1, populates SCRAM cache via connect loop
- Changes password to pass2, reloads users (invalidates cache)
- Verifies pass2 works and pass1 is rejected
- Assigned to legacy-g4 test groups

Benchmark tool (tools/bench_connect):
- Multi-threaded SCRAM+TLS connect/disconnect benchmark
- Uses vendored PostgreSQL from deps/
- Added to tools/Makefile with clean target
@renecannao
renecannao force-pushed the perf/scram-cached-hmac branch from 6d8dd5f to e9637be Compare May 4, 2026 08:09
@sonarqubecloud

sonarqubecloud Bot commented May 4, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
Image 1 Security Hotspot

See analysis details on SonarQube Cloud

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.

1 participant