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

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

このコミットは、Go言語の標準ライブラリ encoding/json パッケージにおける、nil ではないインターフェース値へのJSONアンマーシャリング時に発生するパニック(panic)を修正するものです。具体的には、json.Unmarshal が、既に値が設定されているインターフェース型変数(特にポインタ型をラップしている場合)に null をアンマーシャリングしようとした際に、不正なメモリアクセスを引き起こす可能性があったバグに対処しています。この修正により、nil ではないインターフェース値への安全なアンマーシャリングが保証され、堅牢性が向上しました。

コミット

commit 09b736a2ab56ee520e3f5909c09c8417fe61db26
Author: Russ Cox <rsc@golang.org>
Date:   Thu Jun 7 01:48:55 2012 -0400

    encoding/json: fix panic unmarshaling into non-nil interface value
    
    Fixes #3614.
    
    R=golang-dev, adg
    CC=golang-dev
    https://golang.org/cl/6306051

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

https://github.com/golang/go/commit/09b736a2ab56ee520e3f5909c09c8417fe61db26

元コミット内容

commit 09b736a2ab56ee520e3f5909c09c8417fe61db26
Author: Russ Cox <rsc@golang.org>
Date:   Thu Jun 7 01:48:55 2012 -0400

    encoding/json: fix panic unmarshaling into non-nil interface value
    
    Fixes #3614.
    
    R=golang-dev, adg
    CC=golang-dev
    https://golang.org/cl/6306051
---
 src/pkg/encoding/json/decode.go      |  9 +++++--
 src/pkg/encoding/json/decode_test.go | 46 ++++++++++++++++++++++++++++++++++++\
 2 files changed, 53 insertions(+), 2 deletions(-)

diff --git a/src/pkg/encoding/json/decode.go b/src/pkg/encoding/json/decode.go
index 0018e534cc..44dc5784be 100644
--- a/src/pkg/encoding/json/decode.go
+++ b/src/pkg/encoding/json/decode.go
@@ -273,9 +273,14 @@ func (d *decodeState) indirect(v reflect.Value, decodingNull bool) (Unmarshaler,\
 			_, isUnmarshaler = v.Interface().(Unmarshaler)\
 		}\
 
+\t\t// Load value from interface, but only if the result will be\
+\t\t// usefully addressable.\
 \t\tif iv := v; iv.Kind() == reflect.Interface && !iv.IsNil() {\
-\t\t\tv = iv.Elem()\
-\t\t\tcontinue\
+\t\t\te := iv.Elem()\
+\t\t\tif e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) {\
+\t\t\t\tv = e\
+\t\t\t\tcontinue\
+\t\t\t}\
 \t\t}\
 
 \t\tpv := v
diff --git a/src/pkg/encoding/json/decode_test.go b/src/pkg/encoding/json/decode_test.go
index c7dce53f29..5a85e3f751 100644
--- a/src/pkg/encoding/json/decode_test.go
+++ b/src/pkg/encoding/json/decode_test.go
@@ -683,3 +683,49 @@ func TestEmptyString(t *testing.T) {
 		t.Fatal("Decode: did not set Number1")
 	}\
 }\
