Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 15722] ファイルの概要

このコミットは、Goランタイムにおけるハッシュ関数の高速化とセキュリティ強化を目的としています。特に、IntelおよびAMDプロセッサが提供するAES (Advanced Encryption Standard) ハードウェア命令(AES-NI)を活用することで、ハッシュ計算のパフォーマンスを大幅に向上させています。また、ハッシュ衝突によるサービス拒否(DoS)攻撃を防ぐために、ハッシュ関数にランダムなシードを導入しています。

コミット

commit a5d4024139231ca10b5347d17bbf702cfdf5fd5b
Author: Keith Randall <khr@golang.org>
Date:   Tue Mar 12 10:47:44 2013 -0700

    runtime: faster & safer hash function
    
    Uses AES hardware instructions on 386/amd64 to implement
    a fast hash function.  Incorporates a random key to
    thwart hash collision DOS attacks.
    Depends on CL#7548043 for new assembly instructions.
    
    Update #3885
    Helps some by making hashing faster.  Go time drops from
    0.65s to 0.51s.
    
    R=rsc, r, bradfitz, remyoudompheng, khr, dsymonds, minux.ma, elias.naur
    CC=golang-dev
    https://golang.org/cl/7543043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/a5d4024139231ca10b5347d17bbf702cfdf5fd5b

元コミット内容

runtime: faster & safer hash function

Uses AES hardware instructions on 386/amd64 to implement
a fast hash function.  Incorporates a random key to
thwart hash collision DOS attacks.
Depends on CL#7548043 for new assembly instructions.

Update #3885
Helps some by making hashing faster.  Go time drops from
0.65s to 0.51s.

R=rsc, r, bradfitz, remyoudompheng, khr, dsymonds, minux.ma, elias.naur
CC=golang-dev
https://golang.org/cl/7543043

変更の背景

この変更の主な背景には、Go言語のランタイムにおけるハッシュ関数の性能とセキュリティの課題がありました。

  1. 性能の向上: 従来のハッシュ関数はソフトウェアで実装されており、特に大量のデータをハッシュする際に性能ボトルネックとなる可能性がありました。コミットメッセージにあるように、「Go time drops from 0.65s to 0.51s」という具体的な性能改善が示されており、ハッシュ処理がGoプログラム全体の実行時間に与える影響が大きかったことが伺えます。
  2. ハッシュ衝突DoS攻撃への対策: ハッシュ関数は、異なる入力に対して異なる出力を生成することが理想ですが、悪意のある攻撃者が意図的に多数の入力に対して同じハッシュ値を生成させる「ハッシュ衝突」を引き起こす可能性があります。これにより、ハッシュテーブル(Goのmap型など)の性能が著しく低下し、サービス拒否(DoS)状態に陥る可能性があります。このコミットでは、ハッシュ関数にランダムなシードを導入することで、攻撃者が事前に衝突を計算することを困難にし、この種の攻撃に対する耐性を高めています。
  3. ハードウェア支援の活用: 近年のCPUには、暗号化処理を高速化するための専用命令セット(AES-NIなど)が搭載されています。これらの命令を活用することで、ソフトウェア実装よりもはるかに高速にハッシュ計算を行うことが可能になります。このコミットは、これらのハードウェア機能をGoランタイムに統合し、性能とセキュリティの両面でメリットを享受しようとしています。

前提知識の解説

このコミットを理解するためには、以下の技術的な前提知識が必要です。

  1. ハッシュ関数:

    • 任意の長さの入力データ(メッセージ)を受け取り、固定長の出力データ(ハッシュ値、メッセージダイジェスト、フィンガープリントなどと呼ばれる)を生成する関数です。
    • 主な特性として、入力が少しでも変わると出力が大きく変わる「雪崩効果」、異なる入力から同じ出力が生成される「衝突」が起こりにくいこと(衝突耐性)、一方向性(ハッシュ値から元の入力を復元するのが困難であること)などがあります。
    • Go言語のmap型(ハッシュマップ)は、キーのハッシュ値を使ってデータを格納・検索するため、ハッシュ関数の性能と衝突耐性が直接的にmapの性能とセキュリティに影響します。
  2. AES (Advanced Encryption Standard) ハードウェア命令 (AES-NI):

    • IntelおよびAMDの最新のCPUに搭載されている、AES暗号化・復号化処理を高速化するための専用命令セットです。
    • AESENC (AES Encrypt), AESENCLAST (AES Encrypt Last), AESDEC (AES Decrypt), AESDECLAST (AES Decrypt Last), AESIMC (AES Inverse Mix Columns), AESKEYGENASSIST (AES Key Generation Assist) などの命令が含まれます。
    • これらの命令は、ソフトウェアでAESを実装するよりもはるかに少ないCPUサイクルで処理を完了できるため、暗号化処理だけでなく、ハッシュ関数など、同様のビット操作や巡回シフトを多用する処理の高速化にも利用されます。
  3. CPUID命令:

    • x86アーキテクチャのCPUが持つ命令で、プロセッサの機能や情報を取得するために使用されます。
    • 特定のレジスタ(EAXなど)に値を設定してCPUID命令を実行すると、CPUがサポートする機能(例: SSE, AVX, AES-NIなど)に関する情報が他のレジスタ(EBX, ECX, EDXなど)に返されます。
    • このコミットでは、runtime·cpuid_ecxruntime·cpuid_edxという変数にCPUIDの結果を格納し、AES-NI命令が利用可能かどうかをチェックするために使用しています。
  4. ハッシュ衝突DoS攻撃:

    • 攻撃者が、ハッシュテーブルの性能を意図的に低下させるために、多数の異なる入力データに対して同じハッシュ値(または少数のハッシュ値)を生成するような入力セットを作成し、それをサーバーに送信する攻撃です。
    • ハッシュテーブルは、衝突が発生すると線形探索などの遅いアルゴリズムにフォールバックすることがあり、これによりCPU使用率が急増し、正当なリクエストを処理できなくなる可能性があります。
    • ランダムなシードをハッシュ関数に導入することで、攻撃者が事前に衝突を計算することが困難になり、この種の攻撃に対する耐性が向上します。
  5. Goランタイムとアセンブリ:

    • Go言語のランタイムは、ガベージコレクション、スケジューラ、メモリ管理など、Goプログラムの実行を支える低レベルな機能を提供します。
    • 性能が重視される部分や、特定のハードウェア機能を利用する部分では、Goのコードではなく、アセンブリ言語で直接実装されることがあります。このコミットのasm_386.sasm_amd64.sがこれに該当します。
    • Goのアセンブリは、Plan 9アセンブラの構文に似ており、一般的なx86アセンブラとは異なる表記規則を持ちます。
  6. ELF AT_RANDOM:

    • Linuxシステムにおいて、ELF (Executable and Linkable Format) 実行ファイルが起動される際に、カーネルがプロセスに渡す補助ベクトル(Auxiliary Vector)の一つです。
    • AT_RANDOMは、セキュリティ目的で使用されるランダムなバイト列へのポインタを提供します。これにより、アプリケーションは/dev/urandomなどのデバイスファイルを開くことなく、起動時にカーネルから提供される高品質な乱数を取得できます。これは、特にサンドボックス化された環境や、ファイルシステムへのアクセスが制限される環境で有用です。
  7. Windows CryptGenRandom:

    • Windows APIの一部で、暗号学的に強力な乱数を生成するために使用されます。
    • CryptAcquireContext関数で暗号サービスプロバイダ(CSP)のハンドルを取得し、そのハンドルを使ってCryptGenRandomを呼び出すことで乱数を生成します。
    • このコミットでは、Windows環境でハッシュ関数のランダムシードを生成するために利用されています。

