CryptographicException « Unknown error -1073741816 » après mise à jour Windows : cause STATUS_INVALID_HANDLE et correctifs .NET/CNG pour SHA‑256/SHA‑512

Après les mises à jour cumulatives Windows de mai‑juin 2024, des applications .NET et des composants natifs se mettent à échouer avec CryptographicException: Unknown error -1073741816 lors d’un hachage SHA‑256/512. Voici la cause, des correctifs pérennes et des exemples de code prêts à l’emploi.

Sommaire

Erreur System.Security.Cryptography.CryptographicException: Unknown error "-1073741816" après mise à jour Windows

Vue d’ensemble de la question

  • Après l’installation de mises à jour cumulatives Windows de mai‑juin 2024 (notamment KB5037771, KB5037782, KB5038282, KB5035432 et correctifs mensuels ultérieurs), de nombreuses applications .NET 4.x et composants natifs (SharePoint, Siemens TIA Portal, Windows Admin Center, solutions SFTP, etc.) commencent à lever l’exception ci‑dessus lors du calcul d’un hachage SHA‑256/SHA‑512.
  • Le même code fonctionnait auparavant : l’erreur n’apparaît qu’après les mises à jour. Le message « Unknown error » masque le code NTSTATUS 0xC0000008 (‑1073741816), c’est‑à‑dire STATUS_INVALID_HANDLE.
  • Le problème se déclenche souvent après un certain temps de fonctionnement : une fois qu’il survient, toutes les opérations de hachage continuent d’échouer jusqu’au redémarrage du processus.

Symptômes typiques et signaux faibles

  • Une application .NET qui réalisait des hachages en parallèle devient instable après plusieurs minutes/heures de charge : l’exception apparaît de façon soudaine puis persiste.
  • Des services IIS retournent des erreurs 500/502 lorsque des middleware ou handlers calculent un hash (authentification, signature de payload, vérification d’intégrité).
  • Des outils d’administration, portails web ou SFTP échouent à des étapes qui incluent du hachage ou de la signature (vérification de packages, import/export, contrôle d’intégrité de fichiers).
  • Les journaux .NET ou Windows ne montrent que Unknown error avec le code négatif -1073741816.

Cause racine identifiée

  1. Non‑respect du contrat de thread‑safety des classes HashAlgorithm/SHAxxx :
    • Le wrapper CNG (SHA256Cng, SHA512Cng, etc.) n’a jamais été thread‑safe.
    • Des appels parallèles sur la même instance (ou un double appel à TransformFinalBlock) corrompent l’état natif ; CNG/BCrypt détecte désormais un handle invalide et jette l’exception plutôt que de retourner un résultat silencieusement corrompu.
  2. Renforcement du comportement des bibliothèques CNG livrées avec les mises à jour : la validation des handles et la détection des mauvaises séquences d’appels ont été durcies. Le même code applicatif, incorrect mais toléré auparavant, échoue désormais explicitement.

Ce qui se passe sous le capot

Les implémentations Windows de SHA256/SHA512 reposent sur CNG (bcrypt.dll). Chaque instance .NET encapsule un BCRYPT_HASH_HANDLE. Lorsqu’une instance est utilisée concurremment, ou qu’on appelle deux fois TransformFinalBlock sur la même instance, la séquence natale (BCryptCreateHashBCryptHashDataBCryptFinishHash) est violée ; CNG invalide le handle et remonte STATUS_INVALID_HANDLE (0xC0000008-1073741816 côté .NET).

Comment reproduire rapidement l’erreur (à but de diagnostic)

Ne pas utiliser ce modèle en production ; il illustre l’anti‑pattern : partage d’une même instance SHA256 entre threads.

// Anti‑pattern volontaire (repro)
using System.Security.Cryptography;
using System.Threading.Tasks;

var sha = SHA256.Create(); // UNE SEULE instance partagée
Parallel.For(0, 1000, i =>
{
var data = BitConverter.GetBytes(i);
// Appels concurrents sur la même instance = état corrompu et, après MAJ 2024, CryptographicException
var hash = sha.ComputeHash(data);
}); 

Exemples de correctifs côté .NET

Créer une instance par appel (recommandé)

byte[] HashOnce(ReadOnlySpan<byte> data)
{
    using var sha = SHA256.Create();
    return sha.ComputeHash(data.ToArray());
}

API modernes en .NET 5+ : HashData

