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

[インデックス 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サーバーがクライアントからのリクエストを処理する際に、以下のような状況で発生していました。

  1. 不正な入力形式: クライアントがRPCプロトコルに準拠しない、例えば不正なJSON形式のデータを送信した場合。
  2. 予期せぬ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.Readerio.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が追加されました。

  • keepReadingtrueの場合、エラーが発生したとしても、サーバーは次のリクエストの読み込みを試みるべきであることを示します。これは、例えば、リクエストヘッダーは正常に読み込めたが、リクエストボディが不正であった場合などです。この場合、現在のリクエストは失敗しますが、接続自体は有効であり、次のリクエストを処理できる可能性があります。
  • keepReadingfalseの場合、エラーが致命的であり、これ以上リクエストを読み込むべきではないことを示します。これは、os.EOFや、接続が切断されたことを示すような基盤となるI/Oエラーなどです。この場合、サーバーはループを終了し、接続を閉じます。

この変更により、RPCサーバーはエラーの種類に応じて適切にループを終了するか、または次のリクエストの処理を試みるかを判断できるようになり、無限ループが回避されます。

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

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

  1. src/pkg/rpc/jsonrpc/all_test.go:
    • TestMalformedInputTestUnexpectedErrorという新しいテストケースが追加されました。これらは、不正なJSON入力と予期せぬI/OエラーがRPCサーバーの無限ループを引き起こすことを再現し、修正が正しく機能することを確認します。
    • net.Pipeを模倣したmyPipeヘルパー関数と関連するpipe構造体が追加され、テスト環境でI/Oエラーをシミュレートできるようにしています。
  2. src/pkg/rpc/server.go:
    • ServeCodec関数とServeRequest関数で、server.readRequestの戻り値にkeepReadingが追加され、エラー処理ロジックが変更されました。以前はerr == os.EOF || err == io.ErrUnexpectedEOFでループを終了していましたが、!keepReadingで終了するように変更されました。
    • readRequest関数とreadRequestHeader関数のシグネチャが変更され、keepReading boolが戻り値として追加されました。
    • readRequestHeader関数内で、リクエストヘッダーが正常に読み込まれた後にkeepReading = trueが設定されるようになりました。これにより、ヘッダー読み込み後のエラーは、次のリクエストの処理を妨げない「回復可能」なエラーとして扱われます。
  3. src/pkg/rpc/server_test.go:
    • CodecEmulatorWriteResponseメソッドのロジックが修正され、エラーがない場合にのみ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.
		...
	}
	...
}

ServeCodecServeRequestは、RPCリクエストを継続的に処理するサーバーのメインループです。以前は、readRequestから返されたエラーがos.EOFまたはio.ErrUnexpectedEOFの場合にのみループを終了していました。しかし、これ以外のエラー(例えば、不正なJSON形式など)が発生した場合、ループは継続し、同じ不正な入力を繰り返し読み込もうとして無限ループに陥る可能性がありました。

修正後は、readRequestが返す新しいkeepReadingブール値が導入されました。keepReadingfalseの場合(つまり、致命的なエラーが発生し、これ以上読み込みを続けるべきではない場合)にのみループを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のままであり、readRequestServeCodecはループを終了します。

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は、myPipenet.Pipeのカスタム実装)を使用して、PipeWriterCloseWithErrorで閉じ、読み取り側に予期せぬエラーを発生させます。このテストは、基盤となるI/Oストリームでos.EOFio.ErrUnexpectedEOF以外のエラーが発生した場合に、サーバーが無限ループに陥らずに終了することを確認します。

これらのテストは、修正がRPCサーバーの堅牢性を向上させ、様々なエラー条件下で安定して動作することを示しています。

関連リンク

参考にした情報源リンク