技術的詳細

このコミットの技術的な核心は、Goランタイムのハッシュ関数をAES-NI命令セットを利用するように変更し、さらにランダムなシードを導入した点にあります。

  1. AES-NIによるハッシュ関数の実装:

    • src/pkg/runtime/asm_386.ssrc/pkg/runtime/asm_amd64.sに、runtime·aeshashruntime·aeshashstrruntime·aeshash32runtime·aeshash64といった新しいアセンブリ関数が追加されました。これらは、それぞれ任意のメモリブロック、文字列、32ビット整数、64ビット整数のハッシュを計算します。
    • これらの関数は、内部的にruntime·aeshashbodyを呼び出します。このaeshashbodyがAES-NI命令(特にAESENC)を駆使してハッシュ計算を行います。
    • 基本的なアイデアは、入力データをAESのブロック暗号のように扱い、ハッシュ値を「状態」として、入力ブロックと状態を組み合わせてAESENC命令で繰り返し処理することで、最終的なハッシュ値を生成するというものです。
    • PINSRD (Packed Insert Doubleword) や PINSRQ (Packed Insert Quadword) 命令は、XMMレジスタ(SSE命令で使用される128ビットレジスタ)の特定の位置にデータを挿入するために使用されます。これにより、シード値や入力データの一部をハッシュ計算の初期状態に効率的に組み込むことができます。
    • PSHUFB (Packed Shuffle Bytes) 命令は、バイト単位のシャッフルを行う命令で、部分的なブロックの処理や、アライメントされていないデータからの読み込み後のバイト操作に利用されます。特に、入力データの残りの部分(16バイト未満の「partial block」)を効率的に処理するために使用されます。
  2. CPU機能の検出と動的なハッシュ関数の選択:

    • src/pkg/runtime/alg.cruntime·hashinit関数が、Goランタイムの初期化時に呼び出されます。
    • この関数は、CPUID命令の結果(runtime·cpuid_ecx)をチェックし、CPUがAES-NI ((1 << 25))、SSE3 ((1 << 9), PSHUFBのために必要)、SSE4.1 ((1 << 19), PINSRD/PINSRQのために必要) の各命令セットをサポートしているかを確認します。
    • もしこれらの命令がすべて利用可能であれば、runtime·algarrayというテーブルのエントリを、新しく実装されたAES-NIベースのハッシュ関数(runtime·aeshash, runtime·aeshash32, runtime·aeshash64, runtime·aeshashstr)に置き換えます。これにより、Goのmap操作などでこれらの高速なハッシュ関数が自動的に使用されるようになります。
    • 利用できない場合は、従来のソフトウェアベースのハッシュ関数が引き続き使用されます。
  3. ランダムシードの導入:

    • ハッシュ衝突DoS攻撃を防ぐため、runtime·hashinit関数内でハッシュ関数にランダムなシード(runtime·aeskeysched)が初期化されます。
    • このシードは、システムから提供される乱数データを使用して生成されます。
      • Linux: src/pkg/runtime/signal_linux_386.cおよびsrc/pkg/runtime/vdso_linux_amd64.cで、ELF補助ベクトルのAT_RANDOMからランダムデータを取得しようとします。これが利用できない場合は、/dev/urandomから読み込みます。
      • macOS (Darwin)/FreeBSD/NetBSD/OpenBSD: src/pkg/runtime/thread_darwin.cなどの各OS固有のファイルで、/dev/urandomからランダムデータを読み込みます。
      • Windows: src/pkg/runtime/thread_windows.cで、Windows CryptoAPIのCryptAcquireContextWCryptGenRandom関数を使用して乱数を生成します。
    • 取得したランダムデータがHashRandomBytes(32バイト)に満たない場合、runtime·nanotime()(現在のナノ秒単位の時刻)をシードとして使用し、不足分を埋めます。これは、完全にランダムではないものの、全くランダムでないよりはマシという考えに基づいています。
    • このランダムシードは、ハッシュ計算の初期状態に組み込まれることで、同じ入力データであっても、プログラムの実行ごとに異なるハッシュ値が生成される可能性があり、攻撃者が事前に衝突を計算することを困難にします。
  4. ベンチマークの追加:

    • src/pkg/runtime/mapspeed_test.goという新しいテストファイルが追加されました。
    • このファイルには、文字列、32ビット整数、64ビット整数、文字列配列のハッシュ性能を測定するためのベンチマークテストが含まれています。これにより、ハッシュ関数の変更が実際の性能にどのように影響するかを継続的に監視できるようになります。

コアとなるコードの変更箇所