+\n+func intp(x int) *int {\
+\tp := new(int)\
+\t*p = x\
+\treturn p\
+}\
+\n+func intpp(x *int) **int {\
+\tpp := new(*int)\
+\t*pp = x\
+\treturn pp\
+}\
+\n+var interfaceSetTests = []struct {\
+\tpre  interface{}\
+\tjson string\
+\tpost interface{}\
+}{\
+\t{\"foo\", `\"bar\"`, \"bar\"},\
+\t{\"foo\", `2`, 2.0},\
+\t{\"foo\", `true`, true},\
+\t{\"foo\", `null`, nil},\
+\n+\t{nil, `null`, nil},\
+\t{new(int), `null`, nil},\
+\t{(*int)(nil), `null`, nil},\
+\t{new(*int), `null`, new(*int)},\
+\t{(**int)(nil), `null`, nil},\
+\t{intp(1), `null`, nil},\
+\t{intpp(nil), `null`, intpp(nil)},\
+\t{intpp(intp(1)), `null`, intpp(nil)},\
+}\
+\n+func TestInterfaceSet(t *testing.T) {\
+\tfor _, tt := range interfaceSetTests {\
+\t\tb := struct{ X interface{} }{tt.pre}\
+\t\tblob := `{\"X\":` + tt.json + `}`\
+\t\tif err := Unmarshal([]byte(blob), &b); err != nil {\
+\t\t\tt.Errorf(\"Unmarshal %#q: %v\", blob, err)\
+\t\t\tcontinue\
+\t\t}\
+\t\tif !reflect.DeepEqual(b.X, tt.post) {\
+\t\t\tt.Errorf(\"Unmarshal %#q into %#v: X=%#v, want %#v\", blob, tt.pre, b.X, tt.post)\
+\t\t}\
+\t}\
+}\

変更の背景

このコミットは、Go言語のIssue #3614「json.Unmarshal panics when unmarshaling into non-nil interface value」を修正するために行われました。

このバグは、encoding/json パッケージの Unmarshal 関数が、既に値が設定されている(nil ではない)インターフェース変数に対してJSONの null 値をデコードしようとした際に発生しました。特に、インターフェースがポインタ型(例: *int**int)をラップしている場合に問題が顕在化しました。

従来の Unmarshal の動作では、インターフェースが nil でない場合、そのインターフェースが保持している具体的な値(Elem() で取得できる)に対して直接デコードを試みていました。しかし、JSONの null はGoの nil に対応するため、ポインタ型の値に null をデコードしようとすると、そのポインタ自体を nil に設定する必要があります。

問題は、reflect.Value.Set(reflect.Zero(v.Type())) のような操作が、アドレス可能でない(CanSet()false の)reflect.Value に対して行われた場合にパニックを引き起こす点にありました。インターフェースがラップしているポインタが、さらに別のポインタを指しているような多重ポインタの場合、Elem() を辿った先の値がアドレス可能でないことがあり、そこに nil を設定しようとするとパニックが発生していました。

この修正は、このような特定のシナリオ、特に null をデコードする際に、インターフェースがラップしている値が「有用にアドレス可能」であるかどうかをより厳密にチェックすることで、パニックを回避することを目的としています。

前提知識の解説

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

encoding/json パッケージは、Goのデータ構造とJSONデータの間で変換を行うための標準ライブラリです。

  • json.Marshal: Goのデータ構造をJSONバイト列にエンコードします。
  • json.Unmarshal: JSONバイト列をGoのデータ構造にデコードします。

Go言語の interface{}

interface{} はGoにおける空のインターフェース型で、あらゆる型の値を保持できます。Goのインターフェースは、内部的に「型」と「値」のペアとして表現されます。

  • nil インターフェース: 型も値も nil の状態。
  • nil ではないインターフェース: 型は存在するが、値が nil の状態(例: var i interface{} = (*int)(nil))。この状態が今回のバグの核心でした。

Go言語の reflect パッケージ

reflect パッケージは、実行時にGoの型情報や値情報を検査・操作するための機能を提供します。

  • reflect.ValueOf(i interface{}) reflect.Value: インターフェース値 ireflect.Value 表現を返します。
  • reflect.Value.Kind() reflect.Kind: reflect.Value が表す値の具体的な種類(例: reflect.Int, reflect.Ptr, reflect.Interface など)を返します。
  • reflect.Value.Elem() reflect.Value: ポインタ、インターフェース、またはスライスが指す要素の reflect.Value を返します。ポインタの場合、そのポインタが指す先の値の reflect.Value を返します。インターフェースの場合、そのインターフェースが保持している具体的な値の reflect.Value を返します。
  • reflect.Value.IsNil() bool: reflect.Valuenil であるかどうかをチェックします。ポインタ、インターフェース、マップ、スライス、チャネル、関数に対して有効です。
  • reflect.Value.CanSet() bool: reflect.Value が変更可能(アドレス可能)であるかどうかをチェックします。CanSet()true でないと、Set() メソッドなどで値を変更することはできません。

JSONの null とGoの nil

JSONの null は、Goの nil 値にデコードされます。これは、ポインタ、スライス、マップ、インターフェース、チャネル、関数などの参照型に適用されます。

技術的詳細

このバグは、encoding/json のデコード処理の中核を担う decodeState.indirect メソッドに存在していました。このメソッドは、reflect.Value を受け取り、それがポインタやインターフェースである場合に、その「実体」を辿ってデコード可能な reflect.Value を取得する役割を担っています。

問題のコードは以下の部分でした。

		if iv := v; iv.Kind() == reflect.Interface && !iv.IsNil() {
			v = iv.Elem()
			continue
		}

このコードは、vnil ではないインターフェースである場合、そのインターフェースが保持している具体的な値(iv.Elem())を v に再代入し、ループを続行していました。

しかし、iv.Elem() が返す reflect.Value は、必ずしもアドレス可能であるとは限りません。特に、interface{}**int のような多重ポインタを保持しており、その **intnil である場合、iv.Elem()*int 型の reflect.Value を返しますが、これは nil ポインタであり、かつアドレス可能ではありません。

JSONの null をデコードする際、encoding/json は最終的に reflect.Value.Set(reflect.Zero(v.Type())) のような操作でターゲットの値を nil に設定しようとします。もし v がアドレス可能でない reflect.Value であった場合、Set メソッドはパニックを引き起こします。

修正は、この iv.Elem() を取得した後の v の再代入に条件を追加することで、このパニックを回避しています。

		if iv := v; iv.Kind() == reflect.Interface && !iv.IsNil() {
			e := iv.Elem() // インターフェースが保持する具体的な値
			// e がポインタであり、かつ nil ではない場合、または
			// null をデコード中で、かつ e が指す先がポインタである場合
			if e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) {
				v = e // e を新しい v として続行
				continue
			}
		}

この新しい条件 (!decodingNull || e.Elem().Kind() == reflect.Ptr) が重要です。

  • !decodingNull: null をデコードしていない場合、つまり通常の値をデコードしている場合は、以前と同様に ev として続行します。
  • decodingNull && e.Elem().Kind() == reflect.Ptr: null をデコードしており、かつ e が指す先(e.Elem())がポインタである場合も ev として続行します。これは、**int のようなケースで、e*int であり、その Elem()int ではなく *int である場合に、さらにそのポインタを辿って nil を設定できるようにするためです。

この条件により、null をデコードする際に、v が実際に nil を設定できるアドレス可能なポインタ型である場合にのみ Elem() を辿るように制御し、それ以外の場合は現在の v のまま処理を続行することで、パニックを回避しています。

テストケース TestInterfaceSet は、この修正が正しく機能することを確認するために追加されました。特に、new(*int), intpp(nil), intpp(intp(1)) のような多重ポインタや nil ポインタを含むインターフェースへの null デコードが正しく処理されることを検証しています。

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

src/pkg/encoding/json/decode.godecodeState.indirect メソッド内の if iv := v; iv.Kind() == reflect.Interface && !iv.IsNil() ブロックが変更されました。

--- a/src/pkg/encoding/json/decode.go
+++ b/src/pkg/encoding/json/decode.go
@@ -273,9 +273,14 @@ func (d *decodeState) indirect(v reflect.Value, decodingNull bool) (Unmarshaler,\
 			_, isUnmarshaler = v.Interface().(Unmarshaler)\
 		}\
 
+\t\t// Load value from interface, but only if the result will be\
+\t\t// usefully addressable.\
 \t\tif iv := v; iv.Kind() == reflect.Interface && !iv.IsNil() {\
-\t\t\tv = iv.Elem()\
-\t\t\tcontinue\
+\t\t\te := iv.Elem()\
+\t\t\tif e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) {\
+\t\t\t\tv = e\
+\t\t\t\tcontinue\
+\t\t\t}\
 \t\t}\
 
 \t\tpv = v

