WINDOWS KERNEL / STORAGE SECURITY · 2026

The Driver Worked. Was the Data Safe?

A modern audit of a legacy encrypted virtual disk—and the gap between successful I/O, durable data, recoverable metadata and trustworthy encryption.

A virtual disk driver can encrypt every byte correctly, return success for every read and write, survive years in production—and still violate the storage guarantees that the filesystem above it depends on. The important question is not whether the cipher works, but whether the complete storage system keeps every promise made to the filesystem above it.

The driver presented an ordinary container file as a disk device. Applications and filesystems saw a normal drive letter. The driver queued block requests, encrypted or decrypted sectors in a system thread, and issued file I/O against a local or network-hosted backing file. The design included an encrypted header, read-only recovery, resumable key rotation and crash-state metadata.

Much of the architecture was sensible for its time. The harder question is: which promises did the driver make to Windows, and which of those promises did the implementation really keep?

1. The system under audit

The product used a Windows NT kernel driver to expose an encrypted container as a block device:

Win32 applications
        ↓
FAT or NTFS
        ↓
\Device\VaultDiskN
        ↓
IRP queue and per-volume system thread
        ↓
sector encryption / decryption
        ↓
ordinary .vlt container file
        ↓
local disk or network share

Each mounted volume owned a device object, a DOS-device link, a backing-file handle, a cryptographic context, an aligned I/O buffer and a worker thread. The device used direct I/O, so read and write buffers normally arrived through MDLs. The backing file was opened for synchronous, unbuffered access. Requests were serialized through an interlocked list and a semaphore.

The device extension held the volume state, backing-file context, request queue and cryptographic context:

typedef struct _VAULT_DISK_EXTENSION {
    PDEVICE_OBJECT DeviceObject;
    PEPROCESS FileProcess;
    HANDLE FileHandle;

    LIST_ENTRY RequestList;
    KSPIN_LOCK RequestLock;
    KSEMAPHORE RequestSemaphore;
    PETHREAD WorkerThread;

    PVOID IoBuffer;
    ULONG IoBufferSize;
    ULONG AlignmentMask;

    ULONGLONG VolumeSize;
    ULONGLONG DataOffset;
    LARGE_INTEGER NonceSeed;

    VAULT_CRYPT_CONTEXT CryptContext;
    BOOLEAN ReadOnly;
    BOOLEAN Stopping;
} VAULT_DISK_EXTENSION, *PVAULT_DISK_EXTENSION;

The container header stored disk geometry, partition information, a volume-specific nonce, format flags and recovery state. Sector encryption was random-access: the cryptographic state for a block was derived from the volume nonce and the block offset, so the driver could decrypt sector 800,000 without reading sectors 0 through 799,999.

The management application mounted a container by sending a variable-length IOCTL containing a drive letter, path, algorithm selector and key material. Once mounted, the filesystem could format and use the virtual disk like ordinary storage.

The architecture addressed production requirements:

  • transparent encryption below applications;
  • local and network-backed containers;
  • independent random access to encrypted sectors;
  • read-only recovery when write access was unavailable;
  • an encrypted metadata header;
  • resumable full-volume key rotation;
  • a marker intended to detect an unclean dismount;
  • traffic and mount-state reporting.

The block path, backing-file access, cryptographic operations and management control were separated cleanly. The recurring weakness lay in operating-system contracts that were completed after only their most visible requirement had been satisfied.

2. Why the original request model was reasonable

The dispatch path placed reads, writes and flushes into the volume queue:

NTSTATUS
VaultDispatchReadWrite(
    PDEVICE_OBJECT DeviceObject,
    PIRP Irp
    )
{
    PVAULT_DISK_EXTENSION extension;

    extension = DeviceObject->DeviceExtension;

    IoMarkIrpPending(Irp);
    Irp->IoStatus.Status = STATUS_PENDING;
    Irp->IoStatus.Information = 0;

    ExInterlockedInsertTailList(
        &extension->RequestList,
        &Irp->Tail.Overlay.ListEntry,
        &extension->RequestLock);

    KeReleaseSemaphore(
        &extension->RequestSemaphore,
        IO_NO_INCREMENT,
        1,
        FALSE);

    return STATUS_PENDING;
}

A dedicated system thread removed IRPs in order, mapped direct-I/O buffers through the MDL, performed encryption or decryption and completed the request:

