[インデックス 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のアセンブリ言語で直接実装されていました。
このコミットで修正された問題は、アセンブリで実装されたSeek
、Time
、Gettimeofday
といったシステムコールが、Goの関数プロトタイプではerror
インターフェースを返すと宣言されていたにもかかわらず、実際にはアセンブリコード内で直接error
インターフェースを正しく構築していなかったことに起因します。
Goのerror
インターフェースは、内部的には「型情報」と「値」の2つのポインタ(またはワード)で構成されます。アセンブリコードは、システムコールが成功した場合(エラー番号errno
が0の場合)には、偶然にもインターフェースの型ワードに0を書き込むことで、Goランタイムがこれをnil
エラーとして正しく解釈していました。しかし、システムコールが失敗し、errno
が非ゼロの値を返した場合、アセンブリコードはerror
インターフェースの型ワードを不正な値で埋めていました。この状態で、Goコードが返されたエラーに対してerr.Error()
メソッドを呼び出そうとすると、不正な型情報に基づいてメソッドディスパッチが行われ、結果としてプログラムがクラッシュするという深刻なバグがありました。
幸いなことに、Seek
、Time
、Gettimeofday
といったシステムコールは、通常はほとんど失敗しない性質を持っていたため、このバグは長期間にわたって顕在化しませんでした。しかし、Goの静的解析ツールであるgo vet
がこの潜在的な問題を検出し、修正の必要性が明らかになりました。
前提知識の解説
Goのerror
インターフェース
Go言語におけるエラーハンドリングは、組み込みのerror
インターフェースを中心に行われます。error
インターフェースは、以下のように定義されています。
type error interface {
Error() string
}
このインターフェースは、Error()
という文字列を返すメソッドを1つだけ持ちます。関数がエラーを返す可能性がある場合、通常は最後の戻り値としてerror
型を宣言します。エラーがない場合はnil
を返します。
Goのインターフェースは、内部的には「型情報」と「値」の2つの要素で構成されるデータ構造です。
- 型情報 (Type Word): インターフェースが保持している具体的な値の型(例:
*os.PathError
、syscall.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のインターフェースの内部表現と、アセンブリ言語からそのインターフェースを正しく構築することの難しさにありました。
アセンブリで実装されたSeek
、Time
、Gettimeofday
関数は、Goのプロトタイプではerror
インターフェースを返すと宣言されていました。しかし、アセンブリコードはGoのインターフェースの内部構造(型情報と値のペア)を直接操作してerror
インターフェースを生成するのではなく、システムコールから返されるerrno
(エラー番号)をuintptr
として直接返していました。
-
成功時の挙動(
errno
が0の場合): システムコールが成功し、errno
が0の場合、アセンブリコードは戻り値として0を返していました。Goランタイムは、この0をerror
インターフェースの「型情報」と「値」の両方のワードに偶然にも0として解釈していました。結果として、Goの観点からはnil
エラーとして認識され、問題なく動作していました。これは、nil
インターフェースが内部的に両方のワードが0であるという事実と一致していたため、たまたま正しく機能していたに過ぎません。 -
失敗時の挙動(
errno
が非ゼロの場合): システムコールが失敗し、errno
が非ゼロの値(例:EFAULT
、EINVAL
など)を返した場合、アセンブリコードはその非ゼロのerrno
値を戻り値として返していました。Goランタイムは、この非ゼロの値をerror
インターフェースの「型情報」ワードとして解釈してしまいました。しかし、この非ゼロの値は有効な型情報を指すポインタではありません。そのため、Goコードが返されたエラーに対してerr.Error()
メソッドを呼び出そうとすると、ランタイムは不正な型情報に基づいてメソッドテーブルを検索しようとし、結果としてセグメンテーション違反などのクラッシュを引き起こしていました。
この問題が長らく見過ごされてきたのは、Seek
、Time
、Gettimeofday
といったシステムコールが、通常の使用シナリオではほとんどエラーを返さないという性質があったためです。エラーパスが頻繁に実行されないため、バグが表面化しにくかったのです。
go vet
は、Goの関数プロトタイプがerror
インターフェースを返すと宣言しているにもかかわらず、対応するアセンブリ実装がインターフェースを正しく構築していない可能性を静的に検出しました。これにより、この潜在的なクラッシュバグが特定され、修正されることになりました。
修正のアプローチは、アセンブリ関数が直接error
インターフェースを返すのではなく、GoのErrno
型(uintptr
に相当)を返すように変更し、その上でGoのコードでラッパー関数を導入するというものです。このラッパー関数がアセンブリ関数を呼び出し、返されたErrno
値をチェックして、必要に応じてerror
インターフェースに変換して返す役割を担います。これにより、Goの型システムとアセンブリの実装との間の整合性が保たれ、安全なエラーハンドリングが実現されます。
コアとなるコードの変更箇所
このコミットでは、主に以下のファイルが変更されています。
-
src/pkg/syscall/asm_linux_386.s
-
src/pkg/syscall/asm_linux_amd64.s
-
src/pkg/syscall/asm_linux_arm.s
- これらのアセンブリファイルでは、
Seek
、Gettimeofday
、Time
(amd64のみ)に対応するアセンブリ関数の名前が変更され(例:TEXT ·Seek(SB)
からTEXT ·seek(SB)
)、Goのプロトタイプでerror
を返すとされていた部分が、実際にはerrno uintptr
を返すように変更されました。具体的には、戻り値のスタック上のオフセットが調整され、error
インターフェースの2ワードではなく、uintptr
の1ワード分の領域が確保されるようになりました。
- これらのアセンブリファイルでは、
-
src/pkg/syscall/syscall_linux_386.go
-
src/pkg/syscall/syscall_linux_amd64.go
-
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
を呼び出す部分が削除されました。
- これらのGoソースファイルでは、アセンブリで実装された低レベルな関数(例:
-
src/pkg/syscall/syscall_unix_test.go
TestSeekFailure
という新しいテストケースが追加されました。このテストは、syscall.Seek
に不正な引数(ファイルディスクリプタ-1
)を渡して意図的にエラーを発生させ、返されたエラーがnil
でないこと、そしてError()
メソッドを呼び出してもクラッシュしないことを検証します。これは、修正が正しく機能していることを確認するための重要なテストです。
コアとなるコードの解説
このコミットの主要な変更は、アセンブリで実装されたシステムコールラッパーと、それらを呼び出すGoのラッパー関数の間のエラーハンドリングの整合性を確保することです。
アセンブリコードの変更(例: asm_linux_386.s
のSeek
):
変更前:
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.go
のSeek
):
変更前:
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.go
のTime
関数も同様に、アセンブリから直接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")
}
}
このテストは、修正が正しく行われたことを検証するために非常に重要です。不正なファイルディスクリプタ-1
をsyscall.Seek
に渡すことで、意図的にシステムコールを失敗させます。
err == nil
でないこと(エラーが正しく返されること)を確認します。- 最も重要なのは、
err.Error()
を呼び出してもクラッシュしないことを確認する点です。これは、以前のバグ(非ゼロのerrno
でerr.Error()
がクラッシュする)が修正されたことを直接検証します。 - エラーメッセージが空でないことも確認し、有効なエラー情報が提供されていることを保証します。
関連リンク
- Go Change-ID:
https://golang.org/cl/99320043
参考にした情報源リンク
- (この解説は、提供されたコミットメッセージとGo言語の一般的な知識に基づいて作成されており、追加のWeb検索は行っていません。)