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

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

このコミットは、Go言語の html/template パッケージにおける Clone メソッドがパニック(クラッシュ)を引き起こすバグを修正するものです。具体的には、テンプレートがまだパースされていない状態で Clone メソッドが呼び出された際に発生する nil ポインタ参照によるパニックを防ぎます。

コミット

commit 5f32c8b88bfe5f6e2ba32bb444dbda88ec741024
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Tue Mar 13 16:55:43 2012 -0700

    html/template: fix panic on Clone
    
    Fixes #3281
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/5819044

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

https://github.com/golang/go/commit/5f32c8b88bfe5f6e2ba32bb444dbda88ec741024

元コミット内容

html/template: fix panic on Clone

このコミットは、html/template パッケージの Clone メソッドがパニックを引き起こす問題を修正します。

関連するIssue: #3281

変更の背景

この変更は、Go言語のIssue 3281「html/template: Clone panics if template not parsed」を修正するために行われました。

元の問題は、html/template パッケージの Template 型の Clone メソッドが、クローン対象のテンプレートがまだパースされていない(つまり、Parse メソッドが呼び出されていない)状態で呼び出された場合に、nil ポインタ参照によるパニックを引き起こすというものでした。

html/template パッケージは、HTMLコンテンツを安全に生成するためのテンプレートエンジンを提供します。これには、クロスサイトスクリプティング(XSS)などの脆弱性を防ぐための自動エスケープ機能が含まれています。Template オブジェクトは、Parse メソッドによってテンプレート文字列を解析し、内部的に parse.Tree 構造体にその解析結果(構文木)を保持します。

Clone メソッドは、既存のテンプレートオブジェクトのコピーを作成するために使用されます。これにより、元のテンプレートを変更せずに、新しいテンプレートオブジェクトで追加の操作(例えば、新しいテンプレートの定義や既存のテンプレートの上書き)を行うことができます。しかし、Clone メソッドの実装が、Template オブジェクトの Tree フィールドが nil である可能性を考慮していなかったため、パースされていないテンプレートをクローンしようとすると、x.Tree.Root.CopyList() の部分で nil ポインタデリファレンスが発生し、プログラムがクラッシュしていました。

このコミットは、この特定のシナリオを適切に処理し、Clone メソッドが安全に動作するようにするための修正です。

前提知識の解説

  • Go言語の html/template パッケージ: Go言語標準ライブラリの一部で、HTMLテンプレートを処理するためのパッケージです。セキュリティを重視しており、デフォルトでHTMLエスケープを行うことで、XSS攻撃などのウェブ脆弱性を防ぎます。テンプレートは、{{.FieldName}} のようなプレースホルダーや、{{range .Items}}...{{end}} のような制御構造を含めることができます。
  • Template: html/template パッケージの中心となる型で、個々のテンプレートを表します。テンプレートのパース、実行、および関連するテンプレートの管理を行います。
  • Parse メソッド: Template 型のメソッドで、テンプレート文字列を解析し、内部的な構文木(parse.Tree)を構築します。この構文木が、テンプレートの実行時に使用されます。
  • Clone メソッド: Template 型のメソッドで、現在のテンプレートオブジェクトのコピーを作成します。このコピーは、元のテンプレートと同じ名前空間と定義されたテンプレートを共有しますが、独立したオブジェクトとして扱われます。これにより、元のテンプレートに影響を与えることなく、コピーに対して変更を加えることができます。
  • parse.Tree 構造体: html/template パッケージの内部で使用される構造体で、パースされたテンプレートの抽象構文木(AST)を表します。Root フィールドは、テンプレートのルートノードを指します。
  • nil ポインタ参照 (nil pointer dereference): プログラミングにおいて、nil(または null)値を持つポインタが指すメモリ領域にアクセスしようとしたときに発生するエラーです。Go言語では、これはランタイムパニックを引き起こし、プログラムが異常終了します。
  • Must 関数: html/template パッケージで提供されるヘルパー関数で、テンプレート操作の結果としてエラーが発生した場合にパニックを引き起こします。これは、初期化時など、エラーが発生してはならない状況でテンプレートを扱う際に便利です。

技術的詳細

html/template パッケージの Template 型には、Tree というフィールドがあります。この Tree フィールドは *parse.Tree 型であり、テンプレートが Parse メソッドによって正常に解析された場合に、そのテンプレートの抽象構文木(AST)を保持します。もしテンプレートがまだパースされていない場合、またはパースに失敗した場合は、この Tree フィールドは nil になります。