このコミットにおける主要なコード変更箇所は以下のファイルに集中しています。

  • src/pkg/runtime/alg.c:
    • runtime·aeskeyschedというグローバル変数の宣言。これはAESハッシュ関数のランダムシードとして使用されます。
    • runtime·hashinit関数の追加。この関数は、CPUがAES-NI命令をサポートしているかをチェックし、サポートしていればGoランタイムのハッシュ関数ポインタをAES-NIベースの実装に切り替えます。また、ハッシュ関数のランダムシードを初期化します。
  • src/pkg/runtime/asm_386.s / src/pkg/runtime/asm_amd64.s:
    • _rt0_386 / _rt0_amd64 (Goプログラムのエントリポイント) にCPUID命令の呼び出しを追加し、CPUの機能情報をruntime·cpuid_ecxruntime·cpuid_edxに格納するように変更。
    • _rt0_386 / _rt0_amd64 からruntime·hashinit(SB)を呼び出すように変更。
    • runtime·aeshash, runtime·aeshashstr, runtime·aeshash32, runtime·aeshash64といったAES-NI命令を利用した新しいハッシュ関数群のアセンブリ実装を追加。
    • これらのハッシュ関数が内部的に使用するmasksshiftsという定数データのアセンブリ定義を追加。
  • src/pkg/runtime/asm_arm.s:
    • _rt0_armからruntime·hashinit(SB)を呼び出すように変更。
    • ARMアーキテクチャではAES-NIが利用できないため、runtime·aeshashなどの関数が未実装であることを示すダミーの定義を追加。
  • src/pkg/runtime/mapspeed_test.go:
    • 新しいファイルとして追加され、文字列、整数、文字列配列のハッシュ性能を測定するベンチマークテストが含まれています。
  • src/pkg/runtime/runtime.c:
    • runtime·cpuid_ecxruntime·cpuid_edxの宣言を追加。
  • src/pkg/runtime/runtime.h:
    • runtime·startup_random_data, runtime·startup_random_data_len, runtime·get_random_dataの宣言を追加。
    • HashRandomBytes定数(32バイト)の定義を追加。
    • runtime·hashinit関数の宣言を追加。
    • 新しいAES-NIベースのハッシュ関数群(runtime·aeshashなど)の宣言を追加。
    • runtime·cpuid_ecxruntime·cpuid_edxの外部宣言を追加。
    • runtime·open, runtime·read, runtime·closeといったシステムコールラッパーの宣言を追加。
  • src/pkg/runtime/signal_linux_386.c / src/pkg/runtime/vdso_linux_amd64.c:
    • Linux環境でELF補助ベクトルのAT_RANDOMからランダムデータを取得するロジックを追加。
  • src/pkg/runtime/sys_darwin_386.s / sys_darwin_amd64.s / sys_freebsd_386.s / sys_freebsd_amd64.s / sys_netbsd_386.s / sys_netbsd_amd64.s / sys_openbsd_386.s / sys_openbsd_amd64.s:
    • 各OS/アーキテクチャ向けに、open, close, readシステムコールのアセンブリラッパーを追加。これらはruntime·get_random_data関数で/dev/urandomから乱数を読み込むために使用されます。
  • src/pkg/runtime/thread_darwin.c / thread_freebsd.c / thread_linux.c / thread_netbsd.c / thread_openbsd.c / thread_windows.c:
    • runtime·get_random_data関数の実装を追加。この関数は、各OSのメカニズム(/dev/urandomAT_RANDOMCryptGenRandom)を使用して、ハッシュ関数のランダムシードを生成するための乱数を取得します。
    • thread_linux.cでは、runtime·open, runtime·close, runtime·readの宣言が削除され、runtime.hに移動しました。
  • src/pkg/runtime/thread_windows.c:
    • Windows CryptoAPIの関数(CryptAcquireContextW, CryptGenRandom, CryptReleaseContext)の動的インポート宣言を追加。

コアとなるコードの解説

src/pkg/runtime/alg.cruntime·hashinit 関数

// used in asm_{386,amd64}.s
byte runtime·aeskeysched[HashRandomBytes];

void
runtime·hashinit(void)
{
	// Install aes hash algorithm if we have the instructions we need
	if((runtime·cpuid_ecx & (1 << 25)) != 0 &&  // aes (aesenc)
	   (runtime·cpuid_ecx & (1 << 9)) != 0 &&   // sse3 (pshufb)
	   (runtime·cpuid_ecx & (1 << 19)) != 0) {  // sse4.1 (pinsr{d,q})
		byte *rnd;
		int32 n;
		runtime·algarray[AMEM].hash = runtime·aeshash;
		runtime·algarray[AMEM8].hash = runtime·aeshash;
		runtime·algarray[AMEM16].hash = runtime·aeshash;
		runtime·algarray[AMEM32].hash = runtime·aeshash32;
		runtime·algarray[AMEM64].hash = runtime·aeshash64;
		runtime·algarray[AMEM128].hash = runtime·aeshash;
		runtime·algarray[ASTRING].hash = runtime·aeshashstr;

		// Initialize with random data so hash collisions will be hard to engineer.
		runtime·get_random_data(&rnd, &n);
		if(n > HashRandomBytes)
			n = HashRandomBytes;
		runtime·memmove(runtime·aeskeysched, rnd, n);
		if(n < HashRandomBytes) {
			// Not very random, but better than nothing.
			int64 t = runtime·nanotime();
			while (n < HashRandomBytes) {
				runtime·aeskeysched[n++] = (int8)(t >> (8 * (n % 8)));
			}
		}
	}
}

このC言語の関数は、Goランタイムの起動時に一度だけ実行されます。

  1. CPU機能チェック: runtime·cpuid_ecxレジスタの値を確認し、CPUがAES-NI ((1 << 25)), SSE3 ((1 << 9)), SSE4.1 ((1 << 19)) 命令をサポートしているかをチェックします。これらのビットは、それぞれAESENCPSHUFBPINSRD/PINSRQ命令の利用可能性を示します。
  2. ハッシュ関数ポインタの切り替え: もし必要な命令がすべて利用可能であれば、runtime·algarrayというテーブルの対応するエントリを、新しく実装されたAES-NIベースのアセンブリハッシュ関数(runtime·aeshash, runtime·aeshash32, runtime·aeshash64, runtime·aeshashstr)に設定します。これにより、Goのmap操作などでこれらの高速なハッシュ関数が使用されるようになります。
  3. ランダムシードの初期化: runtime·get_random_dataを呼び出して、システムからランダムなバイト列を取得します。このデータはruntime·aeskeyschedにコピーされ、ハッシュ関数の初期シードとして使用されます。
  4. シードの補完: 取得したランダムデータがHashRandomBytes(32バイト)に満たない場合、runtime·nanotime()(現在のナノ秒単位の時刻)を基に不足分を埋めます。これは、完全にランダムではないものの、ハッシュ衝突攻撃に対する耐性を高めるためのフォールバックメカニズムです。

