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.
Resumen rápido
- Qué ves:
System.Security.Cryptography.CryptographicException: Unknown error "-1073741816"
al usarSHA256
/SHA512
conComputeHash
,TransformBlock
/TransformFinalBlock
oCryptoStream
. - Qué pasa: una misma instancia de
HashAlgorithm
(ej.SHA256
) se usa desde varios hilos, se llama dos veces aTransformFinalBlock
o se haceDispose
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, unastatic
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 sinInitialize
y sin exclusión mutua). - Patrones como
Parallel.ForEach
, colas conTask.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 sinInitialize()
al devolver. - Uso de
TransformFinalBlock
múltiples veces por mensaje, o mezcla conComputeHash
en la misma instancia. - Acceso desde
Parallel.ForEach
, TPL Dataflow, Rx oIAsyncEnumerable
sin aislamiento por operación. Dispose
en unfinally
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)) > 0)
{
sha.TransformBlock(buffer, 0, read, null, 0);
}
// Cerrar el hash: exactamente UNA llamada a TransformFinalBlock
sha.TransformFinalBlock(Array.Empty<byte>(), 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) > 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<byte>)</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(); // <-- Error: instancia compartida
var data = Enumerable.Range(0, 1024).Select(i => (byte)i).ToArray();```
var tasks = Enumerable.Range(0, 100).Select(_ => Task.Run(() => 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
conParallel.For
para agotar caminos de carrera.
Tabla de síntomas, causas y correcciones
Síntoma | Causa probable | Corrección |
---|---|---|
CryptographicException: Unknown error "-1073741816" | Acceso concurrente a una instancia de HashAlgorithm | Instancia por operación; no compartir; o sincronizar con lock |
Falla persistente tras primer error | Estado interno quedó inválido | Recrear instancia; reiniciar app; evitar reuso tras error |
Hashes inconsistentes sin excepción | Condición de carrera previa a los endurecimientos | Aplicar patrones thread‑safe ahora; añadir tests |
ObjectDisposedException aleatoria | Dispose() en paralelo a uso | Controlar ciclo de vida; evitar compartir/reciclar sin exclusión |
Excepción al llamar dos veces a TransformFinalBlock | Finalización múltiple del mismo cálculo | Llamar 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 enCryptoStream
). - 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) > 0) { }
return sha.Hash;
}
```
}
Checklist de producción
- ✅ Sin instancias
static
deHashAlgorithm
oHMAC
compartidas. - ✅ Cada operación crea su instancia o usa un
ThreadLocal
seguro. - ✅
TransformFinalBlock
se llama exactamente una vez por cálculo. - ✅ No hay
Dispose
concurrente ni reuso tras excepción. - ✅ Tests de estrés y determinismo de hashes incluidos en CI.
- ✅ 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
= Hex0xC0000008
= NTSTATUSSTATUSINVALIDHANDLE
. - 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
- Localiza con búsquedas en tu repo:
static SHA
,HashAlgorithm
,TransformFinalBlock
,Parallel.For
,ComputeHash(
,CryptoStream
. - Decide el patrón: por operación (preferido), por hilo (si volumen extremo) o pool controlado.
- Refactoriza los puntos críticos y añade pruebas de concurrencia.
- Observa en preproducción: cero excepciones, hashes deterministas, latencia aceptable.
- 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.