[インデックス 10165] ファイルの概要
このコミットは、Go言語の標準ライブラリであるnet/rpc
パッケージにおいて、RPCサーバーが不正な入力や予期せぬエラーに遭遇した際に発生する無限ループのバグを修正するものです。具体的には、jsonrpc
プロトコルを使用する際に、クライアントからの入力が不正なJSON形式であったり、基盤となるI/Oストリームで予期せぬエラーが発生した場合に、サーバーが応答不能になる問題を解決します。
コミット
commit 2e79e8e54920c005af29447a85d7b241460c34cb
Author: Russ Cox <rsc@golang.org>
Date: Tue Nov 1 00:29:41 2011 -0400
rpc: avoid infinite loop on input error
Fixes #1828.
Fixes #2179.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/5305084
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2e79e8e54920c005af29447a85d7b241460c34cb
元コミット内容
rpc: avoid infinite loop on input error
Fixes #1828.
Fixes #2179.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/5305084
変更の背景
このコミットは、GoのRPCサーバーが特定の入力エラー条件下で無限ループに陥るという重要なバグを修正するために導入されました。具体的には、Go Issue #1828とGo Issue #2179で報告された問題に対処しています。
これらの問題は、RPCサーバーがクライアントからのリクエストを処理する際に、以下のような状況で発生していました。
- 不正な入力形式: クライアントがRPCプロトコルに準拠しない、例えば不正なJSON形式のデータを送信した場合。
- 予期せぬI/Oエラー: 基盤となるネットワーク接続やパイプで、
os.EOF
(ファイルの終端)やio.ErrUnexpectedEOF
(予期せぬファイルの終端)以外のエラーが発生した場合。
以前の実装では、これらのエラーが発生した際に、サーバーがエラーを適切に処理しきれず、リクエストの読み込みループから抜け出せなくなることがありました。これにより、サーバーはCPUリソースを消費し続け、新しいリクエストを受け付けられなくなり、最終的にはサービス停止につながる可能性がありました。特に、悪意のあるクライアントやバグのあるクライアントからの不正な入力によって、容易にサービス拒否(DoS)攻撃を引き起こす脆弱性となり得ました。
この修正は、RPCサーバーの堅牢性を高め、不正な入力や予期せぬエラーに対しても安定して動作するようにすることを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念と標準ライブラリの知識が必要です。
net/rpc
パッケージ: Go言語でRPC(Remote Procedure Call)を実装するためのパッケージです。クライアントとサーバー間でメソッド呼び出しを可能にし、ネットワーク越しに手続きを呼び出すことができます。io.Reader
,io.Writer
,io.ReadWriteCloser
インターフェース:io.Reader
: データを読み込むためのインターフェースで、Read(p []byte) (n int, err error)
メソッドを持ちます。io.Writer
: データを書き込むためのインターフェースで、Write(p []byte) (n int, err error)
メソッドを持ちます。io.ReadWriteCloser
:io.Reader
とio.Writer
の両方の機能に加え、Close() error
メソッドを持つインターフェースです。ネットワーク接続やファイルなど、読み書きが可能で閉じることができるリソースを表します。
jsonrpc
: JSON-RPCは、JSON形式を使用してリモートプロシージャコールを行うためのプロトコルです。net/rpc/jsonrpc
パッケージは、net/rpc
パッケージでJSON-RPCプロトコルを使用するための実装を提供します。- エラーハンドリング (
os.EOF
,io.ErrUnexpectedEOF
):os.EOF
:io.Reader
がデータの終端に達したことを示すエラーです。これは通常、正常な終了条件として扱われます。io.ErrUnexpectedEOF
: 予期せぬデータの終端、つまり、期待されるデータがすべて読み込まれる前にストリームが終了した場合に発生するエラーです。これは通常、不正な入力や破損したデータを示します。- Goのエラーハンドリングは、エラーを戻り値として返すことで行われます。呼び出し元はエラーをチェックし、適切に処理する必要があります。
net.Pipe()
:net
パッケージで提供される関数で、メモリ内で接続されたio.ReadWriteCloser
ペアを作成します。これは、ネットワーク接続をシミュレートして、I/O操作をテストする際に非常に便利です。一方のパイプに書き込まれたデータは、もう一方のパイプから読み込むことができます。reflect.Value
:reflect
パッケージは、Goの実行時のリフレクション機能を提供します。reflect.Value
は、Goの任意の型の値を表すことができます。RPCでは、メソッドの引数や戻り値を動的に扱うためにリフレクションが使用されます。
技術的詳細
この修正の核心は、RPCサーバーがリクエストを読み込む際のエラー処理ロジックの変更にあります。以前の実装では、ServeCodec
およびServeRequest
メソッド内のリクエスト読み込みループが、os.EOF
またはio.ErrUnexpectedEOF
以外のエラーに遭遇した場合に、ループを継続してしまう可能性がありました。これは、これらのエラーが「接続の終了」を意味するものではないと解釈され、サーバーが次のリクエストを読み込もうとし続けるためです。しかし、不正な入力やその他のI/Oエラーは、実際には現在のリクエストの処理を続行できないことを意味し、ループを終了させるべきでした。
この問題を解決するために、readRequest
およびreadRequestHeader
関数に新しい戻り値keepReading bool
が追加されました。
keepReading
がtrue
の場合、エラーが発生したとしても、サーバーは次のリクエストの読み込みを試みるべきであることを示します。これは、例えば、リクエストヘッダーは正常に読み込めたが、リクエストボディが不正であった場合などです。この場合、現在のリクエストは失敗しますが、接続自体は有効であり、次のリクエストを処理できる可能性があります。keepReading
がfalse
の場合、エラーが致命的であり、これ以上リクエストを読み込むべきではないことを示します。これは、os.EOF
や、接続が切断されたことを示すような基盤となるI/Oエラーなどです。この場合、サーバーはループを終了し、接続を閉じます。
この変更により、RPCサーバーはエラーの種類に応じて適切にループを終了するか、または次のリクエストの処理を試みるかを判断できるようになり、無限ループが回避されます。
コアとなるコードの変更箇所
このコミットでは、主に以下の3つのファイルが変更されています。
src/pkg/rpc/jsonrpc/all_test.go
:TestMalformedInput
とTestUnexpectedError
という新しいテストケースが追加されました。これらは、不正なJSON入力と予期せぬI/OエラーがRPCサーバーの無限ループを引き起こすことを再現し、修正が正しく機能することを確認します。net.Pipe
を模倣したmyPipe
ヘルパー関数と関連するpipe
構造体が追加され、テスト環境でI/Oエラーをシミュレートできるようにしています。
src/pkg/rpc/server.go
:ServeCodec
関数とServeRequest
関数で、server.readRequest
の戻り値にkeepReading
が追加され、エラー処理ロジックが変更されました。以前はerr == os.EOF || err == io.ErrUnexpectedEOF
でループを終了していましたが、!keepReading
で終了するように変更されました。readRequest
関数とreadRequestHeader
関数のシグネチャが変更され、keepReading bool
が戻り値として追加されました。readRequestHeader
関数内で、リクエストヘッダーが正常に読み込まれた後にkeepReading = true
が設定されるようになりました。これにより、ヘッダー読み込み後のエラーは、次のリクエストの処理を妨げない「回復可能」なエラーとして扱われます。
src/pkg/rpc/server_test.go
:CodecEmulator
のWriteResponse
メソッドのロジックが修正され、エラーがない場合にのみcodec.reply
が更新されるようになりました。これは、テストの正確性を向上させるための小さな修正です。
コアとなるコードの解説
src/pkg/rpc/server.go
の変更
ServeCodec
関数と ServeRequest
関数の変更点
// 変更前 (ServeCodec):
// for {
// service, mtype, req, argv, replyv, err := server.readRequest(codec)
// if err != nil {
// if err != os.EOF {
// log.Println("rpc:", err)
// }
// if err == os.EOF || err == io.ErrUnexpectedEOF {
// break
// }
// // send a response if we actually managed to read a header.
// ...
// }
// ...
// }
// 変更後 (ServeCodec):
func (server *Server) ServeCodec(codec ServerCodec) {
sending := new(sync.Mutex)
for {
service, mtype, req, argv, replyv, keepReading, err := server.readRequest(codec) // keepReadingが追加
if err != nil {
if err != os.EOF {
log.Println("rpc:", err)
}
if !keepReading { // 条件が変更
break
}
// send a response if we actually managed to read a header.
...
}
...
}
}
// ServeRequestも同様に変更
// 変更前:
// if err != nil {
// if err == os.EOF || err == io.ErrUnexpectedEOF {
// return err
// }
// ...
// }
// 変更後:
func (server *Server) ServeRequest(codec ServerCodec) os.Error {
sending := new(sync.Mutex)
service, mtype, req, argv, replyv, keepReading, err := server.readRequest(codec) // keepReadingが追加
if err != nil {
if !keepReading { // 条件が変更
return err
}
// send a response if we actually managed to read a header.
...
}
...
}
ServeCodec
とServeRequest
は、RPCリクエストを継続的に処理するサーバーのメインループです。以前は、readRequest
から返されたエラーがos.EOF
またはio.ErrUnexpectedEOF
の場合にのみループを終了していました。しかし、これ以外のエラー(例えば、不正なJSON形式など)が発生した場合、ループは継続し、同じ不正な入力を繰り返し読み込もうとして無限ループに陥る可能性がありました。
修正後は、readRequest
が返す新しいkeepReading
ブール値が導入されました。keepReading
がfalse
の場合(つまり、致命的なエラーが発生し、これ以上読み込みを続けるべきではない場合)にのみループをbreak
またはreturn
するようになりました。これにより、不正な入力による無限ループが回避されます。
readRequest
関数と readRequestHeader
関数の変更点
// 変更前 (readRequest):
// func (server *Server) readRequest(codec ServerCodec) (service *service, mtype *methodType, req *Request, argv, replyv reflect.Value, err os.Error) {
// service, mtype, req, err = server.readRequestHeader(codec)
// if err != nil {
// if err == os.EOF || err == io.ErrUnexpectedEOF {
// return
// }
// // discard body
// ...
// }
// ...
// }
// 変更後 (readRequest):
func (server *Server) readRequest(codec ServerCodec) (service *service, mtype *methodType, req *Request, argv, replyv reflect.Value, keepReading bool, err os.Error) { // keepReadingが追加
service, mtype, req, keepReading, err = server.readRequestHeader(codec) // keepReadingを受け取る
if err != nil {
if !keepReading { // 条件が変更
return
}
// discard body
...
}
...
}
// 変更前 (readRequestHeader):
// func (server *Server) readRequestHeader(codec ServerCodec) (service *service, mtype *methodType, req *Request, err os.Error) {
// // Grab the request header.
// req = server.getRequest()
// err = codec.ReadRequestHeader(req)
// if err != nil {
// server.freeRequest(req)
// return
// }
// ...
// }
// 変更後 (readRequestHeader):
func (server *Server) readRequestHeader(codec ServerCodec) (service *service, mtype *methodType, req *Request, keepReading bool, err os.Error) { // keepReadingが追加
// Grab the request header.
req = server.getRequest()
err = codec.ReadRequestHeader(req)
if err != nil {
server.freeRequest(req)
return
}
// We read the header successfully. If we see an error now,
// we can still recover and move on to the next request.
keepReading = true // ヘッダーが正常に読み込まれたらtrueに設定
serviceMethod := strings.Split(req.ServiceMethod, ".")
if len(serviceMethod) != 2 {
err = os.NewError("rpc: service/method request ill-formed: " + req.ServiceMethod)
return
}
...
}
readRequest
はリクエストヘッダーとボディを読み込み、readRequestHeader
はリクエストヘッダーのみを読み込みます。
readRequestHeader
では、リクエストヘッダーの読み込みが成功した場合にkeepReading = true
が設定されます。これは、ヘッダーが正しく解析できた場合、その後のボディの読み込みでエラーが発生しても、接続自体は有効であり、次のリクエストを処理できる可能性があることを示します。
もしReadRequestHeader
自体がエラーを返した場合(例えば、接続が切断された場合など)、keepReading
はデフォルト値のfalse
のままであり、readRequest
やServeCodec
はループを終了します。
src/pkg/rpc/jsonrpc/all_test.go
の変更
import (
"fmt"
"io" // 追加
"json"
"net"
"os"
"rpc"
"sync"
"testing"
"time"
)
// ... 既存のテストコード ...
func TestMalformedInput(t *testing.T) {
cli, srv := net.Pipe()
go cli.Write([]byte(`{id:1}`)) // invalid json
ServeConn(srv) // must return, not loop
}
func TestUnexpectedError(t *testing.T) {
cli, srv := myPipe()
go cli.PipeWriter.CloseWithError(os.NewError("unexpected error!")) // reader will get this error
ServeConn(srv) // must return, not loop
}
// Copied from package net.
func myPipe() (*pipe, *pipe) {
r1, w1 := io.Pipe()
r2, w2 := io.Pipe()
return &pipe{r1, w2}, &pipe{r2, w1}
}
type pipe struct {
*io.PipeReader
*io.PipeWriter
}
type pipeAddr int
func (pipeAddr) Network() string {
return "pipe"
}
func (pipeAddr) String() string {
return "pipe"
}
func (p *pipe) Close() os.Error {
err := p.PipeReader.Close()
err1 := p.PipeWriter.Close()
if err == nil {
err = err1
}
return err
}
func (p *pipe) LocalAddr() net.Addr {
return pipeAddr(0)
}
func (p *pipe) RemoteAddr() net.Addr {
return pipeAddr(0)
}
func (p *pipe) SetTimeout(nsec int64) os.Error {
return os.NewError("net.Pipe does not support timeouts")
}
func (p *pipe) SetReadTimeout(nsec int64) os.Error {
return os.NewError("net.Pipe does not support timeouts")
}
func (p *pipe) SetWriteTimeout(nsec int64) os.Error {
return os.NewError("net.Pipe does not support timeouts")
}
TestMalformedInput
は、不正なJSON文字列{id:1}
をnet.Pipe
を通じてサーバーに送信します。このテストの目的は、サーバーがこの不正な入力を受け取った際に無限ループに陥らず、適切に処理を終了することを確認することです。
TestUnexpectedError
は、myPipe
(net.Pipe
のカスタム実装)を使用して、PipeWriter
をCloseWithError
で閉じ、読み取り側に予期せぬエラーを発生させます。このテストは、基盤となるI/Oストリームでos.EOF
やio.ErrUnexpectedEOF
以外のエラーが発生した場合に、サーバーが無限ループに陥らずに終了することを確認します。
これらのテストは、修正がRPCサーバーの堅牢性を向上させ、様々なエラー条件下で安定して動作することを示しています。
関連リンク
- Go Issue #1828: https://github.com/golang/go/issues/1828 (Web検索結果によると、このIssueは直接見つからず、関連するIssue #2317が言及されています。)
- Go Issue #2179: https://github.com/golang/go/issues/2179 (Web検索結果によると、このIssueは「Infinite loop in RPC server」として報告され、後に重複としてマークされています。)
- Go CL 5305084: https://golang.org/cl/5305084
参考にした情報源リンク
- Web search results for "Go issue 1828 rpc infinite loop":
- Web search results for "Go issue 2179 rpc infinite loop":