byte[] HashOnceFast(ReadOnlySpan<byte> data)
{
    return SHA256.HashData(data); // crée et gère l'instance en interne
}

Flux incrémental sans erreur de finalisation

byte[] HashStream(Stream input)
{
    using var sha = SHA256.Create();
    var buffer = new byte[81920];
    int read;
    while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
    {
        sha.TransformBlock(buffer, 0, read, null, 0);
    }
    sha.TransformFinalBlock(Array.Empty<byte>(), 0, 0); // UNE seule fois
    return sha.Hash!;
}

Synchroniser l’accès si un singleton doit rester (option tolérée)

private static readonly object ShaLock = new();
private static readonly SHA256 ShaSingleton = SHA256.Create();

byte[] HashWithLock(byte[] data)
{
lock (ShaLock)
{
return ShaSingleton.ComputeHash(data);
}
} 

IncrementalHash pour les pipelines

byte[] HashWithIncremental(ReadOnlySpan<byte> data)
{
    using var ih = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
    ih.AppendData(data);
    return ih.GetHashAndReset(); // finalise une seule fois
}

Mise en pool d’instances pour charges parallèles

Évitez un verrou global en empruntant une instance par requête ; retournez‑la ensuite au pool. Ne partagez jamais la même instance entre deux threads au même moment.

using System.Collections.Concurrent;
using System.Security.Cryptography;

public static class Sha256Pool
{
private static readonly ConcurrentBag _pool = new();

```
public static byte[] ComputeHashPooled(ReadOnlySpan<byte> data)
{
    if (!_pool.TryTake(out var sha))
        sha = SHA256.Create();
    try
    {
        return sha.ComputeHash(data.ToArray());
    }
    finally
    {
        _pool.Add(sha); // l'instance redevient disponible pour un AUTRE thread
    }
}
```

} 

Correctifs côté natif C/C++ (CNG)

En natif, créez un hash handle par appel ou par thread ; ne réutilisez jamais le même BCRYPT_HASH_HANDLE en parallèle, et appelez BCryptFinishHash une seule fois avant BCryptDestroyHash.

NTSTATUS HashDataSha256(_In_reads_bytes_(len) const BYTE* data, ULONG len, _Out_writes_bytes_(32) BYTE* out)
{
    BCRYPT_ALG_HANDLE hAlg = NULL;
    BCRYPT_HASH_HANDLE hHash = NULL;
    DWORD cbHashObject = 0, cbData = 0, cbHash = 0;
    PBYTE pbHashObject = NULL;
    NTSTATUS status = 0;

```
status = BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_SHA256_ALGORITHM, NULL, 0);
if (status < 0) goto Cleanup;

status = BCryptGetProperty(hAlg, BCRYPT_OBJECT_LENGTH, (PBYTE)&cbHashObject, sizeof(cbHashObject), &cbData, 0);
if (status < 0) goto Cleanup;

pbHashObject = (PBYTE)HeapAlloc(GetProcessHeap(), 0, cbHashObject);
if (!pbHashObject) { status = STATUS_NO_MEMORY; goto Cleanup; }

status = BCryptGetProperty(hAlg, BCRYPT_HASH_LENGTH, (PBYTE)&cbHash, sizeof(cbHash), &cbData, 0);
if (status < 0 || cbHash != 32) goto Cleanup;

// Handle par APPEL (pas partagé)
status = BCryptCreateHash(hAlg, &hHash, pbHashObject, cbHashObject, NULL, 0, 0);
if (status < 0) goto Cleanup;

status = BCryptHashData(hHash, (PUCHAR)data, len, 0);
if (status < 0) goto Cleanup;

status = BCryptFinishHash(hHash, out, cbHash, 0); // finalise UNE fois
```

Cleanup:
if (hHash) BCryptDestroyHash(hHash);
if (pbHashObject) HeapFree(GetProcessHeap(), 0, pbHashObject);
if (hAlg) BCryptCloseAlgorithmProvider(hAlg, 0);
return status;
} 

Traduire le code négatif en hexadécimal

// -1073741816 = 0xC0000008 (STATUS_INVALID_HANDLE)
int hr = -1073741816;
string hex = $"0x{hr:X8}"; // 0xC0000008

Solutions et contournements