src/pkg/runtime/asm_amd64.sruntime·aeshashbody 関数 (AMD64版)

// AX: data
// CX: length
// DX: ptr to seed input / hash output
TEXT runtime·aeshashbody(SB),7,$0
	MOVQ	(DX), X0	// seed to low 64 bits of xmm0
	PINSRQ	$1, CX, X0	// size to high 64 bits of xmm0
	MOVOU	runtime·aeskeysched+0(SB), X2
	MOVOU	runtime·aeskeysched+16(SB), X3
aesloop:
	CMPQ	CX, $16
	JB	aesloopend
	MOVOU	(AX), X1
	AESENC	X2, X0
	AESENC	X1, X0
	SUBQ	$16, CX
	ADDQ	$16, AX
	JMP	aesloop
aesloopend:
	TESTQ	CX, CX
	JE	finalize	// no partial block

	TESTQ	$16, AX
	JNE	highpartial

	// address ends in 0xxxx.  16 bytes loaded
	// at this address won't cross a page boundary, so
	// we can load it directly.
	MOVOU	(AX), X1
	ADDQ	CX, CX
	PAND	masks(SB)(CX*8), X1
	JMP	partial
highpartial:
	// address ends in 1xxxx.  Might be up against
	// a page boundary, so load ending at last byte.
	// Then shift bytes down using pshufb.
	MOVOU	-16(AX)(CX*1), X1
	ADDQ	CX, CX
	PSHUFB	shifts(SB)(CX*8), X1
partial:
	// incorporate partial block into hash
	AESENC	X3, X0
	AESENC	X1, X0
finalize:	
	// finalize hash
	AESENC	X2, X0
	AESENC	X3, X0
	AESENC	X2, X0
	MOVQ	X0, (DX)
	RET

このアセンブリ関数は、実際のハッシュ計算のコアロジックを実装しています。

  1. 初期化:
    • DXレジスタが指すシード値(ハッシュの初期値)をXMM0レジスタの下位64ビットにロードします。
    • 入力データの長さ(CX)をXMM0レジスタの上位64ビットにPINSRQ命令で挿入します。これにより、シードと長さがハッシュ計算の初期状態に組み込まれます。
    • runtime·aeskeyschedからAESキーをXMM2とXMM3レジスタにロードします。これらはハッシュ計算のラウンドキーとして機能します。
  2. メインループ (aesloop):
    • 入力データの残りの長さ(CX)が16バイト以上である限りループします。
    • MOVOU (AX), X1:入力データから16バイトをXMM1レジスタにロードします。
    • AESENC X2, X0:XMM0(現在のハッシュ状態)とXMM2(キー)を使ってAES暗号化ラウンドを実行し、結果をXMM0に格納します。
    • AESENC X1, X0:XMM0(現在のハッシュ状態)とXMM1(入力データブロック)を使ってAES暗号化ラウンドを実行し、結果をXMM0に格納します。これにより、入力データがハッシュ状態に組み込まれます。
    • SUBQ $16, CX:処理した16バイト分、長さを減らします。
    • ADDQ $16, AX:入力データポインタを16バイト進めます。
  3. 部分ブロックの処理 (aesloopend, highpartial, partial):
    • メインループの後に、残りのデータが16バイト未満の場合(部分ブロック)を処理します。
    • データのメモリアドレスのアライメント(16バイト境界に揃っているか)によって処理が分岐します。
    • MOVOUでデータをロードし、PAND(論理AND)とPSHUFB(バイトシャッフル)命令を組み合わせて、残りのバイトだけをXMM1レジスタに適切に配置します。
    • AESENC X3, X0AESENC X1, X0:部分ブロックをハッシュ状態に組み込みます。
  4. 最終処理 (finalize):
    • 残りのデータがすべて処理された後、ハッシュ状態をさらに数回AESENC命令で処理し、最終的なハッシュ値を生成します。これにより、ハッシュ値の拡散性が高まります。
    • MOVQ X0, (DX):最終的なハッシュ値をDXが指すメモリ位置に書き込みます。

runtime·get_random_data 関数 (Linux版の例)

// Random bytes initialized at startup.  These come
// from the ELF AT_RANDOM auxiliary vector (vdso_linux_amd64.c).
byte*	runtime·startup_random_data;
uint32	runtime·startup_random_data_len;

void
runtime·get_random_data(byte **rnd, int32 *rnd_len)
{
	if(runtime·startup_random_data != nil) {
		*rnd = runtime·startup_random_data;
		*rnd_len = runtime·startup_random_data_len;
	} else {
		static byte urandom_data[HashRandomBytes];
		int32 fd;
		fd = runtime·open("/dev/urandom", 0 /* O_RDONLY */, 0);
		if(runtime·read(fd, urandom_data, HashRandomBytes) == HashRandomBytes) {
			*rnd = urandom_data;
			*rnd_len = HashRandomBytes;
		} else {
			*rnd = nil;
			*rnd_len = 0;
		}
		runtime·close(fd);
	}
}

この関数は、ハッシュ関数のランダムシードを生成するための乱数を取得します。

  1. AT_RANDOMからの取得: まず、Goランタイムの起動時にカーネルから提供されたAT_RANDOM補助ベクトルにデータがあるかを確認します。もしあれば、そのデータを直接使用します。これは、最も効率的で安全な方法です。
  2. /dev/urandomからの読み込み: AT_RANDOMが利用できない場合、/dev/urandomデバイスファイルを開き、そこからHashRandomBytes(32バイト)の乱数を読み込みます。
  3. エラーハンドリング: 乱数の取得に失敗した場合(例: /dev/urandomからの読み込みが不足した場合)、rndrnd_lenをnil/0に設定します。この場合、runtime·hashinit関数がruntime·nanotime()を使用してシードを補完します。

関連リンク

参考にした情報源リンク

[インデックス 15722] ファイルの概要

このコミットは、Go言語のランタイムにおけるハッシュ関数の性能とセキュリティを大幅に向上させることを目的としています。具体的には、IntelおよびAMDプロセッサに搭載されているAES (Advanced Encryption Standard) ハードウェア命令(AES-NI)を活用してハッシュ計算を高速化し、さらにハッシュ衝突によるサービス拒否(DoS)攻撃を防ぐために、ハッシュ関数にランダムなシードを導入しています。これにより、Goのmap型などのハッシュテーブルを利用する操作がより高速かつ安全になります。

