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

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

このコミットは、Go言語のsyscallパッケージにおけるLinuxシステムコール(Seek, Time, Gettimeofday)のバグ修正に関するものです。具体的には、アセンブリ言語で実装されたこれらの関数が、Goのプロトタイプでerrorインターフェースを返すと宣言されていたにもかかわらず、実際にはerrno(エラー番号)をuintptrとして返しており、これが非ゼロの場合にクラッシュを引き起こす可能性があった問題を解決します。

コミット

commit bf68f6623afd589ebaed1f868729f707701c6ddc
Author: Russ Cox <rsc@golang.org>
Date:   Fri May 16 12:15:32 2014 -0400

    syscall: fix a few Linux system calls
    
    These functions claimed to return error (an interface)
    and be implemented entirely in assembly, but it's not
    possible to create an interface from assembly
    (at least not easily).
    
    In reality the functions were written to return an errno uintptr
    despite the Go prototype saying error.
    When the errno was 0, they coincidentally filled out a nil error
    by writing the 0 to the type word of the interface.
    If the errno was ever non-zero, the functions would
    create a non-nil error that would crash when trying to
    call err.Error().
    
    Luckily these functions (Seek, Time, Gettimeofday) pretty
    much never fail, so it was all kind of working.
    
    Found by go vet.
    
    LGTM=bradfitz, r
    R=golang-codereviews, bradfitz, r
    CC=golang-codereviews
    https://golang.org/cl/99320043

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

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

元コミット内容

syscall: fix a few Linux system calls

These functions claimed to return error (an interface)
and be implemented entirely in assembly, but it's not
possible to create an interface from assembly
(at least not easily).

In reality the functions were written to return an errno uintptr
despite the Go prototype saying error.
When the errno was 0, they coincidentally filled out a nil error
by writing the 0 to the type word of the interface.
If the errno was ever non-zero, the functions would
create a non-nil error that would crash when trying to
call err.Error().

Luckily these functions (Seek, Time, Gettimeofday) pretty
much never fail, so it was all kind of working.

Found by go vet.

LGTM=bradfitz, r
R=golang-codereviews, bradfitz, r
CC=golang-codereviews
https://golang.org/cl/99320043

変更の背景

Go言語のsyscallパッケージは、オペレーティングシステム(OS)の低レベルな機能にアクセスするための重要なインターフェースを提供します。特にLinux環境では、パフォーマンスが要求される一部のシステムコールが、Goのアセンブリ言語で直接実装されていました。

このコミットで修正された問題は、アセンブリで実装されたSeekTimeGettimeofdayといったシステムコールが、Goの関数プロトタイプではerrorインターフェースを返すと宣言されていたにもかかわらず、実際にはアセンブリコード内で直接errorインターフェースを正しく構築していなかったことに起因します。

Goのerrorインターフェースは、内部的には「型情報」と「値」の2つのポインタ(またはワード)で構成されます。アセンブリコードは、システムコールが成功した場合(エラー番号errnoが0の場合)には、偶然にもインターフェースの型ワードに0を書き込むことで、Goランタイムがこれをnilエラーとして正しく解釈していました。しかし、システムコールが失敗し、errnoが非ゼロの値を返した場合、アセンブリコードはerrorインターフェースの型ワードを不正な値で埋めていました。この状態で、Goコードが返されたエラーに対してerr.Error()メソッドを呼び出そうとすると、不正な型情報に基づいてメソッドディスパッチが行われ、結果としてプログラムがクラッシュするという深刻なバグがありました。

幸いなことに、SeekTimeGettimeofdayといったシステムコールは、通常はほとんど失敗しない性質を持っていたため、このバグは長期間にわたって顕在化しませんでした。しかし、Goの静的解析ツールであるgo vetがこの潜在的な問題を検出し、修正の必要性が明らかになりました。

前提知識の解説

Goのerrorインターフェース

