Repository navigation
Conversation
There was a problem hiding this comment.
🟡 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
| zend_hash_apply_with_argument( | ||
| &EG(persistent_list), couchbase::php::check_persistent_connection, &expire); |
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.
d61ae13 to
8cbe5ea
Compare
There was a problem hiding this comment.
🟡 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
Keep old-authenticated connections out of the new credential cache · New Reusing the handle can retain the previous user's authentication Clean up the user when propagation wait times out · New Verify the moved connection has a healthy endpoint Add persistent-worker coverage for detached-resource cleanup Derive the new key from the connection's original options
Review effort: Balanced
8cbe5ea to
fe5b9b6
Compare
fe5b9b6 to
172b923
Compare
172b923 to
93a997b
Compare
93a997b to
79b1dd4
Compare
79b1dd4 to
5b4dbed
Compare
5b4dbed to
a9628bb
Compare
a9628bb to
0a41717
Compare
There was a problem hiding this comment.
🟡 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
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.
0a41717 to
8061519
Compare
| ZEND_MODULE_POST_ZEND_DEACTIVATE_D(couchbase) | ||
| { | ||
| couchbase::php::sweep_detached_connections(); |
| /* 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(); |



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):setAuthenticator(B)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_listbucket runsplist_entry_destructor, which frees thezend_resourcewhile PHP values still reference it. The destructor is suspended around the delete: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_timeoutandcouchbase.max_persistentare. 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_REFCOUNTis 0, so a long-running CLI request does not keep one connection open persetAuthenticator()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()reportscouchbase_persistent_connections, the number of open persistent connections.TransactionsandScanResultshold 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.testSetAuthenticatorRemovesConnectionFromCachesets 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 withAuthenticationFailureExceptioninstead of reusing the detached one.testSetAuthenticatorDoesNotAccumulateDetachedConnectionsrepeats connect,setAuthenticator()and release, and checks that the connection count stays flat.testSweepKeepsDetachedConnectionUsedByTransactionsandtestSweepKeepsDetachedConnectionUsedByScaneach release the cluster aftersetAuthenticator(), force a sweep, and then run a transaction or a scan created earlier.testSetAuthenticatorToCredentialsOfAnotherCachedConnectionswitches 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.