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

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

コミット

commit ccf2b8843e26731a2215d0d8b0ac04d7c2d42074
Author: Russ Cox <rsc@golang.org>
Date:   Tue Sep 18 14:22:55 2012 -0400

    encoding/json: do not read beyond array literal
    
    Fixes #3942.
    
    R=golang-dev, mike.rosset, r
    CC=golang-dev
    https://golang.org/cl/6524043

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

https://github.com/golang/go/commit/ccf2b8843e26731a2215d0d8b0ac04d7c2d42074

元コミット内容

encoding/json パッケージにおいて、JSON配列リテラルの終端を超えて読み込まないように修正。Issue #3942 を修正します。

変更の背景

このコミットは、Go言語の encoding/json パッケージにおける重要なバグ修正に対応しています。具体的には、GitHub Issue #3942「encoding/json: do not read beyond } in object」で報告された問題に対処しています。

この問題は、json.Decoder がJSONオブジェクトまたは配列の閉じ括弧(} または ])を超えてデータを読み込もうとすると、プログラムがデッドロックに陥る可能性があるというものでした。特に、ネットワークストリームなどからJSONデータを読み込む際に、デコーダが期待するデータがストリームの終端に存在しない場合、デコーダは無限に次のバイトを待ち続け、結果としてアプリケーションが応答しなくなるという深刻な問題を引き起こしていました。

このバグは、JSONパーサーがオブジェクトの終端(})または配列の終端(])を検出した後も、不必要に次のバイトを読み込もうとするロジックに起因していました。これにより、有効なJSONデータが与えられた場合でも、デコーダが余分な読み込みを試み、データソースがそれ以上のデータを提供しない場合にブロッキングが発生していました。

前提知識の解説

  • encoding/json パッケージ: Go言語の標準ライブラリの一部で、JSONデータのエンコード(Goのデータ構造からJSONへ)とデコード(JSONからGoのデータ構造へ)を提供します。
  • json.Decoder: JSONストリームを読み込み、Goのデータ構造にデコードするための型です。通常、NewDecoder(io.Reader) を使用して作成され、Decode(&v) メソッドでデコードを実行します。
  • JSONの構文: JSON (JavaScript Object Notation) は、人間が読み書きしやすく、機械が解析しやすいデータ交換フォーマットです。オブジェクトは {} で囲まれ、キーと値のペアの集合です。配列は [] で囲まれ、値の順序付きリストです。
  • ストリーム処理: データが一度にすべて利用可能になるのではなく、連続的に到着する形式で処理されることを指します。ネットワーク通信やファイルI/Oで一般的です。
  • デッドロック: 複数のプロセスやスレッドが互いに相手が保持しているリソースの解放を待ち続け、結果としてどのプロセスも処理を進められなくなる状態です。このコミットの背景にある問題は、json.Decoder が不必要に次のバイトを待ち続けることで、一種のデッドロック状態を引き起こしていました。
  • net.Pipe(): Go言語の net パッケージで提供される関数で、メモリ内で双方向のパイプ(接続)を作成します。これは、ネットワーク接続をシミュレートしたり、テスト目的でデータのストリームを制御したりするのに非常に便利です。r (Reader) と w (Writer) の2つの net.Conn インターフェースを返します。

技術的詳細

このコミットの核心は、encoding/json/stream.go 内の scan.step メソッドの呼び出し条件の変更にあります。

json.Decoder は、JSONデータを解析するために内部的にステートマシン(scan)を使用しています。このステートマシンは、入力ストリームからバイトを読み込み、JSONのトークン(オブジェクトの開始、キー、値、配列の開始など)を識別します。

変更前のコードでは、scanEndObject (JSONオブジェクトの終端 }) を検出した後、dec.scan.step(&dec.scan, ' ') == scanEnd という条件で、不必要に次のバイトを読み込もうとしていました。この scan.step の呼び出しは、入力ストリームから1バイトを読み込む可能性があり、もしストリームが終端に達していてそれ以上のデータがない場合、この読み込み操作がブロッキングを引き起こしていました。

このコミットでは、この条件に v == scanEndArray (JSON配列の終端 ]) を追加しています。つまり、オブジェクトの終端または配列の終端に達した場合、デコーダはそれ以上読み込みを試みずに、現在のJSONリテラルの解析を終了するように変更されました。

具体的には、以下の行が変更されました。

変更前:

if v == scanEndObject && dec.scan.step(&dec.scan, ' ') == scanEnd {

変更後:

if (v == scanEndObject || v == scanEndArray) && dec.scan.step(&dec.scan, ' ') == scanEnd {

この変更により、デコーダはJSONオブジェクトまたは配列の終端を検出した際に、そのリテラルの範囲を超えて読み込みを試みなくなり、不必要なブロッキングを防ぐことができます。

テストケース TestBlocking は、この修正の有効性を検証するために追加されました。このテストでは、net.Pipe() を使用して、JSONデータが部分的に書き込まれるストリームをシミュレートしています。go w.Write([]byte(enc)) でJSONデータがパイプに書き込まれ、NewDecoder(r).Decode(&val) でデコードが試みられます。もしデコーダがJSONリテラルの終端を超えて読み込もうとすると、w.Write が書き込んだデータが不足しているため、Decode はブロックし、テストはデッドロックに陥ります。このテストが正常に完了することは、デコーダがJSONリテラルの範囲を超えて読み込みを試みていないことを証明します。

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

変更は主に以下の2つのファイルで行われています。

  1. src/pkg/encoding/json/stream.go: JSONデコーダのストリーム処理ロジックが含まれるファイル。
  2. src/pkg/encoding/json/stream_test.go: stream.go のテストファイル。

src/pkg/encoding/json/stream.go の変更

--- a/src/pkg/encoding/json/stream.go
+++ b/src/pkg/encoding/json/stream.go
@@ -78,7 +78,7 @@ Input:
 			// scanEnd is delayed one byte.
 			// We might block trying to get that byte from src,
 			// so instead invent a space byte.
-			if v == scanEndObject && dec.scan.step(&dec.scan, ' ') == scanEnd {
+			if (v == scanEndObject || v == scanEndArray) && dec.scan.step(&dec.scan, ' ') == scanEnd {
 				scanp += i + 1
 				break Input
 			}

src/pkg/encoding/json/stream_test.go の変更

--- a/src/pkg/encoding/json/stream_test.go
+++ b/src/pkg/encoding/json/stream_test.go
@@ -6,6 +6,7 @@ package json
 
  import (
  	"bytes"
+	"net"
  	"reflect"
  	"testing"
  )
@@ -145,3 +146,24 @@ func TestNullRawMessage(t *testing.T) {
  	t.Fatalf("Marshal: have %#q want %#q", b, msg)
  	}
  }\n+\n+var blockingTests = []string{\n+\t`{\"x\": 1}`,\n+\t`[1, 2, 3]`,\n+}\n+\n+func TestBlocking(t *testing.T) {\n+\tfor _, enc := range blockingTests {\n+\t\tr, w := net.Pipe()\n+\t\tgo w.Write([]byte(enc))\n+\t\tvar val interface{}\n+\n+\t\t// If Decode reads beyond what w.Write writes above,\n+\t\t// it will block, and the test will deadlock.\n+\t\tif err := NewDecoder(r).Decode(&val); err != nil {\n+\t\t\tt.Errorf("decoding %s: %v", enc, err)\n+\t\t}\n+\t\tr.Close()\n+\t\tw.Close()\n+\t}\n+}\n```

## コアとなるコードの解説

`stream.go` の変更は、JSONデコードの内部ロジックにおける重要な修正です。

`Input:` ラベルで示されるループ内で、デコーダは入力ストリームをスキャンし、JSONの構造を解析しています。`v` は現在のスキャン状態または検出されたトークンを示します。

変更前のコードでは、`v == scanEndObject` (JSONオブジェクトの終端 `}` を検出した状態) の場合にのみ、`dec.scan.step(&dec.scan, ' ') == scanEnd` という条件が評価されていました。この `scan.step` は、次のバイトを読み込み、スキャン状態を更新する関数です。`scanEnd` はスキャンが終了したことを示す状態です。

問題は、オブジェクトの終端を検出した後でも、デコーダが「スペースバイト」を期待して `scan.step` を呼び出していた点にありました。もし入力ストリームがオブジェクトの終端で完全に終了している場合、この `scan.step` の呼び出しは、存在しないバイトを読み込もうとしてブロッキングを引き起こしていました。

このコミットでは、この条件に `|| v == scanEndArray` が追加されました。これにより、JSONオブジェクトの終端 (`}`) またはJSON配列の終端 (`]`) のいずれかを検出した場合に、同じロジックが適用されるようになりました。つまり、配列の終端を検出した場合も、オブジェクトの終端と同様に、不必要な追加のバイト読み込みを避けるようになりました。

この修正により、デコーダはJSONリテラルの論理的な終端に達した時点で、それ以上ストリームからバイトを読み込もうとしないため、ストリームがその時点で終了している場合に発生するデッドロックやブロッキングの問題が解消されます。

`stream_test.go` に追加された `TestBlocking` は、この修正の有効性を実証するためのものです。
`blockingTests` 変数には、テスト対象となるJSON文字列(オブジェクトと配列)が含まれています。
テストループ内で、`net.Pipe()` を使用して、読み書き可能なメモリ内パイプを作成します。
`go w.Write([]byte(enc))` は、JSON文字列をパイプの書き込み側に非同期で書き込みます。これにより、デコーダが読み込むためのデータが提供されます。
`NewDecoder(r).Decode(&val)` は、パイプの読み込み側からJSONデータをデコードしようとします。
コメントにあるように、「If Decode reads beyond what w.Write writes above, it will block, and the test will deadlock.」という点が重要です。つまり、`w.Write` が書き込んだJSON文字列の終端を超えて `Decode` が読み込もうとすると、パイプにはそれ以上のデータがないため、`Decode` はブロックし、テストはタイムアウトまたはデッドロックで失敗します。
このテストが成功するということは、`Decode` がJSONリテラルの終端で適切に読み込みを停止し、不必要なブロッキングが発生しないことを意味します。

## 関連リンク

*   Go言語の `encoding/json` パッケージのドキュメント: [https://pkg.go.dev/encoding/json](https://pkg.go.dev/encoding/json)
*   Go言語の `net` パッケージのドキュメント: [https://pkg.go.dev/net](https://pkg.go.dev/net)

## 参考にした情報源リンク

*   GitHub Issue #3942: [https://github.com/golang/go/issues/3942](https://github.com/golang.org/cl/6524043) (Web検索結果より)
*   Go CL 6524043: [https://golang.org/cl/6524043](https://golang.org/cl/6524043)