Limit PKCS11 session creation#49
Conversation
46082c7 to
b33d2e5
Compare
b33d2e5 to
692b67a
Compare
pfi79
left a comment
There was a problem hiding this comment.
Thank you for the error you found. But it seems to me that the fix is not ready yet.
7d9868e to
692eef6
Compare
|
@pfi79 Pushed Design changes
Test
Fabric integration
|
692eef6 to
62e0181
Compare
This is such a bad change that I have only one question: why are you needed? You need to leave just me and your AI. |
I'm sorry, I did use AI to solve the problem I encountered. You don't have to accept my pull request, but that's the issue, and you can use better ways to resolve it. |
Let's take it one step at a time. I need a test that would show that there really is a problem. That is, it seems to me that you have a sound grain in the problem you have found, but I need to make sure. To do this, you need to artificially recreate the situation. Can you do it? |
62e0181 to
c4b0938
Compare
…n creation This test is added on top of upstream/main without the fix from hyperledger#49, so running it here demonstrates that unbounded session creation is observable today: the test spawns 25 concurrent getSession callers with sessionCacheSize=5, tracks the peak number of open PKCS#11 sessions (len(csp.sessions)) over the run, and asserts that it never exceeds the configured cache size. On this branch (no fix) the test fails with peak == 25 because every caller whose arrival finds an empty sessPool falls through to createSession() and opens a new PKCS#11 session unconditionally. Under high sign concurrency that translates to CKR_SESSION_COUNT on OpenSession and subsequent CKR_DEVICE_ERROR on operations (see hyperledger#50). Apply hyperledger#49 on top of this commit and the same test passes with peak == 5. Signed-off-by: Evan <evanyan@sign.global>
c4b0938 to
fe00338
Compare
…n creation This test is added on top of upstream/main without the fix from hyperledger#49, so running it here demonstrates that unbounded session creation is observable today: the test spawns 5 * defaultSessionCacheSize concurrent getSession callers, tracks the peak number of open PKCS#11 sessions (len(csp.sessions)) over the run, and asserts that it never exceeds defaultSessionCacheSize. On this branch (no fix) the test fails with peak equal to the caller count because every caller whose arrival finds an empty sessPool falls through to createSession() and opens a new PKCS#11 session unconditionally. Under high sign concurrency that translates to CKR_SESSION_COUNT on OpenSession and subsequent CKR_DEVICE_ERROR on operations (see hyperledger#50). Apply hyperledger#49 on top of this commit and the same test passes because getSession then acquires a semaphore.Weighted slot before opening a new session, capping the peak at defaultSessionCacheSize. Signed-off-by: Evan <evanyan@sign.global>
I added a test that reproduces the problem to my branch; you can take a look. Thanks |
I took your test and ran it at my place. Yes, he fell, but I didn't see any of the mistakes you described. |
Thanks for running it. The CKR_SESSION_COUNT / CKR_DEVICE_ERROR errors in issue #50 come from a production deployment that exposes a remote KMS via a PKCS#11 shim, where What the test demonstrates is the BCCSP-layer cause:
The peak |
|
The pipeline is red, there's a mistake in your code somewhere. |
fe00338 to
7a31c71
Compare
fixed |
2c04058 to
38b7958
Compare
The PKCS#11 BCCSP previously fell back to OpenSession unbounded whenever the session cache was empty, allowing the number of concurrently checked out sessions to grow with the number of concurrent callers. Under high sign concurrency this surfaces from HSM/PKCS#11 libraries as CKR_SESSION_COUNT on OpenSession and CKR_DEVICE_ERROR on subsequent operations (see issue hyperledger#50). Gate concurrent OpenSession calls behind a semaphore.Weighted whose weight equals sessionCacheSize, so the number of outstanding sessions is bounded. Cached sessions in sessPool intentionally do not occupy a slot: getSession reacquires its own slot when pulling one out, and returnSession releases the slot when caching, so a caller blocked on the bound is unblocked the moment another session is returned or closed. Expose the bound via a new public PKCS11Opts.SessionCacheSize field (with json/yaml/mapstructure tags) so it can be configured through the existing BCCSP factory configuration alongside Library/Label/Pin etc., with no environment variable required. A value of 0 selects the default (30); a negative value disables the bound and preserves the original unbounded code path for callers that want it. Update closeSession to release the slot only for sessions that were previously registered, guarding against double-release when an open succeeds but the subsequent Login fails and the partial session is closed before being tracked. Tests in pkcs11_test.go (TestPKCS11GetSession, TestSessionHandleCaching) exercise the bound: they fill the cache, spawn an extra getSession in a goroutine, and assert it blocks until a session is returned. Without this change those tests would fail because the goroutine would proceed straight through to createSession instead of blocking. Signed-off-by: Evan <evanyan@sign.global>
38b7958 to
730a932
Compare
| default: | ||
| // have plenty of sessions in cache, dropping | ||
| csp.closeSession(session) |
There was a problem hiding this comment.
When you limited the number of slots and made them equal to the channel capacity, can we get in here? It seems to me that if we got here, we have very big problems.
There was a problem hiding this comment.
You're right — with sessSem and sessPool both bounded by cacheSize,
the default branch is unreachable.
Invariant: every returnSession() call comes from a path that previously
acquired a slot via getSession() → sem.Acquire(1), and the Release
happens after the push. With A = acquired-but-not-returned and
L = cached, the semaphore enforces A + L <= cacheSize. The caller
about to push contributes A >= 1, so L <= cacheSize-1 and the send
never blocks.
Simplified to a direct send in 65b4b8f:
func (csp *Provider) returnSession(session pkcs11.SessionHandle) {
csp.sessPool <- session
csp.sessSem.Release(1)
}
TestPKCS11GetSession at pkcs11_test.go:668-673 already exercises
the worst case (all N in-flight sessions returned simultaneously)
and asserts len(sessPool) == cacheSize, which would fail if any
return path dropped a session via the old default branch.
sessPool and sessSem now share cacheSize as capacity, so the channel send in returnSession can never block: the caller still holds an outstanding sem.Acquire(1) from its prior getSession(), which bounds acquired-but-not-returned sessions A >= 1, leaving cached L <= cacheSize-1. The select+default branch was therefore unreachable; remove it so the control flow reflects the actual invariant. Signed-off-by: Evan <evanyan@sign.global>
Summary
Testing
Note: go test -tags pkcs11 ./bccsp/pkcs11 requires a local PKCS#11 library configuration and was not runnable in this environment without PKCS11_LIB.