Skip to content

PCBC-1057: Detach the cached connection in setAuthenticator() - #268

Open
avsej wants to merge 2 commits into
couchbase:mainfrom
avsej:PCBC-1057-rekey-set-authenticator
Open

avsej wants to merge 2 commits into
couchbase:mainfrom
avsej:PCBC-1057-rekey-set-authenticator

Conversation

@avsej

@avsej avsej commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Fixes PCBC-1057.

Problem

Cluster::setAuthenticator() changes the credentials of the cached connection, but the connection stays under the key derived from the previous credentials (Cluster.php#L353-L356):

step result
connect with credentials A connection cached under key(A)
setAuthenticator(B) new sockets authenticate as B; sockets already open stay authenticated as A
connect again with A gets that connection, so requests may run as B; no authentication error for A

Depends on #267.

Change

  • The connection leaves the cache. After the credentials are applied, the entry moves to a key no lookup produces and is marked detached. The cluster that called setAuthenticator() keeps using it. Clusters connected later bootstrap their own connection, whichever credentials they carry. The ticket's option A instead re-registers the connection under key(B). That would hand a cluster connecting with B sockets still authenticated as A, even when B is invalid or less privileged.

  • Detaching does not free the resource. Deleting a persistent_list bucket runs plist_entry_destructor, which frees the zend_resource while PHP values still reference it. The destructor is suspended around the delete:

    auto* destructor = EG(persistent_list).pDestructor;
    EG(persistent_list).pDestructor = nullptr;   // <- res stays allocated
    zend_hash_del_bucket(&EG(persistent_list), bucket);
    EG(persistent_list).pDestructor = destructor;
  • Detached connections are freed after the request releases its variables. The sweep runs in the module's post-deactivate hook, after PHP has destroyed request globals, so a detached cluster kept in a global array is freed at the end of that request. It does so whatever couchbase.persistent_timeout and couchbase.max_persistent are. The expiry pass is evaluated after that sweep, and walks the persistent list only when those settings enable expiry.

  • Unreferenced detached connections are also freed during the request. Releasing the last PHP value of a persistent resource does not destroy it. Creating or detaching a connection therefore sweeps detached entries whose GC_REFCOUNT is 0, so a long-running CLI request does not keep one connection open per setAuthenticator() call. Detached connections are kept in a per-thread list, so the sweep visits only them and cache hits do not walk the persistent list.

  • phpinfo() reports couchbase_persistent_connections, the number of open persistent connections.

  • Transactions and ScanResults hold the connection resource. They run on the connection's core cluster, but held no PHP reference to it, so the sweep could close the cluster under a live transaction or scan. Their native objects are destroyed on separate threads, so each also holds a token from the connection handle. The sweep skips a handle whose token is still held.

  • testSetAuthenticatorRemovesConnectionFromCache sets invalid credentials on a cached cluster. It checks that a cluster connected afterwards gets a different connection, and that connecting with the invalid credentials fails with AuthenticationFailureException instead of reusing the detached one.

  • testSetAuthenticatorDoesNotAccumulateDetachedConnections repeats connect, setAuthenticator() and release, and checks that the connection count stays flat.

  • testSweepKeepsDetachedConnectionUsedByTransactions and testSweepKeepsDetachedConnectionUsedByScan each release the cluster after setAuthenticator(), force a sweep, and then run a transaction or a scan created earlier.

  • testSetAuthenticatorToCredentialsOfAnotherCachedConnection switches a cluster to the credentials of a second cached connection. The second connection stays the one shared under those credentials, and the switched cluster keeps serving KV operations on its own.

Copilot AI 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.

🟡 Changes recommended

Rekeying a cached handle can expose sockets authenticated as the previous user, and mutable options can also cause connections to be reused with the wrong configuration.

4 open issues
What changed in this PR

This PR addresses PCBC-1057 by moving cached connections to a new credential-derived key when Cluster::setAuthenticator() is called, building on the full-options cache key introduced in #267.

Changes:

  • Derives cache keys from all exported options and recalculates them when credentials change.
  • Adds resource-preserving cache moves, collision handling, and shutdown cleanup for detached connections.
  • Adds regression tests for option-based sharing, credential changes, and occupied destination keys.
File Description
tests/​ConnectionCacheTest.php Tests cache sharing, credential re-keying, and collisions.
src/​wrapper/​persistent_connections_cache.hxx Declares cache movement and updates the cleanup callback signature.
src/​wrapper/​persistent_connections_cache.cxx Implements resource-preserving moves and detached-entry cleanup.
src/​wrapper/​connection_handle.hxx Tracks detached state and allows updating the connection hash.
src/​php_couchbase.cxx Passes new cache keys to native code and always runs the shutdown sweep.
Couchbase/​ClusterOptions.php Removes the authenticator-only hash helper.
Couchbase/​Cluster.php Computes full-options cache keys and requests re-keying after authenticator changes.

Review effort: Balanced

Comment thread Couchbase/Cluster.php Resolved
Comment thread src/php_couchbase.cxx Outdated
Comment on lines +66 to +67
zend_hash_apply_with_argument(
&EG(persistent_list), couchbase::php::check_persistent_connection, &expire);
Comment thread tests/ConnectionCacheTest.php Resolved
Motivation
----------
The persistent connection cache keyed a connection on the connection
string, the authenticator and the core tracing, core metrics and span
buffering flags. Clusters that differed in any other option received
the same cached connection. Examples are a timeout, TLS verification,
the network and DNS-SRV. The second cluster ran with the options of
the first.

Modifications
-------------
The cache key is the SHA-256 of the connection string and the
serialized ClusterOptions::export(). export() carries every option
passed to the extension, the authenticator included. Its keys are in a
fixed order, so the key does not depend on the order the setters were
called in. serialize() is used rather than json_encode(), which fails
on strings that are not valid UTF-8.

The internal ClusterOptions::authenticatorHash() is removed, since the
exported options already cover the authenticator.

Results
-------
Clusters share a connection only when their connection strings and
all exported options are equal.
@avsej
avsej force-pushed the PCBC-1057-rekey-set-authenticator branch from d61ae13 to 8cbe5ea Compare October 30, 2026 21:34
@avsej
avsej requested a balanced review from Copilot October 30, 2026 21:34

Copilot AI 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.

🟡 Changes recommended

Re-keying can expose sockets authenticated as a privileged prior user to callers with new credentials, and mutable options can cause connections to be reused with the wrong configuration.

6 open issues

Review effort: Balanced

Comment thread src/php_couchbase.cxx Resolved
Comment thread tests/ConnectionCacheTest.php Resolved
@avsej
avsej force-pushed the PCBC-1057-rekey-set-authenticator branch from 8cbe5ea to fe5b9b6 Compare October 30, 2026 21:49
@avsej avsej changed the title PCBC-1057: Re-key the cached connection in setAuthenticator() PCBC-1057: Detach the cached connection in setAuthenticator() Oct 30, 2026
@avsej
avsej force-pushed the PCBC-1057-rekey-set-authenticator branch from fe5b9b6 to 172b923 Compare October 30, 2026 21:50
@avsej
avsej requested a balanced review from Copilot October 30, 2026 21:50

Copilot AI 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.

Comment thread src/php_couchbase.cxx Resolved
Comment thread tests/ConnectionCacheTest.php Resolved
@avsej
avsej force-pushed the PCBC-1057-rekey-set-authenticator branch from 172b923 to 93a997b Compare October 30, 2026 21:57
@avsej
avsej requested a balanced review from Copilot October 30, 2026 21:57

Copilot AI 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.

🟡 Changes recommended

The sweep can close connections still used by live dependent objects, while also missing some request-end cleanup and slowing cache lookups.

3 open issues
2 resolved since last review

Review effort: Balanced

@avsej
avsej force-pushed the PCBC-1057-rekey-set-authenticator branch from 93a997b to 79b1dd4 Compare October 30, 2026 22:32
@avsej
avsej requested a balanced review from Copilot October 30, 2026 22:32

Copilot AI 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.

🟡 Changes recommended

Detached connections can leave sockets open after request shutdown, while cleanup scans of the full persistent resource list can degrade performance.

3 open issues
2 resolved since last review

Review effort: Balanced

Comment thread src/php_couchbase.cxx Resolved
Comment thread tests/ConnectionCacheTest.php Resolved
@avsej
avsej force-pushed the PCBC-1057-rekey-set-authenticator branch from 79b1dd4 to 5b4dbed Compare October 30, 2026 22:42
@avsej
avsej requested a balanced review from Copilot October 30, 2026 22:42

Copilot AI 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.

🔵 Needs a closer look

Detached connections can cause full persistent-list scans on every lookup and remain open after request shutdown when held in globals.

1 open issue
2 resolved since last review

Review effort: Balanced

@avsej
avsej force-pushed the PCBC-1057-rekey-set-authenticator branch from 5b4dbed to a9628bb Compare October 30, 2026 23:10
@avsej
avsej requested a balanced review from Copilot October 30, 2026 23:10

Copilot AI 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.

🟡 Changes recommended

Detached connections can remain open after request shutdown, while referenced detached connections cause every cache lookup to scan the entire persistent list.

2 open issues

Review effort: Balanced

@avsej
avsej force-pushed the PCBC-1057-rekey-set-authenticator branch from a9628bb to 0a41717 Compare October 30, 2026 23:18
@avsej
avsej requested a balanced review from Copilot October 30, 2026 23:19

Copilot AI 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.

🟡 Changes recommended

The shutdown sweep can leave detached connections open after request globals are released, while computing expiry before the sweep can unnecessarily evict reusable cached connections.

3 open issues
1 resolved since last review

Review effort: Balanced

Comment thread src/php_couchbase.cxx Resolved
Comment thread src/php_couchbase.cxx Resolved
Motivation
----------
Cluster::setAuthenticator() replaced the credentials of the cached
connection but left it under the key derived from the previous
credentials. A cluster connected later with the previous credentials
received that connection and ran with the new credentials. Sockets
opened before the change stay authenticated with the previous
credentials, so the connection cannot serve either set of
credentials.

Modifications
-------------
After the credentials are applied, the extension moves the cache
entry to a key that no lookup produces and marks it detached. The
cluster that called setAuthenticator() keeps using the connection.

Removing the old entry bypasses the persistent list destructor, which
would free the resource while PHP values still reference it.

After each request has released its variables, detached connections
that nothing references are freed, whatever the
couchbase.persistent_timeout and couchbase.max_persistent settings
are. The expiry rule still applies only when those settings enable
it, and is evaluated after that sweep.
Creating or detaching a connection frees them as well, so a
long-running request does not keep one open per setAuthenticator()
call. Detached connections are kept in a per-thread list, so that
sweep visits only them. phpinfo() reports the number of
persistent connections as couchbase_persistent_connections.

Transactions and ScanResults keep a reference to the connection
resource. They use the connection's core cluster, so freeing the
connection while either is alive would close it under them. Their
native objects are destroyed on separate threads, so these also hold
a token that keeps the connection from being freed until that
teardown has finished.

Results
-------
A cluster connected after setAuthenticator() bootstraps its own
connection and authenticates with its own credentials, whichever
they are.
@avsej
avsej force-pushed the PCBC-1057-rekey-set-authenticator branch from 0a41717 to 8061519 Compare October 30, 2026 23:29
@avsej
avsej requested a balanced review from Copilot October 30, 2026 23:29

Copilot AI 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.

🟡 Changes recommended

Detached teardown can leave idle connections open indefinitely, and no multi-request regression test verifies post-deactivation cleanup.

3 open issues
2 resolved since last review

Review effort: Balanced

Comment thread src/php_couchbase.cxx
Comment on lines +67 to +69
ZEND_MODULE_POST_ZEND_DEACTIVATE_D(couchbase)
{
couchbase::php::sweep_detached_connections();
Comment thread src/php_couchbase.cxx
/* runs after the request's variables are released, so their connections count as unused */
ZEND_MODULE_POST_ZEND_DEACTIVATE_D(couchbase)
{
couchbase::php::sweep_detached_connections();
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