VOID
VaultDiskWorker(
    PVOID Context
    )
{
    PVAULT_DISK_EXTENSION extension = Context;

    for (;;) {
        PLIST_ENTRY entry;
        PIRP irp;
        PIO_STACK_LOCATION irpStack;
        NTSTATUS status;

        KeWaitForSingleObject(
            &extension->RequestSemaphore,
            Executive,
            KernelMode,
            FALSE,
            NULL);

        while ((entry = ExInterlockedRemoveHeadList(
                    &extension->RequestList,
                    &extension->RequestLock)) != NULL) {

            irp = CONTAINING_RECORD(
                entry,
                IRP,
                Tail.Overlay.ListEntry);

            irpStack = IoGetCurrentIrpStackLocation(irp);
            status = VaultProcessRequest(extension, irp, irpStack);

            irp->IoStatus.Status = status;
            IoCompleteRequest(
                irp,
                NT_SUCCESS(status) ? IO_DISK_INCREMENT : IO_NO_INCREMENT);
        }
    }
}

For late-1990s storage and processor speeds, this design had defensible advantages.

First, it preserved request ordering. A filesystem write that entered the queue before another write would normally reach the backing file first. Second, potentially blocking file operations and cryptographic work ran in a known thread context rather than in an arbitrary caller’s dispatch context. Third, a single worker simplified access to a cryptographic device or session that might not tolerate parallel operations. Fourth, one aligned scratch buffer could be reused safely because only one request was active at a time.

The design also reduced concurrency bugs. There was no need to coordinate several writes against the same cryptographic state, no parallel reallocation of the shared buffer, and no complex completion graph. On slow disks, the serialization cost could be hidden by the physical device itself.

The problem was not that the architecture was old. The problem was that several special operations—flush, shutdown, re-keying and recovery-state updates—were treated as ordinary functions even though they define transaction boundaries for the entire filesystem.

3. Finding one: the flush IRP was ordered but not durable

The strongest audit finding concerned IRP_MJ_FLUSH_BUFFERS.

A flush IRP entered the same queue as reads and writes. This was already better than completing it in the dispatch routine, because the worker would not see the flush until earlier queued writes had finished.

The worker, however, handled it like this:

static NTSTATUS
VaultProcessRequest(
    PVAULT_DISK_EXTENSION Extension,
    PIRP Irp,
    PIO_STACK_LOCATION IrpStack
    )
{
    switch (IrpStack->MajorFunction) {
    case IRP_MJ_READ:
        return VaultReadBlocks(Extension, Irp, IrpStack);

    case IRP_MJ_WRITE:
        return VaultWriteBlocks(Extension, Irp, IrpStack);

    case IRP_MJ_FLUSH_BUFFERS:
        return STATUS_SUCCESS;

    default:
        return STATUS_INVALID_DEVICE_REQUEST;
    }
}

This code preserves queue ordering, but it does not establish durability.

By the time the flush is processed, earlier calls to ZwWriteFile may have completed. That proves that the I/O request reached the backing-file stack and was accepted. It does not necessarily prove that the data crossed every volatile cache below that stack. The filesystem above the virtual disk does not know that the “disk” is actually a file. It only knows that the disk driver returned success for a flush request.

That success is a promise:

All writes ordered before this flush have reached the persistence boundary required by the storage contract.

The driver did not perform an explicit flush against the backing file. It did not send a flush to the lower storage device. It did not document that durability was intentionally weaker than a physical disk. It simply completed the IRP successfully.

A minimal correction would flush the backing file after all previous writes had drained:

static NTSTATUS
VaultFlushBackingFile(
    PVAULT_DISK_EXTENSION Extension
    )
{
    IO_STATUS_BLOCK ioStatus;
    NTSTATUS status;

    if (Extension->ReadOnly)
        return STATUS_SUCCESS;

    status = ZwFlushBuffersFile(
        Extension->FileHandle,
        &ioStatus);

    if (!NT_SUCCESS(status))
        return status;

    return ioStatus.Status;
}

Correct flush handling also depends on what object the handle references, how the underlying filesystem and storage stack implement the flush, whether write-through or removable-media semantics matter, and what happens when the container lives on a network share. The virtual disk must not claim a stronger guarantee than the backing path can provide.