また、src/pkg/encoding/json/decode_test.goTestInterfaceSet という新しいテスト関数と関連するヘルパー関数 (intp, intpp) およびテストデータ (interfaceSetTests) が追加されました。

コアとなるコードの解説

変更された decodeState.indirect メソッドのコードは、json.Unmarshal がインターフェース値を処理する際のロジックを改善しています。

		if iv := v; iv.Kind() == reflect.Interface && !iv.IsNil() {
			e := iv.Elem() // インターフェースが保持する具体的な値を取得
			// Load value from interface, but only if the result will be
			// usefully addressable.
			// e がポインタであり、かつ nil ではない場合、
			// さらに以下の条件のいずれかを満たす場合にのみ、e を v として処理を続行する:
			// 1. decodingNull が false (null 以外の値をデコード中)
			// 2. decodingNull が true (null をデコード中) かつ e.Elem() がポインタ型である
			if e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) {
				v = e // e を新しい v として、さらに間接参照を辿る
				continue
			}
		}
  • if iv := v; iv.Kind() == reflect.Interface && !iv.IsNil():
    • v がインターフェース型であり、かつ nil ではない(つまり、型情報と値が設定されているが、その値自体は nil かもしれない)場合にこのブロックに入ります。
  • e := iv.Elem():
    • インターフェース iv が保持している具体的な値の reflect.Valuee に代入します。例えば、interface{}((*int)(nil)) の場合、e*int 型の reflect.Value になります。
  • if e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr):
    • これが新しい条件分岐です。
    • e.Kind() == reflect.Ptr: e がポインタ型であること。
    • !e.IsNil(): e が指すポインタ自体が nil ではないこと。
    • (!decodingNull || e.Elem().Kind() == reflect.Ptr): この部分が null デコード時のパニックを防ぐための肝です。
      • !decodingNull: もし現在 null 以外の値をデコードしているのであれば、この条件は true になり、以前と同様に ev として処理を続行します。これは、null 以外の値をデコードする際には、e がアドレス可能であれば問題ないためです。
      • decodingNull || e.Elem().Kind() == reflect.Ptr: もし現在 null をデコードしているのであれば、e.Elem().Kind() == reflect.Ptrtrue である場合にのみ、ev として処理を続行します。これは、**int のような多重ポインタの場合に重要です。e*int であり、その Elem() がさらにポインタ(int ではなく *int)である場合、null を設定するためにはさらにそのポインタを辿る必要があるためです。これにより、nil を設定する対象が適切にアドレス可能なポインタであることを保証します。