ObjectifAction recommandéeCommentaires
Rendre le code sûr et pérenneCréer une instance de SHA256/SHA512 par thread ou par appel :
using var sha = SHA256.Create();
Idéal : évite tout partage d’état.
Synchroniser l’accès si l’on doit conserver une instance statique :
lock (shaLock) { sha.ComputeHash(buf); }
Le verrou doit englober l’appel complet, y compris TransformFinalBlock.
Ne jamais appeler TransformFinalBlock plus d’une fois sur la même instance.Problème souvent rencontré dans les flux incrémentaux.
Limiter l’impact immédiatRedémarrer l’application/service lorsque l’erreur apparaît.Remède temporaire.
Installer les correctifs applicatifs fournis par l’éditeur tiers (ex. Siemens).Microsoft ne peut pas corriger le code tiers.
Revenir à l’ancien comportement (non recommandé en production)Désinstaller les KB incriminés ou appliquer un GPO de blocage.Élimine l’exception… mais réintroduit le risque de hachage incorrect et expose à d’autres failles corrigées par les mises à jour.

Informations complémentaires utiles

  • Les implémentations gérées pures (SHA256Managed, SHA512Managed, ou la classe IncrementalHash) sont intrinsèquement sûres si vous créez une instance par appel ; elles peuvent servir de remplacement si la performance CNG n’est pas critique.
  • Le code d’erreur négatif -1073741816 correspond à l’hexadécimal 0xC0000008 ; connaître cette correspondance facilite le diagnostic dans les journaux.
  • Sur des serveurs fortement parallèles, la mise en pool d’instances (via ConcurrentBag<T> + SHA256.Create() par requête) offre de meilleures performances qu’un verrou global.
  • Si vous exploitez un framework externe qui n’est pas encore patché :
    • Limitez la concurrence (ex. règles de crawl/BDC dans SharePoint, réduction du parallèle dans des jobs).
    • Surveillez la publication de correctifs propres à l’éditeur et appliquez‑les dès disponibilité.

Checklist d’audit de code

  • Rechercher toute variable statique de type SHA256/SHA512/HashAlgorithm et vérifier si elle est utilisée depuis plusieurs threads.
  • Inspecter les pipelines de streaming : la paire TransformBlock / TransformFinalBlock ne doit être appelée qu’une seule fois par instance.
  • Vérifier que chaque tâche asynchrone qui calcule un hash possède sa propre instance (ne jamais passer une instance à travers un await si un autre code peut l’utiliser en parallèle).
  • Contrôler les bibliothèques internes : adapters, helpers de sécurité, middlewares d’authentification, calculs d’ETag, signatures de webhooks.
  • Dans le code natif, confirmer que BCryptCreateHash est invoqué par appel/fil (et non partagé), et que BCryptFinishHash est appelé exactement une fois avant BCryptDestroyHash.

Stratégies de performance sûres

StratégieConcurrenceLatenceComplexitéRemarques
Instance par appelExcellenteFaible à modéréeFaibleChoix par défaut recommandé ; allocation/déallocation simple.
Singleton + lockFaible (sérialisation)Peut augmenter sous chargeFaibleAcceptable pour faible trafic ; goulot d’étranglement sous forte charge.
Pool d’instancesTrès bonneFaibleMoyenneBien adapté aux services multi‑threads ; attention à remettre l’instance dans le pool.
ThreadLocal<SHA256>Bonne (usage synchrone)FaibleMoyenneRéservé aux scénarios purs synchrones ; éviter si le contexte peut changer de thread.

Detection et monitoring en production

Améliorer les logs d’exception

try
{
    using var sha = SHA256.Create();
    var hash = sha.ComputeHash(data);
}
catch (CryptographicException ex)
{
    // Ex.HResult est souvent l'NTSTATUS converti en int signé
    Console.Error.WriteLine($"Crypto error: {ex.Message}, HResult={ex.HResult} (0x{ex.HResult:X8})");
    // Attendu en cas de problème: -1073741816 (0xC0000008)
}

Recherche rapide des machines potentiellement touchées

# Rechercher l'erreur dans les journaux d'application
Get-WinEvent -LogName Application -ErrorAction SilentlyContinue |
  Where-Object { $_.Message -match "CryptographicException" -and $_.Message -match "-1073741816" } |
  Select-Object TimeCreated, ProviderName, Id, Message

# Lister la présence des KB connues

Get-HotFix | Where-Object { $_.HotFixID -match "KB5037771|KB5037782|KB5038282|KB5035432" } |
Select-Object HotFixID, InstalledOn 

