Release header allocations after early setup failures

Free the key-info buffer and owned crypto context on pre-KDF errors,
preserving borrowed contexts and avoiding uninitialized drain resources.

Inject every setup allocation and event failure to verify cleanup.
This commit is contained in:
Mounir IDRASSI
2026-10-16 21:27:25 +09:00
parent 8b1b0d5222
commit 7eab121669
4 changed files with 253 additions and 11 deletions
+54
View File
@@ -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()
+165
View File
@@ -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 <stdio.h>
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;
}
+3 -1
View File
@@ -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)
+31 -10
View File
@@ -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)