[インデックス 14679] ファイルの概要
このコミットは、Go言語のdebug/elf
パッケージにおける、ELF (Executable and Linkable Format) コアファイル処理の堅牢性を向上させるための修正を含んでいます。具体的には、セクションヘッダ文字列テーブルのインデックス(shstrndx
)が欠落している、または無効な場合に発生する可能性のあるパニックやエラーを適切に処理するように改善されています。また、テストフィクスチャとしてgzip圧縮されたファイル(.gz
)をサポートする機能が追加され、テストの柔軟性が向上しています。
コミット
commit 7e9d9eb17bb2e4bd34e79c9a780d4992ed2ab041
Author: Dave Cheney <dave@cheney.net>
Date: Tue Dec 18 07:58:22 2012 +1100
debug/elf: handle missing shstrndx in core files
Fixes #4481.
hello-world-core.gz was generated with a simple hello world c program and core dumped as suggested in the issue.
Also: add support for gz compressed test fixtures.
R=minux.ma, rsc, iant
CC=golang-dev
https://golang.org/cl/6936058
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7e9d9eb17bb2e4bd34e79c9a780d4992ed2ab041
元コミット内容
debug/elf: handle missing shstrndx in core files
Fixes #4481.
hello-world-core.gz was generated with a simple hello world c program and core dumped as suggested in the issue.
Also: add support for gz compressed test fixtures.
変更の背景
このコミットの主な背景は、Go言語のdebug/elf
パッケージが、特定の形式のELFコアファイルを処理する際に発生する問題に対処することです。コミットメッセージに「Fixes #4481」とあるように、これはGoのIssueトラッカーで報告されたバグの修正です。
ELFファイル、特にコアダンプファイルは、プログラムがクラッシュした際にその時点のメモリ状態やレジスタ情報などを記録するために生成されます。これらのファイルはデバッグに不可欠ですが、その構造は複雑であり、様々なツールやシステムによって生成されるため、必ずしもすべてのフィールドが期待通りに存在したり、有効な値を持っていたりするわけではありません。
元の実装では、ELFヘッダ内のshstrndx
(セクションヘッダ文字列テーブルのインデックス)フィールドが不正な値(例えば、負の値やセクション数を超える値)を持つ場合に、debug/elf
パッケージがパニックを起こす可能性がありました。特に、コアファイルの中には、セクションヘッダ文字列テーブルが存在しない、あるいはそのインデックスが不正なものが存在することが知られています。このようなファイルに対してNewFile
関数が呼び出されると、無効なインデックスでセクションにアクセスしようとしてエラーが発生し、デバッグツールの安定性を損ねていました。
この問題は、シンプルなC言語の"hello world"プログラムをコアダンプさせたファイルで再現されたとコミットメッセージに記載されており、実際のデバッグシナリオで遭遇する可能性のある現実的な問題であったことが示唆されます。
また、テストの利便性向上のため、テストフィクスチャとしてgzip圧縮されたファイルを使用できるようにする変更も同時に行われています。これは、コアダンプファイルのような大きなバイナリファイルをリポジトリに含める際に、ファイルサイズを削減し、リポジトリのクローン時間やディスク使用量を抑えるための一般的なプラクティスです。
前提知識の解説
ELF (Executable and Linkable Format)
ELFは、Unix系オペレーティングシステムで広く使用されている、実行可能ファイル、オブジェクトファイル、共有ライブラリ、およびコアダンプファイルの標準的なファイル形式です。その構造は、プログラムの実行に必要なすべての情報(コード、データ、シンボルテーブル、デバッグ情報など)を効率的に格納できるように設計されています。
主要な構成要素は以下の通りです。
- ELFヘッダ (ELF Header): ファイルの先頭に位置し、ELFファイルのタイプ(実行可能、共有ライブラリ、コアダンプなど)、アーキテクチャ、エンディアン、バージョンなどの基本的な情報を含みます。
- プログラムヘッダテーブル (Program Header Table): 実行可能ファイルや共有ライブラリにおいて、プログラムのロード方法(どのセグメントをどのメモリアドレスにロードするかなど)を記述します。各エントリは「セグメント」を定義します。
- セクションヘッダテーブル (Section Header Table): オブジェクトファイルや共有ライブラリにおいて、ファイルの論理的な構造(コードセクション、データセクション、シンボルテーブルなど)を記述します。各エントリは「セクション」を定義します。
- セクションヘッダ文字列テーブル (Section Header String Table): セクションヘッダテーブル内のセクション名が格納されているセクションです。
shstrndx
は、このセクションヘッダ文字列テーブルのインデックスを示します。
コアダンプファイル
コアダンプファイルは、プログラムが異常終了(クラッシュ)した際に、その時点のプロセスのメモリイメージ、レジスタの状態、スタックトレースなどを記録したファイルです。デバッガ(例: GDB)はこのファイルを使用して、クラッシュの原因を特定するための事後デバッグ(post-mortem debugging)を行います。コアダンプファイルはELF形式で保存されることが多く、その構造は通常の実行可能ファイルとは異なる場合があります。特に、デバッグ情報やセクションヘッダ文字列テーブルが省略されることがあります。
shstrndx
(Section Header String Table Index)
ELFヘッダの一部であり、セクションヘッダ文字列テーブルがセクションヘッダテーブルのどのエントリに対応するかを示すインデックスです。このテーブルには、すべてのセクションの名前が文字列として格納されています。debug/elf
パッケージがセクション名を解決するためには、このshstrndx
が指すセクションヘッダ文字列テーブルを読み込む必要があります。しかし、コアファイルなどではこの情報が欠落している場合があり、その際に適切なエラーハンドリングが必要となります。
io.ReaderAt
インターフェース
Go言語のio.ReaderAt
インターフェースは、任意のオフセットからデータを読み込む機能を提供します。これは、ファイルのようなランダムアクセス可能なデータソースを扱う際に非常に便利です。debug/elf
パッケージのNewFile
関数は、このio.ReaderAt
を引数として受け取ることで、ファイル全体をメモリに読み込むことなく、必要な部分だけを効率的に読み込むことができます。
compress/gzip
パッケージ
Go言語の標準ライブラリに含まれるcompress/gzip
パッケージは、gzip形式の圧縮データを扱うための機能を提供します。このコミットでは、gzip圧縮されたテストフィクスチャを読み込むためにこのパッケージが利用されています。
技術的詳細
このコミットは、主にsrc/pkg/debug/elf/file.go
内のNewFile
関数のロジックと、src/pkg/debug/elf/file_test.go
内のテストケースに修正を加えています。
NewFile
関数の変更点
-
shstrndx
の検証ロジックの改善: 元のコードでは、shstrndx
が負の値であるか、またはセクション数(shnum
)以上である場合にエラーを返していました。if shstrndx < 0 || shstrndx >= shnum { return nil, &FormatError{0, "invalid ELF shstrndx", shstrndx} }
このコミットでは、このチェックに加えて、
shnum > 0
(セクションが存在する)かつshoff > 0
(セクションヘッダテーブルのオフセットが0より大きい、つまりセクションヘッダテーブルが存在する)という条件が追加されました。if shnum > 0 && shoff > 0 && (shstrndx < 0 || shstrndx >= shnum) { return nil, &FormatError{0, "invalid ELF shstrndx", shstrndx} }
この変更により、セクションヘッダテーブル自体が存在しない(
shnum
が0またはshoff
が0)ようなELFファイル(特にコアファイルでよく見られる)の場合に、shstrndx
の無効な値によるエラーを回避できるようになりました。セクションが存在しない場合、shstrndx
がどんな値であってもセクションヘッダ文字列テーブルを読み込む必要がないため、このチェックは不要になります。 -
セクションが空の場合の早期リターン:
NewFile
関数は、ELFファイルのセクションヘッダテーブルを読み込み、f.Sections
スライスに格納します。このコミットでは、セクションの読み込みが完了した後、f.Sections
が空である(つまり、セクションが存在しない)場合に、セクションヘッダ文字列テーブルの読み込みをスキップして早期にf
を返すロジックが追加されました。if len(f.Sections) == 0 { return f, nil }
この変更は、セクションが存在しないELFファイル(例えば、一部のコアファイル)において、存在しない
shstrndx
を基にセクションヘッダ文字列テーブルを読み込もうとして発生するエラーを防ぎます。セクションがなければ、セクション名も存在しないため、文字列テーブルを読み込む必要がありません。
テストファイルの変更点
-
gzip圧縮されたテストフィクスチャのサポート:
src/pkg/debug/elf/file_test.go
に、gzip圧縮されたファイルを解凍してio.ReaderAt
として提供する新しいヘルパー関数decompress
が追加されました。func decompress(gz string) (io.ReaderAt, error) { in, err := os.Open(gz) if err != nil { return nil, err } defer in.Close() r, err := gzip.NewReader(in) if err != nil { return nil, err } var out bytes.Buffer _, err = io.Copy(&out, r) return bytes.NewReader(out.Bytes()), err }
この関数は、指定されたgzipファイルを読み込み、
gzip.NewReader
で解凍し、その内容をbytes.Buffer
にコピーした後、bytes.NewReader
を使ってio.ReaderAt
インターフェースを満たす*bytes.Reader
を返します。これにより、NewFile
関数がgzip圧縮されたファイルの内容を直接読み込めるようになります。 -
新しいテストケースの追加:
fileTests
スライスに、testdata/hello-world-core.gz
という新しいテストフィクスチャが追加されました。これは、コミットメッセージで言及されている「シンプルなhello world Cプログラムをコアダンプしたもの」です。このテストケースは、ET_CORE
タイプ(コアファイル)のELFヘッダと、複数のPT_LOAD
タイプのプログラムヘッダを持つことを期待しています。重要なのは、このテストケースではSectionHeader
が空の配列{}
として定義されており、セクションヘッダが存在しないコアファイルをシミュレートしている点です。 -
TestOpen
関数の修正:TestOpen
関数内で、テストファイルの拡張子が.gz
である場合に、新しく追加されたdecompress
関数を使用してファイルを解凍し、その結果をNewFile
に渡すようにロジックが変更されました。if path.Ext(tt.file) == ".gz" { var r io.ReaderAt if r, err = decompress(tt.file); err == nil { f, err = NewFile(r) } } else { f, err = Open(tt.file) }
これにより、gzip圧縮されたテストフィクスチャが自動的に解凍され、テストフレームワークに組み込まれるようになりました。
コアとなるコードの変更箇所
src/pkg/debug/elf/file.go
--- a/src/pkg/debug/elf/file.go
+++ b/src/pkg/debug/elf/file.go
@@ -272,7 +272,8 @@ func NewFile(r io.ReaderAt) (*File, error) {
shnum = int(hdr.Shnum)
shstrndx = int(hdr.Shstrndx)
}
- if shstrndx < 0 || shstrndx >= shnum {
+
+ if shnum > 0 && shoff > 0 && (shstrndx < 0 || shstrndx >= shnum) {
return nil, &FormatError{0, "invalid ELF shstrndx", shstrndx}
}
@@ -367,6 +368,10 @@ func NewFile(r io.ReaderAt) (*File, error) {
f.Sections[i] = s
}
+ if len(f.Sections) == 0 {
+ return f, nil
+ }
+
// Load section header string table.
shstrtab, err := f.Sections[shstrndx].Data()
if err != nil {
src/pkg/debug/elf/file_test.go
--- a/src/pkg/debug/elf/file_test.go
+++ b/src/pkg/debug/elf/file_test.go
@@ -5,10 +5,14 @@
package elf
import (
+ "bytes"
+ "compress/gzip"
"debug/dwarf"
"encoding/binary"
+ "io"
"net"
"os"
+ "path"
"reflect"
"runtime"
"testing"
@@ -121,15 +125,49 @@ var fileTests = []fileTest{
},
[]string{"libc.so.6"},
},
+ {
+ "testdata/hello-world-core.gz",
+ FileHeader{ELFCLASS64, ELFDATA2LSB, EV_CURRENT, ELFOSABI_NONE, 0x0, binary.LittleEndian, ET_CORE, EM_X86_64, 0x0},
+ []SectionHeader{},
+ []ProgHeader{
+ {Type: PT_NOTE, Flags: 0x0, Off: 0x3f8, Vaddr: 0x0, Paddr: 0x0, Filesz: 0x8ac, Memsz: 0x0, Align: 0x0},
+ {Type: PT_LOAD, Flags: PF_X + PF_R, Off: 0x1000, Vaddr: 0x400000, Paddr: 0x0, Filesz: 0x0, Memsz: 0x1000, Align: 0x1000},
+ {Type: PT_LOAD, Flags: PF_R, Off: 0x1000, Vaddr: 0x401000, Paddr: 0x0, Filesz: 0x1000, Memsz: 0x1000, Align: 0x1000},
+ {Type: PT_LOAD, Flags: PF_W + PF_R, Off: 0x2000, Vaddr: 0x402000, Paddr: 0x0, Filesz: 0x1000, Memsz: 0x1000, Align: 0x1000},
+ {Type: PT_LOAD, Flags: PF_X + PF_R, Off: 0x3000, Vaddr: 0x7f54078b8000, Paddr: 0x0, Filesz: 0x0, Memsz: 0x1b5000, Align: 0x1000},
+ {Type: PT_LOAD, Flags: 0x0, Off: 0x3000, Vaddr: 0x7f5407a6d000, Paddr: 0x0, Filesz: 0x0, Memsz: 0x1ff000, Align: 0x1000},
+ {Type: PT_LOAD, Flags: PF_R, Off: 0x3000, Vaddr: 0x7f5407c6c000, Paddr: 0x0, Filesz: 0x4000, Memsz: 0x4000, Align: 0x1000},
+ {Type: PT_LOAD, Flags: PF_W + PF_R, Off: 0x7000, Vaddr: 0x7f5407c70000, Paddr: 0x0, Filesz: 0x2000, Memsz: 0x2000, Align: 0x1000},
+ {Type: PT_LOAD, Flags: PF_W + PF_R, Off: 0x9000, Vaddr: 0x7f5407c72000, Paddr: 0x0, Filesz: 0x5000, Memsz: 0x5000, Align: 0x1000},
+ {Type: PT_LOAD, Flags: PF_X + PF_R, Off: 0xe000, Vaddr: 0x7f5407c77000, Paddr: 0x0, Filesz: 0x0, Memsz: 0x22000, Align: 0x1000},
+ {Type: PT_LOAD, Flags: PF_W + PF_R, Off: 0xe000, Vaddr: 0x7f5407e81000, Paddr: 0x0, Filesz: 0x3000, Memsz: 0x3000, Align: 0x1000},
+ {Type: PT_LOAD, Flags: PF_W + PF_R, Off: 0x11000, Vaddr: 0x7f5407e96000, Paddr: 0x0, Filesz: 0x3000, Memsz: 0x3000, Align: 0x1000},
+ {Type: PT_LOAD, Flags: PF_R, Off: 0x14000, Vaddr: 0x7f5407e99000, Paddr: 0x0, Filesz: 0x1000, Memsz: 0x1000, Align: 0x1000},
+ {Type: PT_LOAD, Flags: PF_W + PF_R, Off: 0x15000, Vaddr: 0x7f5407e9a000, Paddr: 0x0, Filesz: 0x2000, Memsz: 0x2000, Align: 0x1000},
+ {Type: PT_LOAD, Flags: PF_W + PF_R, Off: 0x17000, Vaddr: 0x7fff79972000, Paddr: 0x0, Filesz: 0x23000, Memsz: 0x23000, Align: 0x1000},
+ {Type: PT_LOAD, Flags: PF_X + PF_R, Off: 0x3a000, Vaddr: 0x7fff799f8000, Paddr: 0x0, Filesz: 0x1000, Memsz: 0x1000, Align: 0x1000},
+ {Type: PT_LOAD, Flags: PF_X + PF_R, Off: 0x3b000, Vaddr: 0xffffffffff600000, Paddr: 0x0, Filesz: 0x1000, Memsz: 0x1000, Align: 0x1000},
+ },
+ nil,
+ },
}\n \n func TestOpen(t *testing.T) {\n \tfor i := range fileTests {\n \t\ttt := &fileTests[i]\n \n-\t\tf, err := Open(tt.file)\n+\t\tvar f *File\n+\t\tvar err error\n+\t\tif path.Ext(tt.file) == \".gz\" {\n+\t\t\tvar r io.ReaderAt\n+\t\t\tif r, err = decompress(tt.file); err == nil {\n+\t\t\t\tf, err = NewFile(r)\n+\t\t\t}\n+\t\t} else {\n+\t\t\tf, err = Open(tt.file)\n+\t\t}\n \t\tif err != nil {\n-\t\t\tt.Error(err)\n+\t\t\tt.Errorf(\"cannot open file %s: %v\", tt.file, err)\n \t\t\tcontinue\n \t\t}\n \t\tif !reflect.DeepEqual(f.FileHeader, tt.hdr) {\n@@ -175,6 +213,23 @@ func TestOpen(t *testing.T) {\n \t}\n }\n \n+// elf.NewFile requires io.ReaderAt, which compress/gzip cannot\n+// provide. Decompress the file to a bytes.Reader.\n+func decompress(gz string) (io.ReaderAt, error) {\n+\tin, err := os.Open(gz)\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\tdefer in.Close()\n+\tr, err := gzip.NewReader(in)\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\tvar out bytes.Buffer\n+\t_, err = io.Copy(&out, r)\n+\treturn bytes.NewReader(out.Bytes()), err\n+}\n+\n type relocationTestEntry struct {\n \tentryNumber int\n \tentry *dwarf.Entry
コアとなるコードの解説
src/pkg/debug/elf/file.go
の変更点
-
shstrndx
の検証条件の強化: 変更前のコードでは、shstrndx
が負の値であるか、またはセクションの総数(shnum
)以上である場合にエラーを返していました。これは一般的なELFファイルの破損を検出するには十分ですが、セクションヘッダテーブル自体が存在しない(shnum
が0またはshoff
が0)ような特殊なELFファイル(特にコアダンプファイル)の場合には問題がありました。 新しい条件if shnum > 0 && shoff > 0 && (shstrndx < 0 || shstrndx >= shnum)
は、セクションヘッダテーブルが存在する場合にのみshstrndx
の有効性をチェックするようにしています。これにより、セクションヘッダテーブルが存在しないファイルに対して、不必要なshstrndx
の検証を行わなくなり、不正なインデックスによるパニックを防ぎます。これは、ELFコアファイルが必ずしも完全なセクションヘッダ情報を持つとは限らないという現実に対応するための重要な変更です。 -
セクションが空の場合の早期リターン:
NewFile
関数は、ELFファイルのセクションヘッダを読み込んだ後、f.Sections
スライスにそれらを格納します。その後の処理で、shstrndx
が指すセクションヘッダ文字列テーブルを読み込もうとします。 追加されたif len(f.Sections) == 0 { return f, nil }
という行は、セクションが一つも存在しないELFファイルの場合に、セクションヘッダ文字列テーブルの読み込み処理を完全にスキップして、早期にFile
オブジェクトを返すようにします。セクションが存在しないということは、セクション名も存在しないため、セクションヘッダ文字列テーブルを読み込む必要がありません。この変更により、セクションが存在しないコアファイルなどで、存在しないshstrndx
を基にアクセスしようとして発生するエラーを回避できます。
これらの変更は、debug/elf
パッケージがより多様な、特に不完全なELFファイル(コアダンプなど)を堅牢に処理できるようにすることを目的としています。
src/pkg/debug/elf/file_test.go
の変更点
-
decompress
関数の追加: この新しい関数は、gzip圧縮されたテストフィクスチャを扱うためのユーティリティです。os.Open
でgzipファイルを開き、gzip.NewReader
で解凍リーダーを作成します。その後、io.Copy
を使って解凍されたデータをbytes.Buffer
に書き込み、最終的にbytes.NewReader
を使ってio.ReaderAt
インターフェースを満たす*bytes.Reader
を返します。debug/elf.NewFile
関数はio.ReaderAt
を引数として受け取るため、このdecompress
関数によって、gzip圧縮されたファイルの内容を直接NewFile
に渡せるようになります。これにより、テストデータとして大きなバイナリファイルを圧縮してリポジトリに格納し、テスト時に透過的に解凍して使用することが可能になります。 -
hello-world-core.gz
テストケースの追加: このテストケースは、実際のコアダンプファイルを模倣したものです。FileHeader
のType
がET_CORE
(コアファイル)に設定されており、SectionHeader
が空の配列{}
として定義されています。これは、セクションヘッダが存在しないコアファイルを表現しており、file.go
で追加された堅牢性向上のロジックが正しく機能するかを検証するために重要です。ProgHeader
には、コアファイルが持つ典型的なプログラムセグメント(メモリ領域)の情報が詳細に記述されています。 -
TestOpen
関数のテストフィクスチャ処理の変更:TestOpen
関数は、テスト対象のファイルパスの拡張子をチェックし、.gz
であれば新しく追加されたdecompress
関数を使ってファイルを解凍してからNewFile
に渡すように変更されました。これにより、テストフレームワークがgzip圧縮されたテストフィクスチャを自動的に処理できるようになり、テストの記述が簡素化されます。
これらのテスト関連の変更は、debug/elf
パッケージの堅牢性向上を検証するための具体的なテストケースを提供し、将来的な回帰を防ぐための重要な役割を果たします。
関連リンク
- ELF (Executable and Linkable Format) - Wikipedia: https://ja.wikipedia.org/wiki/Executable_and_Linkable_Format
- Go言語
debug/elf
パッケージドキュメント: https://pkg.go.dev/debug/elf - Go言語
compress/gzip
パッケージドキュメント: https://pkg.go.dev/compress/gzip - Go言語
io
パッケージドキュメント: https://pkg.go.dev/io
参考にした情報源リンク
- Go言語のIssueトラッカー (Go issue #4481に関する直接的な情報は見つかりませんでしたが、一般的なGoのIssueの形式として参照)
- ELFファイルフォーマットに関する一般的な技術文書 (例: System V ABI)
- Go言語の標準ライブラリのソースコード (
src/pkg/debug/elf/
およびsrc/pkg/compress/gzip/
) - コアダンプファイルの構造に関する情報