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

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

このコミットは、Go言語のリンカー(cmd/ld)に関連する複数のファイルを変更しています。主な目的は、実行可能ファイル内の読み取り専用データを、実行不可能なメモリセグメントに配置することで、セキュリティを強化することです。

変更されたファイルは以下の通りです。

  • src/cmd/5l/asm.c: ARMアーキテクチャ向けのリンカー/アセンブラ関連のコード。読み取り専用データセグメントの出力ロジックが変更されました。
  • src/cmd/6l/asm.c: x86-64アーキテクチャ向けのリンカー/アセンブラ関連のコード。読み取り専用データセグメントの出力ロジックが変更されました。
  • src/cmd/8l/asm.c: x86アーキテクチャ向けのリンカー/アセンブラ関連のコード。読み取り専用データセグメントの出力ロジックが変更されました。
  • src/cmd/ld/data.c: リンカーのデータセグメントと読み取り専用データセグメントの配置、アライメント、シンボル値の計算に関する主要なロジックが含まれています。
  • src/cmd/ld/elf.c: ELF(Executable and Linkable Format)形式の実行ファイル生成に関するコード。新しい読み取り専用セグメントがELF構造に正しく組み込まれるように変更されました。
  • src/cmd/ld/lib.c: リンカーのユーティリティ関数や外部リンカーとの連携に関するコード。goldリンカー向けのオプションが追加されました。
  • src/cmd/ld/lib.h: リンカーで使用されるデータ構造、定数、セグメントタイプなどの定義が含まれています。新しい読み取り専用セグメントの型が追加されました。

コミット

commit d6d83c918c5847bbdbaf8c9de101e9ca32535df8
Author: Russ Cox <rsc@golang.org>
Date:   Thu Jul 11 22:52:48 2013 -0400

    cmd/ld: place read-only data in non-executable segment
    
    R=golang-dev, dave, r
    CC=golang-dev, nigeltao
    https://golang.org/cl/10713043

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

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

元コミット内容

cmd/ld: place read-only data in non-executable segment

変更の背景

このコミットの背景には、現代のオペレーティングシステムとセキュリティのベストプラクティスにおける重要な原則である「W^X (Write XOR Execute)」があります。W^Xは、「書き込み可能」と「実行可能」という2つのメモリパーミッションが同時に設定されるべきではないというセキュリティモデルです。つまり、あるメモリ領域が書き込み可能であれば実行不可能であるべきであり、実行可能であれば書き込み不可能であるべきです。

従来のリンカーの実装では、読み取り専用データ(定数、文字列リテラル、シンボルテーブルなど)が、実行可能コードを含むテキストセグメント(.text)に配置されることがありました。これは、メモリの効率的な利用やリンカーの実装の単純化のためでしたが、セキュリティ上の脆弱性を生む可能性がありました。もし攻撃者がプログラムの脆弱性を悪用して、テキストセグメント内の読み取り専用データを書き換えることができた場合、その書き換えられたデータを実行してしまうことで、任意のコード実行(Arbitrary Code Execution)につながる恐れがありました。

このコミットは、このセキュリティリスクを軽減するために、読み取り専用データを専用の「読み取り専用かつ実行不可能な」セグメントに分離することを目的としています。これにより、たとえ読み取り専用データが何らかの理由で書き換えられたとしても、そのデータが実行されることはなくなり、攻撃の成功を困難にします。特にELF形式のバイナリでは、このようなセグメントの分離がOSレベルでサポートされており、この変更はその機能を活用するものです。

前提知識の解説

ELF (Executable and Linkable Format)

ELFは、Unix系OS(Linux、BSDなど)で広く使用されている実行可能ファイル、オブジェクトファイル、共有ライブラリの標準フォーマットです。ELFファイルは、プログラムヘッダとセクションヘッダという2つの主要な構造を持ちます。

  • セクション (Sections): コンパイル時に生成されるコードやデータの論理的なまとまり(例: .text (コード), .data (初期化済みデータ), .bss (初期化なしデータ), .rodata (読み取り専用データ))。
  • セグメント (Segments): リンカーが実行可能ファイルを生成する際に、関連するセクションをまとめてメモリにロードするための単位。プログラムヘッダによって定義され、メモリ上の連続した領域に対応します。セグメントには、読み取り(R)、書き込み(W)、実行(X)などのパーミッションが設定されます。

メモリセグメントの種類とパーミッション