コミット

commit a5d4024139231ca10b5347d17bbf702cfdf5fd5b
Author: Keith Randall <khr@golang.org>
Date:   Tue Mar 12 10:47:44 2013 -0700

    runtime: faster & safer hash function
    
    Uses AES hardware instructions on 386/amd64 to implement
    a fast hash function.  Incorporates a random key to
    thwart hash collision DOS attacks.
    Depends on CL#7548043 for new assembly instructions.
    
    Update #3885
    Helps some by making hashing faster.  Go time drops from
    0.65s to 0.51s.
    
    R=rsc, r, bradfitz, remyoudompheng, khr, dsymonds, minux.ma, elias.naur
    CC=golang-dev
    https://golang.org/cl/7543043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/a5d4024139231ca10b5347d17bbf702cfdf5fd5b

元コミット内容

runtime: faster & safer hash function

Uses AES hardware instructions on 386/amd64 to implement
a fast hash function.  Incorporates a random key to
thwart hash collision DOS attacks.
Depends on CL#7548043 for new assembly instructions.

Update #3885
Helps some by making hashing faster.  Go time drops from
0.65s to 0.51s.

R=rsc, r, bradfitz, remyoudompheng, khr, dsymonds, minux.ma, elias.naur
CC=golang-dev
https://golang.org/cl/7543043

変更の背景

このコミットは、Go言語のランタイムにおけるハッシュ関数の性能とセキュリティに関する複数の課題に対処するために行われました。

  1. 性能の最適化: Go言語のmap型は内部的にハッシュテーブルを使用しており、その性能は基盤となるハッシュ関数の効率に大きく依存します。従来のソフトウェアベースのハッシュ関数では、特に大量のデータや頻繁なmap操作において性能のボトルネックとなる可能性がありました。コミットメッセージに「Go time drops from 0.65s to 0.51s」とあるように、ハッシュ処理の高速化がGoプログラム全体の実行時間短縮に寄与することが期待されました。
  2. ハッシュ衝突DoS攻撃への対策: ハッシュ関数は、異なる入力に対して異なるハッシュ値を生成することが理想ですが、意図的に同じハッシュ値を生成するような入力(ハッシュ衝突)を多数作成することが可能です。悪意のある攻撃者がこのような衝突を多数引き起こす入力データをサーバーに送信すると、ハッシュテーブルの性能が著しく低下し、CPUリソースを大量に消費することでサービス拒否(DoS)状態に陥る可能性があります。このコミットでは、ハッシュ関数にランダムなシードを導入することで、攻撃者が事前に衝突を計算することを困難にし、この種の攻撃に対する耐性を高めることを目指しました。
  3. ハードウェア支援の活用: 近年のIntelおよびAMDプロセッサには、AES暗号化・復号化処理をハードウェアレベルで高速化するAES-NI(Advanced Encryption Standard New Instructions)が搭載されています。これらの命令は、暗号処理だけでなく、ハッシュ計算のようなビット操作や巡回シフトを多用する処理にも応用でき、ソフトウェア実装に比べて格段に高い性能を発揮します。このコミットは、これらのハードウェア機能をGoランタイムに統合し、性能とセキュリティの両面でメリットを享受しようとするものです。

前提知識の解説

このコミットの技術的詳細を理解するためには、以下の概念を把握しておく必要があります。

  1. ハッシュ関数:

    • 定義: 任意の長さの入力データ(メッセージ)を受け取り、固定長の短い出力データ(ハッシュ値、メッセージダイジェスト、フィンガープリントなど)を生成する一方向性の関数です。
    • 特性:
      • 一方向性: ハッシュ値から元の入力データを効率的に復元することは困難です。
      • 衝突耐性: 異なる入力データから同じハッシュ値が生成されること(ハッシュ衝突)が非常に稀であるべきです。
      • 雪崩効果: 入力データがわずかに変化するだけで、ハッシュ値が大きく変化します。
    • Goにおける利用: Go言語のmap型はハッシュテーブルとして実装されており、キーのハッシュ値を用いてデータの格納と検索を行います。そのため、ハッシュ関数の性能と衝突耐性は、map操作の効率とセキュリティに直結します。
  2. AES (Advanced Encryption Standard) ハードウェア命令 (AES-NI):

    • 概要: IntelおよびAMDのCPUに搭載されている、AES暗号化・復号化処理を高速化するための専用命令セットです。
    • 主要命令: AESENC (AES Encrypt), AESENCLAST (AES Encrypt Last) などが含まれます。これらの命令は、ソフトウェアでAESを実装するよりもはるかに少ないCPUサイクルで処理を完了できます。
    • ハッシュへの応用: AES-NIはブロック暗号の命令ですが、その高速なビット操作能力は、暗号学的ハッシュ関数だけでなく、Goのmapのような非暗号学的ハッシュ関数の高速化にも利用されます。入力データをAESのブロックのように扱い、ハッシュ値を「状態」として、入力ブロックと状態を組み合わせてAESENC命令で繰り返し処理することで、効率的にハッシュ値を生成できます。
  3. CPUID命令:

    • 概要: x86アーキテクチャのCPUが提供する命令で、プロセッサの機能や情報をプログラムから問い合わせるために使用されます。
    • 利用方法: 特定の値をEAXレジスタに設定してCPUID命令を実行すると、CPUがサポートする機能(例: SSE, AVX, AES-NIなど)に関する情報がEBX, ECX, EDXなどのレジスタに返されます。
    • 本コミットでの利用: Goランタイムは、このCPUID命令を用いて、実行中のCPUがAES-NI命令をサポートしているかを検出し、サポートしている場合にのみAES-NIベースの高速なハッシュ関数を使用するように動的に切り替えます。
  4. ハッシュ衝突DoS攻撃:

    • 概要: 攻撃者が、ハッシュテーブルの性能を意図的に低下させるために、多数の異なる入力データに対して同じハッシュ値(または少数のハッシュ値)を生成するような入力セットを作成し、それをサーバーに送信する攻撃です。
    • 影響: ハッシュテーブルは、衝突が発生すると、線形探索などの効率の悪いアルゴリズムにフォールバックすることがあります。これにより、CPU使用率が急増し、正当なリクエストを処理できなくなることで、サービス拒否(DoS)状態を引き起こします。
    • 対策: ハッシュ関数にランダムなシードを導入することで、攻撃者が事前に衝突を計算することが困難になり、この種の攻撃に対する耐性が向上します。
  5. Goランタイムとアセンブリ:

    • Goランタイム: Goプログラムの実行を支える低レベルな機能(ガベージコレクション、スケジューラ、メモリ管理など)を提供する部分です。
    • アセンブリ言語: 性能が極めて重要となる部分や、特定のハードウェア機能(本コミットのAES-NIなど)を直接利用する部分では、Go言語ではなく、アセンブリ言語で実装されることがあります。Goのアセンブリは、Plan 9アセンブラの構文に似ており、一般的なx86アセンブラとは異なる表記規則を持ちます。
  6. ELF AT_RANDOM:

    • 概要: Linuxシステムにおいて、ELF (Executable and Linkable Format) 実行ファイルが起動される際に、カーネルがプロセスに渡す補助ベクトル(Auxiliary Vector)の一つです。
    • 目的: AT_RANDOMは、セキュリティ目的で使用されるランダムなバイト列へのポインタを提供します。これにより、アプリケーションは/dev/urandomなどのデバイスファイルを開くことなく、起動時にカーネルから提供される高品質な乱数を取得できます。これは、特にサンドボックス化された環境や、ファイルシステムへのアクセスが制限される環境で有用です。
  7. Windows CryptGenRandom:

    • 概要: Windows APIの一部で、暗号学的に強力な乱数を生成するために使用されます。
    • 利用方法: CryptAcquireContext関数で暗号サービスプロバイダ(CSP)のハンドルを取得し、そのハンドルを使ってCryptGenRandomを呼び出すことで乱数を生成します。
    • 本コミットでの利用: Windows環境でハッシュ関数のランダムシードを生成するために利用されています。