Foire aux questions

Est‑ce un bug Windows ?
Plutôt un durcissement : des séquences d’utilisation incorrectes (non thread‑safe, double finalisation) étaient parfois tolérées. Les mises à jour 2024 les font échouer explicitement au lieu de produire un hash potentiellement erroné.

Pourquoi l’erreur n’apparaît‑elle qu’après un certain temps ?
Parce que la course entre threads finit par invalider l’état interne. Une fois le handle corrompu, toutes les opérations suivantes échouent jusqu’au redémarrage du processus ou la recréation propre de l’instance.

Utiliser SHA256Managed résout‑il le problème ?
Cela peut contourner temporairement l’erreur CNG, mais la règle fondamentale demeure : une instance par appel. Si vous partagez quand même une instance, vous retombez sur des comportements indéfinis.

Puis‑je désinstaller les mises à jour KB ?
C’est un palliatif risqué. Vous perdrez des correctifs de sécurité et, pire, vous pourriez revenir à des résultats de hachage silencieusement incorrects. Privilégiez le correctif applicatif.

Un verrou global est‑il acceptable ?
Oui pour un faible trafic. Pour la haute concurrence, préférez un pool d’instances ou une instance par appel, qui éliminent la contention.

Et côté natif ?
Créez un BCRYPT_HASH_HANDLE distinct par opération, ne réutilisez pas un handle en parallèle, finalisez une seule fois, puis détruisez proprement.

Plan d’action recommandé

  1. Isoler les emplacements de calcul de hash (auditer utilitaires, middleware, bibliothèques).
  2. Remplacer toute instance partagée par une instance par appel ou une mise en pool d’instances bien confinée.
  3. Corriger les pipelines incrémentaux pour garantir TransformFinalBlock unique.
  4. Renforcer la journalisation (log hex/HResult) et ajouter des tests de charge multi‑threads.
  5. Déployer progressivement, surveiller les métriques d’erreurs et performances, puis retirer d’éventuels contournements temporaires.

Exemples de tests de non‑régression

[Fact]
public void Hash_IsolatedInstance_IsThreadSafe()
{
    var data = Enumerable.Range(0, 10000).Select(i => BitConverter.GetBytes(i)).ToArray();
    Parallel.ForEach(data, d =>
    {
        var _ = SHA256.HashData(d); // instance gérée en interne
    });
}

[Fact]
public void Hash_TransformFinalBlock_Once()
{
using var sha = SHA256.Create();
sha.TransformBlock(new byte[] {1,2,3}, 0, 3, null, 0);
sha.TransformFinalBlock(Array.Empty(), 0, 0);
Assert.Throws(() => // finalisation double interdite
sha.TransformFinalBlock(Array.Empty(), 0, 0)
);
} 

Cas particuliers et bonnes pratiques

  • Async/await : si vous conservez un HashAlgorithm dans un champ et vous l’utilisez après un await, il peut être utilisé depuis un autre thread. Créez l’instance à l’intérieur de la méthode ou passez‑la par paramètre à un bloc local.
  • Durée de vie : HashAlgorithm est IDisposable. Libérez systématiquement les instances (using).
  • Interop : évitez de mélanger des appels managés et natifs sur la même instance/handle ; encapsulez‑les proprement derrière une API.
  • ETag / Web API : calculez l’ETag avec une instance jetable par requête ou via HashData.

Résumé exécutif

L’exception -1073741816 révèle une utilisation concurrente non autorisée des objets de hachage. Les mises à jour Windows 2024 rendent cette erreur explicite et plus sûre. La correction durable n’est ni un rollback KB ni un contournement fragile : c’est la discipline d’accès suivante : une instance de hachage = un seul thread ou une section critique. En modernisant le code (instances locales, HashData, IncrementalHash, pool d’instances), vous restaurez la stabilité tout en renforçant l’intégrité cryptographique.


Annexe : aide‑mémoire rapide

  • Erreur : CryptographicException: Unknown error -10737418160xC0000008 (STATUS_INVALID_HANDLE).
  • Déclencheur : partage d’instance HashAlgorithm entre threads / double finalisation.
  • Correctif : instance par appel, ou lock intégral si singleton imposé.
  • API modernes : SHA256.HashData, IncrementalHash.
  • Natif : un BCRYPT_HASH_HANDLE par opération, finaliser une fois, détruire.
Sommaire