この修正により、json.Unmarshal は、nil ではないインターフェース値に null をデコードする際に、不適切にアドレス可能でない reflect.Value に対して Set 操作を行おうとすることを防ぎ、パニックを回避できるようになりました。

関連リンク

参考にした情報源リンク

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

このコミットは、Go言語の標準ライブラリ encoding/json パッケージにおける、nil ではないインターフェース値へのJSONアンマーシャリング時に発生するパニック(panic)を修正するものです。具体的には、json.Unmarshal が、既に値が設定されているインターフェース型変数(特にポインタ型をラップしている場合)に null をアンマーシャリングしようとした際に、不正なメモリアクセスを引き起こす可能性があったバグに対処しています。この修正により、nil ではないインターフェース値への安全なアンマーシャリングが保証され、堅牢性が向上しました。

コミット

commit 09b736a2ab56ee520e3f5909c09c8417fe61db26
Author: Russ Cox <rsc@golang.org>
Date:   Thu Jun 7 01:48:55 2012 -0400

    encoding/json: fix panic unmarshaling into non-nil interface value
    
    Fixes #3614.
    
    R=golang-dev, adg
    CC=golang-dev
    https://golang.org/cl/6306051

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

https://github.com/golang/go/commit/09b736a2ab56ee520e3f5909c09c8417fe61db26

元コミット内容

commit 09b736a2ab56ee520e3f5909c09c8417fe61db26
Author: Russ Cox <rsc@golang.org>
Date:   Thu Jun 7 01:48:55 2012 -0400

    encoding/json: fix panic unmarshaling into non-nil interface value
    
    Fixes #3614.
    
    R=golang-dev, adg
    CC=golang-dev
    https://golang.org/cl/6306051
---
 src/pkg/encoding/json/decode.go      |  9 +++++--
 src/pkg/encoding/json/decode_test.go | 46 ++++++++++++++++++++++++++++++++++++\
 2 files changed, 53 insertions(+), 2 deletions(-)

diff --git a/src/pkg/encoding/json/decode.go b/src/pkg/encoding/json/decode.go
index 0018e534cc..44dc5784be 100644
--- a/src/pkg/encoding/json/decode.go
+++ b/src/pkg/encoding/json/decode.go
@@ -273,9 +273,14 @@ func (d *decodeState) indirect(v reflect.Value, decodingNull bool) (Unmarshaler,\
 			_, isUnmarshaler = v.Interface().(Unmarshaler)\
 		}\
 