技術的詳細

このコミットの技術的な核心は、Goランタイムのハッシュ関数をAES-NI命令セットを利用するように変更し、さらにハッシュ衝突DoS攻撃を防ぐためのランダムなシードを導入した点にあります。

  1. AES-NIによるハッシュ関数の実装:

    • src/pkg/runtime/asm_386.ssrc/pkg/runtime/asm_amd64.sに、runtime·aeshashruntime·aeshashstrruntime·aeshash32runtime·aeshash64といった新しいアセンブリ関数が追加されました。これらは、それぞれ任意のメモリブロック、文字列、32ビット整数、64ビット整数のハッシュを計算します。
    • これらの関数は、内部的にruntime·aeshashbodyという共通のアセンブリルーチンを呼び出します。このaeshashbodyが、入力データと現在のハッシュ状態を組み合わせてAESENC命令を繰り返し実行することで、ハッシュ値を生成します。
    • PINSRD (Packed Insert Doubleword) や PINSRQ (Packed Insert Quadword) 命令は、XMMレジスタ(SSE命令で使用される128ビットレジスタ)の特定の位置にデータを挿入するために使用されます。これにより、シード値や入力データの一部をハッシュ計算の初期状態に効率的に組み込むことができます。
    • PSHUFB (Packed Shuffle Bytes) 命令は、バイト単位のシャッフルを行う命令で、特に16バイト未満の「部分ブロック」の処理や、アライメントされていないデータからの読み込み後のバイト操作に利用され、効率的なデータ処理を実現します。
  2. CPU機能の検出と動的なハッシュ関数の選択:

    • Goランタイムの初期化時に、src/pkg/runtime/alg.c内のruntime·hashinit関数が呼び出されます。
    • この関数は、CPUID命令の結果を格納したruntime·cpuid_ecxレジスタのビットをチェックし、CPUがAES-NI ((1 << 25)), SSE3 ((1 << 9), PSHUFBのために必要), SSE4.1 ((1 << 19), PINSRD/PINSRQのために必要) の各命令セットをサポートしているかを確認します。
    • もしこれらの命令がすべて利用可能であれば、runtime·algarrayというGoランタイム内部のハッシュ関数ポインタテーブルのエントリを、新しく実装されたAES-NIベースのハッシュ関数(runtime·aeshash, runtime·aeshash32, runtime·aeshash64, runtime·aeshashstr)に置き換えます。これにより、Goのmap操作などでこれらの高速なハッシュ関数が自動的に使用されるようになります。
    • 必要な命令が利用できない環境では、Goランタイムは引き続き従来のソフトウェアベースのハッシュ関数を使用します。
  3. ランダムシードの導入:

    • ハッシュ衝突DoS攻撃を防ぐため、runtime·hashinit関数内でハッシュ関数にランダムなシード(runtime·aeskeysched)が初期化されます。
    • このシードは、システムから提供される乱数データを使用して生成されます。
      • Linux: src/pkg/runtime/signal_linux_386.cおよびsrc/pkg/runtime/vdso_linux_amd64.cで、ELF補助ベクトルのAT_RANDOMからランダムデータを取得しようとします。これが利用できない場合は、/dev/urandomから読み込みます。
      • macOS (Darwin)/FreeBSD/NetBSD/OpenBSD: src/pkg/runtime/thread_darwin.cなどの各OS固有のファイルで、/dev/urandomからランダムデータを読み込みます。
      • Windows: src/pkg/runtime/thread_windows.cで、Windows CryptoAPIのCryptAcquireContextWCryptGenRandom関数を使用して乱数を生成します。
    • 取得したランダムデータがHashRandomBytes(32バイト)に満たない場合、runtime·nanotime()(現在のナノ秒単位の時刻)をシードとして使用し、不足分を埋めます。これは、完全にランダムではないものの、全くランダムでないよりはマシという考えに基づいています。
    • このランダムシードは、ハッシュ計算の初期状態に組み込まれることで、同じ入力データであっても、プログラムの実行ごとに異なるハッシュ値が生成される可能性があり、攻撃者が事前に衝突を計算することを困難にします。
  4. ベンチマークの追加:

    • src/pkg/runtime/mapspeed_test.goという新しいテストファイルが追加されました。このファイルには、文字列、32ビット整数、64ビット整数、文字列配列のハッシュ性能を測定するためのベンチマークテストが含まれており、ハッシュ関数の変更が実際の性能にどのように影響するかを継続的に監視できるようになります。

コアとなるコードの変更箇所

