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

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

このコミットは、Go言語の html/template パッケージにおけるnilポインタバグを修正するものです。具体的には、テンプレートが適切にパースされていない、あるいは存在しない場合に ExecuteTemplate メソッドがnilポインタ参照を引き起こす可能性があった問題に対処しています。

コミット

commit 214a1ca3c5d7f8d633587cf6faff2868f341b31b
Author: Rob Pike <r@golang.org>
Date:   Wed Mar 14 15:08:54 2012 +1100

    html/template: fix nil pointer bug
    Fixes #3272.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/5819046

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

https://github.com/golang/go/commit/214a1ca3c5d7f8d633587cf6faff2868f341b31b

元コミット内容

html/template: fix nil pointer bug Fixes #3272.

このコミットは、html/template パッケージにおけるnilポインタバグを修正します。関連するIssueは #3272 です。

変更の背景

このコミットの背景には、Go言語の html/template パッケージが、存在しないテンプレートや不完全にパースされたテンプレートに対して ExecuteTemplate メソッドが呼び出された際に、予期せぬnilポインタ参照を引き起こす可能性があったという問題があります。

Goのテンプレートエンジンは、WebアプリケーションにおいてHTMLコンテンツを安全に生成するために設計されており、特にクロスサイトスクリプティング(XSS)攻撃を防ぐための自動エスケープ機能を提供しています。しかし、テンプレートのロードやパースの段階でエラーが発生した場合、その後の実行フェーズでnilポインタ例外が発生し、アプリケーションがクラッシュする可能性がありました。

Issue #3272("html/template: ExecuteTemplate on empty template panics")は、この具体的な問題点を指摘しています。ユーザーが html/template.New().ParseFiles(os.DevNull) のように、実質的に空のテンプレートを作成し、それを ExecuteTemplate で実行しようとすると、パニック(nilポインタ参照)が発生するという報告でした。これは、テンプレートオブジェクト自体は作成されるものの、その内部の text.Treetext.Root といった重要な構造体がnilのままであるために発生していました。

このバグは、開発者がテンプレートのロードやパースの失敗を適切にハンドリングしない場合に、実行時エラーとして顕在化し、アプリケーションの安定性を損なう可能性がありました。したがって、このコミットは、このような状況下での堅牢性を向上させることを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念と html/template パッケージの基本的な動作について理解しておく必要があります。

Go言語の text/template および html/template パッケージ

Go言語には、テキストベースのテンプレートを扱う text/template パッケージと、HTMLコンテンツを安全に生成するための html/template パッケージがあります。html/templatetext/template をベースにしており、XSS攻撃を防ぐための自動エスケープ機能が追加されています。

  • テンプレート (Template): テンプレートは、プレースホルダーや制御構造(条件分岐、ループなど)を含むテキストファイルまたは文字列です。データが適用されると、プレースホルダーが実際の値に置き換えられ、最終的な出力が生成されます。
  • パース (Parse): テンプレート文字列やファイルを読み込み、Goの内部表現(構文木、text.Tree)に変換するプロセスです。このプロセスでテンプレートの構文がチェックされます。
  • 実行 (Execute): パースされたテンプレートにデータ(通常はGoの構造体、マップ、スライスなど)を適用し、最終的な出力を生成するプロセスです。
  • Template.ParseFiles() / Template.ParseGlob(): これらの関数は、指定されたファイルやパターンにマッチするファイルからテンプレートを読み込み、パースします。複数のテンプレートをまとめて管理する Template オブジェクトに登録されます。
  • Template.ExecuteTemplate(wr io.Writer, name string, data interface{}): 指定された名前のテンプレートを検索し、与えられたデータを使って実行し、結果を wr に書き込みます。

nil ポインタとパニック

Go言語では、ポインタが何も指していない状態を nil と表現します。nil ポインタに対してメソッドを呼び出したり、フィールドにアクセスしようとすると、ランタイムパニック(panic)が発生します。パニックは、プログラムの異常終了を引き起こすGoのエラーハンドリングメカニズムの一つです。

Issue Tracking (GitHub Issues)

GitHubのIssueトラッキングシステムは、ソフトウェア開発においてバグ報告、機能要望、タスク管理などを行うために広く利用されています。Fixes #3272 のような記述は、このコミットがGitHub上の特定のIssue(この場合はIssue番号3272)を修正したことを示します。これにより、コードの変更がどの問題に対応しているのかを明確にすることができます。

os.DevNull

