[インデックス 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言語の基本的な概念は現在にも通じるものです。)