The failure mode is subtle because ordinary testing rarely detects it. Files are written, read back and appear correct. The defect emerges during sudden power loss, controller reset, network failure or host crash—exactly when the filesystem depends most heavily on ordering and flush boundaries.

A mathematically correct cipher does not save a filesystem from a false persistence promise.

4. Finding two: resumable re-keying was not a transaction

The driver supported full-volume key rotation. It could stop after a batch of blocks, persist progress and continue later. This was an advanced feature: large containers did not have to be converted in one uninterrupted session.

The re-keying loop followed this pattern:

static NTSTATUS
VaultRekeyRange(
    PVAULT_REKEY_CONTEXT Context,
    ULONG BlocksToProcess
    )
{
    NTSTATUS status = STATUS_SUCCESS;
    ULONG index;

    for (index = 0; index < BlocksToProcess; index++) {
        status = VaultReadEncryptedBlock(
            Context->FileHandle,
            Context->CurrentOffset,
            Context->Ciphertext,
            Context->BlockSize);
        if (!NT_SUCCESS(status))
            break;

        status = VaultDecryptBlock(
            &Context->OldKey,
            Context->CurrentOffset,
            Context->Ciphertext,
            Context->Plaintext,
            Context->BlockSize);
        if (!NT_SUCCESS(status))
            break;

        status = VaultEncryptBlock(
            &Context->NewKey,
            Context->CurrentOffset,
            Context->Plaintext,
            Context->Ciphertext,
            Context->BlockSize);
        if (!NT_SUCCESS(status))
            break;

        status = VaultWriteEncryptedBlock(
            Context->FileHandle,
            Context->CurrentOffset,
            Context->Ciphertext,
            Context->BlockSize);
        if (!NT_SUCCESS(status))
            break;

        Context->CurrentOffset += Context->BlockSize;
    }

    if (NT_SUCCESS(status))
        status = VaultSaveRekeyProgress(Context);

    return status;
}

The code maintained a new key and a stop position outside the data stream. After each block was rewritten, the in-memory position advanced. The persistent progress record was updated after the processing batch.

That creates a crash-consistency window:

  1. Block N is read and decrypted with the old key.
  2. Block N is encrypted with the new key.
  3. The new ciphertext is written successfully.
  4. The machine loses power before the progress record is durably advanced.
  5. Recovery reads the old checkpoint and assumes block N still uses the old key.

The feature is resumable in normal operation, but not necessarily recoverable after failure at every instruction boundary. The distinction matters:

  • Resumable means the program can stop intentionally and continue from a recorded position.
  • Crash-consistent means every durable state can be interpreted unambiguously after an unplanned interruption.

A safe design needs a transaction protocol. Several approaches are possible.

One is copy-on-write re-keying: write converted data to a new region or container, make the new metadata durable, and switch the active generation atomically. Another is region-based metadata with explicit key generations. Each region records whether it belongs to generation A or B, and the generation record is made durable before old key material is retired. A journal can record the intended transition and replay or roll it back. A smaller design can use two headers with monotonically increasing sequence numbers and checksums, but the order of data, metadata and flushes must still be specified precisely.

The generation record stored both active and pending metadata:

typedef struct _VAULT_REGION_STATE {
    ULONGLONG FirstSector;
    ULONG SectorCount;
    ULONG KeyGeneration;
    ULONG State;
    ULONG Crc32;
} VAULT_REGION_STATE, *PVAULT_REGION_STATE;

The critical point is not the exact structure. It is that the system must be able to answer one question after any crash:

Which key decrypts this region, based only on durable metadata?

If the answer depends on whether a write “probably finished” before a checkpoint, the re-keying protocol is incomplete.

5. Finding three: the unclean-dismount flag did not form a complete state machine

The container header included a field intended to warn that the previous mount had ended abnormally. The expected protocol is simple:

clean container
    ↓ mount
write unclean = 1 and flush header
    ↓ normal I/O
clean dismount
    ↓
write unclean = 0 and flush header

If the machine crashes while mounted, the value remains one. On the next mount, the driver or management application can warn the user, force read-only access or request filesystem repair.

The implementation read the field when mounting and updated it during dismount, but did not reliably arm the marker after a successful mount. The state update was:

NTSTATUS
VaultMountVolume(
    PVAULT_VOLUME Volume
    )
{
    NTSTATUS status;

    status = VaultReadAndDecryptHeader(Volume);
    if (!NT_SUCCESS(status))
        return status;

    if (Volume->Header.UncleanDismount)
        Volume->MountWarning = VaultWarningUnclean;

    return VaultCreateDiskDevice(Volume);
}

