diff --git a/Tests/test_windows_header_cleanup.py b/Tests/test_windows_header_cleanup.py new file mode 100644 index 00000000..36f75aac --- /dev/null +++ b/Tests/test_windows_header_cleanup.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Inject failures into the production Windows header setup/cleanup paths. + +Extracts those paths verbatim; replaces only the intervening cryptographic search +with a chosen success/failure result. Tracks allocation, event and lock ownership. +Run in an x64 MSVC environment: python Tests/test_windows_header_cleanup.py +""" +import argparse +import os +from pathlib import Path +import shutil +import subprocess +import tempfile + +from test_windows_kdf_completion import ROOT, block + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--build-dir", type=Path) + args = parser.parse_args() + if os.name != "nt" or not shutil.which("cl.exe"): + parser.error("An x64 MSVC developer environment on Windows is required") + build = (args.build_dir or Path(tempfile.mkdtemp(prefix="vc-header-cleanup-"))).resolve() + build.mkdir(parents=True, exist_ok=True) + source = (ROOT / "src/Common/Volumes.c").read_text() + start = source.index("int ReadVolumeHeaderWithAbort (") + header = block(source, start) + setup = header[:header.index("\tcrypto_loadkey (keyInfo,")] + cleanup = header[header.index("\nerr:"):] + signature = source[start:source.index("{", start)] + (build / "header_cleanup.inc").write_text(signature + setup + """ + if (TestHeaderError) { status = ERR_PASSWORD_WRONG; goto err; } + status = ERR_SUCCESS; + goto ret; +""" + cleanup) + item_end = source.index("} KeyDerivationWorkItem;") + len("} KeyDerivationWorkItem;") + item_start = source.rindex("typedef struct", 0, item_end) + (build / "kdf_item.inc").write_text(source[item_start:item_end]) + exe = build / "windows_header_cleanup_test.exe" + command = ["cl.exe", "/nologo", "/O2", "/W3", "/WX", "/MD", "/wd4101", + "/DUNICODE", "/D_UNICODE", f"/I{build}"] + command += [f"/I{ROOT / 'src' / directory}" + for directory in ("", "Common", "Crypto", "Crypto/Argon2/include")] + command += [str(ROOT / "Tests/windows_header_cleanup_test.c"), f"/Fe:{exe}"] + result = subprocess.run(command, cwd=build, capture_output=True, text=True) + (build / "build.log").write_text(result.stdout + result.stderr) + if result.returncode: + raise RuntimeError(result.stdout + result.stderr) + subprocess.run([str(exe)], cwd=build, check=True, timeout=30) + + +if __name__ == "__main__": + main() diff --git a/Tests/windows_header_cleanup_test.c b/Tests/windows_header_cleanup_test.c new file mode 100644 index 00000000..e371c9f3 --- /dev/null +++ b/Tests/windows_header_cleanup_test.c @@ -0,0 +1,165 @@ +/* Copyright (c) 2026 AM Crypto. Licensed under the Apache License 2.0. */ +#include "Common/EncryptionThreadPool.h" +#include "Common/Password.h" +#include "Common/Volumes.h" +#include + +typedef struct { void *Pointer; size_t Size; BOOL Open; } Allocation; +typedef struct { void *Pointer; size_t Size; BOOL Locked; } LockedRange; +static Allocation Allocations[32]; +static LockedRange Locks[32]; +static HANDLE Events[8]; +static int AllocationCount, LockCount, EventCount, Attempts, FailAt, TestHeaderError, Checks; +static size_t PoolThreads; +static CRYPTO_INFO BorrowedCrypto; + +static void Require (BOOL condition, const char *message) +{ + if (!condition) { fprintf (stderr, "FAIL: %s (failure point %d)\n", message, FailAt); ExitProcess (1); } +} + +static BOOL Fail (void) { return ++Attempts == FailAt; } + +static void *TestAlloc (size_t size) +{ + void *memory; + Allocation *allocation; + if (Fail()) return NULL; + Require (AllocationCount < 32, "allocation fixture capacity"); + memory = malloc (size); + Require (memory != NULL, "fixture allocation"); + memset (memory, 0xa6, size); + allocation = &Allocations[AllocationCount++]; + allocation->Pointer = memory; + allocation->Size = size; + allocation->Open = TRUE; + return memory; +} + +static void TestFree (void *memory) +{ + int i; + for (i = AllocationCount - 1; i >= 0; --i) + if (Allocations[i].Open && Allocations[i].Pointer == memory) + { + Allocations[i].Open = FALSE; + free (memory); + return; + } + Require (FALSE, "unknown allocation or double free"); +} + +static BOOL TestLock (void *memory, size_t size) +{ + LockedRange *range; + Require (LockCount < 32, "lock fixture capacity"); + range = &Locks[LockCount++]; + range->Pointer = memory; + range->Size = size; + range->Locked = TRUE; + return TRUE; +} + +static BOOL TestUnlock (void *memory, size_t size) +{ + int i; + for (i = LockCount - 1; i >= 0; --i) + if (Locks[i].Locked && Locks[i].Pointer == memory && Locks[i].Size == size) + { + Locks[i].Locked = FALSE; + return TRUE; + } + Require (FALSE, "unlock of an uninitialized range"); + return FALSE; +} + +static HANDLE TestCreateEvent (void *attributes, BOOL manual, BOOL initial, void *name) +{ + HANDLE event; + if (Fail()) return NULL; + Require (EventCount < 8, "event fixture capacity"); + event = CreateEventW (attributes, manual, initial, name); + Require (event != NULL, "fixture event creation"); + Events[EventCount++] = event; + return event; +} + +static BOOL TestClose (HANDLE event) +{ + int i; + for (i = 0; i < EventCount; ++i) + if (Events[i] == event && event != NULL) + { + Events[i] = NULL; + return CloseHandle (event); + } + Require (FALSE, "close of an uninitialized event"); + return FALSE; +} + +static PCRYPTO_INFO TestCryptoOpen (void) { return TestAlloc (sizeof (CRYPTO_INFO)); } +static void TestCryptoClose (PCRYPTO_INFO cryptoInfo) +{ + Require (cryptoInfo != &BorrowedCrypto, "closed a borrowed crypto context"); + TestFree (cryptoInfo); +} +static size_t TestThreads (void) { return PoolThreads; } +static void TestWait (HANDLE event) { (void) event; Require (FALSE, "wait without submitted workers"); } + +#undef TCalloc +#undef TCfree +#undef TC_WAIT_EVENT +#undef burn +#define TCalloc TestAlloc +#define TCfree TestFree +#define TC_WAIT_EVENT(event) TestWait (event) +#define burn(memory,size) memset (memory, 0, size) +#define crypto_open TestCryptoOpen +#define crypto_close TestCryptoClose +#define VirtualLock TestLock +#define VirtualUnlock TestUnlock +#define CreateEventW TestCreateEvent +#define CloseHandle TestClose +#define GetEncryptionThreadCount TestThreads +#include "kdf_item.inc" +#include "header_cleanup.inc" + +static int Run (int failure, BOOL borrowed, BOOL missingOutput, int prf, size_t threads, BOOL headerError) +{ + PCRYPTO_INFO result = NULL; + int status, i; + AllocationCount = LockCount = EventCount = Attempts = 0; + FailAt = failure; + PoolThreads = threads; + TestHeaderError = headerError; + status = ReadVolumeHeaderWithAbort (FALSE, NULL, NULL, prf, 0, + missingOutput ? NULL : &result, borrowed ? &BorrowedCrypto : NULL, NULL, NULL); + if (failure) Require (status == ERR_OUTOFMEMORY, "allocation failure returned the wrong status"); + else if (missingOutput && !borrowed) Require (status == ERR_PARAMETER_INCORRECT, "missing output accepted"); + else Require (status == (headerError ? ERR_PASSWORD_WRONG : ERR_SUCCESS), "header status changed"); + if (status != ERR_SUCCESS) Require (result == NULL, "failed header retained owned crypto context"); + if (result) TestCryptoClose (result); + for (i = 0; i < AllocationCount; ++i) Require (!Allocations[i].Open, "allocation leaked"); + for (i = 0; i < LockCount; ++i) Require (!Locks[i].Locked, "locked range leaked"); + for (i = 0; i < EventCount; ++i) Require (!Events[i], "event leaked"); + ++Checks; + return Attempts; +} + +int main (void) +{ + int borrowed, setup, error; + for (borrowed = 0; borrowed <= 1; ++borrowed) + for (setup = 0; setup < 3; ++setup) + for (error = 0; error <= 1; ++error) + { + int prf = setup == 1 ? 1 : 0; + size_t threads = setup == 2 ? 1 : 2; + int attempts = Run (0, borrowed, borrowed, prf, threads, error), failure; + for (failure = 1; failure <= attempts; ++failure) + Run (failure, borrowed, borrowed, prf, threads, error); + } + Run (0, FALSE, TRUE, 0, 2, FALSE); + printf ("PASS: %d header setup/cleanup scenarios, including every allocation/event failure and borrowed contexts\n", Checks); + return 0; +} diff --git a/Tests/windows_kdf_completion_test.c b/Tests/windows_kdf_completion_test.c index 0b882bb7..2d297303 100644 --- a/Tests/windows_kdf_completion_test.c +++ b/Tests/windows_kdf_completion_test.c @@ -221,13 +221,15 @@ static int InitializeRequest (Request *request) TC_EVENT *keyDerivationCompletedEvent = NULL, *noOutstandingWorkItemEvent = NULL; LONG *outstandingWorkItemCount = NULL; KeyDerivationWorkItem *keyDerivationWorkItems = NULL; - int keyDerivationWorkItemsSize = 0, pkcs5PrfCount = PRF_COUNT, i; + int keyDerivationWorkItemsSize = 0, pkcs5PrfCount = PRF_COUNT, i, status; #include "kdf_init.inc" request->Ready = keyDerivationCompletedEvent; request->Drained = noOutstandingWorkItemEvent; request->References = outstandingWorkItemCount; request->Items = keyDerivationWorkItems; return 0; +err_early: + return status; } static Request *NewRequest (void) diff --git a/src/Common/Volumes.c b/src/Common/Volumes.c index 90c52996..acf0fae5 100644 --- a/src/Common/Volumes.c +++ b/src/Common/Volumes.c @@ -193,7 +193,7 @@ int ReadVolumeHeaderWithAbort (BOOL bBoot, unsigned char *encryptedHeader, Passw int keyInfoBufferSize = sizeof (KEY_INFO) + TC_KEY_INFO_BUFFER_ALIGNMENT; size_t keyInfoBufferOffset; PKEY_INFO keyInfo; - PCRYPTO_INFO cryptoInfo; + PCRYPTO_INFO cryptoInfo = NULL; CRYPTOPP_ALIGN_DATA(TC_DERIVED_KEY_BUFFER_ALIGNMENT) unsigned char dk[MASTER_KEYDATA_SIZE]; int enqPkcs5Prf, pkcs5_prf; uint16 headerVersion; @@ -240,12 +240,15 @@ int ReadVolumeHeaderWithAbort (BOOL bBoot, unsigned char *encryptedHeader, Passw } else { - if (!retInfo) - return ERR_PARAMETER_INCORRECT; + if (!retInfo) + goto err_early; cryptoInfo = *retInfo = crypto_open (); if (cryptoInfo == NULL) - return ERR_OUTOFMEMORY; + { + status = ERR_OUTOFMEMORY; + goto err_early; + } } #if !defined(_UEFI) /* use thread pool only if no PRF was specified */ @@ -253,13 +256,17 @@ int ReadVolumeHeaderWithAbort (BOOL bBoot, unsigned char *encryptedHeader, Passw { keyDerivationCompletedEvent = TCalloc (sizeof (TC_EVENT)); if (!keyDerivationCompletedEvent) - return ERR_OUTOFMEMORY; + { + status = ERR_OUTOFMEMORY; + goto err_early; + } noOutstandingWorkItemEvent = TCalloc (sizeof (TC_EVENT)); if (!noOutstandingWorkItemEvent) { TCfree(keyDerivationCompletedEvent); - return ERR_OUTOFMEMORY; + status = ERR_OUTOFMEMORY; + goto err_early; } outstandingWorkItemCount = TCalloc (sizeof (LONG)); @@ -267,7 +274,8 @@ int ReadVolumeHeaderWithAbort (BOOL bBoot, unsigned char *encryptedHeader, Passw { TCfree(keyDerivationCompletedEvent); TCfree(noOutstandingWorkItemEvent); - return ERR_OUTOFMEMORY; + status = ERR_OUTOFMEMORY; + goto err_early; } keyDerivationWorkItemsSize = sizeof (KeyDerivationWorkItem) * pkcs5PrfCount; @@ -277,7 +285,8 @@ int ReadVolumeHeaderWithAbort (BOOL bBoot, unsigned char *encryptedHeader, Passw TCfree(keyDerivationCompletedEvent); TCfree(noOutstandingWorkItemEvent); TCfree(outstandingWorkItemCount); - return ERR_OUTOFMEMORY; + status = ERR_OUTOFMEMORY; + goto err_early; } for (i = 0; i < pkcs5PrfCount; ++i) @@ -296,7 +305,8 @@ int ReadVolumeHeaderWithAbort (BOOL bBoot, unsigned char *encryptedHeader, Passw TCfree(keyDerivationCompletedEvent); TCfree(noOutstandingWorkItemEvent); TCfree(outstandingWorkItemCount); - return ERR_OUTOFMEMORY; + status = ERR_OUTOFMEMORY; + goto err_early; } *noOutstandingWorkItemEvent = CreateEvent (NULL, FALSE, FALSE, NULL); @@ -307,7 +317,8 @@ int ReadVolumeHeaderWithAbort (BOOL bBoot, unsigned char *encryptedHeader, Passw TCfree(keyDerivationCompletedEvent); TCfree(noOutstandingWorkItemEvent); TCfree(outstandingWorkItemCount); - return ERR_OUTOFMEMORY; + status = ERR_OUTOFMEMORY; + goto err_early; } VirtualLock (keyDerivationWorkItems, keyDerivationWorkItemsSize); @@ -759,12 +770,22 @@ ret: } #endif +ret_key_info: burn (keyInfo, sizeof (KEY_INFO)); #if !defined(DEVICE_DRIVER) && !defined(_UEFI) VirtualUnlock (keyInfoBuffer, keyInfoBufferSize); #endif TCfree(keyInfoBuffer); return status; + +err_early: + // Partial KDF setup has already been freed; no worker was submitted. + if (cryptoInfo && cryptoInfo != retHeaderCryptoInfo) + { + crypto_close (cryptoInfo); + *retInfo = NULL; + } + goto ret_key_info; } int ReadVolumeHeader (BOOL bBoot, unsigned char *encryptedHeader, Password *password, int selected_pkcs5_prf, int pim, PCRYPTO_INFO *retInfo, CRYPTO_INFO *retHeaderCryptoInfo)