Go言語におけるエラーハンドリングは、組み込みのerrorインターフェースを中心に行われます。errorインターフェースは、以下のように定義されています。

type error interface {
    Error() string
}

このインターフェースは、Error()という文字列を返すメソッドを1つだけ持ちます。関数がエラーを返す可能性がある場合、通常は最後の戻り値としてerror型を宣言します。エラーがない場合はnilを返します。

Goのインターフェースは、内部的には「型情報」と「値」の2つの要素で構成されるデータ構造です。

  • 型情報 (Type Word): インターフェースが保持している具体的な値の型(例: *os.PathErrorsyscall.Errnoなど)を指すポインタ。
  • 値 (Value Word): インターフェースが保持している具体的な値(例: &os.PathError{...}syscall.Errno(1)など)を指すポインタ。

インターフェースがnilであるとは、この両方のポインタがnilである状態を指します。

Goのsyscallパッケージ

syscallパッケージは、Goプログラムからオペレーティングシステムが提供するシステムコールを直接呼び出すための低レベルなインターフェースを提供します。ファイル操作、ネットワーク通信、プロセス管理など、OSの基本的な機能の多くはシステムコールを通じて行われます。Goの標準ライブラリの多くの部分も、内部的にこのsyscallパッケージを利用しています。

Goにおけるアセンブリ言語

Go言語は、一部のパフォーマンスクリティカルな部分や、OSとの低レベルなインタフェース(例えば、システムコールラッパー)において、アセンブリ言語でコードを記述することをサポートしています。Goのアセンブリは、Plan 9アセンブラの構文に基づいています。アセンブリで書かれた関数は、Goの関数と相互に呼び出し可能ですが、GoのABI(Application Binary Interface)に厳密に準拠する必要があります。これには、引数の渡し方、戻り値の受け取り方、レジスタの使用規則などが含まれます。

errno

Unix系オペレーティングシステムにおいて、システムコールがエラーを返した場合、そのエラーの種類を示す整数値が設定されます。これがerrno(エラー番号)です。例えば、ファイルが見つからない場合はENOENT、パーミッションがない場合はEACCESといった値が対応します。Goのsyscallパッケージでは、これらのエラー番号をErrno型としてラップしています。Errno型はerrorインターフェースを実装しており、Error()メソッドを呼び出すことで対応するエラーメッセージ文字列を取得できます。

go vet

go vetは、Goのソースコードを静的に解析し、潜在的なバグや疑わしい構造を報告するツールです。コンパイルエラーにはならないが、実行時に問題を引き起こす可能性のあるコードパターンを検出するのに役立ちます。例えば、フォーマット文字列の不一致、到達不能なコード、ロックの誤用、そして今回のケースのように、アセンブリとGoのプロトタイプ間の不整合などを検出できます。

技術的詳細

このコミットの核心的な問題は、Goのインターフェースの内部表現と、アセンブリ言語からそのインターフェースを正しく構築することの難しさにありました。

アセンブリで実装されたSeekTimeGettimeofday関数は、Goのプロトタイプではerrorインターフェースを返すと宣言されていました。しかし、アセンブリコードはGoのインターフェースの内部構造(型情報と値のペア)を直接操作してerrorインターフェースを生成するのではなく、システムコールから返されるerrno(エラー番号)をuintptrとして直接返していました。

  • 成功時の挙動(errnoが0の場合): システムコールが成功し、errnoが0の場合、アセンブリコードは戻り値として0を返していました。Goランタイムは、この0をerrorインターフェースの「型情報」と「値」の両方のワードに偶然にも0として解釈していました。結果として、Goの観点からはnilエラーとして認識され、問題なく動作していました。これは、nilインターフェースが内部的に両方のワードが0であるという事実と一致していたため、たまたま正しく機能していたに過ぎません。

  • 失敗時の挙動(errnoが非ゼロの場合): システムコールが失敗し、errnoが非ゼロの値(例: EFAULTEINVALなど)を返した場合、アセンブリコードはその非ゼロのerrno値を戻り値として返していました。Goランタイムは、この非ゼロの値をerrorインターフェースの「型情報」ワードとして解釈してしまいました。しかし、この非ゼロの値は有効な型情報を指すポインタではありません。そのため、Goコードが返されたエラーに対してerr.Error()メソッドを呼び出そうとすると、ランタイムは不正な型情報に基づいてメソッドテーブルを検索しようとし、結果としてセグメンテーション違反などのクラッシュを引き起こしていました。