NTSTATUS
VaultDismountVolume(
    PVAULT_VOLUME Volume,
    BOOLEAN Forced
    )
{
    Volume->Header.UncleanDismount = Forced ? 1 : 0;
    return VaultWriteEncryptedHeader(Volume);
}

On a normal mount, the on-disk value remains clean. If Windows then crashes, there may be no durable indication that the container was open.

This is not merely a missing assignment. Recovery metadata is a protocol. The mount transition must be persisted before writable filesystem traffic begins. The clean transition must occur only after queued writes have completed, the backing file has been flushed, and the encrypted header update itself has become durable.

The corrected ordering is closer to:

read and validate header
set unclean = 1
write encrypted header
flush backing file
expose writable disk device
...
stop new requests
complete outstanding requests
flush backing file
set unclean = 0
write encrypted header
flush backing file
remove disk device

Even then, the flag does not prove that the filesystem is consistent. It only records whether the driver observed a clean close protocol. It should be treated as one signal in recovery, not as a substitute for the filesystem’s own journal or repair logic.

6. Finding four: the control interface trusted a caller-controlled layout

The mount IOCTL used METHOD_NEITHER and a variable-length request. The fixed header was followed by a path string and cryptographic data:

#define IOCTL_VAULT_MOUNT \
    CTL_CODE(FILE_DEVICE_UNKNOWN, 0x901, METHOD_NEITHER, FILE_ANY_ACCESS)

typedef struct _VAULT_MOUNT_REQUEST {
    UCHAR DriveLetter;
    ULONG PathLength;
    ULONG KeyLength;
    ULONG Flags;
    LARGE_INTEGER NonceSeed;
    UCHAR Data[1];
} VAULT_MOUNT_REQUEST, *PVAULT_MOUNT_REQUEST;

The dispatch routine checked that the input buffer was at least the fixed structure size and then passed the request to the mount path:

PVAULT_MOUNT_REQUEST request;
ULONG inputLength;

request = irpStack->Parameters.DeviceIoControl.Type3InputBuffer;
inputLength = irpStack->Parameters.DeviceIoControl.InputBufferLength;

if (inputLength < sizeof(VAULT_MOUNT_REQUEST))
    return STATUS_BUFFER_TOO_SMALL;

path = (PCHAR)request->Data;
key = request->Data + request->PathLength + 1;

The fixed-size check does not prove that either variable field lies inside the caller’s buffer. The parser still needs to validate:

  • that PathLength does not exceed the remaining input;
  • that adding the path terminator cannot overflow;
  • that KeyLength fits after the path;
  • that the total does not wrap around ULONG;
  • that the path is terminated within the declared range;
  • that the key length is valid for the selected algorithm;
  • that flags do not request contradictory modes;
  • that the request was made by a caller allowed to mount a disk;
  • that the user pages remain valid for the entire access.

Structured exception handling can catch some invalid addresses. It does not validate the message and does not prevent time-of-check/time-of-use races.

A modernized protocol should be self-contained and copied by the I/O manager. One option is a METHOD_BUFFERED request containing offsets rather than pointers:

#define IOCTL_VAULT_MOUNT \
    CTL_CODE(FILE_DEVICE_UNKNOWN, 0x901, METHOD_BUFFERED, \
             FILE_READ_DATA | FILE_WRITE_DATA)

typedef struct _VAULT_MOUNT_REQUEST_V2 {
    ULONG Size;
    ULONG Version;
    ULONG Flags;
    ULONG PathOffset;
    ULONG PathLength;
    ULONG KeyOffset;
    ULONG KeyLength;
    WCHAR DriveLetter;
    USHORT Reserved;
    LARGE_INTEGER NonceSeed;
} VAULT_MOUNT_REQUEST_V2, *PVAULT_MOUNT_REQUEST_V2;

Parsing must be based on the actual InputBufferLength, not on trust in fields inside the request:

static NTSTATUS
VaultValidateRange(
    ULONG BufferLength,
    ULONG Offset,
    ULONG Length
    )
{
    if (Offset > BufferLength)
        return STATUS_INVALID_PARAMETER;

    if (Length > BufferLength - Offset)
        return STATUS_INVALID_PARAMETER;

    return STATUS_SUCCESS;
}

