CryptographicException -1073741816 en SHA256/SHA512 tras actualizaciones de Windows: causa real y solución definitiva

Tras instalar actualizaciones recientes de Windows, algunas apps .NET empezaron a fallar con CryptographicException: Unknown error "-1073741816" al calcular SHA256/SHA512. La causa no es “la KB”, sino un uso no seguro de HashAlgorithm en concurrencia. Aquí tienes diagnóstico, soluciones y mitigaciones.

Índice

Resumen rápido

  • Qué ves: System.Security.Cryptography.CryptographicException: Unknown error "-1073741816" al usar SHA256/SHA512 con ComputeHash, TransformBlock/TransformFinalBlock o CryptoStream.
  • Qué pasa: una misma instancia de HashAlgorithm (ej. SHA256) se usa desde varios hilos, se llama dos veces a TransformFinalBlock o se hace Dispose mientras otro hilo trabaja.
  • Por qué ahora: tras endurecimientos recientes, el proveedor criptográfico devuelve un error explícito (NTSTATUS STATUSINVALIDHANDLE = decimal -1073741816) en lugar de permitir corrupción silenciosa.
  • Arreglo: no compartas instancias; crea una por operación o usa almacenamiento por hilo; sincroniza si no hay alternativa. Cierra el hash con TransformFinalBlock exactamente una vez.
  • Si no puedes tocar código: reduce el paralelismo del proceso que hashea, aplica “throttle”/serialización, y reinicia la app si queda en estado inválido. Desinstalar KBs solo oculta el bug y reexpone a corrupción.

Contexto: por qué aparece ahora y qué significa el código -1073741816

Las clases de hashing de .NET (SHA256, SHA512, HashAlgorithm en general) nunca han sido thread‑safe. Antes, usar la misma instancia desde varios hilos solía “funcionar” de forma no determinista, con riesgo de resultados corruptos. Tras ciertas actualizaciones de Windows, el proveedor subyacente endureció comprobaciones de validez de handles: cuando detecta secuencias inválidas (concurrencia, doble finalización, acceso tras Dispose), falla explícitamente con STATUSINVALIDHANDLE (0xC0000008, decimal -1073741816).

Lo que ha cambiado, pues, no es la API de .NET sino su comportamiento observable: el mismo patrón incorrecto que antes podía pasar inadvertido, ahora se evidencia con un error claro. En algunos procesos, tras el primer fallo, la instancia queda en un estado no recuperable y los siguientes intentos también fallan hasta reiniciar la aplicación o recrear la instancia de hash.

Cuándo se reproduce (señales de auditoría)

  • Llamadas paralelas a ComputeHash sobre una misma instancia compartida (por ejemplo, una static o una singleton).
  • Dispose() o finalizador ejecutándose mientras otro hilo sigue hasheando con esa instancia.
  • Más de una llamada a TransformFinalBlock en el mismo cálculo (o reuso de la misma instancia para varios mensajes sin Initialize y sin exclusión mutua).
  • Patrones como Parallel.ForEach, colas con Task.Run o pipelines que tocan la misma instancia en hilos distintos.
  • Observado por equipos en .NET Framework 4.8 (y también en .NET modernos si el patrón es el mismo) sobre servidores como Windows Server 2022 y en aplicaciones de terceros (conectores, SFTP, herramientas de administración), todas con el denominador común de concurrencia indebida.

Actualizaciones de Windows que los equipos suelen mencionar

Los reportes varían según edición y canal, y muchas KB ya han sido sustituidas por acumulativas posteriores. Ejemplos citados por afectados: KB5037771, KB5035432, KB5037591, KB5037782, KB5038282, KB5039895. Importante: desinstalar una KB “elimina” el síntoma pero no arregla el uso concurrente del hash.

Diagnóstico rápido en tu código