Template 型の Clone メソッドは、既存のテンプレートオブジェクトを複製する際に、その内部状態、特に Tree フィールドが指す構文木も複製しようとします。元の実装では、Clone メソッド内で以下のようなコードがありました。

x.Tree = &parse.Tree{
    Name: x.Tree.Name,
    Root: x.Tree.Root.CopyList(),
}

このコードは、x.Treenil でないことを前提としていました。しかし、t1 := New("all") のようにテンプレートを新規作成した直後で、まだ Parse メソッドが呼び出されていない場合、t1Tree フィールドは nil です。この状態で t1.Clone() を呼び出すと、x.Tree.Namex.Tree.Root.CopyList() の部分で nil ポインタデリファレンスが発生し、ランタイムパニックを引き起こしていました。

このコミットによる修正は、x.Treenil でない場合にのみ、構文木の複製処理を行うように条件分岐を追加することで、この問題を解決しています。

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

変更は主に src/pkg/html/template/template.go ファイルの Clone メソッドにあります。

--- a/src/pkg/html/template/template.go
+++ b/src/pkg/html/template/template.go
@@ -160,9 +160,11 @@ func (t *Template) Clone() (*Template, error) {
 		if src == nil || src.escaped {
 			return nil, fmt.Errorf("html/template: cannot Clone %q after it has executed", t.Name())
 		}
-\t\tx.Tree = &parse.Tree{
-\t\t\tName: x.Tree.Name,
-\t\t\tRoot: x.Tree.Root.CopyList(),
+\t\tif x.Tree != nil {
+\t\t\tx.Tree = &parse.Tree{
+\t\t\t\tName: x.Tree.Name,
+\t\t\t\tRoot: x.Tree.Root.CopyList(),
+\t\t\t}
 \t\t}
 \t\tret.set[name] = &Template{
 \t\t\tfalse,

また、この修正を検証するためのテストケースが src/pkg/html/template/clone_test.go に追加されています。

--- a/src/pkg/html/template/clone_test.go
+++ b/src/pkg/html/template/clone_test.go
@@ -113,3 +113,10 @@ func TestClone(t *testing.T) {
 		t.Errorf("t3: got %q want %q", got, want)
 	}
 }
+
+// This used to crash; http://golang.org/issue/3281
+func TestCloneCrash(t *testing.T) {
+	t1 := New("all")
+	Must(t1.New("t1").Parse(`{{define "foo"}}foo{{end}}`))
+	t1.Clone()
+}

コアとなるコードの解説

template.go の変更点では、Clone メソッド内で x.Treenil でないことを確認する if x.Tree != nil という条件文が追加されました。これにより、テンプレートがまだパースされておらず x.Treenil の場合でも、nil ポインタデリファレンスが発生することなく、安全に Clone メソッドが実行されるようになります。x.Treenil の場合は、構文木の複製は行われず、Tree フィールドは nil のままコピーされます。これは、パースされていないテンプレートのクローンが、やはりパースされていない状態であることを意味するため、正しい挙動です。

clone_test.go に追加された TestCloneCrash テストケースは、このバグが修正されたことを検証します。

  1. t1 := New("all") で新しいテンプレートセットを作成します。この時点では、t1 自体はまだパースされていません。
  2. Must(t1.New("t1").Parse({{define "foo"}}foo{{end}})) で、t1 の名前空間に新しいテンプレート t1 を定義し、パースします。しかし、これは t1 自体ではなく、そのサブテンプレート t1 をパースしている点に注意が必要です。t1 (親テンプレート) の Tree フィールドは依然として nil のままです。
  3. t1.Clone() を呼び出します。修正前は、この行でパニックが発生していました。修正後は、x.Tree != nil のチェックにより、パニックが回避され、テストが正常に完了します。

このテストは、Clone メソッドが、親テンプレートがパースされていない状態でも安全に動作することを確認しています。

関連リンク

参考にした情報源リンク

  • 上記の関連リンクに記載されたGoのコードレビューとIssueトラッカー。
  • Go言語の html/template パッケージのドキュメント。
  • Go言語の text/template パッケージのドキュメント(html/templatetext/template をベースにしているため)。
  • Go言語における nil ポインタとパニックに関する一般的な知識。