This does not make the interface automatically safe, but it removes dependency on caller virtual addresses and makes the request suitable for versioning, fuzzing and compatibility testing.

The security descriptor on the control device also matters. A mount operation can expose arbitrary files as block devices, consume nonpaged resources, hold file handles and create drive-letter links. FILE_ANY_ACCESS should not be the only authorization decision.

7. Finding five: confidentiality did not provide sector integrity

The driver encrypted sectors independently using a volume nonce combined with the byte or sector offset. That is attractive for virtual disks because it supports random access. The driver can read one sector, derive its cryptographic state and decrypt it without touching neighbouring sectors.

The legacy container protected some header fields with an integrity value, but the data path did not maintain an authentication tag for every sector or region. Encryption concealed plaintext. It did not necessarily detect deliberate or accidental ciphertext modification.

Without authentication, an attacker or storage fault may be able to:

  • modify encrypted bytes and cause controlled or uncontrolled plaintext changes;
  • replay an older encrypted sector at the same offset;
  • copy ciphertext between offsets if the mode permits useful relationships;
  • corrupt filesystem metadata without producing an explicit cryptographic error;
  • exploit the filesystem parser through malformed decrypted structures.

The virtual disk may then return STATUS_SUCCESS with corrupted plaintext because the cipher has no evidence that the sector is invalid.

Authenticated storage adds complexity because tags need reliable placement, versioning and update ordering. A tag stored beside each sector changes the mapping between logical sectors and container offsets. Tags stored in larger authenticated regions increase write amplification. Metadata trees require their own crash-consistency design. Existing filesystems expect sector-sized atomicity that a container format may not naturally provide.

Still, a modern encrypted container should make integrity an explicit part of the format rather than assuming that confidentiality is sufficient. The design needs answers for:

  • tag granularity;
  • replay protection;
  • nonce uniqueness;
  • metadata authentication;
  • partial-write recovery;
  • key rotation across authenticated regions;
  • compatibility upgrades.

This is where storage engineering and cryptography meet. Selecting an authenticated cipher is not enough; the on-disk transaction model must preserve the conditions under which that cipher is secure.

8. Finding six: one worker thread per volume was a policy, not just an implementation detail

The per-volume worker simplified correctness but encoded a strict scheduling policy: only one request could be active for a mounted volume.

On historical disks, that may have been acceptable. On modern NVMe storage, a single worker, one shared scratch buffer and synchronous backing-file operations can become the dominant bottleneck. A network-hosted container makes the problem worse: one stalled remote read can block all other I/O to the volume.

The design also complicates cancellation and teardown. An IRP can wait in the queue while the backing operation ahead of it blocks. The dismount path must stop new requests, drain or cancel pending IRPs, wake the worker, close the backing file and remove the device without racing a completion. A sentinel IRP or special major-function value may terminate the thread, but every exit path must leave the list and object references consistent.

The right modernization is not simply “create more threads.” Parallelism changes cryptographic-buffer ownership, write ordering, flush barriers, re-key exclusion and teardown rules. The architecture needs explicit classes of operations:

  • reads that may execute concurrently;
  • non-overlapping writes that may execute concurrently;
  • barriers that wait for all earlier writes;
  • metadata operations that exclude ordinary I/O;
  • re-key operations that coordinate key generations;
  • teardown that cancels or drains every class safely.

A framework queue, worker pool or asynchronous lower-I/O path can implement that policy, but the policy must exist first. The old single thread made the policy implicit and conservative. A modern design should make it explicit and testable.

9. What the legacy design got right

A useful audit separates obsolete assumptions from sound architecture. Several ideas are worth preserving.

The block layer and cryptographic layer were separated. The virtual disk did not require filesystem modifications or application integration. Encryption remained below the filesystem boundary.

The container supported independent random access. Sector cryptographic state was derived from stable volume data and the logical offset, allowing ordinary block I/O rather than whole-file streaming.

Potentially blocking work was moved out of the dispatch path. File access and encryption ran in a dedicated thread context with clear serialization.

The backing-file path was treated as a storage dependency. The code queried alignment, used aligned buffers and opened the file for unbuffered access instead of relying blindly on cached user-mode semantics.

The format carried versionable metadata. Geometry, partition data, nonce material and state were stored in a defined header rather than scattered through external configuration.