この問題が長らく見過ごされてきたのは、SeekTimeGettimeofdayといったシステムコールが、通常の使用シナリオではほとんどエラーを返さないという性質があったためです。エラーパスが頻繁に実行されないため、バグが表面化しにくかったのです。

go vetは、Goの関数プロトタイプがerrorインターフェースを返すと宣言しているにもかかわらず、対応するアセンブリ実装がインターフェースを正しく構築していない可能性を静的に検出しました。これにより、この潜在的なクラッシュバグが特定され、修正されることになりました。

修正のアプローチは、アセンブリ関数が直接errorインターフェースを返すのではなく、GoのErrno型(uintptrに相当)を返すように変更し、その上でGoのコードでラッパー関数を導入するというものです。このラッパー関数がアセンブリ関数を呼び出し、返されたErrno値をチェックして、必要に応じてerrorインターフェースに変換して返す役割を担います。これにより、Goの型システムとアセンブリの実装との間の整合性が保たれ、安全なエラーハンドリングが実現されます。

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

このコミットでは、主に以下のファイルが変更されています。

  1. src/pkg/syscall/asm_linux_386.s

  2. src/pkg/syscall/asm_linux_amd64.s

  3. src/pkg/syscall/asm_linux_arm.s

    • これらのアセンブリファイルでは、SeekGettimeofdayTime(amd64のみ)に対応するアセンブリ関数の名前が変更され(例: TEXT ·Seek(SB) から TEXT ·seek(SB))、Goのプロトタイプでerrorを返すとされていた部分が、実際にはerrno uintptrを返すように変更されました。具体的には、戻り値のスタック上のオフセットが調整され、errorインターフェースの2ワードではなく、uintptrの1ワード分の領域が確保されるようになりました。
  4. src/pkg/syscall/syscall_linux_386.go

  5. src/pkg/syscall/syscall_linux_amd64.go

  6. src/pkg/syscall/syscall_linux_arm.go

    • これらのGoソースファイルでは、アセンブリで実装された低レベルな関数(例: seek, gettimeofday)を呼び出すための新しいラッパー関数が導入されました。
    • 元の公開関数(例: Seek, Gettimeofday, Time)は、この新しいラッパー関数を呼び出し、アセンブリから返されたErrno値をチェックします。
    • Errnoが非ゼロの場合、そのErrno値をerrorインターフェースとして返します。Errnoが0の場合はnilを返します。
    • syscall_linux_amd64.goでは、Time関数がGettimeofdayを内部的に利用するように変更され、アセンブリから直接Timeを呼び出す部分が削除されました。
  7. src/pkg/syscall/syscall_unix_test.go

    • TestSeekFailureという新しいテストケースが追加されました。このテストは、syscall.Seekに不正な引数(ファイルディスクリプタ-1)を渡して意図的にエラーを発生させ、返されたエラーがnilでないこと、そしてError()メソッドを呼び出してもクラッシュしないことを検証します。これは、修正が正しく機能していることを確認するための重要なテストです。

コアとなるコードの解説

このコミットの主要な変更は、アセンブリで実装されたシステムコールラッパーと、それらを呼び出すGoのラッパー関数の間のエラーハンドリングの整合性を確保することです。

アセンブリコードの変更(例: asm_linux_386.sSeek):

変更前:

TEXT ·Seek(SB),NOSPLIT,$0-32
    // ...
    // 戻り値としてerrorインターフェースを期待

変更後:

TEXT ·seek(SB),NOSPLIT,$0-28
    // ...
    // 戻り値としてerrno uintptrを返す

アセンブリ関数は、Goの公開関数名(例: Seek)から内部的な名前(例: seek)に変更されました。これは、Goのコードで同じ名前のラッパー関数を定義するためです。また、戻り値のスタック上のサイズがerrorインターフェースの2ワード(32バイト)から、uintptrの1ワード(28バイトは引数と戻り値の合計サイズ)に変更され、アセンブリが直接errno uintptrを返すことを明確にしました。

Goラッパー関数の導入(例: syscall_linux_386.goSeek):

変更前:

func Seek(fd int, offset int64, whence int) (newoffset int64, err error)

変更後:

func seek(fd int, offset int64, whence int) (newoffset int64, err Errno) // アセンブリ実装
func Seek(fd int, offset int64, whence int) (newoffset int64, err error) {
    newoffset, errno := seek(fd, offset, whence) // アセンブリ関数を呼び出す
    if errno != 0 {
        return 0, errno // Errnoをerrorインターフェースとして返す
    }
    return newoffset, nil // エラーがない場合はnilを返す
}

Goのソースファイルでは、アセンブリで実装された関数(例: seek)が、戻り値としてErrno型(これはuintptrのエイリアスであり、errorインターフェースを実装している)を返すように定義されました。そして、元の公開関数Seekは、この低レベルなseek関数を呼び出すラッパーとして機能します。

ラッパー関数Seekの内部では、アセンブリから返されたerrno値がチェックされます。

  • errno != 0の場合、システムコールが失敗したことを意味します。このとき、Errno型の値が直接errorインターフェースとして返されます。Errno型はError()メソッドを実装しているため、これはGoのエラーハンドリングの慣習に沿っています。
  • errno == 0の場合、システムコールが成功したことを意味します。このとき、nilが返され、エラーがないことを示します。

このアプローチにより、アセンブリコードはGoのインターフェースの複雑な内部構造を意識することなく、単純にerrno値を返すだけでよくなりました。インターフェースの構築とエラーハンドリングのロジックは、Goの型システムとランタイムの知識を持つGoのラッパー関数に委ねられることで、安全かつ堅牢なエラー処理が実現されました。

syscall_linux_amd64.goTime関数も同様に、アセンブリから直接Timeを呼び出すのではなく、Gettimeofdayを介して時刻情報を取得するように変更されました。これにより、Time関数も同様のエラーハンドリングの恩恵を受けることができます。

テストケースの追加(syscall_unix_test.go):

func TestSeekFailure(t *testing.T) {
    _, err := syscall.Seek(-1, 0, 0)
    if err == nil {
        t.Fatalf("Seek(-1, 0, 0) did not fail")
    }
    str := err.Error() // used to crash on Linux
    t.Logf("Seek: %v", str)
    if str == "" {
        t.Fatalf("Seek(-1, 0, 0) return error with empty message")
    }
}

このテストは、修正が正しく行われたことを検証するために非常に重要です。不正なファイルディスクリプタ-1syscall.Seekに渡すことで、意図的にシステムコールを失敗させます。

  • err == nilでないこと(エラーが正しく返されること)を確認します。
  • 最も重要なのは、err.Error()を呼び出してもクラッシュしないことを確認する点です。これは、以前のバグ(非ゼロのerrnoerr.Error()がクラッシュする)が修正されたことを直接検証します。
  • エラーメッセージが空でないことも確認し、有効なエラー情報が提供されていることを保証します。

関連リンク

  • Go Change-ID: https://golang.org/cl/99320043

参考にした情報源リンク

  • (この解説は、提供されたコミットメッセージとGo言語の一般的な知識に基づいて作成されており、追加のWeb検索は行っていません。)