一般的な実行可能ファイルは、メモリ上でいくつかのセグメントに分割されてロードされます。

  • テキストセグメント (.text): プログラムの実行可能コードが含まれます。通常、読み取り(R)と実行(X)のパーミッションを持ち、書き込み(W)は許可されません。
  • データセグメント (.data): 初期化されたグローバル変数や静的変数が含まれます。通常、読み取り(R)と書き込み(W)のパーミッションを持ち、実行(X)は許可されません。
  • BSSセグメント (.bss): 初期化されていないグローバル変数や静的変数が含まれます。実行時にゼロで初期化されます。通常、読み取り(R)と書き込み(W)のパーミッションを持ち、実行(X)は許可されません。
  • 読み取り専用データセグメント (.rodata): 文字列リテラル、定数、シンボルテーブルなど、実行中に変更されないデータが含まれます。通常、読み取り(R)のみのパーミッションを持ち、書き込み(W)も実行(X)も許可されません。このコミットの焦点となるセグメントです。

W^X (Write XOR Execute)

W^Xは、メモリのセキュリティ原則の一つで、あるメモリページが同時に書き込み可能(W)と実行可能(X)であってはならないというものです。これにより、攻撃者が書き込み可能なメモリ領域に悪意のあるコードを注入し、それを実行することを防ぎます。読み取り専用データを実行不可能なセグメントに配置することは、このW^X原則を遵守するための重要なステップです。

リンカー (ld)

リンカーは、コンパイラによって生成されたオブジェクトファイル(.oファイル)やライブラリを結合し、最終的な実行可能ファイルや共有ライブラリを生成するツールです。リンカーは、各セクションをメモリ上のどのセグメントに配置するか、シンボル参照を解決するかなどを決定します。Go言語のツールチェインでは、cmd/ldがGoプログラムのリンクを担当します。

Go Toolchain (5l, 6l, 8l)

Go言語の初期のツールチェインでは、異なるアーキテクチャ(例: 5lはARM、6lはx86-64、8lはx86)ごとに専用のリンカー/アセンブラが存在しました。これらはGoのソースコードを各アーキテクチャのバイナリに変換する役割を担っていました。このコミットは、これらのアーキテクチャ固有のリンカーの共通部分であるcmd/ldのロジックを変更しています。

gold リンカー

goldは、GNU Binutilsプロジェクトの一部である高速なリンカーです。従来のldリンカーと比較して、大規模なプロジェクトでのリンク時間を大幅に短縮することを目的としています。goldリンカーは、セグメントの配置やパーミッションに関する高度な制御オプションを提供しており、このコミットではその機能の一部(--rosegment)を活用しています。

技術的詳細

このコミットの主要な技術的変更点は、Goリンカーが実行可能ファイルを生成する際に、読み取り専用データを専用の非実行可能セグメントに分離するロジックを導入したことです。

  1. 新しい読み取り専用データセグメント segrodata の導入:

    • src/cmd/ld/lib.hEXTERN Segment segrodata; が追加され、新しいセグメント構造が宣言されました。
    • src/cmd/ld/data.c では、dodata 関数内で segrodata セグメントが初期化され、読み取り専用データ(.rodata.typelink.gosymtab.gopclntab など)がこのセグメントに追加されるようになりました。
    • この分離は、iself (ELF形式であるか) かつ linkmode == LinkInternal (Goリンカーが内部的にリンクを行う場合) の場合にのみ適用されます。それ以外の場合(例: macOSやPlan 9、または外部リンカーを使用する場合)は、読み取り専用データは引き続きテキストセグメントに配置されます。これは、OSや外部リンカーが非実行可能読み取り専用セグメントをサポートしていない可能性があるため、互換性を保つための措置です。
  2. セグメントのパーミッション設定:

    • src/cmd/ld/data.caddress 関数内で、segrodata.rwx = 04; が設定されています。これは、segrodata セグメントが読み取り(R)可能であり、書き込み(W)も実行(X)も不可能であることを示します。これにより、W^X原則が適用されます。
    • segrodata は、テキストセグメントの直後に、ページ境界にアラインされて配置されます。これは、テキストセグメントと読み取り専用データセグメントが混在しないようにするためです。
  3. アーキテクチャ固有のリンカーの変更 (src/cmd/{5,6,8}l/asm.c):

    • これらのファイルでは、以前は読み取り専用データがテキストセグメントの一部として出力されていました。
    • 変更後、segrodata.filelen > 0 の場合に、segrodata の内容が明示的にファイルに出力されるようになりました。
    • シンボルテーブルのオフセット計算 (symo) も、segrodata のサイズを考慮するように更新されました。
  4. ELFファイル生成ロジックの更新 (src/cmd/ld/elf.c):

    • ELFバイナリの生成において、elfemitreloc (再配置情報の出力), elfshalloc (セクションヘッダの割り当て), elfphload (プログラムヘッダのロード), elfshbits (セクションの内容の書き込み), elfshreloc (再配置セクションの書き込み) といった関数が、segrodata セグメントも処理するように変更されました。これにより、生成されるELFファイルが新しいセグメント構造を正しく反映するようになります。
    • .plt (Procedure Linkage Table) のタイプが SELFROSECT から SELFRXSECT に変更されました。これは、読み取り専用だが実行可能なセクション(コードを含む)と、読み取り専用で実行不可能なセクション(データのみ)を明確に区別するための型定義の変更です。
  5. 外部リンカー gold のサポート (src/cmd/ld/lib.c):

    • AssumeGoldLinker というフラグが導入されました(デフォルトは0、つまり無効)。
    • もしこのフラグが有効で、かつELF形式の場合、外部リンカーに "-Wl,--rosegment" オプションが渡されるようになりました。このオプションはgoldリンカーに、読み取り専用データを専用のセグメントに配置するよう指示するものです。これにより、Goリンカーが内部リンクを行わない場合でも、外部リンカーの機能を利用してセキュリティ強化を図ることができます。