このコミットにおける主要なコード変更箇所は以下のファイルに集中しています。

  • src/pkg/runtime/alg.c:
    • runtime·aeskeyschedというAESハッシュ関数のランダムシード用グローバル変数を宣言。
    • runtime·hashinit関数を追加。この関数は、CPUがAES-NI命令をサポートしているかをチェックし、サポートしていればGoランタイムのハッシュ関数ポインタをAES-NIベースの実装に切り替えます。また、ハッシュ関数のランダムシードを初期化します。
  • src/pkg/runtime/asm_386.s / src/pkg/runtime/asm_amd64.s:
    • Goプログラムのエントリポイントである_rt0_386 / _rt0_amd64にCPUID命令の呼び出しを追加し、CPUの機能情報をruntime·cpuid_ecxruntime·cpuid_edxに格納するように変更。
    • _rt0_386 / _rt0_amd64 からruntime·hashinit(SB)を呼び出すように変更。
    • runtime·aeshash, runtime·aeshashstr, runtime·aeshash32, runtime·aeshash64といったAES-NI命令を利用した新しいハッシュ関数群のアセンブリ実装を追加。
    • これらのハッシュ関数が内部的に使用するmasksshiftsという定数データのアセンブリ定義を追加。
  • src/pkg/runtime/asm_arm.s:
    • _rt0_armからruntime·hashinit(SB)を呼び出すように変更。
    • ARMアーキテクチャではAES-NIが利用できないため、runtime·aeshashなどの関数が未実装であることを示すダミーの定義を追加。
  • src/pkg/runtime/mapspeed_test.go:
    • 新しいファイルとして追加され、文字列、整数、文字列配列のハッシュ性能を測定するベンチマークテストが含まれています。
  • src/pkg/runtime/runtime.c:
    • runtime·cpuid_ecxruntime·cpuid_edxの宣言を追加。
  • src/pkg/runtime/runtime.h:
    • runtime·startup_random_data, runtime·startup_random_data_len, runtime·get_random_dataの宣言を追加。
    • HashRandomBytes定数(32バイト)の定義を追加。
    • runtime·hashinit関数の宣言を追加。
    • 新しいAES-NIベースのハッシュ関数群(runtime·aeshashなど)の宣言を追加。
    • runtime·cpuid_ecxruntime·cpuid_edxの外部宣言を追加。
    • runtime·open, runtime·read, runtime·closeといったシステムコールラッパーの宣言を追加。
  • src/pkg/runtime/signal_linux_386.c / src/pkg/runtime/vdso_linux_amd64.c:
    • Linux環境でELF補助ベクトルのAT_RANDOMからランダムデータを取得するロジックを追加。
  • src/pkg/runtime/sys_darwin_386.s / sys_darwin_amd64.s / sys_freebsd_386.s / sys_freebsd_amd64.s / sys_netbsd_386.s / sys_netbsd_amd64.s / sys_openbsd_386.s / sys_openbsd_amd64.s:
    • 各OS/アーキテクチャ向けに、open, close, readシステムコールのアセンブリラッパーを追加。これらはruntime·get_random_data関数で/dev/urandomから乱数を読み込むために使用されます。
  • src/pkg/runtime/thread_darwin.c / thread_freebsd.c / thread_linux.c / thread_netbsd.c / thread_openbsd.c / thread_windows.c:
    • runtime·get_random_data関数の実装を追加。この関数は、各OSのメカニズム(/dev/urandomAT_RANDOMCryptGenRandom)を使用して、ハッシュ関数のランダムシードを生成するための乱数を取得します。
    • thread_linux.cでは、runtime·open, runtime·close, runtime·readの宣言が削除され、runtime.hに移動しました。
  • src/pkg/runtime/thread_windows.c:
    • Windows CryptoAPIの関数(CryptAcquireContextW, CryptGenRandom, CryptReleaseContext)の動的インポート宣言を追加。

コアとなるコードの解説

src/pkg/runtime/alg.cruntime·hashinit 関数

// used in asm_{386,amd64}.s
byte runtime·aeskeysched[HashRandomBytes];

void
runtime·hashinit(void)
{
	// Install aes hash algorithm if we have the instructions we need
	if((runtime·cpuid_ecx & (1 << 25)) != 0 &&  // aes (aesenc)
	   (runtime·cpuid_ecx & (1 << 9)) != 0 &&   // sse3 (pshufb)
	   (runtime·cpuid_ecx & (1 << 19)) != 0) {  // sse4.1 (pinsr{d,q})
		byte *rnd;
		int32 n;
		runtime·algarray[AMEM].hash = runtime·aeshash;
		runtime·algarray[AMEM8].hash = runtime·aeshash;
		runtime·algarray[AMEM16].hash = runtime·aeshash;
		runtime·algarray[AMEM32].hash = runtime·aeshash32;
		runtime·algarray[AMEM64].hash = runtime·aeshash64;
		runtime·algarray[AMEM128].hash = runtime·aeshash;
		runtime·algarray[ASTRING].hash = runtime·aeshashstr;

		// Initialize with random data so hash collisions will be hard to engineer.
		runtime·get_random_data(&rnd, &n);
		if(n > HashRandomBytes)
			n = HashRandomBytes;
		runtime·memmove(runtime·aeskeysched, rnd, n);
		if(n < HashRandomBytes) {
			// Not very random, but better than nothing.
			int64 t = runtime·nanotime();
			while (n < HashRandomBytes) {
				runtime·aeskeysched[n++] = (int8)(t >> (8 * (n % 8)));
			}
		}
	}
}

このC言語の関数は、Goランタイムの起動時に一度だけ実行され、ハッシュ関数の初期設定を行います。

  1. CPU機能チェック: runtime·cpuid_ecxレジスタのビットを検査し、CPUがAES-NI ((1 << 25)), SSE3 ((1 << 9)), SSE4.1 ((1 << 19)) 命令をサポートしているかを確認します。これらの命令は、それぞれAESENCPSHUFBPINSRD/PINSRQ命令の利用可能性を示します。
  2. ハッシュ関数ポインタの切り替え: もし必要な命令がすべて利用可能であれば、Goランタイム内部のruntime·algarrayというハッシュ関数ポインタテーブルの対応するエントリを、新しく実装されたAES-NIベースのアセンブリハッシュ関数(runtime·aeshash, runtime·aeshash32, runtime·aeshash64, runtime·aeshashstr)に設定します。これにより、Goのmap操作などでこれらの高速なハッシュ関数が自動的に使用されるようになります。
  3. ランダムシードの初期化: runtime·get_random_dataを呼び出して、システムからランダムなバイト列を取得します。このデータはruntime·aeskeyschedにコピーされ、ハッシュ関数の初期シードとして使用されます。
  4. シードの補完: 取得したランダムデータがHashRandomBytes(32バイト)に満たない場合、runtime·nanotime()(現在のナノ秒単位の時刻)を基に不足分を埋めます。これは、完全にランダムではないものの、ハッシュ衝突攻撃に対する耐性を高めるためのフォールバックメカニズムです。

