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

[インデックス 13721] ファイルの概要

このコミットは、Go言語の標準ライブラリである net/rpc/jsonrpc パッケージにおいて、JSON-RPCリクエストに params フィールドが存在しない場合に発生するクラッシュを修正するものです。具体的には、接続後の最初のリクエストで params フィールドが欠落していると、c.req.Paramsnil であるためにパニックが発生する問題(Issue #3848)に対処しています。

コミット

commit 3efc482190c9c2fa80cb0fc80d160624514652db
Author: Alexandru Moșoi <brtzsnr@gmail.com>
Date:   Fri Aug 31 15:52:27 2012 -0400

    net/rpc/jsonrpc: handles missing "params" in jsonrpc.
    
    A crash happens in the first request in a connection
    if "params" field is missing because c.req.Params is Nil.
    
    Fixes #3848.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/6446051

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/3efc482190c9c2fa80cb0fc80d160624514652db

元コミット内容

net/rpc/jsonrpc: handles missing "params" in jsonrpc.

A crash happens in the first request in a connection if "params" field is missing because c.req.Params is Nil.

Fixes #3848.

変更の背景

Go言語の net/rpc/jsonrpc パッケージは、JSON形式でRPC(Remote Procedure Call)を実装するための機能を提供します。JSON-RPCプロトコルでは、クライアントからサーバーへのリクエストに methodparamsid などのフィールドが含まれます。params フィールドは、呼び出すメソッドに渡す引数を指定するもので、JSON配列またはJSONオブジェクトの形式を取ります。

このコミットが修正する問題は、クライアントが params フィールドを省略したJSON-RPCリクエストを送信した場合に発生していました。Goの net/rpc/jsonrpc サーバーの実装において、受信したリクエストの params フィールドが nil であることを適切に処理していなかったため、c.req.Params を参照しようとした際に nil ポインタデリファレンスが発生し、サーバーがクラッシュするというバグが存在していました(Issue #3848)。これは、特に接続後の最初のリクエストで顕著に発生する可能性がありました。

この修正は、サーバーの堅牢性を高め、不正な形式のリクエスト(params フィールドの欠落)に対しても安定して動作するようにすることを目的としています。

前提知識の解説

JSON-RPC

JSON-RPCは、JSON形式でデータをやり取りするリモートプロシージャコールプロトコルです。基本的なリクエストの構造は以下のようになります。

{
  "jsonrpc": "2.0",
  "method": "method_name",
  "params": [param1, param2], // または {"key": "value"}
  "id": 1 // リクエストID
}
  • jsonrpc: プロトコルのバージョン(通常 "2.0")。
  • method: 呼び出すリモートメソッドの名前。
  • params: メソッドに渡す引数。これはJSON配列またはJSONオブジェクトのいずれかです。JSON-RPC 2.0の仕様では、params フィールドはオプションとされています。
  • id: リクエストとレスポンスを関連付けるための識別子。

Goの net/rpc パッケージ

Goの標準ライブラリ net/rpc は、RPCクライアントとサーバーを構築するためのフレームワークを提供します。これは、ネットワーク越しにGoの構造体メソッドを呼び出すことを可能にします。net/rpc/jsonrpc は、この net/rpc の上に構築され、JSON形式でのデータエンコーディング/デコーディングを扱います。

nil ポインタとパニック

Go言語では、ポインタが何も指していない状態を nil と表現します。nil ポインタをデリファレンス(つまり、nil ポインタが指す値にアクセスしようとすること)すると、ランタイムパニックが発生し、プログラムが異常終了します。このコミットのバグは、まさにこの nil ポインタデリファレンスによって引き起こされていました。

net.Pipe()

net.Pipe() は、Goの net パッケージで提供される関数で、インメモリの双方向コネクション(パイプ)を作成します。これは、ネットワークI/Oを伴うコード(この場合はRPCサーバーとクライアント間の通信)を、実際のネットワーク接続なしでテストする際に非常に便利です。net.Pipe() は2つの net.Conn インターフェースを実装するオブジェクトを返します。一方はクライアント側、もう一方はサーバー側として使用できます。

技術的詳細

このコミットの技術的詳細は、主に net/rpc/jsonrpc パッケージ内の server.goall_test.go の変更に集約されます。

server.go の変更

  1. エラー変数の追加: var errMissingParams = errors.New("jsonrpc: request body missing params") params フィールドが欠落している場合に返す特定のエラーが定義されました。これにより、エラーの種類を明確に識別できるようになります。

  2. serverRequest.reset() メソッドの変更: serverRequest 構造体は、受信したJSON-RPCリクエストの情報を保持します。reset() メソッドは、リクエスト処理後にこの構造体を再利用するために初期化する役割を担います。 変更前は、r.Paramsr.Idnil でない場合にスライス操作 ([0:0]) を行っていました。しかし、もしこれらが既に nil であった場合、nil スライスに対する操作はパニックを引き起こす可能性があります。 変更後は、単純に r.Params = nilr.Id = nil を代入することで、安全かつ確実にこれらのフィールドを初期化するように修正されました。これにより、nil ポインタデリファレンスのリスクが排除されます。

  3. ReadRequestBody メソッドでの params フィールドのチェック: ReadRequestBody メソッドは、受信したリクエストボディを解析し、RPCメソッドの引数として使用するデータを準備します。 このメソッドの冒頭に以下のチェックが追加されました。

    if c.req.Params == nil {
        return errMissingParams
    }
    

    これにより、c.req.Paramsnil である(つまり、JSON-RPCリクエストに params フィールドがなかった)場合、後続の処理で nil ポインタデリファレンスが発生する前に、errMissingParams エラーを返して早期に処理を終了するようになりました。これは、問題の根本原因に対する直接的な修正です。

all_test.go の変更

テストファイルには、新しいテストケースと既存テストの改善が加えられました。

  1. ArithAddResp 構造体の追加:

    type ArithAddResp struct {
    	Id     interface{} `json:"id"`
    	Result Reply       `json:"result"`
    	Error  interface{} `json:"error"`
    }
    

    これは、JSON-RPCレスポンスをアンマーシャルするための新しい構造体です。以前は addResp という匿名構造体が使われていましたが、新しいテストケースで再利用できるように名前付き構造体として定義されました。

  2. TestServerNoParams テストケースの追加: この新しいテストは、params フィールドが欠落したJSON-RPCリクエストをサーバーに送信するシナリオをシミュレートします。

    fmt.Fprintf(cli, `{"method": "Arith.Add", "id": "123"}`)
    

    このリクエストには params が含まれていません。テストは、サーバーがエラーを返すことを期待し、resp.Error == nil でないことを確認します。これにより、params 欠落時のエラーハンドリングが正しく機能するかを検証します。

  3. TestServerEmptyMessage テストケースの追加: このテストは、空のJSONメッセージ {} をサーバーに送信するシナリオをシミュレートします。

    fmt.Fprintf(cli, "{}")
    

    空のメッセージも不正なリクエストであるため、サーバーがエラーを返すことを期待します。これにより、サーバーが予期せぬ入力に対しても堅牢であることを確認します。

  4. TestServer の変更: 既存の TestServer 関数内で、以前は匿名構造体 addResp を使用していましたが、新しく定義された ArithAddResp に置き換えられました。また、以前 TestServer の最後にあった空メッセージのテストロジックは、新しく追加された TestServerEmptyMessage に移動され、より明確なテスト構造になりました。

これらの変更により、net/rpc/jsonrpc サーバーは params フィールドが欠落したリクエストや空のメッセージに対して適切にエラーを返し、クラッシュを防ぐことができるようになりました。

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

src/pkg/net/rpc/jsonrpc/server.go

--- a/src/pkg/net/rpc/jsonrpc/server.go
+++ b/src/pkg/net/rpc/jsonrpc/server.go
@@ -12,6 +12,8 @@ import (
 	"sync"
 )
 
+var errMissingParams = errors.New("jsonrpc: request body missing params")
+
 type serverCodec struct {
 	dec *json.Decoder // for reading JSON values
 	enc *json.Encoder // for writing JSON values
@@ -50,12 +52,8 @@ type serverRequest struct {
 
 func (r *serverRequest) reset() {
 	r.Method = ""
-	if r.Params != nil {
-		*r.Params = (*r.Params)[0:0]
-	}
-	if r.Id != nil {
-		*r.Id = (*r.Id)[0:0]
-	}
+	r.Params = nil
+	r.Id = nil
 }
 
 type serverResponse struct {
@@ -88,6 +86,9 @@ func (c *serverCodec) ReadRequestBody(x interface{}) error {
 	if x == nil {
 		return nil
 	}
+	if c.req.Params == nil {
+		return errMissingParams
+	}
 	// JSON params is array value.
 	// RPC params is struct.
 	// Unmarshal into array containing struct for now.

src/pkg/net/rpc/jsonrpc/all_test.go

--- a/src/pkg/net/rpc/jsonrpc/all_test.go
+++ b/src/pkg/net/rpc/jsonrpc/all_test.go
@@ -24,6 +24,12 @@ type Reply struct {
 
 type Arith int
 
+type ArithAddResp struct {
+	Id     interface{} `json:"id"`
+	Result Reply       `json:"result"`
+	Error  interface{} `json:"error"`
+}
+
 func (t *Arith) Add(args *Args, reply *Reply) error {
 	reply.C = args.A + args.B
 	return nil
@@ -50,13 +56,39 @@ func init() {
 	rpc.Register(new(Arith))
 }
 
-func TestServer(t *testing.T) {
-	type addResp struct {
-		Id     interface{} `json:"id"`
-		Result Reply       `json:"result"`
-		Error  interface{} `json:"error"`
+func TestServerNoParams(t *testing.T) {
+	cli, srv := net.Pipe()
+	defer cli.Close()
+	go ServeConn(srv)
+	dec := json.NewDecoder(cli)
+
+	fmt.Fprintf(cli, `{"method": "Arith.Add", "id": "123"}`)\n+\tvar resp ArithAddResp
+	if err := dec.Decode(&resp); err != nil {
+		t.Fatalf("Decode after no params: %s", err)
 	}
+	if resp.Error == nil {
+		t.Fatalf("Expected error, got nil")
+	}
+}
+
+func TestServerEmptyMessage(t *testing.T) {
+	cli, srv := net.Pipe()
+	defer cli.Close()
+	go ServeConn(srv)
+	dec := json.NewDecoder(cli)
+
+	fmt.Fprintf(cli, "{}")
+	var resp ArithAddResp
+	if err := dec.Decode(&resp); err != nil {
+		t.Fatalf("Decode after empty: %s", err)
+	}
+	if resp.Error == nil {
+		t.Fatalf("Expected error, got nil")
+	}
+}
+
+func TestServer(t *testing.T) {
 
 	cli, srv := net.Pipe()
 	defer cli.Close()
@@ -65,7 +97,7 @@ func TestServer(t *testing.T) {
 	// Send hand-coded requests to server, parse responses.
 	for i := 0; i < 10; i++ {
 		fmt.Fprintf(cli, `{"method": "Arith.Add", "id": "\u%04d", "params": [{"A": %d, "B": %d}]}`, i, i, i+1)
-		var resp addResp
+		var resp ArithAddResp
 		err := dec.Decode(&resp)
 		if err != nil {
 			t.Fatalf("Decode: %s", err)
@@ -80,15 +112,6 @@ func TestServer(t *testing.T) {
 		if resp.Result.C != i+(i+1) {
 			t.Fatalf("resp: bad result: %d+%d=%d", i, i+1, resp.Result.C)
 		}
-	}
-
-	fmt.Fprintf(cli, "{}\\n")
-	var resp addResp
-	if err := dec.Decode(&resp); err != nil {
-		t.Fatalf("Decode after empty: %s", err)
-	}
-	if resp.Error == nil {
-		t.Fatalf("Expected error, got nil")
 	}
 }
 

コアとなるコードの解説

このコミットの核心は、server.goReadRequestBody メソッドにおける nil チェックと、serverRequest.reset() メソッドの簡素化です。

ReadRequestBody メソッドは、JSON-RPCリクエストの params 部分をGoの構造体にデコードする役割を担います。以前の実装では、params フィールドがJSONリクエストに存在しない場合、c.req.Paramsnil のままになり、その後の処理でこの nil ポインタをデリファレンスしようとするとパニックが発生していました。

修正後のコードでは、ReadRequestBody の冒頭で if c.req.Params == nil というチェックが追加されました。これにより、params が欠落しているリクエストを検出した場合、すぐに errMissingParams を返して処理を中断します。これにより、nil ポインタデリファレンスによるクラッシュを未然に防ぎ、クライアントに対して適切なエラーレスポンスを返すことができるようになります。

また、serverRequest.reset() メソッドの変更は、serverRequest 構造体の再利用時の安全性を高めます。以前は r.Paramsr.Idnil でない場合にスライスを再初期化しようとしていましたが、これが nil ポインタに対して実行されるとパニックの原因となる可能性がありました。新しい実装では、これらのフィールドを直接 nil に設定することで、常に安全に初期化が行われるようになります。

テストファイル all_test.go の変更は、これらの修正が正しく機能することを保証するためのものです。特に TestServerNoParamsTestServerEmptyMessage は、params フィールドが欠落しているケースや、リクエストが空であるケースといった、以前クラッシュを引き起こしていた具体的なシナリオを網羅しています。これらのテストが追加されたことで、将来的な回帰を防ぎ、サーバーの堅牢性を維持するのに役立ちます。

関連リンク

参考にした情報源リンク