为什么在高负载和多线程情况下使用SHA1.ComputeHash会失败?

16

我发现我维护的一些代码存在问题。下面的代码有一个private static SHA1成员(它是一个IDisposable,但由于它是static,因此永远不应该被完成)。然而,在压力下,这段代码会抛出一个异常,表明它已经关闭:

Caught exception.  Safe handle has been closed" 
Stack trace: Call stack where exception was thrown
at System.Runtime.InteropServices.SafeHandle.DangerousAddRef(Boolean& success)
at System.Security.Cryptography.Utils.HashData(SafeHashHandle hHash, Byte[] data, Int32 cbData, Int32 ibStart, Int32 cbSize)
at System.Security.Cryptography.Utils.HashData(SafeHashHandle hHash, Byte[] data, Int32 ibStart, Int32 cbSize)
at System.Security.Cryptography.HashAlgorithm.ComputeHash(Byte[] buffer)

涉及的代码如下:

internal class TokenCache
{
    private static SHA1 _sha1 = SHA1.Create();

    private string ComputeHash(string password)
    {
        byte[] passwordBytes = UTF8Encoding.UTF8.GetBytes(password);
        return UTF8Encoding.UTF8.GetString(_sha1.ComputeHash(passwordBytes));
    }

我显然的问题是,这个问题可能是由什么引起的。调用SHA1.Create会悄无声息地失败吗(有多少密码资源可用)?这可能是应用程序域崩溃引起的吗?

还有其他理论吗?


1
你确定SHA1类是线程安全的吗?当哈希失败时,你能否获取被哈希的密码? - Rob
@Rob,MSDN帮助页面没有提到线程安全性。然而,所有使用它的WCF服务都使用instancecontextmode == perCall和ConcurrencyMode == Single。 - MvdD
1
@user18044,我刚刚在本地运行了一个压力测试(10,000个随机哈希)。它正常工作。但是,将其改为并行处理后,出现了与您收到的完全相同的错误(安全句柄已关闭)。我非常确定这是因为您的应用程序在某个地方进行了线程处理。 - Rob
3
以下复制了您的错误:`var strings = Enumerable.Range(1,10000).Select(r => Guid.NewGuid().ToString()).ToList(); Parallel.ForEach(strings, s => { ComputeHash(s).Dump(); });使用foreach`却不行。 - Rob
@Rob,谢谢,非常有趣!但我仍然困惑为什么句柄会关闭。因为那个类永远不会有多个实例。我会开始在我们的应用程序中寻找线程。 - MvdD
显示剩余2条评论
1个回答

36
根据 HashAlgorithm 基类的文档:

此类型的任何公共静态成员(在 Visual Basic 中为 Shared)都是线程安全的。任何实例成员不能保证线程安全。

您不应该在不同线程之间共享这些类,其中不同线程尝试同时调用相同实例上的 ComputeHash

编辑 这就是导致错误的原因。下面的压力测试会产生各种错误,因为多个线程同时在同一个哈希算法实例上调用 ComputeHash。您的错误就是其中之一。

具体来说,我在这个压力测试中看到了以下错误:

  • System.Security.Cryptography.CryptographicException:指定状态中无法使用哈希。
  • System.ObjectDisposedException:安全句柄已关闭

压力测试代码示例:

const int threadCount = 2;
var sha1 = SHA1.Create();
var b = new Barrier(threadCount);
Action start = () => {
                    b.SignalAndWait();
                    for (int i = 0; i < 10000; i++)
                    {
                        var pwd = Guid.NewGuid().ToString();
                        var bytes = Encoding.UTF8.GetBytes(pwd);
                        sha1.ComputeHash(bytes);
                    }
                };
var threads = Enumerable.Range(0, threadCount)
                        .Select(_ => new ThreadStart(start))
                        .Select(x => new Thread(x))
                        .ToList();
foreach (var t in threads) t.Start();
foreach (var t in threads) t.Join();

非常意外,安全句柄看起来没问题。但不可否认,是个好答案。 - Hans Passant
1
谢谢@HansPassant。我猜测可能是SHA1CryptoServiceProvider.Initialize方法,它似乎执行了一个非线程安全的Dispose,然后在_safeHashHandle字段上重新创建。 - Andy Brown
呵呵,bizarro,Initialize()在计算哈希值之后调用。一定是某种安全措施。你懂的。 - Hans Passant
1
@HansPassant。是的,最好称之为“重置”。经过进一步检查,“MD5CryptoServiceProvider”确实完全相同,而其他类别具有内部状态的变体也会产生类似的影响。重点是任何“HashAlgorithm”子类的实例方法都不是线程安全的。 - Andy Brown
1
公正的观点。下次我会检查日期,讨论的异常仍然存在。 - m1nkeh
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接