src/pkg/runtime/asm_amd64.sruntime·aeshashbody 関数 (AMD64版)

// AX: data
// CX: length
// DX: ptr to seed input / hash output
TEXT runtime·aeshashbody(SB),7,$0
	MOVQ	(DX), X0	// seed to low 64 bits of xmm0
	PINSRQ	$1, CX, X0	// size to high 64 bits of xmm0
	MOVOU	runtime·aeskeysched+0(SB), X2
	MOVOU	runtime·aeskeysched+16(SB), X3
aesloop:
	CMPQ	CX, $16
	JB	aesloopend
	MOVOU	(AX), X1
	AESENC	X2, X0
	AESENC	X1, X0
	SUBQ	$16, CX
	ADDQ	$16, AX
	JMP	aesloop
aesloopend:
	TESTQ	CX, CX
	JE	finalize	// no partial block

	TESTQ	$16, AX
	JNE	highpartial

	// address ends in 0xxxx.  16 bytes loaded
	// at this address won\'t cross a page boundary, so
	// we can load it directly.
	MOVOU	(AX), X1
	ADDQ	CX, CX
	PAND	masks(SB)(CX*8), X1
	JMP	partial
highpartial:
	// address ends in 1xxxx.  Might be up against
	// a page boundary, so load ending at last byte.
	// Then shift bytes down using pshufb.
	MOVOU	-16(AX)(CX*1), X1
	ADDQ	CX, CX
	PSHUFB	shifts(SB)(CX*8), X1
partial:
	// incorporate partial block into hash
	AESENC	X3, X0
	AESENC	X1, X0
finalize:	
	// finalize hash
	AESENC	X2, X0
	AESENC	X3, X0
	AESENC	X2, X0
	MOVQ	X0, (DX)
	RET

このアセンブリ関数は、実際のハッシュ計算のコアロジックを実装しています。

  1. 初期化:
    • DXレジスタが指すシード値(ハッシュの初期値)をXMM0レジスタの下位64ビットにロードします。
    • 入力データの長さ(CX)をXMM0レジスタの上位64ビットにPINSRQ命令で挿入します。これにより、シードと長さがハッシュ計算の初期状態に組み込まれます。
    • runtime·aeskeyschedからAESキーをXMM2とXMM3レジスタにロードします。これらはハッシュ計算のラウンドキーとして機能します。
  2. メインループ (aesloop):
    • 入力データの残りの長さ(CX)が16バイト以上である限りループします。
    • MOVOU (AX), X1:入力データから16バイトをXMM1レジスタにロードします。
    • AESENC X2, X0:XMM0(現在のハッシュ状態)とXMM2(キー)を使ってAES暗号化ラウンドを実行し、結果をXMM0に格納します。
    • AESENC X1, X0:XMM0(現在のハッシュ状態)とXMM1(入力データブロック)を使ってAES暗号化ラウンドを実行し、結果をXMM0に格納します。これにより、入力データがハッシュ状態に組み込まれます。
    • SUBQ $16, CX:処理した16バイト分、長さを減らします。
    • ADDQ $16, AX:入力データポインタを16バイト進めます。
  3. 部分ブロックの処理 (aesloopend, highpartial, partial):
    • メインループの後に、残りのデータが16バイト未満の場合(部分ブロック)を処理します。
    • データのメモリアドレスのアライメント(16バイト境界に揃っているか)によって処理が分岐します。
    • MOVOUでデータをロードし、PAND(論理AND)とPSHUFB(バイトシャッフル)命令を組み合わせて、残りのバイトだけをXMM1レジスタに適切に配置します。
    • AESENC X3, X0AESENC X1, X0:部分ブロックをハッシュ状態に組み込みます。
  4. 最終処理 (finalize):
    • 残りのデータがすべて処理された後、ハッシュ状態をさらに数回AESENC命令で処理し、最終的なハッシュ値を生成します。これにより、ハッシュ値の拡散性が高まります。
    • MOVQ X0, (DX):最終的なハッシュ値をDXが指すメモリ位置に書き込みます。

runtime·get_random_data 関数 (Linux版の例)

// Random bytes initialized at startup.  These come
// from the ELF AT_RANDOM auxiliary vector (vdso_linux_amd64.c).
byte*	runtime·startup_random_data;
uint32	runtime·startup_random_data_len;

void
runtime·get_random_data(byte **rnd, int32 *rnd_len)
{
	if(runtime·startup_random_data != nil) {
		*rnd = runtime·startup_random_data;
		*rnd_len = runtime·startup_random_data_len;
	} else {
		static byte urandom_data[HashRandomBytes];
		int32 fd;
		fd = runtime·open("/dev/urandom", 0 /* O_RDONLY */, 0);
		if(runtime·read(fd, urandom_data, HashRandomBytes) == HashRandomBytes) {
			*rnd = urandom_data;
			*rnd_len = HashRandomBytes;
		} else {
			*rnd = nil;
			*rnd_len = 0;
		}
		runtime·close(fd);
	}
}

この関数は、ハッシュ関数のランダムシードを生成するための乱数を取得します。

  1. AT_RANDOMからの取得: まず、Goランタイムの起動時にカーネルから提供されたAT_RANDOM補助ベクトルにデータがあるかを確認します。もしあれば、そのデータを直接使用します。これは、最も効率的で安全な方法です。
  2. /dev/urandomからの読み込み: AT_RANDOMが利用できない場合、/dev/urandomデバイスファイルを開き、そこからHashRandomBytes(32バイト)の乱数を読み込みます。
  3. エラーハンドリング: 乱数の取得に失敗した場合(例: /dev/urandomからの読み込みが不足した場合)、rndrnd_lenをnil/0に設定します。この場合、runtime·hashinit関数がruntime·nanotime()を使用してシードを補完します。

関連リンク

参考にした情報源リンク