これらの変更により、Go言語でビルドされた実行可能ファイルは、特にLinuxなどのELFベースのシステムにおいて、より堅牢なメモリレイアウトとセキュリティ特性を持つようになりました。

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

src/cmd/ld/data.c における segrodata の導入と条件付き使用

--- a/src/cmd/ld/data.c
+++ b/src/cmd/ld/data.c
@@ -1252,27 +1253,56 @@ dodata(void)
 	\tdiag("unexpected symbol type %d for %s", s->type, s->name);
 	}
 
-	/* we finished segdata, begin segtext */
+	/*
+	 * We finished data, begin read-only data.
+	 * Not all systems support a separate read-only non-executable data section.
+	 * ELF systems do.
+	 * OS X and Plan 9 do not.
+	 * Windows PE may, but if so we have not implemented it.
+	 * And if we're using external linking mode, the point is moot,
+	 * since it's not our decision; that code expects the sections in
+	 * segtext.
+	 */
+	if(iself && linkmode == LinkInternal)
+		segro = &segrodata;
+	else
+		segro = &segtext;
+
 	s = datap;
-+	
-+	datsize = 0;
-+	
-+	/* read-only executable ELF, Mach-O sections */
-+	for(; s != nil && s->type < STYPE; s = s->next) {
-+		sect = addsection(&segtext, s->name, 04);
-+		sect->align = symalign(s);
-+		datsize = rnd(datsize, sect->align);
-+		sect->vaddr = datsize;
-+		s->sect = sect;
-+		s->type = SRODATA;
-+		s->value = datsize - sect->vaddr;
-+		growdatsize(&datsize, s);
-+		sect->len = datsize - sect->vaddr;
-+	}
 
 	/* read-only data */
-	sect = addsection(&segtext, ".rodata", 04);
+	sect = addsection(segro, ".rodata", 04);
 	sect->align = maxalign(s, STYPELINK-1);
+	datsize = rnd(datsize, sect->align);
 	sect->vaddr = 0;
 	lookup("rodata", 0)->sect = sect;
 	lookup("erodata", 0)->sect = sect;
-	datsize = 0;
 	for(; s != nil && s->type < STYPELINK; s = s->next) {
 		datsize = aligndatsize(datsize, s);
 		s->sect = sect;
 		s->type = SRODATA;
-		s->value = datsize;
+		s->value = datsize - sect->vaddr;
 		growdatsize(&datsize, s);
 	}
 	sect->len = datsize - sect->vaddr;

src/cmd/ld/data.c における segrodata のパーミッション設定とアライメント

