[インデックス 1128] ファイルの概要
このコミットは、Go言語の初期のosパッケージにおけるファイルディスクリプタ(FD)のReadおよびWriteメソッドが、空のバッファ([]byte)を渡された場合に正しく動作しないバグを修正するものです。具体的には、空のバッファに対してb[0]のような要素アクセスを試みると、ランタイムパニック(インデックス範囲外エラー)が発生する可能性がありました。このコミットは、syscall.readおよびsyscall.writeを呼び出す前にバッファの長さが0より大きいことを確認する条件分岐を追加することで、この問題を解決しています。
コミット
buf fix: make FD.Read, FD.Write work for empty buffers
R=r
DELTA=8 (6 added, 0 deleted, 2 changed)
OCL=19273
CL=19275
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/23c8faaf856f7ee531c118a90efba2dbbe50eda2
元コミット内容
commit 23c8faaf856f7ee531c118a90efba2dbbe50eda2
Author: Robert Griesemer <gri@golang.org>
Date: Fri Nov 14 15:13:29 2008 -0800
buf fix: make FD.Read, FD.Write work for empty buffers
R=r
DELTA=8 (6 added, 0 deleted, 2 changed)
OCL=19273
CL=19275
---
src/lib/os/os_file.go | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/src/lib/os/os_file.go b/src/lib/os/os_file.go
index ee4deef72a..2667a1e212 100644
--- a/src/lib/os/os_file.go
+++ b/src/lib/os/os_file.go
@@ -57,7 +57,10 @@ func (fd *FD) Read(b *[]byte) (ret int, err *Error) {
if fd == nil {
return -1, EINVAL
}
- r, e := syscall.read(fd.fd, &b[0], int64(len(b)));
+ var r, e int64;
+ if len(b) > 0 { // because we access b[0]
+ r, e = syscall.read(fd.fd, &b[0], int64(len(b)));
+ }
return int(r), ErrnoToError(e)
}
@@ -65,7 +68,10 @@ func (fd *FD) Write(b *[]byte) (ret int, err *Error) {
if fd == nil {
return -1, EINVAL
}
- r, e := syscall.write(fd.fd, &b[0], int64(len(b)));
+ var r, e int64;
+ if len(b) > 0 { // because we access b[0]
+ r, e = syscall.write(fd.fd, &b[0], int64(len(b)));
+ }
return int(r), ErrnoToError(e)
}
変更の背景
この変更の背景には、Go言語の初期バージョンにおけるosパッケージのFD.ReadおよびFD.Writeメソッドが、空のバイトスライス([]byte{})を引数として受け取った際に発生するランタイムエラーがありました。
Go言語では、スライスbが空である場合(len(b) == 0)、b[0]のような要素アクセスはインデックス範囲外エラー(panic: runtime error: index out of range)を引き起こします。これは、syscall.readやsyscall.writeといったシステムコールが、データの読み書きを行うバッファの開始アドレスを必要とするため、Goの内部でスライスの最初の要素のアドレス(&b[0])を渡そうとした際に問題となりました。
ファイルI/O操作において、空のバッファを渡すことは有効なシナリオです。例えば、Read操作で0バイトを読み込むことを意図したり、Write操作で何も書き込まないことを意図したりする場合です。しかし、当時の実装ではこのエッジケースが考慮されておらず、プログラムがクラッシュする原因となっていました。このコミットは、このような状況でもReadおよびWriteメソッドが安全に、かつ期待通りに(0バイトの読み書きとして)動作するようにするためのバグ修正です。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびシステムプログラミングに関する前提知識が必要です。
-
Go言語のスライス (
[]byte):- スライスはGo言語における可変長シーケンス型です。内部的には、要素へのポインタ、長さ(
len)、容量(cap)の3つの情報で構成されます。 len(b)はスライスbに含まれる要素の数を返します。b[0]のようにインデックスを使ってスライスの要素にアクセスできますが、len(b)が0の場合にb[0]にアクセスしようとすると、ランタイムパニックが発生します。&b[0]はスライスの最初の要素のアドレス(ポインタ)を取得します。これは、C言語のポインタ渡しに相当し、システムコールにバッファの開始位置を伝えるためによく使われます。
- スライスはGo言語における可変長シーケンス型です。内部的には、要素へのポインタ、長さ(
-
ファイルディスクリプタ (
FD):- ファイルディスクリプタは、オペレーティングシステムがファイルやソケットなどのI/Oリソースを識別するために使用する抽象的なハンドルです。通常は非負の整数値です。
- Goの
osパッケージ(特に初期バージョン)では、FD構造体がこのファイルディスクリプタをラップし、ReadやWriteといったI/O操作を提供していました。
-
syscallパッケージ:syscallパッケージは、Goプログラムから低レベルのオペレーティングシステムコール(システムコール)を直接呼び出すための機能を提供します。syscall.readは、指定されたファイルディスクリプタからデータを読み込むためのシステムコールをラップします。引数としてファイルディスクリプタ、バッファの開始アドレス、読み込むバイト数を取ります。syscall.writeは、指定されたファイルディスクリプタにデータを書き込むためのシステムコールをラップします。引数としてファイルディスクリプタ、バッファの開始アドレス、書き込むバイト数を取ります。- これらのシステムコールは通常、読み書きされたバイト数とエラーコードを返します。
-
エラーハンドリング (
*Error,ErrnoToError):- Go言語では、関数がエラーを返す際に多値戻り値を使用するのが一般的です。
- このコミットの時点では、Goのエラー型は現在のような
errorインターフェースではなく、*Errorのような具体的な型が使われていた可能性があります(Goの進化の過程でエラーハンドリングの慣習は変化しています)。 ErrnoToErrorは、システムコールが返す数値のエラーコード(errno)をGoのエラー型に変換するためのヘルパー関数です。
技術的詳細
このコミットが修正している問題は、Go言語のsyscallパッケージを介して低レベルのreadおよびwriteシステムコールを呼び出す際の、空のバッファの取り扱いに関するものです。
元のコードでは、FD.ReadおよびFD.Writeメソッド内で、引数として渡されたバイトスライスbの最初の要素のアドレス&b[0]を直接syscall.readまたはsyscall.writeに渡していました。
// 修正前 (Readの例)
r, e := syscall.read(fd.fd, &b[0], int64(len(b)));
ここで問題となるのは、Goのスライスの特性です。もしbが空のスライス(len(b) == 0)である場合、b[0]という式は存在しない要素へのアクセスを試みることになり、Goランタイムは「インデックス範囲外」のエラーでパニックを起こします。
システムコール自体は、読み書きするバイト数が0であれば、バッファのポインタがNULLであっても(あるいは無効なアドレスであっても)問題なく0を返すことが期待されます。しかし、Goの言語仕様上、&b[0]の評価自体がパニックを引き起こすため、システムコールが呼び出される前にプログラムが異常終了してしまいます。
このコミットは、この問題を解決するために、syscall.readおよびsyscall.writeの呼び出しをif len(b) > 0という条件文で囲むというシンプルな修正を導入しました。
// 修正後 (Readの例)
var r, e int64;
if len(b) > 0 { // because we access b[0]
r, e = syscall.read(fd.fd, &b[0], int64(len(b)));
}
// len(b) == 0 の場合、r と e はゼロ値 (0) のまま
この修正により、以下の挙動が保証されます。
len(b) > 0の場合: 以前と同様にsyscall.readまたはsyscall.writeが呼び出され、データの読み書きが行われます。len(b) == 0の場合:ifブロック内のコードは実行されません。rとeはGoのゼロ値(int64型なので0)のままとなり、結果としてFD.ReadやFD.Writeはretとして0(読み書きされたバイト数)と、errとしてnil(エラーなし)を返します(ErrnoToError(0)はnilエラーを返すため)。これは、空のバッファに対するI/O操作の期待される振る舞いです。
この修正は、Go言語の設計思想である「明示的なエラーハンドリング」と「エッジケースの安全な取り扱い」に沿ったものです。
コアとなるコードの変更箇所
--- a/src/lib/os/os_file.go
+++ b/src/lib/os/os_file.go
@@ -57,7 +57,10 @@ func (fd *FD) Read(b *[]byte) (ret int, err *Error) {
if fd == nil {
return -1, EINVAL
}
- r, e := syscall.read(fd.fd, &b[0], int64(len(b)));
+ var r, e int64;
+ if len(b) > 0 { // because we access b[0]
+ r, e = syscall.read(fd.fd, &b[0], int64(len(b)));
+ }
return int(r), ErrnoToError(e)
}
@@ -65,7 +68,10 @@ func (fd *FD) Write(b *[]byte) (ret int, err *Error) {
if fd == nil {
return -1, EINVAL
}
- r, e := syscall.write(fd.fd, &b[0], int64(len(b)));
+ var r, e int64;
+ if len(b) > 0 { // because we access b[0]
+ r, e = syscall.write(fd.fd, &b[0], int64(len(b)));
+ }
return int(r), ErrnoToError(e)
}
コアとなるコードの解説
変更はFD.ReadとFD.Writeの2つのメソッドに適用されています。どちらのメソッドも同様の修正が施されています。
修正前:
r, e := syscall.read(fd.fd, &b[0], int64(len(b))); // または syscall.write
この行では、syscall.read(またはsyscall.write)関数が直接呼び出されています。ここで問題となるのは、第2引数である&b[0]です。Go言語では、空のスライス(len(b) == 0)に対してb[0]のようなインデックスアクセスを行うと、ランタイムパニックが発生します。つまり、syscall.readが呼び出される前にプログラムがクラッシュしてしまう可能性がありました。
修正後:
var r, e int64; // rとeを事前に宣言
if len(b) > 0 { // because we access b[0]
r, e = syscall.read(fd.fd, &b[0], int64(len(b))); // または syscall.write
}
var r, e int64;: まず、rとeという2つのint64型変数が宣言され、それぞれのゼロ値(0)で初期化されます。これにより、ifブロックが実行されない場合でも、これらの変数には有効な値が保持されます。if len(b) > 0 { ... }: この条件文が追加された最も重要な部分です。len(b)が0より大きい場合、つまりバッファbに1つ以上の要素がある場合にのみ、syscall.read(またはsyscall.write)が呼び出されます。このとき、&b[0]は安全に評価され、バッファの開始アドレスがシステムコールに渡されます。len(b)が0の場合、つまりバッファbが空である場合、ifブロック内のコードはスキップされます。この場合、rとeは初期化されたゼロ値(0)のままとなります。
return int(r), ErrnoToError(e): 最後に、rとeの値が返されます。len(b) > 0の場合は、システムコールからの実際の戻り値が返されます。len(b) == 0の場合は、rは0、eも0のままなので、int(0)(読み書きされたバイト数0)とErrnoToError(0)(エラーなし、nilエラーに相当)が返されます。これは、空のバッファに対するI/O操作として期待される正しい振る舞いです。
この修正により、空のバッファが渡された場合でも、Goプログラムがパニックを起こすことなく、安全に0バイトの読み書きとして処理されるようになりました。
関連リンク
- Go言語公式ウェブサイト: https://go.dev/
- Go言語のドキュメント: https://go.dev/doc/
- Go言語の
osパッケージドキュメント (現在のバージョン): https://pkg.go.dev/os - Go言語の
syscallパッケージドキュメント (現在のバージョン): https://pkg.go.dev/syscall
参考にした情報源リンク
- Go言語の公式ドキュメント(スライス、エラーハンドリング、
syscallパッケージに関する一般的な情報) - Go言語のソースコードリポジトリ(コミット履歴と関連ファイルの確認)
- Go言語の初期の設計に関する議論やメーリングリストのアーカイブ(もし関連する具体的な議論が見つかれば)
(注:このコミットは2008年の非常に初期のGo言語のものであるため、当時の正確なドキュメントや議論を特定することは困難ですが、Go言語の基本的な概念は現在にも通じるものです。)