+\t\t// Load value from interface, but only if the result will be\
+\t\t// usefully addressable.\
 \t\tif iv := v; iv.Kind() == reflect.Interface && !iv.IsNil() {\
-\t\t\tv = iv.Elem()\
-\t\t\tcontinue\
+\t\t\te := iv.Elem()\
+\t\t\tif e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) {\
+\t\t\t\tv = e\
+\t\t\t\tcontinue\
+\t\t\t}\
 \t\t}\
 
 \t\tpv = v
diff --git a/src/pkg/encoding/json/decode_test.go b/src/pkg/encoding/json/decode_test.go
index c7dce53f29..5a85e3f751 100644
--- a/src/pkg/encoding/json/decode_test.go
+++ b/src/pkg/encoding/json/decode_test.go
@@ -683,3 +683,49 @@ func TestEmptyString(t *testing.T) {
 		t.Fatal("Decode: did not set Number1")
 	}\
 }\
+\n+func intp(x int) *int {\
+\tp := new(int)\
+\t*p = x\
+\treturn p\
+}\
+\n+func intpp(x *int) **int {\
+\tpp := new(*int)\
+\t*pp = x\
+\treturn pp\
+}\
+\n+var interfaceSetTests = []struct {\
+\tpre  interface{}\
+\tjson string\
+\tpost interface{}\
+}{\
+\t{\"foo\", `\"bar\"`, \"bar\"},\
+\t{\"foo\", `2`, 2.0},\
+\t{\"foo\", `true`, true},\
+\t{\"foo\", `null`, nil},\
+\n+\t{nil, `null`, nil},\
+\t{new(int), `null`, nil},\
+\t{(*int)(nil), `null`, nil},\
+\t{new(*int), `null`, new(*int)},\
+\t{(**int)(nil), `null`, nil},\
+\t{intp(1), `null`, nil},\
+\t{intpp(nil), `null`, intpp(nil)},\
+\t{intpp(intp(1)), `null`, intpp(nil)},\
+}\
+\n+func TestInterfaceSet(t *testing.T) {\
+\tfor _, tt := range interfaceSetTests {\
+\t\tb := struct{ X interface{} }{tt.pre}\
+\t\tblob := `{\"X\":` + tt.json + `}`\
+\t\tif err := Unmarshal([]byte(blob), &b); err != nil {\
+\t\t\tt.Errorf(\"Unmarshal %#q: %v\", blob, err)\
+\t\t\tcontinue\
+\t\t}\
+\t\tif !reflect.DeepEqual(b.X, tt.post) {\
+\t\t\tt.Errorf(\"Unmarshal %#q into %#v: X=%#v, want %#v\", blob, tt.pre, b.X, tt.post)\
+\t\t}\
+\t}\
+}\

変更の背景

このコミットは、Go言語のIssue #3614「json.Unmarshal panics when unmarshaling into non-nil interface value」を修正するために行われました。

このバグは、encoding/json パッケージの Unmarshal 関数が、既に値が設定されている(nil ではない)インターフェース変数に対してJSONの null 値をデコードしようとした際に発生しました。特に、インターフェースがポインタ型(例: *int**int)をラップしている場合に問題が顕在化しました。

従来の Unmarshal の動作では、インターフェースが nil でない場合、そのインターフェースが保持している具体的な値(Elem() で取得できる)に対して直接デコードを試みていました。しかし、JSONの null はGoの nil に対応するため、ポインタ型の値に null をデコードしようとすると、そのポインタ自体を nil に設定する必要があります。

問題は、reflect.Value.Set(reflect.Zero(v.Type())) のような操作が、アドレス可能でない(CanSet()false の)reflect.Value に対して行われた場合にパニックを引き起こす点にありました。インターフェースがラップしているポインタが、さらに別のポインタを指しているような多重ポインタの場合、Elem() を辿った先の値がアドレス可能でないことがあり、そこに nil を設定しようとするとパニックが発生していました。

この修正は、このような特定のシナリオ、特に null をデコードする際に、インターフェースがラップしている値が「有用にアドレス可能」であるかどうかをより厳密にチェックすることで、パニックを回避することを目的としています。

前提知識の解説

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

encoding/json パッケージは、Goのデータ構造とJSONデータの間で変換を行うための標準ライブラリです。

  • json.Marshal: Goのデータ構造をJSONバイト列にエンコードします。
  • json.Unmarshal: JSONバイト列をGoのデータ構造にデコードします。

Go言語の interface{}

interface{} はGoにおける空のインターフェース型で、あらゆる型の値を保持できます。Goのインターフェースは、内部的に「型」と「値」のペアとして表現されます。

  • nil インターフェース: 型も値も nil の状態。
  • nil ではないインターフェース: 型は存在するが、値が nil の状態(例: var i interface{} = (*int)(nil))。この状態が今回のバグの核心でした。

Go言語の reflect パッケージ

reflect パッケージは、実行時にGoの型情報や値情報を検査・操作するための機能を提供します。

  • reflect.ValueOf(i interface{}) reflect.Value: インターフェース値 ireflect.Value 表現を返します。
  • reflect.Value.Kind() reflect.Kind: reflect.Value が表す値の具体的な種類(例: reflect.Int, reflect.Ptr, reflect.Interface など)を返します。
  • reflect.Value.Elem() reflect.Value: ポインタ、インターフェース、またはスライスが指す要素の reflect.Value を返します。ポインタの場合、そのポインタが指す先の値の reflect.Value を返します。インターフェースの場合、そのインターフェースが保持している具体的な値の reflect.Value を返します。
  • reflect.Value.IsNil() bool: reflect.Valuenil であるかどうかをチェックします。ポインタ、インターフェース、マップ、スライス、チャネル、関数に対して有効です。
  • reflect.Value.CanSet() bool: reflect.Value が変更可能(アドレス可能)であるかどうかをチェックします。CanSet()true でないと、Set() メソッドなどで値を変更することはできません。

JSONの null とGoの nil

JSONの null は、Goの nil 値にデコードされます。これは、ポインタ、スライス、マップ、インターフェース、チャネル、関数などの参照型に適用されます。

技術的詳細

このバグは、encoding/json のデコード処理の中核を担う decodeState.indirect メソッドに存在していました。このメソッドは、reflect.Value を受け取り、それがポインタやインターフェースである場合に、その「実体」を辿ってデコード可能な reflect.Value を取得する役割を担っています。

問題のコードは以下の部分でした。

		if iv := v; iv.Kind() == reflect.Interface && !iv.IsNil() {
			v = iv.Elem()
			continue
		}

このコードは、vnil ではないインターフェースである場合、そのインターフェースが保持している具体的な値(iv.Elem())を v に再代入し、ループを続行していました。

しかし、iv.Elem() が返す reflect.Value は、必ずしもアドレス可能であるとは限りません。特に、interface{}**int のような多重ポインタを保持しており、その **intnil である場合、iv.Elem()*int 型の reflect.Value を返しますが、これは nil ポインタであり、かつアドレス可能ではありません。

JSONの null をデコードする際、encoding/json は最終的に reflect.Value.Set(reflect.Zero(v.Type())) のような操作でターゲットの値を nil に設定しようとします。もし v がアドレス可能でない reflect.Value であった場合、Set メソッドはパニックを引き起こします。

修正は、この iv.Elem() を取得した後の v の再代入に条件を追加することで、このパニックを回避しています。

		if iv := v; iv.Kind() == reflect.Interface && !iv.IsNil() {
			e := iv.Elem() // インターフェースが保持する具体的な値
			// e がポインタであり、かつ nil ではない場合、または
			// null をデコード中で、かつ e が指す先がポインタである場合
			if e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) {
				v = e // e を新しい v として続行
				continue
			}
		}

この新しい条件 (!decodingNull || e.Elem().Kind() == reflect.Ptr) が重要です。

  • !decodingNull: null をデコードしていない場合、つまり通常の値をデコードしている場合は、以前と同様に ev として続行します。
  • decodingNull && e.Elem().Kind() == reflect.Ptr: null をデコードしており、かつ e が指す先(e.Elem())がポインタである場合も ev として続行します。これは、**int のようなケースで、e*int であり、その Elem()int ではなく *int である場合に、さらにそのポインタを辿って nil を設定できるようにするためです。

この条件により、null をデコードする際に、v が実際に nil を設定できるアドレス可能なポインタ型である場合にのみ Elem() を辿るように制御し、それ以外の場合は現在の v のまま処理を続行することで、パニックを回避しています。

テストケース TestInterfaceSet は、この修正が正しく機能することを確認するために追加されました。特に、new(*int), intpp(nil), intpp(intp(1)) のような多重ポインタや nil ポインタを含むインターフェースへの null デコードが正しく処理されることを検証しています。

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

src/pkg/encoding/json/decode.godecodeState.indirect メソッド内の if iv := v; iv.Kind() == reflect.Interface && !iv.IsNil() ブロックが変更されました。

--- a/src/pkg/encoding/json/decode.go
+++ b/src/pkg/encoding/json/decode.go
@@ -273,9 +273,14 @@ func (d *decodeState) indirect(v reflect.Value, decodingNull bool) (Unmarshaler,\
 			_, isUnmarshaler = v.Interface().(Unmarshaler)\
 		}\
 
+\t\t// Load value from interface, but only if the result will be\
+\t\t// usefully addressable.\
 \t\tif iv := v; iv.Kind() == reflect.Interface && !iv.IsNil() {\
-\t\t\tv = iv.Elem()\
-\t\t\tcontinue\
+\t\t\te := iv.Elem()\
+\t\t\tif e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) {\
+\t\t\t\tv = e\
+\t\t\t\tcontinue\
+\t\t\t}\
 \t\t}\
 
 \t\tpv = v