--- a/src/cmd/ld/data.c
+++ b/src/cmd/ld/data.c
@@ -1402,6 +1434,7 @@ address(void)
 	segtext.vaddr = va;
 	segtext.fileoff = HEADR;
 	for(s=segtext.sect; s != nil; s=s->next) {
+//print("%s at %#llux + %#llux\n", s->name, va, (vlong)s->len);
 	\tva = rnd(va, s->align);
 	\ts->vaddr = va;
 	\tva += s->len;
@@ -1409,8 +1442,25 @@ address(void)
 	segtext.len = va - INITTEXT;
 	segtext.filelen = segtext.len;
 
-\tva = rnd(va, INITRND);\n
+\tif(segrodata.sect != nil) {
+\t\t// align to page boundary so as not to mix
+\t\t// rodata and executable text.
+\t\tva = rnd(va, INITRND);
+\n
+\t\tsegrodata.rwx = 04;
+\t\tsegrodata.vaddr = va;
+\t\tsegrodata.fileoff = va - segtext.vaddr + segtext.fileoff;
+\t\tsegrodata.filelen = 0;
+\t\tfor(s=segrodata.sect; s != nil; s=s->next) {
+\t\t\tva = rnd(va, s->align);
+\t\t\ts->vaddr = va;
+\t\t\tva += s->len;
+\t\t}
+\t\tsegrodata.len = va - segrodata.vaddr;
+\t\tsegrodata.filelen = segrodata.len;
+\t}
 \n
+\tva = rnd(va, INITRND);
 	segdata.rwx = 06;
 	segdata.vaddr = va;
 	segdata.fileoff = va - segtext.vaddr + segtext.fileoff;

コアとなるコードの解説

src/cmd/ld/data.c における segrodata の導入と条件付き使用

このコードブロックは、リンカーが読み取り専用データをどのように扱うかを決定する中心的な部分です。

	if(iself && linkmode == LinkInternal)
		segro = &segrodata;
	else
		segro = &segtext;

ここで、segro というポインタが導入され、読み取り専用データを配置するセグメントが動的に選択されます。

  • iself: 現在ビルドしているバイナリがELF形式であるかを示すフラグです。
  • linkmode == LinkInternal: Goリンカーが、外部のシステムリンカー(例: gcc)を呼び出すのではなく、自身でリンク処理を完結させる「内部リンク」モードであるかを示すフラグです。

この条件文は、「もしELF形式のバイナリを内部リンクでビルドしているならば、読み取り専用データは新しく導入された segrodata セグメントに配置する。そうでなければ、既存の segtext セグメント(通常は実行可能コードを含む)に配置する」というロジックを表しています。これは、macOSやPlan 9のようなELF以外のシステムや、外部リンカーを使用する場合には、専用の非実行可能読み取り専用セグメントのサポートが期待できないため、互換性を維持するための重要な分岐です。

その後の addsection(segro, ".rodata", 04); の行は、実際に .rodata セクションを、上記の条件で選択された segro セグメントに追加しています。04 は、このセクションが読み取り可能であることを示します。

src/cmd/ld/data.c における segrodata のパーミッション設定とアライメント

このコードブロックは、segrodata セグメントがメモリ上でどのように配置され、どのようなパーミッションを持つかを定義しています。

	if(segrodata.sect != nil) {
		// align to page boundary so as not to mix
		// rodata and executable text.
		va = rnd(va, INITRND);

		segrodata.rwx = 04;
		segrodata.vaddr = va;
		segrodata.fileoff = va - segtext.vaddr + segtext.fileoff;
		segrodata.filelen = 0;
		for(s=segrodata.sect; s != nil; s=s->next) {
			va = rnd(va, s->align);
			s->vaddr = va;
			va += s->len;
		}
		segrodata.len = va - segrodata.vaddr;
		segrodata.filelen = segrodata.len;
	}
  • if(segrodata.sect != nil): segrodata セグメントに実際にセクションが追加されている場合にのみ、以下の処理が実行されます。
  • va = rnd(va, INITRND);: va (仮想アドレス) を INITRND (通常はページのサイズ、例: 4KB) の倍数にアラインします。これは、実行可能コードを含むテキストセグメントと読み取り専用データセグメントが、メモリ上で異なるページに配置されるようにするためです。これにより、W^X原則を厳密に適用し、セキュリティを強化します。
  • segrodata.rwx = 04;: ここがこのコミットの核心的な変更点の一つです。rwx はセグメントのパーミッションを表し、04 は読み取り(R)のみを意味します。これにより、segrodata セグメントは書き込みも実行も不可能になります。
  • segrodata.vaddr = va;: segrodata セグメントの仮想アドレスを設定します。
  • segrodata.fileoff = va - segtext.vaddr + segtext.fileoff;: ファイル内でのオフセットを計算します。
  • 続く for ループは、segrodata に含まれる各セクションの仮想アドレスと長さを計算し、最終的に segrodata 全体の長さ (segrodata.lensegrodata.filelen) を決定します。

これらの変更により、Goリンカーは、ELF形式のバイナリにおいて、読み取り専用データを実行不可能な専用のメモリ領域に配置するようになり、セキュリティが向上しました。

関連リンク

  • Go言語のリンカーに関するドキュメント(公式ドキュメントに直接的な詳細なリンカーの内部構造に関するドキュメントは少ないですが、Goのソースコード自体が最も正確な情報源です)
  • ELFファイルフォーマットに関する資料
  • W^X (Write XOR Execute) に関するセキュリティ資料

参考にした情報源リンク