os.DevNull は、Unix系システムにおける /dev/null に相当する特殊なファイルパスです。このファイルに書き込まれたデータはすべて破棄され、読み込もうとしても何も返されません。テストケースで一時的なファイルや不要な出力を扱う際によく使用されます。このコミットのテストケースでは、ParseFiles(os.DevNull) を使用して、実質的に内容のないテンプレートファイルをパースしようとする状況を再現しています。

技術的詳細

このコミットは、src/pkg/html/template/template.go ファイル内の lookupAndEscapeTemplate 関数と、parseFiles および parseGlob 関数におけるエラーメッセージの改善に焦点を当てています。

lookupAndEscapeTemplate 関数の変更

lookupAndEscapeTemplate 関数は、html/template パッケージの内部で、指定された名前のテンプレートを検索し、必要に応じてエスケープ処理を行う役割を担っています。この関数は、Template.ExecuteTemplate などから呼び出されます。

変更前のコードでは、テンプレート tmplnil であるかどうか、および t.text.Lookup(name)nil であるかどうかを比較していました。t.text.Lookup(name) は、基盤となる text/template のテンプレートが存在するかどうかを確認します。

// 変更前
if (tmpl == nil) != (t.text.Lookup(name) == nil) {
    panic("html/template internal error: template escaping out of sync")
}

この条件式は、html/template の内部状態と text/template の内部状態が同期していることを確認するためのものでした。しかし、Issue #3272で報告されたケースのように、New().ParseFiles(os.DevNull) のようにテンプレートが不完全にパースされた場合、tmpl 自体は nil ではないが、その内部の tmpl.text.Treetmpl.text.Rootnil になる可能性がありました。この状態では、上記の if 文の条件は満たされず、パニックが発生しないまま、後続の処理で tmpl.text.Treetmpl.text.Root にアクセスしようとしてnilポインタパニックが発生していました。

変更後のコードでは、このロジックがより堅牢になっています。

// 変更後
if tmpl == nil {
    return nil, fmt.Errorf("html/template: %q is undefined", name)
}
if tmpl.text.Tree == nil || tmpl.text.Root == nil {
    return nil, fmt.Errorf("html/template: %q is an incomplete template", name)
}
if t.text.Lookup(name) == nil {
    panic("html/template internal error: template escaping out of sync")
}
  1. if tmpl == nil: まず、指定された名前のテンプレート tmpl が全く存在しない(nil である)場合に、明確なエラーメッセージ "html/template: %q is undefined" を返します。これにより、nilポインタパニックを未然に防ぎます。
  2. if tmpl.text.Tree == nil || tmpl.text.Root == nil: 次に、テンプレートオブジェクト tmpl は存在するものの、その内部の text.Tree または text.Rootnil である場合(つまり、テンプレートが不完全にパースされた状態)に、"html/template: %q is an incomplete template" というエラーを返します。これがIssue #3272で報告されたnilポインタパニックの直接の原因となっていた状況を捕捉し、実行時パニックではなく、より扱いやすいエラーを返すように修正しています。
  3. if t.text.Lookup(name) == nil: 最後に、元の panic を引き起こす条件が残されています。これは、html/template の内部状態と text/template の内部状態の同期が取れていないという、より深刻な内部エラーを示すものです。この条件は、通常の使用では発生しないはずの、ライブラリ自体のバグを示唆するため、パニックとして残されています。

これらの変更により、ExecuteTemplate が呼び出された際に、テンプレートが存在しない場合や不完全な場合に、より適切なエラーが返されるようになり、nilポインタパニックが回避されます。

エラーメッセージの統一

parseFiles および parseGlob 関数において、エラーメッセージのプレフィックスが "template: " から "html/template: " に変更されています。

  • parseFiles: template: no files named in call to ParseFileshtml/template: no files named in call to ParseFiles
  • parseGlob: template: pattern matches no files: %#qhtml/template: pattern matches no files: %#q

これは、html/template パッケージから発生するエラーメッセージの一貫性を保つための小さな改善です。これにより、ユーザーはエラーがどのパッケージから発生したのかをより明確に識別できるようになります。

テストケースの追加

src/pkg/html/template/escape_test.goTestEmptyTemplate という新しいテストケースが追加されています。

// This is a test for issue 3272.
func TestEmptyTemplate(t *testing.T) {
    page := Must(New("page").ParseFiles(os.DevNull))
    if err := page.ExecuteTemplate(os.Stdout, "page", "nothing"); err == nil {
        t.Fatal("expected error")
    }
}

