[インデックス 14674] ファイルの概要
このコミットは、Go言語の標準ライブラリであるfmt
パッケージとencoding/gob
パッケージにおけるio.Reader
の誤用を修正するものです。具体的には、io.Reader.Read
メソッドが0, nil
(読み込むデータがないがエラーではない)やlen(buf), err
(一部のデータは読み込めたがエラーが発生した)のような予期せぬ挙動を示す可能性があるため、より安全なio.ReadFull
を使用するように変更されています。これにより、データの読み込みが不完全になることや、それに起因するバグを防ぐことが目的です。
コミット
commit 5a2c275be125d935440ddad3042bfc7bb2ce5027
Author: Shenghou Ma <minux.ma@gmail.com>
Date: Tue Dec 18 01:26:48 2012 +0800
fmt, encoding/gob: fix misuse of Read
reader.Read() can return both 0,nil and len(buf),err.
To be safe, we use io.ReadFull instead of doing reader.Read directly.
Fixes #3472.
R=bradfitz, rsc, ality
CC=golang-dev
https://golang.org/cl/6285050
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/5a2c275be125d935440ddad3042bfc7bb2ce5027
元コミット内容
fmt, encoding/gob: fix misuse of Read
reader.Read() can return both 0,nil and len(buf),err.
To be safe, we use io.ReadFull instead of doing reader.Read directly.
Fixes #3472.
変更の背景
この変更は、Go言語のIssue #3472("io: ReadFull should be used more often")に対応するものです。io.Reader
インターフェースのRead
メソッドは、呼び出し元が要求したバイト数よりも少ないバイト数を読み込むことが許容されています。また、データが読み込めなかった場合でも、エラーを返さずに0, nil
を返すことがあります。これは、ストリームの終端に達した場合や、非ブロッキングI/Oで一時的にデータが利用できない場合などに発生します。
しかし、特定の状況下では、呼び出し元が指定したバイト数を確実に読み込みたい場合があります。例えば、プロトコルのヘッダーや固定長のデータを読み込む場合などです。io.Reader.Read
のこの挙動は、開発者が意図しない不完全な読み込みを引き起こし、後続の処理でパースエラーやデータ破損などのバグにつながる可能性があります。
このコミットでは、encoding/gob
パッケージ(Goのバイナリシリアライゼーションフォーマット)とfmt
パッケージ(フォーマットI/O)において、このような不完全な読み込みが問題となる箇所が特定されました。特に、gob
パッケージではメッセージの長さを読み込む際に、fmt
パッケージではバイトを読み込む際に、指定されたバイト数が確実に読み込まれることが重要でした。
この問題を解決するため、Go標準ライブラリが提供するio.ReadFull
関数を使用するようにコードが修正されました。io.ReadFull
は、指定されたバイト数を正確に読み込むことを保証し、読み込みが不完全な場合はエラーを返します。これにより、これらのパッケージの堅牢性と信頼性が向上しました。
前提知識の解説
io.Reader
インターフェース
Go言語におけるio.Reader
インターフェースは、データを読み込むための最も基本的な抽象化です。その定義は以下の通りです。
type Reader interface {
Read(p []byte) (n int, err error)
}
Read
メソッドは、p
に最大len(p)
バイトのデータを読み込み、読み込んだバイト数n
とエラーerr
を返します。
重要な点は以下の通りです。
n
は0
からlen(p)
までの範囲で、len(p)
より小さい値でも有効です。これは、Read
が必ずしも要求されたすべてのバイトを読み込むとは限らないことを意味します。Read
がn > 0
を返した場合、たとえエラーが発生したとしても、そのエラーはn
バイトのデータが読み込まれた後に発生したことを示します。Read
が0, nil
を返すことは、有効な操作であり、読み込むデータが一時的にないことを示します(例:非ブロッキングI/O)。ストリームの終端に達した場合は、通常0, io.EOF
を返します。
この柔軟性は、様々なI/Oソース(ファイル、ネットワーク、メモリなど)に対応するために設計されていますが、同時に、呼び出し元がRead
の戻り値を適切に処理しないと、意図しない動作につながる可能性があります。
io.ReadFull
関数
io.ReadFull
関数は、io.Reader
のRead
メソッドの挙動を補完するために提供されるユーティリティ関数です。そのシグネチャは以下の通りです。
func ReadFull(r Reader, buf []byte) (n int, err error)
ReadFull
は、r
からlen(buf)
バイトを正確に読み込み、buf
に格納します。
ReadFull
は、len(buf)
バイトを読み込むまでr.Read
を繰り返し呼び出します。len(buf)
バイトを読み込む前にエラーが発生した場合、またはストリームの終端に達した場合、ReadFull
はエラーを返します。特に、ストリームの終端に達して要求されたバイト数を読み込めなかった場合は、io.ErrUnexpectedEOF
を返します。ReadFull
は、要求されたバイト数を読み込めなかった場合、n < len(buf)
となることはありません。常にn == len(buf)
またはエラーを返します。
この関数は、固定長のデータを確実に読み込みたい場合に非常に有用です。
encoding/gob
パッケージ
encoding/gob
パッケージは、Goのデータ構造をバイナリ形式でエンコード(シリアライズ)およびデコード(デシリアライズ)するためのGo固有の形式を提供します。gob
は、自己記述的なストリームであり、データ型情報も含まれるため、受信側は送信側がどのような型を送信したかを知らなくてもデータをデコードできます。これは、RPC(Remote Procedure Call)や永続化など、Goプログラム間でデータを交換する際に特に便利です。
fmt
パッケージ
fmt
パッケージは、Go言語におけるフォーマットI/Oを実装します。C言語のprintf
やscanf
に似た機能を提供し、文字列、数値、その他のGoの値を整形して出力したり、入力ストリームから値をスキャンしてGoの変数に格納したりする機能を提供します。このコミットで関連するのは、入力ストリームからのスキャン機能です。
技術的詳細
このコミットの技術的詳細の核心は、io.Reader.Read
の「不完全な読み込み」の可能性と、それをio.ReadFull
でどのように安全に扱うかという点にあります。
io.Reader.Read
の挙動の再確認
io.Reader
のRead
メソッドは、そのインターフェースの設計上、以下の挙動が許容されています。
- 要求されたバイト数より少ないバイト数を返す: たとえバッファに十分なスペースがあっても、
Read
はlen(p)
より少ないバイト数n
を返すことがあります。これは、基になるI/Oデバイスが一度にすべてのデータを供給できない場合(例:ネットワークソケットからの読み込みで、まだすべてのデータが到着していない場合)に発生します。 0, nil
を返す: データが一時的に利用できないが、エラーではない場合(例:非ブロッキングI/Oでデータがまだ準備できていない場合)、Read
はn=0
とerr=nil
を返すことがあります。これは、呼び出し元が再度Read
を試みるべきであることを示唆します。n > 0
と同時にエラーを返す: 読み込み中にエラーが発生したが、それまでに一部のデータが読み込まれていた場合、Read
はn > 0
と同時に非nil
のエラーを返すことがあります。
これらの挙動は、一般的なストリーム処理においては柔軟性を提供しますが、固定長のデータを期待する場面では、開発者が明示的にループを回して必要なバイト数をすべて読み込むか、io.ReadFull
のようなユーティリティを使用する必要があります。
io.ReadFull
による安全な読み込み
io.ReadFull
は、この問題を解決するために設計されています。ReadFull(r, buf)
は、len(buf)
バイトを読み込むまでr.Read
を繰り返し呼び出します。
- もし
len(buf)
バイトを読み込む前にr.Read
がio.EOF
を返した場合、ReadFull
はio.ErrUnexpectedEOF
を返します。 - もし
len(buf)
バイトを読み込む前にr.Read
が他のエラーを返した場合、ReadFull
はそのエラーをそのまま返します。 ReadFull
が成功した場合、n
は常にlen(buf)
と等しくなります。
このコミットでは、encoding/gob
とfmt
のコードが、io.Reader.Read
の戻り値n
を適切にチェックせずに、読み込みが完了したと仮定していた箇所が修正されました。特に、n
が0
である可能性や、要求されたバイト数よりも少ない可能性があるにもかかわらず、その後の処理が完全な読み込みを前提としていた点が問題でした。
io.ReadFull
を使用することで、これらの箇所では、必要なバイト数が確実に読み込まれるか、そうでなければエラーが返されることが保証されます。これにより、不完全なデータに基づいて処理が続行されることを防ぎ、プログラムの堅牢性が向上します。
コアとなるコードの変更箇所
このコミットでは、以下の2つのファイルが変更されています。
src/pkg/encoding/gob/decode.go
src/pkg/fmt/scan.go
src/pkg/encoding/gob/decode.go
の変更
--- a/src/pkg/encoding/gob/decode.go
+++ b/src/pkg/encoding/gob/decode.go
@@ -62,15 +62,15 @@ func overflow(name string) error {
// Used only by the Decoder to read the message length.
func decodeUintReader(r io.Reader, buf []byte) (x uint64, width int, err error) {
width = 1
- _, err = r.Read(buf[0:width])
- if err != nil {
+ n, err := io.ReadFull(r, buf[0:width])
+ if n == 0 {
return
}
b := buf[0]
if b <= 0x7f {
return uint64(b), width, nil
}
- n := -int(int8(b))
+ n = -int(int8(b))
if n > uint64Size {
err = errBadUint
return
変更点:
- 元のコードでは
r.Read(buf[0:width])
を直接呼び出し、エラーチェックのみを行っていました。 - 変更後、
io.ReadFull(r, buf[0:width])
を使用するように変更されました。 io.ReadFull
は要求されたバイト数を読み込めなかった場合にエラーを返すため、if err != nil
のチェックは不要になり、代わりにif n == 0
のチェックが追加されました。ただし、io.ReadFull
が成功した場合、n
は常に要求されたバイト数(ここではwidth
、つまり1
)と等しくなるため、n == 0
のチェックは実質的にerr != nil
と同じ意味を持ちます(io.ReadFull
が0
を返すのはエラーの場合のみ)。n := -int(int8(b))
の行で、以前のRead
呼び出しで宣言されたn
がシャドーイングされていましたが、io.ReadFull
の戻り値n
と区別するために、このn
は新しいn
として再宣言されています。
src/pkg/fmt/scan.go
の変更
--- a/src/pkg/fmt/scan.go
+++ b/src/pkg/fmt/scan.go
@@ -337,7 +337,10 @@ func (r *readRune) readByte() (b byte, err error) {
\tr.pending--
\treturn
}\n-\t_, err = r.reader.Read(r.pendBuf[0:1])
+\tn, err := io.ReadFull(r.reader, r.pendBuf[0:1])
+\tif n != 1 {
+\t\treturn 0, err
+\t}\n \treturn r.pendBuf[0], err
}\n \n```
**変更点:**
* 元のコードでは `r.reader.Read(r.pendBuf[0:1])` を直接呼び出し、戻り値のバイト数`n`を無視していました。
* 変更後、`io.ReadFull(r.reader, r.pendBuf[0:1])` を使用するように変更されました。
* `io.ReadFull`は要求されたバイト数(ここでは1バイト)を読み込むことを保証するため、`if n != 1` のチェックが追加されました。これにより、1バイトが確実に読み込まれたことを確認し、そうでない場合はエラーを返します。
## コアとなるコードの解説
### `src/pkg/encoding/gob/decode.go` の変更解説
`decodeUintReader`関数は、`gob`ストリームから符号なし整数(`uint64`)の長さを読み込むために使用されます。`gob`では、可変長エンコーディングを使用して数値を表現するため、最初の1バイトを読み込んで、それが続くバイト数を示すプレフィックスであるか、それとも単一のバイトで表現できる値であるかを判断します。
元のコードでは、`r.Read(buf[0:width])`(ここで`width`は`1`)を呼び出して最初のバイトを読み込んでいました。しかし、`io.Reader.Read`は`0, nil`を返す可能性があるため、もし`Read`が`0`バイトを読み込んだ場合、`buf[0]`は未初期化のままになり、その後の`b := buf[0]`の処理が不正なデータに基づいて行われる可能性がありました。また、`Read`がエラーを返した場合のみ`return`していましたが、`0, nil`の場合は処理が続行されてしまう問題がありました。
修正後のコードでは、`n, err := io.ReadFull(r, buf[0:width])`を使用しています。`io.ReadFull`は、要求された1バイトを確実に読み込むか、エラーを返します。したがって、`n`が`0`になるのはエラーが発生した場合のみです。`if n == 0`のチェックは、`io.ReadFull`がエラーを返した(つまり、1バイトを読み込めなかった)場合に早期リターンするためのものです。これにより、`buf[0]`が常に有効なデータを含むことが保証され、`gob`のデコード処理の堅牢性が向上します。
### `src/pkg/fmt/scan.go` の変更解説
`readRune`構造体の`readByte`メソッドは、`fmt`パッケージのスキャン処理において、基になるリーダーから1バイトを読み込むために使用されます。これは、文字(ルーン)をデコードする前段階で、バイト単位の読み込みが必要な場合に呼び出されます。
元のコードでは、`_, err = r.reader.Read(r.pendBuf[0:1])`を呼び出して1バイトを読み込んでいました。ここでも、`io.Reader.Read`が`0`バイトを読み込む可能性や、エラーが発生しても`n`がチェックされない問題がありました。もし`Read`が`0`バイトを読み込んだ場合、`r.pendBuf[0]`は更新されず、古いデータや未初期化のデータが返される可能性がありました。
修正後のコードでは、`n, err := io.ReadFull(r.reader, r.pendBuf[0:1])`を使用しています。これにより、`r.pendBuf[0]`に確実に1バイトが読み込まれるか、エラーが返されることが保証されます。`if n != 1`のチェックは、`io.ReadFull`が1バイトを読み込めなかった場合に早期リターンするためのものです。これにより、`readByte`が常に有効なバイトを返すか、エラーを返すことが保証され、`fmt`パッケージのスキャン処理の信頼性が向上します。
## 関連リンク
* Go Issue #3472: [io: ReadFull should be used more often](https://github.com/golang/go/issues/3472)
* Go CL 6285050: [https://golang.org/cl/6285050](https://golang.org/cl/6285050)
## 参考にした情報源リンク
* Go Documentation: `io` package: [https://pkg.go.dev/io](https://pkg.go.dev/io)
* Go Documentation: `encoding/gob` package: [https://pkg.go.dev/encoding/gob](https://pkg.go.dev/encoding/gob)
* Go Documentation: `fmt` package: [https://pkg.go.dev/fmt](https://pkg.go.dev/fmt)
* "Go Concurrency Patterns: Pipelines and Cancellation" - Rob Pike, Sameer Ajmani (Goにおける`io.Reader`の挙動に関する一般的な議論): [https://blog.golang.org/pipelines](https://blog.golang.org/pipelines) (直接的な参照ではないが、`io.Reader`の設計思想を理解する上で役立つ)
* "Effective Go" - The Go Authors (Goの慣用的な書き方に関する一般的なガイドライン): [https://go.dev/doc/effective_go](https://go.dev/doc/effective_go) (直接的な参照ではないが、Goの標準ライブラリの設計原則を理解する上で役立つ)