Last active
April 21, 2026 01:03
-
-
Save 4x0v7/45d57a5a29da9706c8a122942bd9b45d to your computer and use it in GitHub Desktop.
BouncyHsm LastActivity race condition reproducer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Minimal reproducer for BouncyHsm LastActivity race condition. | |
| # | |
| # Build: | |
| # docker build -f Dockerfile.repro -t bouncyhsm-repro . | |
| # | |
| # Run: | |
| # docker run --rm bouncyhsm-repro | |
| # ── Build the native .so from source ──────────────────────────── | |
| FROM cgr.dev/chainguard/wolfi-base AS bouncy-builder | |
| RUN apk add --no-cache clang-19 make glibc-dev | |
| COPY . /bouncyhsm | |
| WORKDIR /bouncyhsm/build_linux | |
| # sprintf_s is MSVC-only; snprintf is the POSIX equivalent. | |
| # Upstream introduced sprintf_s in recent commits (built on Windows CI). | |
| RUN make CC=clang-19 CFLAGS="-Dsprintf_s=snprintf" all | |
| # ── Build the .NET server from source (includes our fix) ──────── | |
| FROM mcr.microsoft.com/dotnet/sdk:10.0-preview AS dotnet-builder | |
| COPY src/Src/ /bouncyhsm/src/Src/ | |
| WORKDIR /bouncyhsm/src/Src/BouncyHsm | |
| # Fix API break: createScopeForStatusCodePages parameter was removed | |
| # in a later .NET 10 preview. BouncyHsm CI builds on an older preview. | |
| RUN sed -i 's/UseStatusCodePagesWithReExecute("\/not-found", createScopeForStatusCodePages: true)/UseStatusCodePagesWithReExecute("\/not-found")/' Program.cs | |
| RUN dotnet publish -c Release -o /out/bouncyhsm | |
| # ── Runtime ───────────────────────────────────────────────────── | |
| FROM cgr.dev/chainguard/wolfi-base | |
| RUN apk add --no-cache \ | |
| aspnet-10-runtime \ | |
| curl \ | |
| gcc \ | |
| glibc-dev | |
| COPY --from=dotnet-builder /out/bouncyhsm/ /opt/bouncyhsm/ | |
| RUN mkdir -p /var/BouncyHsm | |
| # Native .so from builder | |
| COPY --from=bouncy-builder /bouncyhsm/build_linux/BouncyHsm.Pkcs11Lib-x64.so /usr/local/lib/ | |
| # Repro source | |
| COPY repro-race.c /repro/repro-race.c | |
| RUN gcc -o /repro/repro-race /repro/repro-race.c -ldl -lpthread -Wall | |
| ENV BOUNCY_HSM_CFG_STRING="Server=127.0.0.1; Port=8765;" | |
| COPY <<'ENTRYPOINT_EOF' /entrypoint.sh | |
| #!/bin/sh | |
| set -eu | |
| # Start .NET server in background | |
| cd /opt/bouncyhsm | |
| ASPNETCORE_ENVIRONMENT=Docker dotnet BouncyHsm.dll > /tmp/bouncyhsm.log 2>&1 & | |
| # Wait for REST API | |
| printf "Waiting for BouncyHsm server..." | |
| for i in $(seq 1 30); do | |
| if curl -sf http://localhost:5000/Slot > /dev/null 2>&1; then | |
| printf " ready\n" | |
| break | |
| fi | |
| if [ "$i" = "30" ]; then | |
| printf " TIMEOUT\n" | |
| cat /tmp/bouncyhsm.log | |
| exit 1 | |
| fi | |
| sleep 1 | |
| done | |
| # Create a token | |
| curl -s -X POST http://localhost:5000/Slot \ | |
| -H 'Content-Type: application/json' \ | |
| -d '{ | |
| "IsHwDevice": false, | |
| "IsRemovableDevice": false, | |
| "Description": "repro slot", | |
| "Token": { | |
| "Label": "repro", | |
| "SerialNumber": "0000000000000001", | |
| "SimulateHwRng": false, | |
| "SimulateHwMechanism": false, | |
| "SimulateQualifiedArea": false, | |
| "SimulateProtectedAuthPath": false, | |
| "SpeedMode": "WithoutRestriction", | |
| "UserPin": "1234", | |
| "SoPin": "5678" | |
| } | |
| }' 2>&1 || { echo "Token creation failed"; cat /tmp/bouncyhsm.log; exit 1; } | |
| echo "" | |
| echo "Token created." | |
| echo "" | |
| # Run the repro | |
| /repro/repro-race /usr/local/lib/BouncyHsm.Pkcs11Lib-x64.so | |
| echo "" | |
| echo "=== BouncyHsm server errors ===" | |
| grep -i "error\|exception\|LastActivity" /tmp/bouncyhsm.log || echo "(none)" | |
| ENTRYPOINT_EOF | |
| RUN chmod +x /entrypoint.sh | |
| ENTRYPOINT ["/entrypoint.sh"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * Minimal reproducer for BouncyHsm LastActivity race condition. | |
| * | |
| * Spawns N threads, each with its own session, hammering | |
| * FindObjectsInit/FindObjects/FindObjectsFinal in a tight loop. | |
| * These are heavier server-side calls that overlap in the .NET | |
| * thread pool, racing on MemorySession.LastActivity. | |
| * | |
| * Build: | |
| * gcc -o repro-race repro-race.c -ldl -lpthread -Wall | |
| * | |
| * Run (BouncyHsm server must be running): | |
| * ./repro-race /path/to/BouncyHsm.Pkcs11Lib-x64.so | |
| */ | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <dlfcn.h> | |
| #include <pthread.h> | |
| #include <stdatomic.h> | |
| typedef unsigned long CK_ULONG; | |
| typedef unsigned long CK_RV; | |
| typedef unsigned long CK_SLOT_ID; | |
| typedef unsigned long CK_SESSION_HANDLE; | |
| typedef unsigned long CK_OBJECT_HANDLE; | |
| typedef unsigned long CK_FLAGS; | |
| typedef unsigned long CK_ATTRIBUTE_TYPE; | |
| typedef unsigned char CK_BBOOL; | |
| typedef unsigned char CK_BYTE; | |
| typedef void *CK_VOID_PTR; | |
| #define CKR_OK 0x00000000UL | |
| #define CKR_GENERAL_ERROR 0x00000005UL | |
| #define CKF_OS_LOCKING_OK 0x00000002UL | |
| #define CKF_SERIAL_SESSION 0x00000004UL | |
| #define CKF_RW_SESSION 0x00000002UL | |
| #define CKO_PUBLIC_KEY 0x00000002UL | |
| #define CKA_CLASS 0x00000000UL | |
| #define CK_TRUE 1 | |
| #define CK_FALSE 0 | |
| typedef struct { | |
| void *CreateMutex; | |
| void *DestroyMutex; | |
| void *LockMutex; | |
| void *UnlockMutex; | |
| CK_FLAGS flags; | |
| void *pReserved; | |
| } CK_C_INITIALIZE_ARGS; | |
| typedef struct { | |
| CK_ATTRIBUTE_TYPE type; | |
| void *pValue; | |
| CK_ULONG ulValueLen; | |
| } CK_ATTRIBUTE; | |
| typedef CK_RV (*fn_C_Initialize)(CK_VOID_PTR); | |
| typedef CK_RV (*fn_C_Finalize)(CK_VOID_PTR); | |
| typedef CK_RV (*fn_C_GetSlotList)(CK_BBOOL, CK_SLOT_ID *, CK_ULONG *); | |
| typedef CK_RV (*fn_C_OpenSession)(CK_SLOT_ID, CK_FLAGS, void *, void *, | |
| CK_SESSION_HANDLE *); | |
| typedef CK_RV (*fn_C_CloseSession)(CK_SESSION_HANDLE); | |
| typedef CK_RV (*fn_C_Login)(CK_SESSION_HANDLE, CK_ULONG, CK_BYTE *, CK_ULONG); | |
| typedef CK_RV (*fn_C_FindObjectsInit)(CK_SESSION_HANDLE, CK_ATTRIBUTE *, CK_ULONG); | |
| typedef CK_RV (*fn_C_FindObjects)(CK_SESSION_HANDLE, CK_OBJECT_HANDLE *, | |
| CK_ULONG, CK_ULONG *); | |
| typedef CK_RV (*fn_C_FindObjectsFinal)(CK_SESSION_HANDLE); | |
| typedef CK_RV (*fn_C_GetTokenInfo)(CK_SLOT_ID, void *); | |
| #define NUM_THREADS 8 | |
| #define ITERATIONS 1000 | |
| static fn_C_OpenSession p11_OpenSession; | |
| static fn_C_CloseSession p11_CloseSession; | |
| static fn_C_Login p11_Login; | |
| static fn_C_FindObjectsInit p11_FindObjectsInit; | |
| static fn_C_FindObjects p11_FindObjects; | |
| static fn_C_FindObjectsFinal p11_FindObjectsFinal; | |
| static fn_C_GetTokenInfo p11_GetTokenInfo; | |
| static CK_SLOT_ID slot_id; | |
| static atomic_int general_errors = 0; | |
| static atomic_int total_calls = 0; | |
| static void check(const char *fn, CK_RV rv, int tid, int iter) { | |
| if (rv == CKR_GENERAL_ERROR) { | |
| atomic_fetch_add(&general_errors, 1); | |
| printf("[thread %d] iter %d: %s → CKR_GENERAL_ERROR (0x5)\n", | |
| tid, iter, fn); | |
| } else if (rv != CKR_OK) { | |
| printf("[thread %d] iter %d: %s → 0x%lx\n", tid, iter, fn, rv); | |
| } | |
| atomic_fetch_add(&total_calls, 1); | |
| } | |
| static void *worker(void *arg) { | |
| int tid = *(int *)arg; | |
| CK_RV rv; | |
| /* Each thread gets its own session. */ | |
| CK_SESSION_HANDLE session; | |
| rv = p11_OpenSession(slot_id, | |
| CKF_SERIAL_SESSION | CKF_RW_SESSION, | |
| NULL, NULL, &session); | |
| if (rv != CKR_OK) { | |
| printf("[thread %d] OpenSession failed: 0x%lx\n", tid, rv); | |
| return NULL; | |
| } | |
| rv = p11_Login(session, 0 /* CKU_SO=0 → CKU_USER=1... use 1 */, | |
| (CK_BYTE *)"1234", 4); | |
| /* Login may fail with CKR_USER_ALREADY_LOGGED_IN — that's fine. */ | |
| CK_ULONG obj_class = CKO_PUBLIC_KEY; | |
| CK_ATTRIBUTE tmpl[] = { | |
| { CKA_CLASS, &obj_class, sizeof(obj_class) }, | |
| }; | |
| for (int i = 0; i < ITERATIONS; i++) { | |
| /* FindObjectsInit + FindObjects + FindObjectsFinal = 3 HTTP | |
| * round-trips in quick succession. Interleaved across threads, | |
| * these create the concurrent-request window needed to race. */ | |
| rv = p11_FindObjectsInit(session, tmpl, 1); | |
| check("FindObjectsInit", rv, tid, i); | |
| if (rv == CKR_OK) { | |
| CK_OBJECT_HANDLE objs[8]; | |
| CK_ULONG found = 0; | |
| rv = p11_FindObjects(session, objs, 8, &found); | |
| check("FindObjects", rv, tid, i); | |
| rv = p11_FindObjectsFinal(session); | |
| check("FindObjectsFinal", rv, tid, i); | |
| } | |
| /* Also mix in a GetTokenInfo — different code path, same | |
| * EnsureMemorySession timestamp update. */ | |
| unsigned char info[4096]; | |
| rv = p11_GetTokenInfo(slot_id, info); | |
| check("GetTokenInfo", rv, tid, i); | |
| } | |
| p11_CloseSession(session); | |
| return NULL; | |
| } | |
| int main(int argc, char **argv) { | |
| if (argc != 2) { | |
| fprintf(stderr, "usage: %s /path/to/BouncyHsm.Pkcs11Lib-x64.so\n", | |
| argv[0]); | |
| return 1; | |
| } | |
| void *lib = dlopen(argv[1], RTLD_NOW); | |
| if (!lib) { | |
| fprintf(stderr, "dlopen: %s\n", dlerror()); | |
| return 1; | |
| } | |
| fn_C_Initialize p11_Init = dlsym(lib, "C_Initialize"); | |
| fn_C_Finalize p11_Fin = dlsym(lib, "C_Finalize"); | |
| fn_C_GetSlotList p11_GetSlotList = dlsym(lib, "C_GetSlotList"); | |
| p11_OpenSession = dlsym(lib, "C_OpenSession"); | |
| p11_CloseSession = dlsym(lib, "C_CloseSession"); | |
| p11_Login = dlsym(lib, "C_Login"); | |
| p11_FindObjectsInit = dlsym(lib, "C_FindObjectsInit"); | |
| p11_FindObjects = dlsym(lib, "C_FindObjects"); | |
| p11_FindObjectsFinal = dlsym(lib, "C_FindObjectsFinal"); | |
| p11_GetTokenInfo = dlsym(lib, "C_GetTokenInfo"); | |
| if (!p11_Init || !p11_Fin || !p11_GetSlotList || !p11_OpenSession || | |
| !p11_CloseSession || !p11_FindObjectsInit || !p11_FindObjects || | |
| !p11_FindObjectsFinal || !p11_GetTokenInfo) { | |
| fprintf(stderr, "missing PKCS#11 symbols\n"); | |
| return 1; | |
| } | |
| CK_C_INITIALIZE_ARGS init_args; | |
| memset(&init_args, 0, sizeof(init_args)); | |
| init_args.flags = CKF_OS_LOCKING_OK; | |
| CK_RV rv = p11_Init(&init_args); | |
| if (rv != CKR_OK) { | |
| fprintf(stderr, "C_Initialize failed: 0x%lx\n", rv); | |
| return 1; | |
| } | |
| CK_ULONG slot_count = 0; | |
| p11_GetSlotList(CK_TRUE, NULL, &slot_count); | |
| if (slot_count == 0) { | |
| fprintf(stderr, "no slots with tokens\n"); | |
| return 1; | |
| } | |
| CK_SLOT_ID *slots = malloc(slot_count * sizeof(CK_SLOT_ID)); | |
| p11_GetSlotList(CK_TRUE, slots, &slot_count); | |
| slot_id = slots[0]; | |
| free(slots); | |
| printf("Hammering slot %lu with %d threads x %d iterations " | |
| "(4 calls/iter = %d total)...\n\n", | |
| slot_id, NUM_THREADS, ITERATIONS, NUM_THREADS * ITERATIONS * 4); | |
| pthread_t threads[NUM_THREADS]; | |
| int tids[NUM_THREADS]; | |
| for (int i = 0; i < NUM_THREADS; i++) { | |
| tids[i] = i; | |
| pthread_create(&threads[i], NULL, worker, &tids[i]); | |
| } | |
| for (int i = 0; i < NUM_THREADS; i++) { | |
| pthread_join(threads[i], NULL); | |
| } | |
| p11_Fin(NULL); | |
| dlclose(lib); | |
| int errs = atomic_load(&general_errors); | |
| int calls = atomic_load(&total_calls); | |
| printf("\n%d calls, %d CKR_GENERAL_ERROR (%.2f%%)\n", | |
| calls, errs, calls ? (errs * 100.0 / calls) : 0.0); | |
| return errs > 0 ? 1 : 0; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment