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

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

このコミットは、Go言語の標準ライブラリである encoding/json パッケージにおける、空の文字列を整数型にアンマーシャルしようとした際に発生するパニック(panic)を修正するものです。具体的には、構造体のフィールドに json:",string" タグが指定されている場合、JSONデータ内でそのフィールドに対応する値が空文字列 ("") であると、アンマーシャル処理中にパニックが発生するという問題に対処しています。

コミット

commit 3fab2a97e4ae677e74a4569e924ddd0d56cf4a78
Author: Michael Chaten <mchaten@gmail.com>
Date:   Thu May 3 17:35:44 2012 -0400

    encoding/json: Fix panic when trying to unmarshal the empty string into an integer
    
    Fixes #3450.
    
    R=rsc, bradfitz
    CC=golang-dev
    https://golang.org/cl/6035050

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

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

元コミット内容

encoding/json: Fix panic when trying to unmarshal the empty string into an integer

Fixes #3450.

R=rsc, bradfitz
CC=golang-dev
https://golang.org/cl/6035050

変更の背景

このコミットの背景には、Goの encoding/json パッケージがJSONデータをGoの構造体にデコード(アンマーシャル)する際の特定の挙動に起因するバグがありました。 Goの encoding/json パッケージでは、構造体のフィールドタグに json:",string" を指定することで、JSONの文字列値をGoの数値型(整数や浮動小数点数)に変換してデコードすることができます。これは、JSONデータが数値として表現されるべき値を文字列として含んでいる場合に便利です。例えば、{"id": "123"} のようなJSONを struct { ID int json:"id,string" } にデコードする際に利用されます。

しかし、この機能を使用している際に、JSONデータ内の対応する値が空文字列 ("") であった場合、encoding/json パッケージの内部処理でパニックが発生するという問題が報告されました(Fixes #3450 で示される問題)。具体的には、空のバイトスライスに対して要素アクセスを行おうとした際に、インデックスが範囲外であるためにランタイムパニックが発生していました。このパニックは、アプリケーションのクラッシュを引き起こす可能性があり、堅牢なJSONデコード処理を妨げるものでした。

このコミットは、この特定のシナリオにおけるパニックを回避し、より適切なエラーハンドリングを行うことで、encoding/json パッケージの安定性と信頼性を向上させることを目的としています。

前提知識の解説

Go言語の encoding/json パッケージ

encoding/json パッケージは、Go言語でJSONデータとGoのデータ構造(構造体、マップ、スライスなど)の間で変換を行うための標準ライブラリです。

  • マーシャリング (Marshaling): Goのデータ構造をJSONデータに変換するプロセスです。json.Marshal 関数が使用されます。
  • アンマーシャリング (Unmarshaling): JSONデータをGoのデータ構造に変換するプロセスです。json.Unmarshal 関数や json.NewDecoder が使用されます。

構造体タグ (json:"...")

Goの構造体フィールドには「タグ」と呼ばれるメタデータを付与できます。encoding/json パッケージは、このタグを利用してJSONとGoのデータ構造のマッピングを制御します。

  • フィールド名のマッピング: json:"fieldName" のように指定することで、Goのフィールド名と異なるJSONのキー名を指定できます。
  • オプション: カンマ区切りで追加のオプションを指定できます。
    • json:"-,omitempty": フィールドがゼロ値の場合、JSON出力から省略されます。
    • json:",string": このオプションが今回のコミットの核心です。JSONの文字列値をGoの数値型(int, float64 など)やブール型に変換してデコードするよう指示します。例えば、JSONの "123" をGoの int(123) に、"true"bool(true) に変換します。これは、JSONが数値やブール値を文字列として扱う場合に特に有用です。

パニック (Panic) とエラーハンドリング

Go言語では、予期せぬエラーや回復不可能な状況が発生した場合に「パニック」が発生します。パニックは通常、プログラムの実行を停止させます。一方、エラーは error インターフェースを介して明示的に返され、プログラムが回復可能な状況で利用されます。

今回の問題は、空のバイトスライスに対してインデックスアクセスを試みるという、Goのランタイムが検出する「インデックス範囲外」のパニックでした。このコミットは、パニックを発生させる代わりに、error を返すように修正しています。

技術的詳細

このコミットの技術的詳細は、encoding/json パッケージの内部、特にJSONリテラル(数値、ブール値、null、文字列)のデコード処理に焦点を当てています。

encoding/json パッケージの内部では、JSONの値をGoの型にデコードする際に、decodeState という構造体がデコードの状態を管理します。decodeStateliteralStore メソッドは、JSONの文字列、数値、ブール値、nullなどのリテラル値をGoの reflect.Value に格納する役割を担っています。

json:",string" タグが指定された場合、encoding/json はJSONの文字列値を読み取り、それをGoの対応する数値型に変換しようとします。この変換プロセスにおいて、JSONの文字列値が空文字列 ("") であった場合、literalStore メソッドに渡される item 引数(デコード対象のバイトスライス)は空になります。

元のコードでは、literalStore メソッドの冒頭で item[0] のように item スライスの最初の要素にアクセスしようとしていました。しかし、item が空のバイトスライスである場合、item[0] はインデックス範囲外となり、Goランタイムはパニックを発生させます。

このコミットは、この脆弱性を修正するために、literalStore メソッドの冒頭に以下のチェックを追加しました。

if len(item) == 0 {
    //Empty string given
    d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type()))
    return
}

このコードは、item スライスの長さが0である(つまり、空文字列が与えられた)場合に、パニックを回避し、代わりに decodeStatesaveError メソッドを通じて適切なエラーを記録します。エラーメッセージは、「json:,string 構造体タグの不正な使用、%q%v にアンマーシャルしようとしています」という内容で、デバッグに役立つ情報を提供します。そして、return することで、それ以降のパニックを引き起こす可能性のある処理をスキップします。

この修正により、空文字列が json:",string" タグ付きの数値フィールドにデコードされようとした際に、パニックではなく、明確なエラーが返されるようになり、アプリケーションはより堅牢にエラーをハンドリングできるようになります。

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

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

  1. src/pkg/encoding/json/decode.go: パニックを修正するための主要なロジックが追加されました。
  2. src/pkg/encoding/json/decode_test.go: 修正が正しく機能することを確認するための新しいテストケースが追加されました。

src/pkg/encoding/json/decode.go の変更点

--- a/src/pkg/encoding/json/decode.go
+++ b/src/pkg/encoding/json/decode.go
@@ -593,6 +593,11 @@ func (d *decodeState) literal(v reflect.Value) {
 // produce more helpful error messages.
 func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool) {
 	// Check for unmarshaler.
+	if len(item) == 0 {
+		//Empty string given
+		d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type()))
+		return
+	}
 	wantptr := item[0] == 'n' // null
 	unmarshaler, pv := d.indirect(v, wantptr)
 	if unmarshaler != nil {

src/pkg/encoding/json/decode_test.go の変更点

--- a/src/pkg/encoding/json/decode_test.go
+++ b/src/pkg/encoding/json/decode_test.go
@@ -646,3 +646,22 @@ func TestAnonymous(t *testing.T) {
 	\tt.Fatal("Unmarshal: did set T.Y")
 	}
 }
+
+// Test that the empty string doesn't panic decoding when ,string is specified
+// Issue 3450
+func TestEmptyString(t *testing.T) {
+	type T2 struct {
+		Number1 int `json:",string"`
+		Number2 int `json:",string"`
+	}
+	data := `{"Number1":"1", "Number2":""}`
+	dec := NewDecoder(strings.NewReader(data))
+	var t2 T2
+	err := dec.Decode(&t2)
+	if err == nil {
+		t.Fatal("Decode: did not return error")
+	}
+	if t2.Number1 != 1 {
+		t.Fatal("Decode: did not set Number1")
+	}
+}

コアとなるコードの解説

src/pkg/encoding/json/decode.go の変更解説

decodeState 構造体の literalStore メソッドは、JSONのプリミティブ値(文字列、数値、ブール値、null)をGoの reflect.Value に格納する汎用的な関数です。この関数は、JSONパーサーによって抽出されたリテラル値のバイトスライス item を受け取ります。

追加されたコードブロックは以下の通りです。

	if len(item) == 0 {
		//Empty string given
		d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type()))
		return
	}
  • if len(item) == 0: これは、item バイトスライスが空であるかどうかをチェックします。json:",string" タグが指定されたフィールドに対してJSONの空文字列 ("") が与えられた場合、item は空になります。
  • d.saveError(...): item が空の場合、decodeStatesaveError メソッドを呼び出してエラーを記録します。fmt.Errorf を使用して、具体的なエラーメッセージを生成しています。
    • "json: invalid use of ,string struct tag, trying to unmarshal %q into %v": このエラーメッセージは、json:",string" タグが不適切に使用されたこと、具体的には空文字列を %v で示されるGoの型(この場合は整数型)にアンマーシャルしようとしたことを示しています。%qitem の内容(空文字列)を引用符付きで表示し、%vv.Type()(対象のGoの型)を表示します。
  • return: エラーを記録した後、関数から即座にリターンします。これにより、item[0] へのアクセスなど、空のバイトスライスに対してパニックを引き起こす可能性のある後続の処理が実行されるのを防ぎます。

この修正により、json:",string" タグ付きの数値フィールドに空文字列が与えられた場合、パニックではなく、明確なエラーが返されるようになり、開発者はこの問題を適切に処理できるようになります。

src/pkg/encoding/json/decode_test.go の変更解説

追加された TestEmptyString 関数は、この修正の動作を検証するためのテストケースです。

func TestEmptyString(t *testing.T) {
	type T2 struct {
		Number1 int `json:",string"`
		Number2 int `json:",string"`
	}
	data := `{"Number1":"1", "Number2":""}`
	dec := NewDecoder(strings.NewReader(data))
	var t2 T2
	err := dec.Decode(&t2)
	if err == nil {
		t.Fatal("Decode: did not return error")
	}
	if t2.Number1 != 1 {
		t.Fatal("Decode: did not set Number1")
	}
}
  • type T2 struct { ... }: json:",string" タグが適用された2つの整数フィールド Number1Number2 を持つ構造体 T2 を定義しています。
  • data := {"Number1":"1", "Number2":""}``: テスト用のJSONデータです。Number1 は有効な文字列数値 "1" を持ち、Number2 は問題のトリガーとなる空文字列 "" を持っています。
  • dec := NewDecoder(strings.NewReader(data)): JSONデータを読み込むための json.Decoder を作成します。
  • var t2 T2: デコード結果を格納するための T2 型の変数を宣言します。
  • err := dec.Decode(&t2): JSONデータを t2 にデコードしようとします。
  • if err == nil { t.Fatal("Decode: did not return error") }: 修正が適用されていれば、空文字列のデコードはエラーを返すはずなので、エラーが返されない場合はテストを失敗させます。
  • if t2.Number1 != 1 { t.Fatal("Decode: did not set Number1") }: Number1 が正しくデコードされていることを確認します。これは、空文字列の処理が他の有効なフィールドのデコードに影響を与えないことを保証するためです。

このテストケースは、空文字列が json:",string" タグ付きの数値フィールドにデコードされようとした際に、パニックではなくエラーが返されることを効果的に検証しています。

関連リンク

参考にした情報源リンク

  • コミットハッシュ: 3fab2a97e4ae677e74a4569e924ddd0d56cf4a78
  • Goのコードレビューシステム (Gerrit) の変更リスト: https://golang.org/cl/6035050
  • GitHub上のコミットページ: https://github.com/golang/go/commit/3fab2a97e4ae677e74a4569e924ddd0d56cf4a78
  • (注:コミットメッセージに記載されている Fixes #3450 のIssueは、現在のGitHubリポジトリでは直接見つかりませんでしたが、当時のGoのIssueトラッカーで報告された問題に対応するものです。)