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

[インデックス 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関数の変更点

  1. 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がどんな値であってもセクションヘッダ文字列テーブルを読み込む必要がないため、このチェックは不要になります。

  2. セクションが空の場合の早期リターン: NewFile関数は、ELFファイルのセクションヘッダテーブルを読み込み、f.Sectionsスライスに格納します。このコミットでは、セクションの読み込みが完了した後、f.Sectionsが空である(つまり、セクションが存在しない)場合に、セクションヘッダ文字列テーブルの読み込みをスキップして早期にfを返すロジックが追加されました。

    if len(f.Sections) == 0 {
        return f, nil
    }
    

    この変更は、セクションが存在しないELFファイル(例えば、一部のコアファイル)において、存在しないshstrndxを基にセクションヘッダ文字列テーブルを読み込もうとして発生するエラーを防ぎます。セクションがなければ、セクション名も存在しないため、文字列テーブルを読み込む必要がありません。

テストファイルの変更点

  1. 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圧縮されたファイルの内容を直接読み込めるようになります。

  2. 新しいテストケースの追加: fileTestsスライスに、testdata/hello-world-core.gzという新しいテストフィクスチャが追加されました。これは、コミットメッセージで言及されている「シンプルなhello world Cプログラムをコアダンプしたもの」です。このテストケースは、ET_COREタイプ(コアファイル)のELFヘッダと、複数のPT_LOADタイプのプログラムヘッダを持つことを期待しています。重要なのは、このテストケースではSectionHeaderが空の配列{}として定義されており、セクションヘッダが存在しないコアファイルをシミュレートしている点です。

  3. 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 の変更点

  1. shstrndxの検証条件の強化: 変更前のコードでは、shstrndxが負の値であるか、またはセクションの総数(shnum)以上である場合にエラーを返していました。これは一般的なELFファイルの破損を検出するには十分ですが、セクションヘッダテーブル自体が存在しない(shnumが0またはshoffが0)ような特殊なELFファイル(特にコアダンプファイル)の場合には問題がありました。 新しい条件if shnum > 0 && shoff > 0 && (shstrndx < 0 || shstrndx >= shnum)は、セクションヘッダテーブルが存在する場合にのみshstrndxの有効性をチェックするようにしています。これにより、セクションヘッダテーブルが存在しないファイルに対して、不必要なshstrndxの検証を行わなくなり、不正なインデックスによるパニックを防ぎます。これは、ELFコアファイルが必ずしも完全なセクションヘッダ情報を持つとは限らないという現実に対応するための重要な変更です。

  2. セクションが空の場合の早期リターン: 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 の変更点

  1. 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に渡せるようになります。これにより、テストデータとして大きなバイナリファイルを圧縮してリポジトリに格納し、テスト時に透過的に解凍して使用することが可能になります。

  2. hello-world-core.gzテストケースの追加: このテストケースは、実際のコアダンプファイルを模倣したものです。FileHeaderTypeET_CORE(コアファイル)に設定されており、SectionHeaderが空の配列{}として定義されています。これは、セクションヘッダが存在しないコアファイルを表現しており、file.goで追加された堅牢性向上のロジックが正しく機能するかを検証するために重要です。ProgHeaderには、コアファイルが持つ典型的なプログラムセグメント(メモリ領域)の情報が詳細に記述されています。

  3. TestOpen関数のテストフィクスチャ処理の変更: TestOpen関数は、テスト対象のファイルパスの拡張子をチェックし、.gzであれば新しく追加されたdecompress関数を使ってファイルを解凍してからNewFileに渡すように変更されました。これにより、テストフレームワークがgzip圧縮されたテストフィクスチャを自動的に処理できるようになり、テストの記述が簡素化されます。

これらのテスト関連の変更は、debug/elfパッケージの堅牢性向上を検証するための具体的なテストケースを提供し、将来的な回帰を防ぐための重要な役割を果たします。

関連リンク

参考にした情報源リンク

  • Go言語のIssueトラッカー (Go issue #4481に関する直接的な情報は見つかりませんでしたが、一般的なGoのIssueの形式として参照)
  • ELFファイルフォーマットに関する一般的な技術文書 (例: System V ABI)
  • Go言語の標準ライブラリのソースコード (src/pkg/debug/elf/ および src/pkg/compress/gzip/)
  • コアダンプファイルの構造に関する情報