このテストは、Issue #3272で報告されたシナリオを再現します。

  1. Must(New("page").ParseFiles(os.DevNull)) を使用して、実質的に空のテンプレートをパースします。os.DevNull は内容がないため、page テンプレートは作成されますが、その内部の構文木は構築されません。
  2. page.ExecuteTemplate(os.Stdout, "page", "nothing") を呼び出し、この不完全なテンプレートを実行しようとします。
  3. 変更前であればここでパニックが発生していましたが、変更後は lookupAndEscapeTemplate 関数によってエラーが返されるようになります。
  4. テストは、ExecuteTemplate がエラーを返すことを期待しており、エラーが返されなかった場合は t.Fatal("expected error") でテストを失敗させます。これにより、nilポインタパニックが修正され、適切なエラーが返されるようになったことを検証しています。

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

src/pkg/html/template/escape_test.go

--- a/src/pkg/html/template/escape_test.go
+++ b/src/pkg/html/template/escape_test.go
@@ -8,6 +8,7 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
+	"os"
 	"strings"
 	"testing"
 	"text/template"
@@ -1637,6 +1638,14 @@ func TestIndirectPrint(t *testing.T) {
 	}
 }
 
+// This is a test for issue 3272.
+func TestEmptyTemplate(t *testing.T) {
+	page := Must(New("page").ParseFiles(os.DevNull))
+	if err := page.ExecuteTemplate(os.Stdout, "page", "nothing"); err == nil {
+		t.Fatal("expected error")
+	}
+}
+
 func BenchmarkEscapedExecute(b *testing.B) {
 	tmpl := Must(New("t").Parse(`<a onclick="alert('{{.}}')"></a>`))
 	var buf bytes.Buffer

src/pkg/html/template/template.go

--- a/src/pkg/html/template/template.go
+++ b/src/pkg/html/template/template.go
@@ -64,7 +64,13 @@ func (t *Template) lookupAndEscapeTemplate(name string) (tmpl *Template, err err
 	t.nameSpace.mu.Lock()
 	defer t.nameSpace.mu.Unlock()
 	tmpl = t.set[name]
-	if (tmpl == nil) != (t.text.Lookup(name) == nil) {
+	if tmpl == nil {
+		return nil, fmt.Errorf("html/template: %q is undefined", name)
+	}
+	if tmpl.text.Tree == nil || tmpl.text.Root == nil {
+		return nil, fmt.Errorf("html/template: %q is an incomplete template", name)
+	}
+	if t.text.Lookup(name) == nil {
 		panic("html/template internal error: template escaping out of sync")
 	}
 	if tmpl != nil && !tmpl.escaped {
@@ -276,7 +282,7 @@ func (t *Template) ParseFiles(filenames ...string) (*Template, error) {
 func parseFiles(t *Template, filenames ...string) (*Template, error) {
 	if len(filenames) == 0 {
 		// Not really a problem, but be consistent.
-		return nil, fmt.Errorf("template: no files named in call to ParseFiles")
+		return nil, fmt.Errorf("html/template: no files named in call to ParseFiles")
 	}
 	for _, filename := range filenames {
 		b, err := ioutil.ReadFile(filename)
@@ -333,7 +339,7 @@ func parseGlob(t *Template, pattern string) (*Template, error) {
 		return nil, err
 	}
 	if len(filenames) == 0 {
-		return nil, fmt.Errorf("template: pattern matches no files: %#q", pattern)
+		return nil, fmt.Errorf("html/template: pattern matches no files: %#q", pattern)
 	}
 	return parseFiles(t, filenames...)
 }

コアとなるコードの解説

src/pkg/html/template/template.go の変更点

lookupAndEscapeTemplate 関数は、テンプレートの実行時に指定された名前のテンプレートを検索し、そのエスケープ状態を管理する重要な役割を担っています。

変更前は、if (tmpl == nil) != (t.text.Lookup(name) == nil) という単一の条件で内部エラーをチェックしていました。これは、html/template のテンプレートオブジェクト (tmpl) の存在と、その基盤となる text/template のテンプレート (t.text.Lookup(name)) の存在が一致しない場合にパニックを引き起こすものでした。しかし、このロジックでは、tmpl オブジェクト自体は存在するものの、その内部状態(特に tmpl.text.Treetmpl.text.Root といった構文木を表すフィールド)が不完全な場合に発生するnilポインタ参照を捕捉できませんでした。

変更後は、このチェックがより詳細かつ段階的に行われるようになりました。

  1. if tmpl == nil:

    • これは最も基本的なチェックで、指定された name のテンプレートが t.set マップ(テンプレートの名前空間)に全く登録されていない場合に真となります。
    • この場合、fmt.Errorf("html/template: %q is undefined", name) を返します。これは、テンプレートが存在しないことを明確に伝えるエラーメッセージであり、呼び出し元がこのエラーを適切に処理できるようになります。nilポインタパニックは発生しません。
  2. if tmpl.text.Tree == nil || tmpl.text.Root == nil:

    • この条件が今回のnilポインタバグ修正の核心です。
    • tmpl 自体は nil ではないが、その内部の text.Tree または text.Rootnil である場合に真となります。これは、ParseFiles(os.DevNull) のように、テンプレートがパースされたものの、その内容が空であるか、あるいは何らかの理由で構文木が正常に構築されなかった場合に発生します。
    • このような「不完全なテンプレート」に対して ExecuteTemplate が呼び出されると、以前は tmpl.text.Treetmpl.text.Root へのアクセスでnilポインタパニックが発生していました。
    • この変更により、fmt.Errorf("html/template: %q is an incomplete template", name) というエラーが返され、パニックが回避されます。
  3. if t.text.Lookup(name) == nil:

    • これは元のコードの panic 条件をほぼそのまま残したものです。
    • html/templatetmpl は存在するが、基盤となる text/templateLookupnil を返すという状況は、html/templatetext/template の内部状態が同期していないことを示します。これはライブラリの内部的な不整合であり、通常の使用では発生しないはずの深刻なバグであるため、引き続き panic を発生させて開発者に問題を知らせるようにしています。

parseFiles および parseGlob 関数におけるエラーメッセージの変更は、単にエラーメッセージのプレフィックスを "template: " から "html/template: " に変更したものです。これは機能的な変更ではなく、html/template パッケージから発生するエラーメッセージの一貫性を向上させるためのものです。これにより、ユーザーはエラーがどのGoパッケージから発生したのかをより迅速に特定できます。

src/pkg/html/template/escape_test.go の変更点

TestEmptyTemplate テストケースは、修正された lookupAndEscapeTemplate 関数の動作を検証するために追加されました。

  • page := Must(New("page").ParseFiles(os.DevNull))

    • New("page") で新しい html/template オブジェクトを作成します。
    • ParseFiles(os.DevNull) は、/dev/null という特殊なファイル(内容が常に空)をパースしようとします。これにより、page という名前のテンプレートオブジェクトは作成されますが、その内部の text.Treetext.Rootnil のままになります。
    • Must ヘルパー関数は、エラーが発生した場合にパニックを引き起こしますが、ParseFiles(os.DevNull) はファイルが存在しないわけではないため、ここではエラーは発生しません。
  • if err := page.ExecuteTemplate(os.Stdout, "page", "nothing"); err == nil

    • page.ExecuteTemplate を呼び出し、"page" という名前のテンプレートを実行しようとします。
    • 修正前は、この呼び出しが lookupAndEscapeTemplate 内でnilポインタパニックを引き起こしていました。
    • 修正後は、lookupAndEscapeTemplateif tmpl.text.Tree == nil || tmpl.text.Root == nil 条件が真となり、"html/template: \"page\" is an incomplete template" というエラーが返されるようになります。
    • テストは、ExecuteTemplate がエラーを返すことを期待しているため、err == nil であれば t.Fatal("expected error") を呼び出してテストを失敗させます。これにより、パニックではなくエラーが返されるようになったことを確認します。

このテストケースの追加により、特定の条件下でのnilポインタバグが修正され、より予測可能なエラーハンドリングが提供されるようになったことが保証されます。

関連リンク

  • Go言語の html/template パッケージのドキュメント: https://pkg.go.dev/html/template
  • Go言語の text/template パッケージのドキュメント: https://pkg.go.dev/text/template
  • Go言語のIssue #3272: html/template: ExecuteTemplate on empty template panics (このコミットが修正したIssue) - 検索しても直接的なリンクは見つかりませんでしたが、GoのIssueトラッカーで検索すると関連情報が見つかる可能性があります。
  • Goのコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージにある https://golang.org/cl/5819046 はGerritの変更リストへのリンクです)

参考にした情報源リンク

  • コミット情報: /home/orange/Project/comemo/commit_data/12627.txt
  • GitHubコミットページ: https://github.com/golang/go/commit/214a1ca3c5d7f8d633587cf6faff2868f341b31b
  • Go言語の公式ドキュメント
  • Go言語のソースコード (特に src/pkg/html/template/template.gosrc/pkg/html/template/escape_test.go)
  • Go言語のIssueトラッカー (Issue #3272に関する情報)
  • Go言語のnilポインタとパニックに関する一般的な情報
  • os.DevNull に関するGo言語のドキュメント