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

[インデックス 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を返します。 重要な点は以下の通りです。

  • n0からlen(p)までの範囲で、len(p)より小さい値でも有効です。これは、Readが必ずしも要求されたすべてのバイトを読み込むとは限らないことを意味します。
  • Readn > 0を返した場合、たとえエラーが発生したとしても、そのエラーはnバイトのデータが読み込まれた後に発生したことを示します。
  • Read0, nilを返すことは、有効な操作であり、読み込むデータが一時的にないことを示します(例:非ブロッキングI/O)。ストリームの終端に達した場合は、通常0, io.EOFを返します。

この柔軟性は、様々なI/Oソース(ファイル、ネットワーク、メモリなど)に対応するために設計されていますが、同時に、呼び出し元がReadの戻り値を適切に処理しないと、意図しない動作につながる可能性があります。

io.ReadFull関数

io.ReadFull関数は、io.ReaderReadメソッドの挙動を補完するために提供されるユーティリティ関数です。そのシグネチャは以下の通りです。

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言語のprintfscanfに似た機能を提供し、文字列、数値、その他のGoの値を整形して出力したり、入力ストリームから値をスキャンしてGoの変数に格納したりする機能を提供します。このコミットで関連するのは、入力ストリームからのスキャン機能です。

技術的詳細

このコミットの技術的詳細の核心は、io.Reader.Readの「不完全な読み込み」の可能性と、それをio.ReadFullでどのように安全に扱うかという点にあります。

io.Reader.Readの挙動の再確認

io.ReaderReadメソッドは、そのインターフェースの設計上、以下の挙動が許容されています。

  1. 要求されたバイト数より少ないバイト数を返す: たとえバッファに十分なスペースがあっても、Readlen(p)より少ないバイト数nを返すことがあります。これは、基になるI/Oデバイスが一度にすべてのデータを供給できない場合(例:ネットワークソケットからの読み込みで、まだすべてのデータが到着していない場合)に発生します。
  2. 0, nilを返す: データが一時的に利用できないが、エラーではない場合(例:非ブロッキングI/Oでデータがまだ準備できていない場合)、Readn=0err=nilを返すことがあります。これは、呼び出し元が再度Readを試みるべきであることを示唆します。
  3. n > 0と同時にエラーを返す: 読み込み中にエラーが発生したが、それまでに一部のデータが読み込まれていた場合、Readn > 0と同時に非nilのエラーを返すことがあります。

これらの挙動は、一般的なストリーム処理においては柔軟性を提供しますが、固定長のデータを期待する場面では、開発者が明示的にループを回して必要なバイト数をすべて読み込むか、io.ReadFullのようなユーティリティを使用する必要があります。

io.ReadFullによる安全な読み込み

io.ReadFullは、この問題を解決するために設計されています。ReadFull(r, buf)は、len(buf)バイトを読み込むまでr.Readを繰り返し呼び出します。

  • もしlen(buf)バイトを読み込む前にr.Readio.EOFを返した場合、ReadFullio.ErrUnexpectedEOFを返します。
  • もしlen(buf)バイトを読み込む前にr.Readが他のエラーを返した場合、ReadFullはそのエラーをそのまま返します。
  • ReadFullが成功した場合、nは常にlen(buf)と等しくなります。

このコミットでは、encoding/gobfmtのコードが、io.Reader.Readの戻り値nを適切にチェックせずに、読み込みが完了したと仮定していた箇所が修正されました。特に、n0である可能性や、要求されたバイト数よりも少ない可能性があるにもかかわらず、その後の処理が完全な読み込みを前提としていた点が問題でした。

io.ReadFullを使用することで、これらの箇所では、必要なバイト数が確実に読み込まれるか、そうでなければエラーが返されることが保証されます。これにより、不完全なデータに基づいて処理が続行されることを防ぎ、プログラムの堅牢性が向上します。

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

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

  1. src/pkg/encoding/gob/decode.go
  2. 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.ReadFull0を返すのはエラーの場合のみ)。
  • 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の標準ライブラリの設計原則を理解する上で役立つ)