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

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

このコミットは、Go言語のランタイムにおけるLinux ARMアーキテクチャ向けのシグナルハンドリング部分の修正です。具体的には、AT_RANDOM補助ベクターから取得するランダムデータのアドレスが、常に4バイトアラインされているとは限らないという問題に対処しています。これにより、アラインメント要件が厳しいARMプロセッサ上でのクラッシュや未定義動作を防ぎ、ランダムデータの安全な読み込みを保証します。

コミット

  • コミットハッシュ: d1e06dab7c23109be4b24912212fedf3b155bfc7
  • 作者: Shenghou Ma minux.ma@gmail.com
  • コミット日時: 2012年11月18日 (日) 02:47:17 +0800

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

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

元コミット内容

runtime: don't assume AT_RANDOM provide 4-byte aligned ptr

R=dave, remyoudompheng
CC=golang-dev
https://golang.org/cl/6854056

変更の背景

この変更の背景には、Linuxシステムがプロセス起動時に提供する「補助ベクター (Auxiliary Vector)」という仕組みと、ARMアーキテクチャにおけるメモリのアラインメント要件があります。

Goランタイムは、セキュリティ上の理由から、起動時にカーネルから提供されるランダムな値を利用して、アドレス空間配置のランダム化 (ASLR) などに役立てています。このランダムな値は、ELF補助ベクターの AT_RANDOM エントリを通じて取得されます。

従来のコードでは、AT_RANDOMが指すアドレスが常に4バイト境界にアラインされていると仮定し、そのアドレスから直接 uint32 (4バイト整数) を読み込んでいました。しかし、特定のLinuxカーネルのバージョンやARMプロセッサの構成によっては、AT_RANDOMが提供するポインタが4バイトアラインされていない場合があります。

ARMアーキテクチャの多くのプロセッサでは、特定のデータ型(特に整数やポインタ)をメモリから読み書きする際に、そのデータ型のサイズに応じたアラインメント(例えば、4バイト整数なら4バイト境界)が要求されます。アラインされていないアドレスから直接マルチバイトデータを読み込もうとすると、SIGBUSシグナルによるクラッシュや、パフォーマンスの低下、あるいは予期せぬ不正なデータ読み込みといった未定義動作を引き起こす可能性があります。

このコミットは、このようなアラインメントの仮定が誤りであったために発生する可能性のある問題を修正し、Goランタイムの堅牢性を向上させることを目的としています。

前提知識の解説

ELF補助ベクター (Auxiliary Vector)

ELF (Executable and Linkable Format) は、Unix系システムで実行可能ファイル、オブジェクトファイル、共有ライブラリなどに用いられる標準的なファイル形式です。プロセスが起動される際、カーネルはプログラムの main 関数に引数 (argc, argv, envp) を渡しますが、それに加えて「補助ベクター (Auxiliary Vector)」と呼ばれる情報も提供します。

補助ベクターは、AT_で始まる定数(例: AT_PHDR, AT_BASE, AT_RANDOMなど)とそれに対応する値のペアのリストで構成されます。これらの情報は、プログラムが実行環境に関する特定の詳細(例えば、プログラムヘッダの場所、ベースアドレス、システムページサイズ、ランダムデータなど)を動的に取得するために使用されます。リストの終端は AT_NULL で示されます。

AT_RANDOM

AT_RANDOM は補助ベクターの一種で、カーネルが提供するランダムなバイト列へのポインタとその長さを示します。このランダムデータは、主にセキュリティ目的(例: ASLRのためのスタックやヒープのアドレスランダム化、ハッシュ関数のシードなど)で使用されます。通常、16バイトのランダムデータが提供されます。

メモリのアラインメント (Memory Alignment)

メモリのアラインメントとは、コンピュータのメモリ上でデータが特定の境界に配置されることを指します。例えば、「4バイトアラインメント」とは、データが4の倍数のアドレス(0x0, 0x4, 0x8, ...)に配置されることを意味します。

  • なぜアラインメントが必要か:
    • パフォーマンス: 多くのCPUアーキテクチャでは、アラインされたデータへのアクセスが最も効率的です。アラインされていないデータへのアクセスは、追加のCPUサイクルを必要としたり、複数回のメモリトランザクションを発生させたりするため、パフォーマンスが低下します。
    • ハードウェア要件: 特に組み込みシステムや特定のRISCアーキテクチャ(ARMなど)では、アラインされていないアクセスがハードウェアレベルで許可されていない場合があります。このような場合、アラインされていないアクセスは「アラインメントフォルト」と呼ばれる例外(LinuxではSIGBUSシグナル)を発生させ、プログラムをクラッシュさせます。
    • アトミック操作: マルチスレッド環境でのアトミック操作(不可分操作)は、通常、アラインされたデータに対してのみ保証されます。

ARMアーキテクチャにおけるアラインメント

ARMアーキテクチャは、歴史的に厳格なアラインメント要件を持つことで知られています。特にARMv5以前のアーキテクチャでは、アラインされていないメモリアクセスはSIGBUSシグナルを発生させ、プログラムを終了させることが一般的でした。ARMv6以降では、アラインされていないアクセスがサポートされるようになりましたが、パフォーマンスのペナルティが伴うため、可能な限りアラインされたアクセスを行うことが推奨されます。

Goランタイムは、様々なアーキテクチャで動作するため、このようなアーキテクチャ固有の制約を考慮する必要があります。

技術的詳細

このコミットが修正している問題は、runtime·setup_auxv 関数内で AT_RANDOM からランダムデータを読み込む際に発生していました。