Read-only recovery existed. If writable access to the container failed, the later design could mount it read-only rather than turning every access problem into total data unavailability.

Key material had an explicit lifetime. At least one implementation zeroed the in-memory key context during teardown. That is not a complete secret-management strategy, but it shows that key lifetime was considered part of the design.

The designers recognized interrupted operations. Resumable re-keying and an unclean-dismount marker were attempts to handle failure, not merely happy-path features. Their protocols were incomplete, but the problems themselves were correctly identified.

These strengths are exactly why a blind rewrite is risky. Replacing every component may discard the accumulated behaviour that users and recovery tools depend on. The safer approach is to map the contracts, prove which ones are correct and replace the parts whose guarantees cannot be established.

10. A staged modernization plan

A responsible modernization would begin with observability and tests before changing the container format.

Establish the current contract

Document every IRP major function, IOCTL, on-disk field, alignment rule, status code and teardown path. Trace request ordering from the virtual disk through the backing file. Determine what user-mode software depends on undocumented behaviour.

Build a failure harness

Normal functional tests are not enough. The test plan should inject failures after each significant event:

  • before and after ciphertext writes;
  • before and after progress checkpoints;
  • before and after header updates;
  • during flush;
  • while a network-backed container is disconnected;
  • during forced dismount;
  • while requests remain queued;
  • after key material changes but before metadata commits.

The harness should forcibly terminate the VM or reset the test machine, then remount and verify every sector and metadata generation.

Repair the I/O contract

Flush requests must be ordered with writes and propagated to the backing object. Shutdown and dismount must stop new traffic, drain or fail outstanding requests, flush data, persist clean state and only then remove the device. Errors from the backing stack must reach the original IRP rather than being converted to success.

Replace the control ABI

Move from METHOD_NEITHER and caller-dependent layouts to a versioned, self-contained protocol. Validate all offsets and lengths against the actual buffer. Restrict device access with an explicit security descriptor. Define WOW64 behaviour rather than inheriting pointer-size assumptions.

Introduce transactional recovery metadata

Use generation numbers, checksums and duplicate headers or a small journal. Make every state transition durable in a documented order. Re-keying should identify the key generation for every region after any crash. Clean-mount state should be armed before writable exposure and cleared only after a durable flush.

Add authenticated storage

Design authentication as an on-disk feature, not as a cipher switch. Decide tag granularity, replay semantics, metadata layout and upgrade strategy. Provide a migration path for existing containers and a recovery tool that can distinguish authentication failure from ordinary I/O failure.

Evolve concurrency only after correctness

Once ordering and recovery are explicit, replace the single worker with a policy-aware queueing model. Preserve barriers and metadata exclusion. Measure whether parallel reads, writes or cryptographic batches actually improve the target workload before increasing complexity.

Keep compatibility visible

Do not silently reinterpret old containers. Mount them through a clearly identified legacy path, preferably read-only by default until they are validated. Migration should produce a new generation rather than modifying the only copy in place.

Conclusion: old code fails at the boundary between success and proof

The encrypted virtual disk did many difficult things correctly. It created disk devices dynamically, translated block I/O into file I/O, handled alignment, maintained per-volume state, encrypted arbitrary sectors, supported local and network containers, and provided recovery-oriented features that many simpler products ignored.

Its most important defects were not obvious memory leaks or missing NULL checks. They were gaps between a successful function call and the guarantee inferred from that success:

  • a completed write was treated as durable storage;
  • a saved offset was treated as a transactional re-key checkpoint;
  • a header flag was treated as a complete crash-state protocol;
  • a fixed-size structure check was treated as validation of variable data;
  • encryption was treated as evidence of integrity;
  • serialization was treated as sufficient lifecycle control.

That pattern is common in legacy kernel software. The code may still compile. The driver may still mount. The happy path may still pass. The risk lives in the contracts that only become visible during power loss, malformed input, cancellation, network failure, concurrency or recovery.

A useful audit therefore asks more than “does the function return success?” It asks: what does success mean to the layer above, and has the driver actually proved it?

LEGACY STORAGE AND DRIVER AUDIT

Does the driver still work—but nobody can prove what happens during a crash?

KernelSoft analyzes undocumented Windows and Linux storage software, audits crash consistency, I/O contracts and security boundaries, and produces a staged modernization plan.

Discuss a legacy storage system