Skip to content

Instantly share code, notes, and snippets.

@4x0v7
Last active April 21, 2026 01:03
Show Gist options
  • Select an option

  • Save 4x0v7/45d57a5a29da9706c8a122942bd9b45d to your computer and use it in GitHub Desktop.

Select an option

Save 4x0v7/45d57a5a29da9706c8a122942bd9b45d to your computer and use it in GitHub Desktop.
BouncyHsm LastActivity race condition reproducer
# 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"]
/*
* 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