Skip to content

test(pgsql): regression tests for #5865 review asks (SCRAM verifier / md5 credential storage) - #5932

Merged
rahim-kanji merged 6 commits into
v3.0_pgsql-auth-5863from
test/pgsql-5865-tests-wt
Jul 20, 2026
Merged

test(pgsql): regression tests for #5865 review asks (SCRAM verifier / md5 credential storage)#5932
rahim-kanji merged 6 commits into
v3.0_pgsql-auth-5863from
test/pgsql-5865-tests-wt

Conversation

@renecannao

@renecannao renecannao commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the six regression tests requested in the review of #5865 (SCRAM verifier / md5 credential storage + SCRAM/md5 backend pass-through for PostgreSQL). Built and run against #5865's own head (v3.0_pgsql-auth-5863) and its patched libpq.

Intended to land WITH #5865's code fixes — see "Two RED tests" below: two of these tests are deterministic failures that document real bugs in the current #5865 credential-lifecycle handling. They turn green with no test change once those two bugs are fixed. Merging this before the fixes will show legacy-g4 (and its mysql-*-g4 variants) red — that is intentional and is the point.

The six tests (one per review ask)

# Test Result
1 pgsql-verifier_backend_kill-t 🔴 BUG — backend not terminated on disconnect for verifier/md5 users
2 pgsql-scram_reload_midhandshake-t 🟢 clean — mid-handshake credential reload is atomic (bound to the verifier server-first used)
3 pgsql-verifier_pool_rotation-t 🔴 BUG — after password rotation A→B, B is served by the A-authenticated backend conn
4 pgsql-md5_passthrough-t (+ md5 infra user) 🟢 clean — md5_secret backend pass-through works (first real exercise of the path)
5 pgsql-libpq_scram_params-t 🟢 clean — patched-libpq param validation; partial/malformed/verifier-as-password all rejected; no security findings
6 pgsql-verifier_auth-t (edit) test-isolation fix — restore the original pgsql-authentication_method, not a hardcoded 3

Full root-cause + fix-direction analysis for the two bugs is posted as a review comment on #5865.

Notes for review

  • Test-only. No lib//src//include/ changes. The pg_lite_client test helper gains a stepwise SASL client (saslBegin/saslFinish) used only by test 2; all its existing consumers were re-linked with -lscram -lusual (that library is already vendored and used by the proxysql binary).
  • Shared infra change (test 4): adds one md5user + a user-scoped md5 line in docker-pgsql16-single's pg_hba.conf, above the scram catch-all (first-match-wins) so every existing scram user is unaffected. Verified: the scram-dependent pgsql tests (pgsql-verifier_auth-t 11/11, pgsql-verifier_passthrough-t 3/3) still pass against the extended infra.
  • Determinism: the two RED tests were confirmed deterministically-red on legacy-g4, each backed by a passing control assertion (e.g. the kill test's kill=false → PID survives; the rotation test's old password rejected + B authenticates) that rules out env/infra artifacts.
  • Heads-up (unrelated, unreproduced): a one-off SIGFPE in PgSQL_Session::writeout() was seen once during off-harness manipulation; it did not recur and involves none of these test files — flagged for awareness, not chased here.

Summary by CodeRabbit

  • Bug Fixes

    • Improved PostgreSQL authentication handling for MD5 and SCRAM credentials.
    • Added validation for SCRAM key parameters and clearer rejection of invalid or incomplete credentials.
    • Improved behavior during credential reloads, connection-pool rotation, and abrupt client disconnects.
    • Preserved authentication settings when running authentication-related operations.
  • Tests

    • Added regression coverage for MD5 pass-through, SCRAM authentication, mid-handshake reloads, backend cleanup, and connection isolation.
    • Expanded PostgreSQL test environments to cover current authentication and schema permission requirements.

…view ask 2)

Adds pgsql-scram_reload_midhandshake-t: drives a raw SCRAM-SHA-256 handshake
stepwise, rotates pgsql_users.password + LOAD PGSQL USERS TO RUNTIME between
server-first and client-final, then sends the client-final computed for the
ORIGINAL verifier. libpq cannot express this (it drives SASL atomically).

Brings the SCRAM/MD5-capable pg_lite_client from test/pgsql-protocol-testing-design
(wholesale copy of the two files -- strictly additive: getLastAuthType, MD5/SASL
handleAuthentication branches, doSASLAuth) and adds a stepwise wrapper on top:
rawConnectStartup() + saslBegin() (client-first -> server-first) + saslFinish()
(client-final -> AuthenticationOk / clean-reject sentinel). The library stays
decoupled from TAP (no diag()). Every Makefile consumer of pg_lite_client.cpp now
links -lscram -lusual (relinked+verified) since the shared source uses libscram
unconditionally.

Maintainer decision (2026-07-11): PIN OBSERVED BEHAVIOR. The test asserts the
observed contract rather than choosing one, and reports it via diag for blessing.
Either outcome (bound-to-original OR fail-closed) is a PASS; a hang/crash/desync/
unusable session would be a FINDING.

OBSERVED (legacy-g4 / docker-pgsql16-single, PR #5865 head): contract (A)
bound-to-original -- the client-final for verifier A is ACCEPTED after the runtime
verifier is rotated to B (LOAD completes before client-final is sent), ReadyForQuery
is received (session in sync), and a fresh login with the current verifier B still
succeeds (ProxySQL healthy, no lasting damage). Deterministic by construction (no
race). 3/3 PASS.
…_auth-t (#5865 review ask 6, test isolation)

pgsql-verifier_auth-t.cpp mutated the pgsql-authentication_method floor
across several scenarios and restored it to a hardcoded "3" instead of
whatever value the suite actually started with, risking a silent floor
change leaking into sibling legacy-g4 tests if the suite default ever
differs from 3. Snapshot the original value from
runtime_global_variables right after connecting, BAIL_OUT if the read
comes back empty, and restore that exact value at every restore point
(including a final unconditional restore before cleanup). No assertions
changed; all 11 existing assertions still pass.
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The changes expand PostgreSQL TAP infrastructure with MD5 and SCRAM authentication support, add stepwise SCRAM handling, provision MD5 test credentials, and introduce regression tests for SCRAM parameters, verifier reloads, backend termination, and connection-pool rotation.

Changes

PostgreSQL authentication regressions

Layer / File(s) Summary
Authentication test infrastructure
test/infra/docker-pgsql16-single/..., test/tap/groups/groups.json, test/tap/tests/Makefile
Adds MD5 test-user provisioning and pg_hba.conf rules, registers new TAP tests, and updates PostgreSQL test link recipes.
Lite client authentication flows
test/tap/tests/pg_lite_client.h, test/tap/tests/pg_lite_client.cpp
Adds MD5 authentication, complete SCRAM handling, error extraction, and stepwise SASL APIs with persistent state.
SCRAM and MD5 parameter coverage
test/tap/tests/pgsql-libpq_scram_params-t.cpp, test/tap/tests/pgsql-md5_passthrough-t.cpp
Tests SCRAM key conninfo parameters, derived key authentication, MD5 verifier pass-through, invalid credentials, and cleanup.
Mid-handshake reload validation
test/tap/tests/pgsql-scram_reload_midhandshake-t.cpp, test/tap/tests/pgsql-verifier_auth-t.cpp
Tests verifier rotation during SCRAM authentication and restores the original authentication-method floor.
Backend termination behavior
test/tap/tests/pgsql-verifier_backend_kill-t.cpp
Tests backend termination after abrupt frontend disconnects for SCRAM and conditional MD5 authentication.
Verifier rotation pool isolation
test/tap/tests/pgsql-verifier_pool_rotation-t.cpp
Tests password rotation, rejection of the old password, and distinct backend identities for successive pooled sessions.

Estimated code review effort: 5 (Critical) | ~90 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Test as pgsql-verifier_pool_rotation-t
  participant ProxySQL
  participant PostgreSQL
  participant Admin as ProxySQL admin
  Test->>ProxySQL: authenticate with password A
  ProxySQL->>PostgreSQL: establish backend session
  Test->>Admin: update verifier to password B and load runtime
  Test->>ProxySQL: authenticate with password B
  ProxySQL->>PostgreSQL: establish new backend session
  Test-->>Test: compare backend identities
Loading

Poem

A rabbit hops through SCRAM’s bright maze,
MD5 keys sparkle in test-suite rays.
Verifiers turn, old pools depart,
Fresh backend paths beat with heart.
“Green taps!” I cheer, and wiggle my ears.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the test-focused changes around SCRAM verifier and MD5 credential storage coverage for #5865.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test/pgsql-5865-tests-wt

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.

@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 comprehensive regression tests and infrastructure updates to validate PostgreSQL authentication features, specifically focusing on SCRAM-SHA-256 and MD5 authentication, verifier pass-through, mid-handshake reloads, backend connection termination, and pool isolation across password rotations. It adds several new test files and extends the mock client pg_lite_client to support stepwise SASL and MD5 authentication. The review feedback highlights critical exception-safety issues in doSASLAuth and saslFinish where raw pointers could be leaked if network or protocol exceptions are thrown, suggesting the use of RAII smart pointers to manage these resources.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +419 to +506
void PgConnection::doSASLAuth(const std::string& password,
const std::vector<uint8_t>& /*mechListMsg*/) {
ScramState* st = scram_state_init();
PgCredentials cred;
memset(&cred, 0, sizeof(cred));
snprintf(cred.name, sizeof(cred.name), "%s", user_.c_str());
snprintf(cred.passwd, sizeof(cred.passwd), "%s", password.c_str());
cred.has_scram_keys = false;

char type;
std::vector<uint8_t> buffer;
char* client_first = nullptr;
char* client_final = nullptr;

// 1) SASLInitialResponse ('p'): mechanism name + Int32 length + client-first-message.
// libscram's build_client_first_message already includes the "n,,"" GS2 header
// (it returns "n,,n=,r=<nonce>"), so we send it verbatim.
client_first = build_client_first_message(st);
if (!client_first) { free_scram_state(st); throw PgException(std::string("scram client-first: ") + scram_error()); }
{
std::vector<uint8_t> pkt;
writeStringToBuffer(pkt, "SCRAM-SHA-256"); // null-terminated mechanism name
int32_t clen = htonl((int32_t)strlen(client_first));
const uint8_t* cp = reinterpret_cast<const uint8_t*>(&clen);
pkt.insert(pkt.end(), cp, cp + 4); // Int32 length of client-first
pkt.insert(pkt.end(), client_first, client_first + strlen(client_first));
sendMessage('p', pkt);
}

// 2) Expect AuthenticationSASLContinue (authType 11) with the server-first-message.
readMessage(type, buffer);
if (type == ERROR_RESPONSE) {
free(client_first); free_scram_state(st);
throw PgException("scram: " + extractErrorMessage(buffer));
}
if (type != AUTH_TYPE || buffer.size() < 4 ||
ntohl(*reinterpret_cast<int32_t*>(buffer.data())) != 11) {
free(client_first); free_scram_state(st);
throw PgException("expected AuthenticationSASLContinue(11)");
}
std::string server_first(reinterpret_cast<const char*>(buffer.data()) + 4, buffer.size() - 4);
char* server_nonce = nullptr; char* salt = nullptr; int saltlen = 0; int iterations = 0;
if (!read_server_first_message(st, const_cast<char*>(server_first.c_str()),
&server_nonce, &salt, &saltlen, &iterations)) {
free(client_first); free_scram_state(st);
throw PgException(std::string("scram read server-first: ") + scram_error());
}

// 3) SASLResponse ('p'): client-final-message (with proof derived from plaintext passwd).
client_final = build_client_final_message(st, &cred, server_nonce, salt, saltlen, iterations);
free(salt); // read_server_first_message malloc'd salt and handed us ownership;
salt = nullptr; // build_client_final_message is its only consumer (just read above).
if (!client_final) { free(client_first); free_scram_state(st); throw PgException(std::string("scram client-final: ") + scram_error()); }
{
std::vector<uint8_t> pkt(client_final, client_final + strlen(client_final));
sendMessage('p', pkt);
}

// 4) Expect AuthenticationSASLFinal (authType 12) with server-final (v=ServerSignature).
// A wrong password surfaces here as an ErrorResponse instead.
readMessage(type, buffer);
if (type == ERROR_RESPONSE) {
free(client_first); free(client_final); free_scram_state(st);
throw PgException("scram: " + extractErrorMessage(buffer));
}
if (type != AUTH_TYPE || buffer.size() < 4 ||
ntohl(*reinterpret_cast<int32_t*>(buffer.data())) != 12) {
free(client_first); free(client_final); free_scram_state(st);
throw PgException("expected AuthenticationSASLFinal(12)");
}
{
std::string server_final(reinterpret_cast<const char*>(buffer.data()) + 4, buffer.size() - 4);
char server_sig[256] = {0};
if (!read_server_final_message(const_cast<char*>(server_final.c_str()), server_sig) ||
!verify_server_signature(st, &cred, server_sig)) {
free(client_first); free(client_final); free_scram_state(st);
throw PgException("scram server signature verification failed");
}
}
free(client_first); free(client_final); free_scram_state(st);

// 5) Expect AuthenticationOk (0).
readMessage(type, buffer);
if (type == ERROR_RESPONSE) throw PgException("scram: " + extractErrorMessage(buffer));
if (type == AUTH_TYPE && buffer.size() >= 4 &&
ntohl(*reinterpret_cast<int32_t*>(buffer.data())) == 0) return;
throw PgException("scram: no AuthenticationOk after SASLFinal");
}

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 doSASLAuth function is not exception-safe. If sendMessage or readMessage throws a PgException, the allocated ScramState (st), client_first, and client_final will be leaked because they are raw pointers and are not managed by RAII. Using std::unique_ptr with custom deleters ensures that these resources are automatically freed under all execution paths, including when exceptions are thrown.

void PgConnection::doSASLAuth(const std::string& password,
                             const std::vector<uint8_t>& /*mechListMsg*/) {
    std::unique_ptr<ScramState, decltype(&free_scram_state)> st(scram_state_init(), free_scram_state);
    if (!st) throw PgException("Failed to initialize scram state");

    PgCredentials cred;
    memset(&cred, 0, sizeof(cred));
    snprintf(cred.name, sizeof(cred.name), "%s", user_.c_str());
    snprintf(cred.passwd, sizeof(cred.passwd), "%s", password.c_str());
    cred.has_scram_keys = false;

    char type;
    std::vector<uint8_t> buffer;
    std::unique_ptr<char, void(*)(void*)> client_first(nullptr, free);
    std::unique_ptr<char, void(*)(void*)> client_final(nullptr, free);

    // 1) SASLInitialResponse ('p'): mechanism name + Int32 length + client-first-message.
    //    libscram's build_client_first_message already includes the "n,,"" GS2 header
    //    (it returns "n,,n=,r=<nonce>"), so we send it verbatim.
    client_first.reset(build_client_first_message(st.get()));
    if (!client_first) { throw PgException(std::string("scram client-first: ") + scram_error()); }
    {
        std::vector<uint8_t> pkt;
        writeStringToBuffer(pkt, "SCRAM-SHA-256");    // null-terminated mechanism name
        int32_t clen = htonl((int32_t)strlen(client_first.get()));
        const uint8_t* cp = reinterpret_cast<const uint8_t*>(&clen);
        pkt.insert(pkt.end(), cp, cp + 4);            // Int32 length of client-first
        pkt.insert(pkt.end(), client_first.get(), client_first.get() + strlen(client_first.get()));
        sendMessage('p', pkt);
    }

    // 2) Expect AuthenticationSASLContinue (authType 11) with the server-first-message.
    readMessage(type, buffer);
    if (type == ERROR_RESPONSE) {
        throw PgException("scram: " + extractErrorMessage(buffer));
    }
    if (type != AUTH_TYPE || buffer.size() < 4 ||
        ntohl(*reinterpret_cast<int32_t*>(buffer.data())) != 11) {
        throw PgException("expected AuthenticationSASLContinue(11)");
    }
    std::string server_first(reinterpret_cast<const char*>(buffer.data()) + 4, buffer.size() - 4);
    char* server_nonce = nullptr; char* salt = nullptr; int saltlen = 0; int iterations = 0;
    if (!read_server_first_message(st.get(), const_cast<char*>(server_first.c_str()),
                                   &server_nonce, &salt, &saltlen, &iterations)) {
        throw PgException(std::string("scram read server-first: ") + scram_error());
    }
    std::unique_ptr<char, void(*)(void*)> salt_guard(salt, free);

    // 3) SASLResponse ('p'): client-final-message (with proof derived from plaintext passwd).
    client_final.reset(build_client_final_message(st.get(), &cred, server_nonce, salt_guard.get(), saltlen, iterations));
    if (!client_final) { throw PgException(std::string("scram client-final: ") + scram_error()); }
    {
        std::vector<uint8_t> pkt(client_final.get(), client_final.get() + strlen(client_final.get()));
        sendMessage('p', pkt);
    }

    // 4) Expect AuthenticationSASLFinal (authType 12) with server-final (v=ServerSignature).
    //    A wrong password surfaces here as an ErrorResponse instead.
    readMessage(type, buffer);
    if (type == ERROR_RESPONSE) {
        throw PgException("scram: " + extractErrorMessage(buffer));
    }
    if (type != AUTH_TYPE || buffer.size() < 4 ||
        ntohl(*reinterpret_cast<int32_t*>(buffer.data())) != 12) {
        throw PgException("expected AuthenticationSASLFinal(12)");
    }
    {
        std::string server_final(reinterpret_cast<const char*>(buffer.data()) + 4, buffer.size() - 4);
        char server_sig[256] = {0};
        if (!read_server_final_message(const_cast<char*>(server_final.c_str()), server_sig) ||
            !verify_server_signature(st.get(), &cred, server_sig)) {
            throw PgException("scram server signature verification failed");
        }
    }

    // 5) Expect AuthenticationOk (0).
    readMessage(type, buffer);
    if (type == ERROR_RESPONSE) throw PgException("scram: " + extractErrorMessage(buffer));
    if (type == AUTH_TYPE && buffer.size() >= 4 &&
        ntohl(*reinterpret_cast<int32_t*>(buffer.data())) == 0) return;
    throw PgException("scram: no AuthenticationOk after SASLFinal");
}

Comment on lines +615 to +680
int PgConnection::saslFinish() {
if (!sasl_st_) throw PgException("saslFinish: saslBegin() was not called");

PgCredentials cred;
memset(&cred, 0, sizeof(cred));
snprintf(cred.name, sizeof(cred.name), "%s", user_.c_str());
snprintf(cred.passwd, sizeof(cred.passwd), "%s", sasl_password_.c_str());
cred.has_scram_keys = false;

char type;
std::vector<uint8_t> buffer;

// 3) SASLResponse ('p'): client-final-message.
char* client_final = build_client_final_message(sasl_st_, &cred, sasl_server_nonce_,
sasl_salt_, sasl_saltlen_, sasl_iterations_);
if (!client_final) {
std::string e = scram_error(); freeSaslState();
throw PgException(std::string("scram client-final: ") + e);
}
{
std::vector<uint8_t> pkt(client_final, client_final + strlen(client_final));
sendMessage('p', pkt);
}

// 4) AuthenticationSASLFinal(12) OR a clean ErrorResponse (rejected verifier).
readMessage(type, buffer);
if (type == ERROR_RESPONSE) {
last_auth_type_ = -1;
last_error_ = extractErrorMessage(buffer); // clean server rejection of the client-final
free(client_final); freeSaslState();
return SASL_FINISH_REJECTED;
}
if (type != AUTH_TYPE || buffer.size() < 4 ||
ntohl(*reinterpret_cast<int32_t*>(buffer.data())) != 12) {
free(client_final); freeSaslState();
throw PgException("expected AuthenticationSASLFinal(12)");
}
{
std::string server_final(reinterpret_cast<const char*>(buffer.data()) + 4, buffer.size() - 4);
char server_sig[256] = {0};
if (!read_server_final_message(const_cast<char*>(server_final.c_str()), server_sig) ||
!verify_server_signature(sasl_st_, &cred, server_sig)) {
free(client_final); freeSaslState();
throw PgException("scram server signature verification failed");
}
}
free(client_final);

// 5) AuthenticationOk(0).
readMessage(type, buffer);
if (type == ERROR_RESPONSE) {
last_auth_type_ = -1;
last_error_ = extractErrorMessage(buffer); // rejected between SASLFinal and AuthenticationOk
freeSaslState();
return SASL_FINISH_REJECTED;
}
if (type == AUTH_TYPE && buffer.size() >= 4 &&
ntohl(*reinterpret_cast<int32_t*>(buffer.data())) == 0) {
last_auth_type_ = 0;
freeSaslState();
return 0;
}
freeSaslState();
throw PgException("scram: no AuthenticationOk after SASLFinal");
}

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 saslFinish function has potential memory leaks. If sendMessage or readMessage throws a PgException, the allocated client_final string is leaked. Wrapping client_final in a std::unique_ptr with a custom deleter ensures it is safely and automatically freed on all execution paths.

int PgConnection::saslFinish() {
    if (!sasl_st_) throw PgException("saslFinish: saslBegin() was not called");

    PgCredentials cred;
    memset(&cred, 0, sizeof(cred));
    snprintf(cred.name, sizeof(cred.name), "%s", user_.c_str());
    snprintf(cred.passwd, sizeof(cred.passwd), "%s", sasl_password_.c_str());
    cred.has_scram_keys = false;

    char type;
    std::vector<uint8_t> buffer;

    // 3) SASLResponse ('p'): client-final-message.
    std::unique_ptr<char, void(*)(void*)> client_final(build_client_final_message(sasl_st_, &cred, sasl_server_nonce_,
                                                     sasl_salt_, sasl_saltlen_, sasl_iterations_), free);
    if (!client_final) {
        std::string e = scram_error(); freeSaslState();
        throw PgException(std::string("scram client-final: ") + e);
    }
    {
        std::vector<uint8_t> pkt(client_final.get(), client_final.get() + strlen(client_final.get()));
        sendMessage('p', pkt);
    }

    // 4) AuthenticationSASLFinal(12) OR a clean ErrorResponse (rejected verifier).
    readMessage(type, buffer);
    if (type == ERROR_RESPONSE) {
        last_auth_type_ = -1;
        last_error_ = extractErrorMessage(buffer);  // clean server rejection of the client-final
        freeSaslState();
        return SASL_FINISH_REJECTED;
    }
    if (type != AUTH_TYPE || buffer.size() < 4 ||
        ntohl(*reinterpret_cast<int32_t*>(buffer.data())) != 12) {
        freeSaslState();
        throw PgException("expected AuthenticationSASLFinal(12)");
    }
    {
        std::string server_final(reinterpret_cast<const char*>(buffer.data()) + 4, buffer.size() - 4);
        char server_sig[256] = {0};
        if (!read_server_final_message(const_cast<char*>(server_final.c_str()), server_sig) ||
            !verify_server_signature(sasl_st_, &cred, server_sig)) {
            freeSaslState();
            throw PgException("scram server signature verification failed");
        }
    }

    // 5) Expect AuthenticationOk(0).
    readMessage(type, buffer);
    if (type == ERROR_RESPONSE) {
        last_auth_type_ = -1;
        last_error_ = extractErrorMessage(buffer);  // rejected between SASLFinal and AuthenticationOk
        freeSaslState();
        return SASL_FINISH_REJECTED;
    }
    if (type == AUTH_TYPE && buffer.size() >= 4 &&
        ntohl(*reinterpret_cast<int32_t*>(buffer.data())) == 0) {
        last_auth_type_ = 0;
        freeSaslState();
        return 0;
    }
    freeSaslState();
    throw PgException("scram: no AuthenticationOk after SASLFinal");
}

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
D Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

💡 Need a hand with PR review? Try Gitar by Sonar!

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/tap/tests/pg_lite_client.cpp`:
- Around line 603-609: Update the SCRAM flow around saslBegin() and
read_server_first_message() so sasl_server_nonce_ owns a copy of the returned
server nonce rather than referencing the temporary server_first buffer. Ensure
that backing storage remains valid until saslFinish() dereferences
sasl_server_nonce_, while preserving the existing server-first parsing and error
handling.

In `@test/tap/tests/pgsql-scram_reload_midhandshake-t.cpp`:
- Around line 91-135: Ensure the test always emits the assertion counted by
plan(3) when rawConnectStartup or saslBegin throws. Update the PgException catch
block to record a failed server-first assertion before producing the existing
final contract assertion, while preserving the current timeout and
clean-rejection outcome handling.
🪄 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: 3ec04923-50d6-44df-a099-81065f2aa28e

📥 Commits

Reviewing files that changed from the base of the PR and between 5793528 and 03f78dd.

📒 Files selected for processing (12)
  • test/infra/docker-pgsql16-single/bin/docker-pgsql-post.bash
  • test/infra/docker-pgsql16-single/conf/pgsql/pgsql1/pg_hba.conf
  • test/tap/groups/groups.json
  • test/tap/tests/Makefile
  • test/tap/tests/pg_lite_client.cpp
  • test/tap/tests/pg_lite_client.h
  • test/tap/tests/pgsql-libpq_scram_params-t.cpp
  • test/tap/tests/pgsql-md5_passthrough-t.cpp
  • test/tap/tests/pgsql-scram_reload_midhandshake-t.cpp
  • test/tap/tests/pgsql-verifier_auth-t.cpp
  • test/tap/tests/pgsql-verifier_backend_kill-t.cpp
  • test/tap/tests/pgsql-verifier_pool_rotation-t.cpp
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
test/tap/tests/**/*.cpp

📄 CodeRabbit inference engine (CLAUDE.md)

test/tap/tests/**/*.cpp: Test files in test/tap/tests/ must follow the naming pattern test_*.cpp or *-t.cpp.
To add a new TAP test, add the <testname>-t.cpp file and register it in test/tap/tests/Makefile/groups.json; no special Makefile target is needed because make <testname>-t is generated by pattern rule.

Files:

  • test/tap/tests/pgsql-md5_passthrough-t.cpp
  • test/tap/tests/pgsql-verifier_auth-t.cpp
  • test/tap/tests/pgsql-verifier_backend_kill-t.cpp
  • test/tap/tests/pgsql-scram_reload_midhandshake-t.cpp
  • test/tap/tests/pgsql-libpq_scram_params-t.cpp
  • test/tap/tests/pgsql-verifier_pool_rotation-t.cpp
  • test/tap/tests/pg_lite_client.cpp
**/*.{cpp,h,hpp}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{cpp,h,hpp}: Class names must use PascalCase with protocol prefixes such as MySQL_, PgSQL_, and ProxySQL_.
Member variables must use snake_case.
Constants and macros must use UPPER_SNAKE_CASE.
Use C++17, and gate conditional code with #ifdef PROXYSQL31, #ifdef PROXYSQL40, #ifdef PROXYSQLFFTO, #ifdef PROXYSQLTSDB, and #ifdef PROXYSQLCLICKHOUSE; PROXYSQLGENAI must not guard core code outside plugins/genai/.
Consider performance implications when changing hot paths or other performance-critical code.
Use RAII for resource management and jemalloc for allocation.
Use pthread mutexes for synchronization and std::atomic<> for counters.

Files:

  • test/tap/tests/pgsql-md5_passthrough-t.cpp
  • test/tap/tests/pgsql-verifier_auth-t.cpp
  • test/tap/tests/pgsql-verifier_backend_kill-t.cpp
  • test/tap/tests/pgsql-scram_reload_midhandshake-t.cpp
  • test/tap/tests/pgsql-libpq_scram_params-t.cpp
  • test/tap/tests/pgsql-verifier_pool_rotation-t.cpp
  • test/tap/tests/pg_lite_client.h
  • test/tap/tests/pg_lite_client.cpp
🧠 Learnings (1)
📚 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-md5_passthrough-t.cpp
  • test/tap/tests/pgsql-verifier_auth-t.cpp
  • test/tap/tests/pgsql-verifier_backend_kill-t.cpp
  • test/tap/tests/pgsql-scram_reload_midhandshake-t.cpp
  • test/tap/tests/pgsql-libpq_scram_params-t.cpp
  • test/tap/tests/pgsql-verifier_pool_rotation-t.cpp
  • test/tap/tests/pg_lite_client.cpp
🪛 ast-grep (0.44.1)
test/tap/tests/pg_lite_client.cpp

[warning] 374-374: This hashing algorithm is insecure. If this hash is used in a security context, such as password hashing, it should be converted to a stronger hashing algorithm.
Context: MD5(reinterpret_cast<const unsigned char*>(in.data()), in.size(), digest);
Note: [CWE-328] Use of Weak Hash.

(insecure-hash-cpp)

🔇 Additional comments (15)
test/infra/docker-pgsql16-single/bin/docker-pgsql-post.bash (1)

28-40: LGTM!

test/infra/docker-pgsql16-single/conf/pgsql/pgsql1/pg_hba.conf (1)

19-24: LGTM!

test/tap/groups/groups.json (1)

156-157: LGTM!

Also applies to: 181-181, 195-197

test/tap/tests/Makefile (2)

366-384: LGTM!


380-384: 🩺 Stability & Availability

No extra link flags needed for the other new tests The targets that build pg_lite_client.cpp already add -lscram -lusual; the other new tests here don’t use pg_lite_client.cpp at all.

			> Likely an incorrect or invalid review comment.
test/tap/tests/pgsql-verifier_backend_kill-t.cpp (2)

42-115: LGTM!


117-219: LGTM!

test/tap/tests/pgsql-verifier_pool_rotation-t.cpp (2)

42-131: LGTM!


133-216: LGTM!

test/tap/tests/pg_lite_client.cpp (2)

373-384: The ast-grep weak-hash warning here is a false positive: MD5 is mandated by the PostgreSQL MD5 authentication protocol ("md5" + md5(md5(password+user)+salt)), so it is protocol conformance, not a security choice.


304-364: LGTM!

Also applies to: 419-506

test/tap/tests/pg_lite_client.h (1)

145-162: LGTM!

Also applies to: 228-246

test/tap/tests/pgsql-libpq_scram_params-t.cpp (1)

161-260: LGTM!

test/tap/tests/pgsql-md5_passthrough-t.cpp (1)

77-141: LGTM!

test/tap/tests/pgsql-verifier_auth-t.cpp (1)

84-94: LGTM!

Also applies to: 126-127, 184-184, 201-204

Comment on lines +603 to +609
std::string server_first(reinterpret_cast<const char*>(buffer.data()) + 4, buffer.size() - 4);
if (!read_server_first_message(sasl_st_, const_cast<char*>(server_first.c_str()),
&sasl_server_nonce_, &sasl_salt_, &sasl_saltlen_, &sasl_iterations_)) {
std::string e = scram_error(); freeSaslState();
throw PgException(std::string("scram read server-first: ") + e);
}
return server_first;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate scram sources and inspect how read_server_first_message assigns *server_nonce_p.
files=$(fd -i 'scram\.(c|cpp|h)$')
echo "candidate files:"; echo "$files"
for f in $files; do
  echo "==== $f ===="
  rg -nP -A45 'read_server_first_message\s*\(' "$f"
done

Repository: sysown/proxysql

Length of output: 3163


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '560,660p' deps/libscram/src/scram.c
echo "----"
sed -n '660,740p' deps/libscram/src/scram.c
echo "----"
sed -n '1,180p' deps/libscram/include/scram.h

Repository: sysown/proxysql

Length of output: 9810


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline test/tap/tests/pg_lite_client.cpp --view expanded | sed -n '1,220p'
echo "----"
rg -n -A80 -B40 'saslBegin|saslFinish|read_server_first_message|server_nonce_' test/tap/tests/pg_lite_client.cpp

Repository: sysown/proxysql

Length of output: 18592


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -A80 -B20 'static char \*read_attr_value|read_attr_value\s*\(' deps/libscram/src/scram.c

Repository: sysown/proxysql

Length of output: 13422


Copy the server nonce before storing it for saslFinish(). read_server_first_message() returns a pointer into the server_first buffer, so sasl_server_nonce_ becomes invalid when saslBegin() returns and is later dereferenced in saslFinish(). Duplicate the nonce or keep its backing storage alive across the two phases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/tap/tests/pg_lite_client.cpp` around lines 603 - 609, Update the SCRAM
flow around saslBegin() and read_server_first_message() so sasl_server_nonce_
owns a copy of the returned server nonce rather than referencing the temporary
server_first buffer. Ensure that backing storage remains valid until
saslFinish() dereferences sasl_server_nonce_, while preserving the existing
server-first parsing and error handling.

Comment on lines +91 to +135
try {
PgConnection c(4000); // 4s read timeout: a hung handshake surfaces as "Read timed out"
c.rawConnectStartup(cl.pgsql_host, cl.pgsql_port, USER /*db*/, USER);
std::string server_first = c.saslBegin(USER, PA); // ProxySQL builds this from verifier A
ok(!server_first.empty(), "server-first received for verifier A (server-first='%s')",
server_first.c_str());

// --- MUTATE runtime creds mid-handshake: rotate the stored verifier A -> B. ---
setVerifier(admin.get(), USER, vB);
diag("rotated pgsql_users['%s'] to verifier B + LOAD PGSQL USERS TO RUNTIME (mid-handshake)", USER);

// --- Send client-final computed for the ORIGINAL verifier A. ---
int final_type = c.saslFinish();
if (final_type == 0) {
// (A) bound-to-original: client-final for A ACCEPTED despite the rotation to B.
// Prove the post-auth protocol is in sync (ReadyForQuery) -- i.e. not a desync/unusable
// session. We deliberately do NOT run a backend query: reload_user is a frontend-only
// user with no backend role, so a query would fail at the BACKEND for reasons unrelated
// to the mid-handshake contract. ReadyForQuery from ProxySQL is the correct in-sync proof.
c.waitForReady();
contract_held = true;
observed = "A: bound-to-original (client-final for A ACCEPTED after reload to B; "
"ReadyForQuery received, session in sync)";
} else {
// (B) fail-closed: ProxySQL's fresh lookup of verifier B rejected the A-proof cleanly.
contract_held = true;
observed = std::string("B: fail-closed (client-final for A REJECTED after reload to B: ")
+ c.getLastError() + ")";
}
} catch (const PgException& e) {
std::string what = e.what();
if (what.find("timed out") != std::string::npos) {
// Hang: the handshake neither completed nor was rejected -> this is the FINDING.
contract_held = false;
observed = std::string("FINDING (hang): the mid-handshake reload left the SASL exchange "
"stalled -- ") + what;
} else {
// A thrown ErrorResponse / peer-close is a clean fail-closed outcome == contract (B).
// (A libscram server-signature mismatch would also land here; still a rejection, not a
// success -- the session never becomes usable, so it is NOT the "unusable-after-success"
// third outcome.)
contract_held = true;
observed = std::string("B: fail-closed (handshake threw a clean rejection: ") + what + ")";
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Plan/assertion count can mismatch on the exception path.

plan(3) counts the ok() at Line 95 (server-first received), but that call sits inside the try. If rawConnectStartup() or saslBegin() throws, control jumps to the catch (Line 120) which only emits the Line 139 assertion, so the run reports 2 tests against a plan of 3 — a TAP planning failure that masks the recorded observed outcome. Consider emitting assertion 1 on the failure path too (e.g. ok(false, ...) in the catch) or moving it out of the try.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/tap/tests/pgsql-scram_reload_midhandshake-t.cpp` around lines 91 - 135,
Ensure the test always emits the assertion counted by plan(3) when
rawConnectStartup or saslBegin throws. Update the PgException catch block to
record a failed server-first assertion before producing the existing final
contract assertion, while preserving the current timeout and clean-rejection
outcome handling.

@rahim-kanji
rahim-kanji merged commit f4c9b10 into v3.0_pgsql-auth-5863 Jul 20, 2026
62 of 68 checks passed
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