Localiza posibles puntos de contención usando búsquedas sencillas:

  • static SHA256, static SHA512, static HashAlgorithm, Singleton, ServiceLocator que entregue el mismo hash a todos.
  • Reutilización en caches: ConcurrentDictionary<string, SHA256>, “pools” caseros sin exclusión mutua o sin Initialize() al devolver.
  • Uso de TransformFinalBlock múltiples veces por mensaje, o mezcla con ComputeHash en la misma instancia.
  • Acceso desde Parallel.ForEach, TPL Dataflow, Rx o IAsyncEnumerable sin aislamiento por operación.
  • Dispose en un finally que puede ejecutarse mientras otro hilo lee/escribe en la misma instancia.

Solución recomendada: patrones seguros (C#)

Crea una instancia por operación (patrón más simple y correcto)

byte[] ComputeSha256(byte[] data)
{
    // Instancia de vida corta; cada llamada obtiene su propio estado interno
    using (var sha = System.Security.Cryptography.SHA256.Create())
    {
        return sha.ComputeHash(data);
    }
}

Hash por streaming / incremental con bloques

byte[] ComputeSha256Stream(Stream stream)
{
    using (var sha = System.Security.Cryptography.SHA256.Create())
    {
        var buffer = new byte[81920]; // ~80 KiB
        int read;
        while ((read = stream.Read(buffer, 0, buffer.Length)) &gt; 0)
        {
            sha.TransformBlock(buffer, 0, read, null, 0);
        }
        // Cerrar el hash: exactamente UNA llamada a TransformFinalBlock
        sha.TransformFinalBlock(Array.Empty&lt;byte&gt;(), 0, 0);
        return sha.Hash;
    }
}

Hash con CryptoStream (lectura)

byte[] ComputeSha256CryptoStream(Stream input)
{
    using (var sha = System.Security.Cryptography.SHA256.Create())
    using (var cs = new CryptoStream(input, sha, CryptoStreamMode.Read))
    {
        var buffer = new byte[81920];
        while (cs.Read(buffer, 0, buffer.Length) &gt; 0) { / drenar / }
        return sha.Hash;
    }
}

Si hoy compartes una instancia estática (no recomendado): sincroniza

private static readonly object _shaLock = new object();
private static readonly SHA256 _sha = SHA256.Create();

byte\[] ComputeSha256Locked(byte\[] data)
{
lock (\_shaLock)
{
// Ningún otro hilo entrará aquí simultáneamente.
return \_sha.ComputeHash(data);
}
}
// Preferible: eliminar la estática y crear la instancia por llamada. 

Almacenamiento por hilo (ThreadLocal)

Útil si tienes altísimo volumen y te preocupa el coste de construir la instancia repetidamente.

private static readonly ThreadLocal<SHA256> _shaTls =
    new ThreadLocal<SHA256>(() => SHA256.Create(), trackAllValues: true);

byte\[] ComputeSha256Tls(byte\[] data)
{
var sha = \_shaTls.Value!;
// Cada hilo obtiene su propia instancia; no hay cruzado entre hilos
return sha.ComputeHash(data);
}

// En el apagado ordenado de la app:
// foreach (var sha in \_shaTls.Values) sha.Dispose(); 

Pool sencillo de instancias (siempre con exclusión de uso simultáneo)

sealed class Sha256Pool
{
    private readonly System.Collections.Concurrent.ConcurrentBag<SHA256> _bag = new();```
public SHA256 Rent() => _bag.TryTake(out var s) ? s : SHA256.Create();

public void Return(SHA256 sha)
{
    // Restablece el estado antes de reciclarla
    sha.Initialize();
    _bag.Add(sha);
}
```
}

static readonly Sha256Pool \_pool = new Sha256Pool();

byte\[] ComputeSha256Pooled(byte\[] data)
{
var sha = \_pool.Rent();
try { return sha.ComputeHash(data); }
finally { \_pool.Return(sha); }
} </code></pre>

<h3>APIs modernas (si tu runtime las ofrece)</h3>
<ul>
  <li><strong><code>IncrementalHash</code></strong>: <code>var inc = IncrementalHash.CreateHash(HashAlgorithmName.SHA256); inc.AppendData(...); var hash = inc.GetHashAndReset();</code></li>
  <li><strong>Métodos estáticos <code>HashData</code></strong>: <code>SHA256.HashData(ReadOnlySpan&lt;byte&gt;)</code> y sobrecargas para <code>Stream</code> simplifican un patrón “crear y descartar”.</li>
</ul>
<p>Aunque la API cambie, la regla <em>sigue siendo la misma</em>: no compartas instancias entre hilos y cierra el hash una sola vez por mensaje.</p>

<h2>Errores frecuentes que disparan el fallo</h2>
<ul>
  <li><strong>Compartir un singleton</strong> de <code>SHA256</code>/<code>SHA512</code> en toda la aplicación “por rendimiento”. El coste de crear la instancia vs. el riesgo no compensa.</li>
  <li><strong>Hacer <code>Dispose</code> concurrente</strong> (ej. patrón <code>using</code> que sale mientras otro hilo no ha terminado).</li>
  <li><strong>Mezclar <code>ComputeHash</code> y <code>TransformBlock/TransformFinalBlock</code></strong> en la misma instancia para varios mensajes sin <code>Initialize</code> y sin sincronización.</li>
  <li><strong>Llamar dos veces a <code>TransformFinalBlock</code></strong> por cálculo o no llamarlo nunca en un flujo incremental.</li>
  <li><strong>Bloquear sobre el propio objeto hash</strong> (ej. <code>lock(sha)</code>) y luego pasarlo a otro componente que también lo bloquea: posible interbloqueo.</li>
</ul>

<h2>Mitigaciones si no puedes tocar código inmediatamente</h2>
<p>No arreglan la raíz, pero reducen impacto mientras preparas un fix:</p>
<ul>
  <li><strong>Serializa el trabajo que usa hashing</strong>: baja el grado de paralelismo a 1 en el pipeline afectado (reglas de “impact”/throttle, <em>batch size</em>=1, <code>MaxDegreeOfParallelism=1</code> en TPL Dataflow, etc.).</li>
  <li><strong>Reinicia la app o el servicio</strong> si, tras el primer fallo, queda en estado de error continuo. El reinicio recrea estados internos.</li>
  <li><strong>Evita desinstalar KBs</strong>: como último recurso y temporal. Quita el síntoma y vuelve a exponer corrupción silenciosa y riesgo de seguridad.</li>
</ul>

<h2>Plan de acción recomendado (paso a paso)</h2>
<ol>
  <li><strong>Inventario</strong>: busca <code>HashAlgorithm</code>, <code>SHA256</code>, <code>SHA512</code>, <code>HMACSHA*</code>, <code>TransformFinalBlock</code> y miembros <code>static</code> o caches.</li>
  <li><strong>Refactor</strong>: instancia por operación (<code>using</code>) o por hilo (<code>ThreadLocal</code>). Elimina singletons.</li>
  <li><strong>Revisión de flujos incrementales</strong>: comprueba que <code>TransformFinalBlock</code> se llama exactamente una vez y que no se reutiliza la instancia para múltiples mensajes sin <code>Initialize</code> o sin exclusión.</li>
  <li><strong>Pruebas de estrés</strong>: añade tests con alta concurrencia y verifica determinismo de hashes.</li>
  <li><strong>Observabilidad</strong>: loguea entrada/salida de fase de “finalización” y excepciones; añade métricas de cuentas de hash por segundo e índices de error.</li>
  <li><strong>Despliegue por anillos</strong>: lleva la corrección a un anillo canario y observa colas, latencia y tasas de error.</li>
</ol>

<h2>Cómo reproducir el problema (ejemplo mínimo)</h2>
<pre><code class="language-csharp">[Test]
public async Task Repro_ShareInstanceAcrossTasks()
{
    using var sha = SHA256.Create(); // &lt;-- Error: instancia compartida
    var data = Enumerable.Range(0, 1024).Select(i =&gt; (byte)i).ToArray();```
var tasks = Enumerable.Range(0, 100).Select(_ =&gt; Task.Run(() =&gt; sha.ComputeHash(data)));
try
{
    await Task.WhenAll(tasks);
    Assert.Fail("Debería fallar por uso concurrente");
}
catch (CryptographicException ex)
{
    Console.WriteLine(ex.Message); // Unknown error "-1073741816"
}
```
} 

Pruebas robustas de concurrencia (patrones de verificación)

  • Paralelismo controlado: ejecuta 10–100 tareas que hasheen buffers aleatorios y comprueba igualdad con un oráculo (ej. el mismo hash calculado en un hilo único).
  • Inyección de cancelación: cancela aleatoriamente operaciones para descubrir interacciones con Dispose.
  • Contención de CPU: repite miles de veces ComputeHash con Parallel.For para agotar caminos de carrera.

Tabla de síntomas, causas y correcciones

SíntomaCausa probableCorrección
CryptographicException: Unknown error "-1073741816"Acceso concurrente a una instancia de HashAlgorithmInstancia por operación; no compartir; o sincronizar con lock
Falla persistente tras primer errorEstado interno quedó inválidoRecrear instancia; reiniciar app; evitar reuso tras error
Hashes inconsistentes sin excepciónCondición de carrera previa a los endurecimientosAplicar patrones thread‑safe ahora; añadir tests
ObjectDisposedException aleatoriaDispose() en paralelo a usoControlar ciclo de vida; evitar compartir/reciclar sin exclusión
Excepción al llamar dos veces a TransformFinalBlockFinalización múltiple del mismo cálculoLlamar a TransformFinalBlock exactamente una vez

Buenas prácticas adicionales

  • Evita mezclar proveedores: usa siempre la misma familia (CNG/gestionado) en la aplicación, a menos que tengas una razón concreta. Mezclas pueden complicar diagnósticos.
  • No caches indiscriminadamente objetos criptográficos; son stateful. Si necesitas rendimiento, usa ThreadLocal o un pool controlado, nunca un único objeto global.
  • Siempre cierra el cálculo con TransformFinalBlock (o leyendo hasta EOF en CryptoStream).
  • Errores en HMAC: la misma regla aplica a HMACSHA256/HMACSHA512; tampoco son thread‑safe.
  • Idempotencia: tras una excepción, no intentes “reciclar” la misma instancia; crea una nueva.

Preguntas frecuentes (FAQ)

¿Es caro crear una instancia por operación? En la práctica, el coste es pequeño frente al I/O y lógica de negocio. Además, evita condiciones de carrera y estados dañados.

¿Puedo usar SHA256Managed para evitar el error? No soluciona el problema fundamental. También es stateful y no thread‑safe. Si lo compartes, puedes volver a corrupción silenciosa.

¿Por qué el error menciona “invalid handle” si solo hago hashes? El proveedor subyacente maneja recursos/handles internos; la concurrencia o el ciclo de vida incorrecto los invalida.

¿Puedo “resetear” con Initialize()? Sí, pero solo cuando la instancia no está siendo usada por otro hilo. Initialize() no hace magia si hay concurrencia.

¿El problema afecta a AES u otros algoritmos? Los objetos criptográficos en general no son thread‑safe. Evita compartirlos entre hilos a menos que la documentación diga lo contrario (lo cual es raro).

Plantillas listas para usar

Wrapper seguro “por operación”

public static class Hashing
{
    public static byte[] Sha256(byte[] data)
    {
        using var sha = SHA256.Create();
        return sha.ComputeHash(data);
    }```
public static byte[] Sha256(Stream stream)
{
    using var sha = SHA256.Create();
    return sha.ComputeHash(stream);
}
```
} </code></pre>

<h3>Extensión para <code>Stream</code> (lectura hasta EOF)</h3>
<pre><code class="language-csharp">public static class StreamHashExtensions
{
    public static byte[] ComputeSha256(this Stream input)
    {
        using var sha = SHA256.Create();
        using var cs  = new CryptoStream(input, sha, CryptoStreamMode.Read);```
    var buffer = new byte[81920];
    while (cs.Read(buffer, 0, buffer.Length) &gt; 0) { }
    return sha.Hash;
}
```
} 

Checklist de producción

  1. ✅ Sin instancias static de HashAlgorithm o HMAC compartidas.
  2. ✅ Cada operación crea su instancia o usa un ThreadLocal seguro.
  3. TransformFinalBlock se llama exactamente una vez por cálculo.
  4. ✅ No hay Dispose concurrente ni reuso tras excepción.
  5. ✅ Tests de estrés y determinismo de hashes incluidos en CI.
  6. ✅ Alertas/métricas para cualquier CryptographicException en producción.

Conclusión

El error CryptographicException: Unknown error "-1073741816" no es una “regresión de Windows”, sino un síntoma de un uso no thread‑safe de los objetos de hashing. Las actualizaciones han hecho visible un bug que siempre estuvo ahí. La corrección real y sostenible pasa por no compartir instancias, respetar el ciclo de vida y finalizar correctamente cada cálculo. Si hoy no puedes tocar el código, reduce el paralelismo y planifica el refactor cuanto antes. Así eliminas las excepciones actuales y, lo más importante, evitas la corrupción silenciosa de datos.

Anexo: mapeo de códigos y notas

  • Decimal -1073741816 = Hex 0xC0000008 = NTSTATUS STATUSINVALIDHANDLE.
  • Mensaje “Unknown error” indica que el handle criptográfico interno se observó en un estado no válido (p. ej., concurrencia, doble finalización o uso tras Dispose).
  • La “solución” de quitar KBs recientes solo devuelve el comportamiento antiguo (potencial corrupción silenciosa). No es recomendable salvo de forma temporal mientras preparas el fix.

Apéndice práctico: guía express de migración

  1. Localiza con búsquedas en tu repo: static SHA, HashAlgorithm, TransformFinalBlock, Parallel.For, ComputeHash(, CryptoStream.
  2. Decide el patrón: por operación (preferido), por hilo (si volumen extremo) o pool controlado.
  3. Refactoriza los puntos críticos y añade pruebas de concurrencia.
  4. Observa en preproducción: cero excepciones, hashes deterministas, latencia aceptable.
  5. Despliega en anillos y monitoriza.

Ejemplos de corrección para SHA512 (idéntico concepto)

byte[] ComputeSha512(byte[] data)
{
    using var sha = SHA512.Create();
    return sha.ComputeHash(data);
}

byte\[] ComputeSha512Stream(Stream stream)
{
using var sha = SHA512.Create();
var buffer = new byte\[81920];
int read;
while ((read = stream.Read(buffer, 0, buffer.Length)) > 0)
{
sha.TransformBlock(buffer, 0, read, null, 0);
}
sha.TransformFinalBlock(Array.Empty\(), 0, 0);
return sha.Hash;
} 

Playbook de operaciones (si no puedes tocar el código hoy)

  • Servicios Windows/IIS: limita la concurrencia del componente que hashea (configura los workers del job/pipeline a 1; en IIS, reduce colas por app si el hashing está en el path de solicitud).
  • Jobs programados: procesa elementos de uno en uno en lugar de lotes paralelos.
  • Reintentos inteligentes: tras una CryptographicException, descarta la instancia afectada y reintenta con una nueva (si el flujo lo permite).
  • Recuperación: si la app entra en un bucle de fallos, reiníciala para recrear estado. Investiga y corrige el patrón de concurrencia cuanto antes.

En resumen: el camino seguro y definitivo es hacer el uso de hashing thread‑safe. El resto son curitas temporales.

Notas finales de implementación

  • TransformFinalBlock debe llamarse exactamente una vez por cálculo; en CryptoStream la finalización ocurre al consumir completamente el stream.
  • Initialize() reinicia el estado, pero no convierte a la instancia en thread‑safe ni “borra” accesos concurrentes en curso.
  • Validación: compara el hash con un oráculo (ej. openssl dgst -sha256 u otra implementación) en tus pruebas de integración.

Con estos cambios, tu aplicación será inmune tanto a la excepción -1073741816 como —lo más importante— a resultados corruptos por carreras de concurrencia.

Índice