[インデックス 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
: インターフェース値i
のreflect.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.Value
がnil
であるかどうかをチェックします。ポインタ、インターフェース、マップ、スライス、チャネル、関数に対して有効です。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
}
このコードは、v
が nil
ではないインターフェースである場合、そのインターフェースが保持している具体的な値(iv.Elem()
)を v
に再代入し、ループを続行していました。
しかし、iv.Elem()
が返す reflect.Value
は、必ずしもアドレス可能であるとは限りません。特に、interface{}
が **int
のような多重ポインタを保持しており、その **int
が nil
である場合、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
をデコードしていない場合、つまり通常の値をデコードしている場合は、以前と同様にe
をv
として続行します。decodingNull && e.Elem().Kind() == reflect.Ptr
:null
をデコードしており、かつe
が指す先(e.Elem()
)がポインタである場合もe
をv
として続行します。これは、**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.go
の decodeState.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.go
に TestInterfaceSet
という新しいテスト関数と関連するヘルパー関数 (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.Value
をe
に代入します。例えば、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
になり、以前と同様にe
をv
として処理を続行します。これは、null
以外の値をデコードする際には、e
がアドレス可能であれば問題ないためです。decodingNull || e.Elem().Kind() == reflect.Ptr
: もし現在null
をデコードしているのであれば、e.Elem().Kind() == reflect.Ptr
がtrue
である場合にのみ、e
をv
として処理を続行します。これは、**int
のような多重ポインタの場合に重要です。e
が*int
であり、そのElem()
がさらにポインタ(int
ではなく*int
)である場合、null
を設定するためにはさらにそのポインタを辿る必要があるためです。これにより、nil
を設定する対象が適切にアドレス可能なポインタであることを保証します。
この修正により、json.Unmarshal
は、nil
ではないインターフェース値に null
をデコードする際に、不適切にアドレス可能でない reflect.Value
に対して Set
操作を行おうとすることを防ぎ、パニックを回避できるようになりました。
関連リンク
- GitHubコミット: https://github.com/golang/go/commit/09b736a2ab56ee520e3f5909c09c8417fe61db26
- Go Issue #3614: https://go.dev/issue/3614
- Go CL 6306051: https://golang.org/cl/6306051
参考にした情報源リンク
- Go Issue Tracker (Issue #3614): https://go.dev/issue/3614
- Go
reflect
パッケージ公式ドキュメント: https://pkg.go.dev/reflect - Go
encoding/json
パッケージ公式ドキュメント: https://pkg.go.dev/encoding/json - Go Code Review (CL 6306051): https://golang.org/cl/6306051
- Go言語のインターフェースとnilについて: https://go.dev/blog/laws-of-reflection (Laws of Reflection)```markdown
[インデックス 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
: インターフェース値i
のreflect.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.Value
がnil
であるかどうかをチェックします。ポインタ、インターフェース、マップ、スライス、チャネル、関数に対して有効です。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
}
このコードは、v
が nil
ではないインターフェースである場合、そのインターフェースが保持している具体的な値(iv.Elem()
)を v
に再代入し、ループを続行していました。
しかし、iv.Elem()
が返す reflect.Value
は、必ずしもアドレス可能であるとは限りません。特に、interface{}
が **int
のような多重ポインタを保持しており、その **int
が nil
である場合、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
をデコードしていない場合、つまり通常の値をデコードしている場合は、以前と同様にe
をv
として続行します。decodingNull && e.Elem().Kind() == reflect.Ptr
:null
をデコードしており、かつe
が指す先(e.Elem()
)がポインタである場合もe
をv
として続行します。これは、**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.go
の decodeState.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.go
に TestInterfaceSet
という新しいテスト関数と関連するヘルパー関数 (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.Value
をe
に代入します。例えば、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
になり、以前と同様にe
をv
として処理を続行します。これは、null
以外の値をデコードする際には、e
がアドレス可能であれば問題ないためです。decodingNull || e.Elem().Kind() == reflect.Ptr
: もし現在null
をデコードしているのであれば、e.Elem().Kind() == reflect.Ptr
がtrue
である場合にのみ、e
をv
として処理を続行します。これは、**int
のような多重ポインタの場合に重要です。e
が*int
であり、そのElem()
がさらにポインタ(int
ではなく*int
)である場合、null
を設定するためにはさらにそのポインタを辿る必要があるためです。これにより、nil
を設定する対象が適切にアドレス可能なポインタであることを保証します。
この修正により、json.Unmarshal
は、nil
ではないインターフェース値に null
をデコードする際に、不適切にアドレス可能でない reflect.Value
に対して Set
操作を行おうとすることを防ぎ、パニックを回避できるようになりました。
関連リンク
- GitHubコミット: https://github.com/golang/go/commit/09b736a2ab56ee520e3f5909c09c8417fe61db26
- Go Issue #3614: https://go.dev/issue/3614
- Go CL 6306051: https://golang.org/cl/6306051
参考にした情報源リンク
- Go Issue Tracker (Issue #3614): https://go.dev/issue/3614
- Go
reflect
パッケージ公式ドキュメント: https://pkg.go.dev/reflect - Go
encoding/json
パッケージ公式ドキュメント: https://pkg.go.dev/encoding/json - Go Code Review (CL 6306051): https://golang.org/cl/6306051
- Go言語のインターフェースとnilについて: https://go.dev/blog/laws-of-reflection (Laws of Reflection)