また、src/pkg/encoding/json/decode_test.goTestInterfaceSet という新しいテスト関数と関連するヘルパー関数 (intp, intpp) およびテストデータ (interfaceSetTests) が追加されました。

コアとなるコードの解説

変更された decodeState.indirect メソッドのコードは、json.Unmarshal がインターフェース値を処理する際のロジックを改善しています。

		if iv := v; iv.Kind() == reflect.Interface && !iv.IsNil() {
			e := iv.Elem() // インターフェースが保持する具体的な値を取得
			// Load value from interface, but only if the result will be
			// usefully addressable.
			// e がポインタであり、かつ nil ではない場合、
			// さらに以下の条件のいずれかを満たす場合にのみ、e を v として処理を続行する:
			// 1. decodingNull が false (null 以外の値をデコード中)
			// 2. decodingNull が true (null をデコード中) かつ e.Elem() がポインタ型である
			if e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) {
				v = e // e を新しい v として、さらに間接参照を辿る
				continue
			}
		}
  • if iv := v; iv.Kind() == reflect.Interface && !iv.IsNil():
    • v がインターフェース型であり、かつ nil ではない(つまり、型情報と値が設定されているが、その値自体は nil かもしれない)場合にこのブロックに入ります。
  • e := iv.Elem():
    • インターフェース iv が保持している具体的な値の reflect.Valuee に代入します。例えば、interface{}((*int)(nil)) の場合、e*int 型の reflect.Value になります。
  • if e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr):
    • これが新しい条件分岐です。
    • e.Kind() == reflect.Ptr: e がポインタ型であること。
    • !e.IsNil(): e が指すポインタ自体が nil ではないこと。
    • (!decodingNull || e.Elem().Kind() == reflect.Ptr): この部分が null デコード時のパニックを防ぐための肝です。
      • !decodingNull: もし現在 null 以外の値をデコードしているのであれば、この条件は true になり、以前と同様に ev として処理を続行します。これは、null 以外の値をデコードする際には、e がアドレス可能であれば問題ないためです。
      • decodingNull || e.Elem().Kind() == reflect.Ptr: もし現在 null をデコードしているのであれば、e.Elem().Kind() == reflect.Ptrtrue である場合にのみ、ev として処理を続行します。これは、**int のような多重ポインタの場合に重要です。e*int であり、その Elem() がさらにポインタ(int ではなく *int)である場合、null を設定するためにはさらにそのポインタを辿る必要があるためです。これにより、nil を設定する対象が適切にアドレス可能なポインタであることを保証します。

この修正により、json.Unmarshal は、nil ではないインターフェース値に null をデコードする際に、不適切にアドレス可能でない reflect.Value に対して Set 操作を行おうとすることを防ぎ、パニックを回避できるようになりました。

関連リンク

参考にした情報源リンク