Files
VeraCrypt/Tests/windows_kdf_completion_test.c
T
Mounir IDRASSI 9603625a3d Windows: drain encryption pool users before shutdown
Close admission and retain references for accepted requests, KDF workers
and submitters until they stop using pool resources. Run new work
synchronously while stopping, then join workers and release handles.

Test queued work, delayed requester wakeup, fallback and restart.
2026-10-16 18:22:09 +09:00

613 lines
22 KiB
C

/* Copyright (c) 2026 AM Crypto. Licensed under the Apache License 2.0.
* Bounded user-mode checks of the production Windows KDF reference protocol.
* Build through test_windows_kdf_completion.py, which supplies caller excerpts.
*/
#include "Common/EncryptionThreadPool.h"
#include "Common/Pkcs5.h"
#include <process.h>
#include <stdio.h>
static BOOL TestSetEvent (HANDLE event);
static DWORD TestWait (HANDLE event);
static HANDLE WINAPI TestCreateEvent (LPSECURITY_ATTRIBUTES attributes, BOOL manualReset, BOOL initialState, LPCWSTR name);
static HANDLE WINAPI TestCreateMutex (LPSECURITY_ATTRIBUTES attributes, BOOL initialOwner, LPCWSTR name);
static uintptr_t TestBeginThread (void *security, unsigned stackSize, unsigned (__stdcall *proc) (void *), void *arg, unsigned flags, unsigned *id);
static BOOL WINAPI TestCloseHandle (HANDLE handle);
#undef TC_WAIT_EVENT
#define TC_WAIT_EVENT(event) TestWait (event)
#define SetEvent TestSetEvent
#define CreateEventW TestCreateEvent
#define CreateMutexW TestCreateMutex
#define _beginthreadex TestBeginThread
#define CloseHandle TestCloseHandle
#include "Common/EncryptionThreadPool.c"
#undef CloseHandle
#undef _beginthreadex
#undef CreateMutexW
#undef CreateEventW
#undef SetEvent
#include "kdf_item.inc"
#define MAX_REQUESTS 16
#define PRF_COUNT (LAST_PRF_ID - FIRST_PRF_ID + 1)
typedef struct Request Request;
typedef struct
{
Request *Owner;
KeyDerivationWorkItem *Item;
HANDLE Started, MayReturn, ReadyPublished, MayReleaseReference, WorkerDone;
int Result;
BOOL HoldAfterReady;
} Job;
struct Request
{
TC_EVENT *Ready, *Drained;
LONG *References;
KeyDerivationWorkItem *Items;
LONG volatile Abort;
LONG volatile Signals;
HANDLE WaitEntered, FinishDone, SignalEntered, MaySignal;
HANDLE RequesterThread;
BOOL HoldFinalSignal;
int JobCount;
Job Jobs[PRF_COUNT];
};
static Request Requests[MAX_REQUESTS];
static int RequestCount;
static __declspec(thread) Job *CurrentJob;
static __declspec(thread) Request *DrainingRequest;
typedef struct
{
unsigned char Data[6 * ENCRYPTION_DATA_UNIT_SIZE];
LONG volatile Started, Processed;
HANDLE BothStarted, MayProcess, Waiting, Paused, MayReturn, Done, Thread;
BOOL HoldWorkers, HoldRequester;
uint32 Units;
EncryptionThreadPoolWorkType Type;
} CipherJob;
static __declspec(thread) CipherJob *CurrentCipherJob;
static HANDLE StopWaiting, StopDone;
typedef struct
{
HANDLE Handle;
BOOL Thread, Open;
} PoolResource;
static PoolResource Resources[1024];
static int ResourceCount, OpenResources, CreateAttempts, FailCreateAt;
static void Require (BOOL condition, const char *message)
{
if (!condition)
{
fprintf (stderr, "FAIL: %s\n", message);
ExitProcess (1);
}
}
static BOOL FailCreation (void)
{
if (++CreateAttempts == FailCreateAt)
{
SetLastError (ERROR_NOT_ENOUGH_MEMORY);
return TRUE;
}
return FALSE;
}
static HANDLE TrackResource (HANDLE handle, BOOL thread)
{
if (handle)
{
PoolResource *resource;
Require (ResourceCount < sizeof (Resources) / sizeof (Resources[0]), "resource fixture capacity");
resource = &Resources[ResourceCount++];
resource->Handle = handle;
resource->Thread = thread;
resource->Open = TRUE;
++OpenResources;
}
return handle;
}
static HANDLE WINAPI TestCreateEvent (LPSECURITY_ATTRIBUTES attributes, BOOL manualReset, BOOL initialState, LPCWSTR name)
{
return FailCreation () ? NULL : TrackResource (CreateEventW (attributes, manualReset, initialState, name), FALSE);
}
static HANDLE WINAPI TestCreateMutex (LPSECURITY_ATTRIBUTES attributes, BOOL initialOwner, LPCWSTR name)
{
return FailCreation () ? NULL : TrackResource (CreateMutexW (attributes, initialOwner, name), FALSE);
}
static uintptr_t TestBeginThread (void *security, unsigned stackSize, unsigned (__stdcall *proc) (void *), void *arg, unsigned flags, unsigned *id)
{
return FailCreation () ? 0 : (uintptr_t) TrackResource ((HANDLE) _beginthreadex (security, stackSize, proc, arg, flags, id), TRUE);
}
static BOOL WINAPI TestCloseHandle (HANDLE handle)
{
int i;
for (i = ResourceCount - 1; i >= 0; --i)
{
PoolResource *resource = &Resources[i];
if (resource->Open && resource->Handle == handle)
{
if (resource->Thread)
Require (WaitForSingleObject (handle, 0) == WAIT_OBJECT_0, "worker handle closed before join");
resource->Open = FALSE;
--OpenResources;
Require (CloseHandle (handle), "close pool resource");
return TRUE;
}
}
Require (FALSE, "closing unknown pool resource");
return FALSE;
}
static HANDLE Event (void)
{
HANDLE event = CreateEvent (NULL, FALSE, FALSE, NULL);
Require (event != NULL, "create test event");
return event;
}
static void Wait (HANDLE event)
{
Require (WaitForSingleObject (event, 10000) == WAIT_OBJECT_0, "test barrier timed out");
}
static LONG References (Request *request)
{
return InterlockedCompareExchange (request->References, 0, 0);
}
static DWORD TestWait (HANDLE event)
{
DWORD result;
if (DrainingRequest && event == *DrainingRequest->Drained)
SetEvent (DrainingRequest->WaitEntered);
if (StopWaiting && event == ThreadPoolDrainedEvent)
SetEvent (StopWaiting);
if (CurrentCipherJob && event != WorkItemCompletedEvent)
SetEvent (CurrentCipherJob->Waiting);
result = WaitForSingleObject (event, INFINITE);
if (CurrentCipherJob && CurrentCipherJob->HoldRequester && event != WorkItemCompletedEvent)
{
SetEvent (CurrentCipherJob->Paused);
Wait (CurrentCipherJob->MayReturn);
}
return result;
}
static BOOL TestSetEvent (HANDLE event)
{
Job *job = CurrentJob;
if (job)
{
Request *request = job->Owner;
if (event == *request->Drained)
{
Require (References (request) == 0, "completion signaled with outstanding references");
Require (InterlockedIncrement (&request->Signals) == 1, "completion signaled twice");
if (request->HoldFinalSignal)
{
SetEvent (request->SignalEntered);
Wait (request->MaySignal);
}
}
if (event == *request->Ready && job->HoldAfterReady)
{
BOOL result = SetEvent (event);
SetEvent (job->ReadyPublished);
Wait (job->MayReleaseReference);
return result;
}
if (event == WorkItemCompletedEvent)
{
BOOL result = SetEvent (event);
CurrentJob = NULL;
SetEvent (job->WorkerDone);
return result;
}
}
return SetEvent (event);
}
static int InitializeRequest (Request *request)
{
TC_EVENT *keyDerivationCompletedEvent = NULL, *noOutstandingWorkItemEvent = NULL;
LONG *outstandingWorkItemCount = NULL;
KeyDerivationWorkItem *keyDerivationWorkItems = NULL;
int keyDerivationWorkItemsSize = 0, pkcs5PrfCount = PRF_COUNT, i;
#include "kdf_init.inc"
request->Ready = keyDerivationCompletedEvent;
request->Drained = noOutstandingWorkItemEvent;
request->References = outstandingWorkItemCount;
request->Items = keyDerivationWorkItems;
return 0;
}
static Request *NewRequest (void)
{
Request *request;
Require (RequestCount < MAX_REQUESTS, "request fixture capacity");
request = &Requests[RequestCount++];
Require (InitializeRequest (request) == 0, "initialize request");
/* Refuse to exercise an implementation without the caller reference. */
Require (References (request) == 1, "missing requester reference");
Require (WaitForSingleObject (*request->Drained, 0) == WAIT_TIMEOUT, "initial completion event signaled");
request->WaitEntered = Event ();
request->FinishDone = Event ();
request->SignalEntered = Event ();
request->MaySignal = Event ();
return request;
}
static unsigned __stdcall FinishRequest (void *context)
{
Request *request = (Request *) context;
LONG *outstandingWorkItemCount = request->References;
TC_EVENT *noOutstandingWorkItemEvent = request->Drained;
DrainingRequest = request;
#include "kdf_drain.inc"
Require (References (request) == 0, "request finished with workers outstanding");
DrainingRequest = NULL;
SetEvent (request->FinishDone);
return 0;
}
static void BeginFinish (Request *request)
{
request->RequesterThread = (HANDLE) _beginthreadex (NULL, 0, FinishRequest, request, 0, NULL);
Require (request->RequesterThread != NULL, "start requester thread");
Wait (request->WaitEntered);
Require (WaitForSingleObject (request->FinishDone, 0) == WAIT_TIMEOUT, "request did not wait for workers");
}
static void EndFinish (Request *request)
{
Wait (request->FinishDone);
Wait (request->RequesterThread);
Require (References (request) == 0 && request->Signals == 1, "last worker completion");
}
static Job *NewJob (Request *request, int prf, BOOL holdAfterReady, int result)
{
Job *job;
Require (request->JobCount < PRF_COUNT, "job fixture capacity");
job = &request->Jobs[request->JobCount];
job->Owner = request;
job->Item = &request->Items[request->JobCount++];
job->Item->Free = FALSE;
job->Item->KeyReady = FALSE;
job->Item->DerivationResult = 0;
job->Item->Pkcs5Prf = prf;
job->Result = result;
job->HoldAfterReady = holdAfterReady;
job->Started = Event (); job->MayReturn = Event (); job->ReadyPublished = Event ();
job->MayReleaseReference = Event (); job->WorkerDone = Event ();
return job;
}
static void SubmitJob (Job *job)
{
Request *request = job->Owner;
EncryptionThreadPoolBeginKeyDerivation (request->Ready, request->Drained, &job->Item->KeyReady,
request->References, job->Item->Pkcs5Prf, (unsigned char *) job, sizeof (*job), NULL, 1, 8,
job->Item->DerivedKey, &job->Item->DerivationResult, &request->Abort);
}
static Job *Submit (Request *request, int prf, BOOL holdAfterReady, int result)
{
Job *job = NewJob (request, prf, holdAfterReady, result);
SubmitJob (job);
return job;
}
static void JobDone (Job *job)
{
Wait (job->WorkerDone);
Require (job->Item->KeyReady == TRUE && job->Item->DerivationResult == job->Result,
"result was not published before completion");
}
static void FakeKdf (const unsigned char *password, unsigned char *output, int length)
{
Job *job = (Job *) password;
CurrentJob = job;
SetEvent (job->Started);
Wait (job->MayReturn);
memset (output, job->Owner->Abort || job->Result ? 0 : 0x5a, length);
}
#define FAKE_KDF(name) \
void name (const unsigned char *pwd, int pwd_len, const unsigned char *salt, int salt_len, \
uint32 iterations, unsigned char *dk, int dklen, long volatile *abortFlag) \
{ FakeKdf (pwd, dk, dklen); }
FAKE_KDF (derive_key_blake2s)
FAKE_KDF (derive_key_sha512)
FAKE_KDF (derive_key_whirlpool)
FAKE_KDF (derive_key_sha256)
FAKE_KDF (derive_key_streebog)
int derive_key_argon2 (const unsigned char *pwd, int pwd_len, const unsigned char *salt, int salt_len,
uint32 iterations, uint32 memory, unsigned char *dk, int dklen, long volatile *abortFlag)
{ FakeKdf (pwd, dk, dklen); return ((Job *) pwd)->Result; }
int GetMaxPkcs5OutSize (void) { return MASTER_KEYDATA_SIZE; }
BOOL IsRamEncryptionEnabled (void) { return FALSE; }
uint64 VcGetEncryptionID (PCRYPTO_INFO ci) { return 0; }
void VcUnprotectKeys (PCRYPTO_INFO ci, uint64 id) { }
static void FakeCrypt (unsigned char *data, TC_LARGEST_COMPILER_UINT count, PCRYPTO_INFO ci, int value)
{
CipherJob *job = (CipherJob *) ci;
if (job->HoldWorkers)
{
if (InterlockedIncrement (&job->Started) == 2) SetEvent (job->BothStarted);
Wait (job->MayProcess);
}
memset (data, value, (size_t) count * ENCRYPTION_DATA_UNIT_SIZE);
InterlockedExchangeAdd (&job->Processed, (LONG) count);
}
void EncryptDataUnitsCurrentThread (unsigned __int8 *data, const UINT64_STRUCT *start, TC_LARGEST_COMPILER_UINT count, PCRYPTO_INFO ci)
{ FakeCrypt (data, count, ci, 0xa5); }
void DecryptDataUnitsCurrentThread (unsigned __int8 *data, const UINT64_STRUCT *start, TC_LARGEST_COMPILER_UINT count, PCRYPTO_INFO ci)
{ FakeCrypt (data, count, ci, 0x5a); }
static void RequesterLast (int count)
{
Request *request = NewRequest ();
int i;
for (i = 0; i < count; ++i)
{
Job *job = Submit (request, FIRST_PRF_ID + i, FALSE, 0);
SetEvent (job->MayReturn);
JobDone (job);
Require (References (request) == 1 && request->Signals == 0, "idle submission phase completed");
Require (WaitForSingleObject (*request->Drained, 0) == WAIT_TIMEOUT, "stale idle notification");
}
FinishRequest (request);
Wait (request->FinishDone);
Require (request->Signals == 0, "requester-last path unnecessarily signaled");
}
static void WorkerLast (int count, BOOL cancel, int argonResult)
{
Request *request = NewRequest ();
int i;
for (i = 0; i < count; ++i)
Submit (request, i == count - 1 ? ARGON2 : SHA512, FALSE, i == count - 1 ? argonResult : 0);
BeginFinish (request);
InterlockedExchange (&request->Abort, cancel);
for (i = 0; i < count; ++i)
SetEvent (request->Jobs[i].MayReturn);
EndFinish (request);
for (i = 0; i < count; ++i)
JobDone (&request->Jobs[i]);
}
static void PublicationBoundaries (void)
{
Request *request = NewRequest ();
Job *job = Submit (request, SHA256, TRUE, 0);
SetEvent (job->MayReturn);
Wait (job->ReadyPublished);
BeginFinish (request);
Require (References (request) == 1, "ready result released reference too early");
SetEvent (job->MayReleaseReference);
EndFinish (request);
JobDone (job);
request = NewRequest ();
request->HoldFinalSignal = TRUE;
job = Submit (request, SHA512, FALSE, 0);
BeginFinish (request);
SetEvent (job->MayReturn);
Wait (request->SignalEntered);
Require (References (request) == 0, "final notification boundary");
Require (WaitForSingleObject (request->FinishDone, 0) == WAIT_TIMEOUT, "zero count bypassed final notification");
SetEvent (request->MaySignal);
EndFinish (request);
JobDone (job);
}
static void ResetResources (int failurePoint)
{
Require (OpenResources == 0, "pool resources leaked");
ResourceCount = CreateAttempts = 0;
FailCreateAt = failurePoint;
}
static void StartupFailures (size_t available)
{
int i, creationCount;
Require (EncryptionThreadPoolStart (available - 2), "start baseline pool");
creationCount = CreateAttempts;
EncryptionThreadPoolStop ();
ResetResources (0);
for (i = 1; i <= creationCount; ++i)
{
ResetResources (i);
Require (!EncryptionThreadPoolStart (available - 2), "injected startup failure accepted");
Require (!IsEncryptionThreadPoolRunning () && GetEncryptionThreadCount () == 0, "failed pool published");
EncryptionThreadPoolStop ();
ResetResources (0);
Require (EncryptionThreadPoolStart (available - 2), "restart after failure");
EncryptionThreadPoolStop ();
}
ResetResources (0);
printf ("PASS: %d startup failure points, each followed by a successful restart\n", creationCount);
}
static unsigned __stdcall StopPool (void *unused)
{
EncryptionThreadPoolStop ();
SetEvent (StopDone);
return 0;
}
static HANDLE BeginStop (void)
{
HANDLE thread;
StopWaiting = Event (); StopDone = Event ();
thread = (HANDLE) _beginthreadex (NULL, 0, StopPool, NULL, 0, NULL);
Require (thread != NULL, "start shutdown thread");
Wait (StopWaiting);
Require (!IsEncryptionThreadPoolRunning (), "shutdown still accepts pool work");
Require (WaitForSingleObject (StopDone, 0) == WAIT_TIMEOUT, "shutdown did not drain accepted work");
return thread;
}
static void EndStop (HANDLE thread)
{
Wait (StopDone); Wait (thread);
CloseHandle (thread); CloseHandle (StopWaiting); CloseHandle (StopDone);
StopWaiting = StopDone = NULL;
ResetResources (0);
}
static unsigned __stdcall RunCipher (void *context)
{
CipherJob *job = (CipherJob *) context;
UINT64_STRUCT start = { 0 };
CurrentCipherJob = job;
EncryptionThreadPoolDoWork (job->Type, job->Data, &start, job->Units, (PCRYPTO_INFO) job);
CurrentCipherJob = NULL;
SetEvent (job->Done);
return 0;
}
static void StartCipher (CipherJob *job, BOOL hold, EncryptionThreadPoolWorkType type)
{
memset (job, 0, sizeof (*job));
job->Units = 6; job->Type = type;
job->HoldWorkers = job->HoldRequester = hold;
job->BothStarted = Event (); job->MayProcess = CreateEvent (NULL, TRUE, FALSE, NULL);
Require (job->MayProcess != NULL, "create cipher gate");
job->Waiting = Event (); job->Paused = Event (); job->MayReturn = Event (); job->Done = Event ();
job->Thread = (HANDLE) _beginthreadex (NULL, 0, RunCipher, job, 0, NULL);
Require (job->Thread != NULL, "start cipher requester");
Wait (job->Waiting);
}
static void CheckCipher (CipherJob *job)
{
size_t i;
unsigned char expected = job->Type == EncryptDataUnitsWork ? 0xa5 : 0x5a;
Require (job->Processed == job->Units, "incorrect number of processed units");
for (i = 0; i < job->Units * ENCRYPTION_DATA_UNIT_SIZE; ++i)
Require (job->Data[i] == expected, "incomplete cipher output");
}
static void EndCipher (CipherJob *job)
{
Wait (job->Done); Wait (job->Thread);
CheckCipher (job);
CloseHandle (job->Thread); CloseHandle (job->BothStarted); CloseHandle (job->MayProcess);
CloseHandle (job->Waiting); CloseHandle (job->Paused); CloseHandle (job->MayReturn); CloseHandle (job->Done);
}
static void InlineKdf (void)
{
Request *request = NewRequest ();
Job *job = NewJob (request, SHA512, FALSE, 0);
SetEvent (job->MayReturn);
SubmitJob (job);
CurrentJob = NULL;
Require (job->Item->KeyReady && job->Item->DerivationResult == 0 && References (request) == 1,
"synchronous KDF fallback");
FinishRequest (request);
Wait (request->FinishDone);
}
static void ShutdownInFlight (size_t available)
{
CipherJob first, queued, synchronous = { 0 };
UINT64_STRUCT start = { 0 };
HANDLE stop;
Request *request;
int i;
Require (EncryptionThreadPoolStart (available - 2), "start cipher pool");
StartCipher (&first, TRUE, EncryptDataUnitsWork);
Wait (first.BothStarted);
StartCipher (&queued, FALSE, DecryptDataUnitsWork);
stop = BeginStop ();
synchronous.Units = 6; synchronous.Type = EncryptDataUnitsWork;
EncryptionThreadPoolDoWork (synchronous.Type, synchronous.Data, &start, synchronous.Units, (PCRYPTO_INFO) &synchronous);
CheckCipher (&synchronous);
SetEvent (first.MayProcess);
Wait (first.Paused);
Require (WaitForSingleObject (StopDone, 0) == WAIT_TIMEOUT, "shutdown closed a requester's completion handle");
SetEvent (first.MayReturn);
EndCipher (&first); EndCipher (&queued); EndStop (stop);
Require (EncryptionThreadPoolStart (available - 2), "restart for KDF shutdown");
request = NewRequest ();
for (i = 0; i < PRF_COUNT; ++i) Submit (request, FIRST_PRF_ID + i, FALSE, 0);
BeginFinish (request);
stop = BeginStop ();
InlineKdf ();
for (i = 0; i < request->JobCount; ++i) SetEvent (request->Jobs[i].MayReturn);
EndFinish (request);
for (i = 0; i < request->JobCount; ++i) JobDone (&request->Jobs[i]);
EndStop (stop);
InlineKdf ();
puts ("PASS: shutdown drains active and queued encryption, KDFs and requesters; synchronous fallback and restart");
}
static void FreeRequests (void)
{
int i, j;
Require (OpenResources == 0, "pool resources leaked");
for (i = 0; i < RequestCount; ++i)
{
Request *r = &Requests[i];
for (j = 0; j < r->JobCount; ++j)
{
Job *job = &r->Jobs[j];
CloseHandle (job->Started); CloseHandle (job->MayReturn); CloseHandle (job->ReadyPublished);
CloseHandle (job->MayReleaseReference); CloseHandle (job->WorkerDone);
}
if (r->RequesterThread) CloseHandle (r->RequesterThread);
CloseHandle (r->WaitEntered); CloseHandle (r->FinishDone); CloseHandle (r->SignalEntered); CloseHandle (r->MaySignal);
CloseHandle (*r->Ready); CloseHandle (*r->Drained);
VirtualUnlock (r->Items, sizeof (*r->Items) * PRF_COUNT);
free (r->Ready); free (r->Drained); free (r->References); free (r->Items);
}
}
int main (int argc, char **argv)
{
WORD groups;
size_t available = GetCpuCount (&groups);
size_t wanted = argc > 1 ? (size_t) atoi (argv[1]) : 2;
if (available < 2) { puts ("SKIP: at least two logical CPUs required"); return 0; }
if (argc > 1 && strcmp (argv[1], "startup") == 0)
{
StartupFailures (available);
return 0;
}
if (argc > 1 && strcmp (argv[1], "shutdown") == 0)
{
ShutdownInFlight (available);
FreeRequests ();
return 0;
}
if (wanted > available) wanted = available;
Require (EncryptionThreadPoolStart (available - wanted), "start production pool");
Require (GetEncryptionThreadCount () == wanted, "pool worker count");
RequesterLast (0); RequesterLast (1); RequesterLast (5); RequesterLast (6);
WorkerLast (2, FALSE, 0); WorkerLast (6, FALSE, 0);
WorkerLast (2, TRUE, ARGON2_OPERATION_CANCELLED); WorkerLast (6, TRUE, ARGON2_OPERATION_CANCELLED);
WorkerLast (2, FALSE, ARGON2_MEMORY_ALLOCATION_ERROR);
PublicationBoundaries ();
/* Join every worker before releasing any request-owned allocation or event. */
EncryptionThreadPoolStop ();
FreeRequests ();
printf ("PASS: %d KDF lifetime scenarios with %u workers\n", RequestCount, (unsigned) wanted);
return 0;
}