元のコードでは、auxv[1]AT_RANDOM の値(ランダムデータへのポインタ)を保持しており、このポインタに直接オフセット + 4 を加算し、その結果のアドレスを uint32* にキャストして間接参照していました (*(uint32*)(auxv[1] + 4))。これは、ランダムデータの先頭から4バイト目以降の4バイトを uint32 として読み込むことを意図しています。

しかし、もし auxv[1] が指すアドレスが4バイトアラインされていなかった場合、auxv[1] + 4 のアドレスも4バイトアラインされない可能性があります。例えば、auxv[1]0x1001 のようなアドレスを指していた場合、auxv[1] + 40x1005 となり、これは4バイトアラインされていません。このようなアラインされていないアドレスから uint32 (4バイト) を直接読み込もうとすると、ARMプロセッサではアラインメントフォルトが発生し、プログラムがクラッシュする原因となります。

修正後のコードでは、このアラインメントの問題を回避するために、バイト単位での読み込みを行っています。auxv[1] が指すアドレスを byte* 型のポインタ rnd にキャストし、rnd[4], rnd[5], rnd[6], rnd[7] といった個々のバイトを読み込み、それらをビットシフトとビットOR演算 (|) を使って結合することで uint32 の値 (runtime·randomNumber) を再構築しています。

このバイト単位の読み込みは、アラインメント要件に縛られません。個々のバイトは常に1バイトアラインされているため、どのようなアドレスからでも安全に読み込むことができます。これにより、AT_RANDOMが提供するポインタのアラインメントに関わらず、ランダムデータを正しく、かつ安全に取得できるようになります。

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

変更は src/pkg/runtime/signal_linux_arm.c ファイル内の runtime·setup_auxv 関数に集中しています。

--- a/src/pkg/runtime/signal_linux_arm.c
+++ b/src/pkg/runtime/signal_linux_arm.c
@@ -173,11 +173,14 @@ runtime·checkgoarm(void)
 void
 runtime·setup_auxv(int32 argc, void *argv_list)
 {
-	byte **argv = &argv_list;
+	byte **argv;
 	byte **envp;
+	byte *rnd;
 	uint32 *auxv;
 	uint32 t;
 
+	argv = &argv_list;
+
 	// skip envp to get to ELF auxiliary vector.
 	for(envp = &argv[argc+1]; *envp != nil; envp++)
 		;
@@ -186,8 +189,10 @@ runtime·setup_auxv(int32 argc, void *argv_list) {
 	for(auxv=(uint32*)envp; auxv[0] != AT_NULL; auxv += 2) {
 		switch(auxv[0]) {
 		case AT_RANDOM: // kernel provided 16-byte worth of random data
-			if(auxv[1])
-				runtime·randomNumber = *(uint32*)(auxv[1] + 4);
+			if(auxv[1]) {
+				rnd = (byte*)auxv[1];
+				runtime·randomNumber = rnd[4] | rnd[5]<<8 | rnd[6]<<16 | rnd[7]<<24;
+			}
 			break;
 		case AT_PLATFORM: // v5l, v6l, v7l
 			if(auxv[1]) {

コアとなるコードの解説

変更された主要な部分は case AT_RANDOM: ブロック内です。

変更前:

			if(auxv[1])
				runtime·randomNumber = *(uint32*)(auxv[1] + 4);
  • auxv[1]AT_RANDOM の値であり、ランダムデータが格納されているメモリ領域の先頭アドレスを指すポインタです。
  • auxv[1] + 4 は、そのアドレスから4バイトオフセットした位置を指します。これは、カーネルが提供する16バイトのランダムデータのうち、5バイト目から8バイト目(0-indexedで[4]から[7])を読み込むことを意図しています。
  • *(uint32*)(auxv[1] + 4) は、このオフセットされたアドレスを uint32 型のポインタにキャストし、そのポインタが指す4バイトのデータを uint32 として読み込んでいます。この操作が、アラインされていないアドレスで実行されると問題を引き起こします。

変更後:

			if(auxv[1]) {
				rnd = (byte*)auxv[1];
				runtime·randomNumber = rnd[4] | rnd[5]<<8 | rnd[6]<<16 | rnd[7]<<24;
			}
  • rnd = (byte*)auxv[1];auxv[1] が指すアドレスを byte* 型のポインタ rnd にキャストします。これにより、rnd を通じてメモリをバイト単位でアクセスできるようになります。
  • runtime·randomNumber = rnd[4] | rnd[5]<<8 | rnd[6]<<16 | rnd[7]<<24;
    • rnd[4]:ランダムデータの5バイト目(0-indexedでインデックス4)を読み込みます。これは uint32 の最下位バイトになります。
    • rnd[5]<<8:ランダムデータの6バイト目を読み込み、8ビット左にシフトします。
    • rnd[6]<<16:ランダムデータの7バイト目を読み込み、16ビット左にシフトします。
    • rnd[7]<<24:ランダムデータの8バイト目を読み込み、24ビット左にシフトします。
    • これらの4つのバイト値をビットOR演算 (|) で結合することで、元の4バイトの uint32 値を再構築します。この方法であれば、個々のバイトアクセスは常にアラインされているため、アラインメントフォルトの心配がありません。

この変更により、AT_RANDOMが提供するポインタが4バイトアラインされていなくても、Goランタイムは安全かつ正確にランダムデータを読み込むことができるようになり、ARMアーキテクチャ上での安定性が向上しました。

関連リンク

参考にした情報源リンク