[インデックス 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.Tree
や text.Root
といった重要な構造体がnilのままであるために発生していました。
このバグは、開発者がテンプレートのロードやパースの失敗を適切にハンドリングしない場合に、実行時エラーとして顕在化し、アプリケーションの安定性を損なう可能性がありました。したがって、このコミットは、このような状況下での堅牢性を向上させることを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念と html/template
パッケージの基本的な動作について理解しておく必要があります。
Go言語の text/template
および html/template
パッケージ
Go言語には、テキストベースのテンプレートを扱う text/template
パッケージと、HTMLコンテンツを安全に生成するための html/template
パッケージがあります。html/template
は text/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
などから呼び出されます。
変更前のコードでは、テンプレート tmpl
が nil
であるかどうか、および 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.Tree
や tmpl.text.Root
が nil
になる可能性がありました。この状態では、上記の if
文の条件は満たされず、パニックが発生しないまま、後続の処理で tmpl.text.Tree
や tmpl.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")
}
if tmpl == nil
: まず、指定された名前のテンプレートtmpl
が全く存在しない(nil
である)場合に、明確なエラーメッセージ"html/template: %q is undefined"
を返します。これにより、nilポインタパニックを未然に防ぎます。if tmpl.text.Tree == nil || tmpl.text.Root == nil
: 次に、テンプレートオブジェクトtmpl
は存在するものの、その内部のtext.Tree
またはtext.Root
がnil
である場合(つまり、テンプレートが不完全にパースされた状態)に、"html/template: %q is an incomplete template"
というエラーを返します。これがIssue #3272で報告されたnilポインタパニックの直接の原因となっていた状況を捕捉し、実行時パニックではなく、より扱いやすいエラーを返すように修正しています。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 ParseFiles
→html/template: no files named in call to ParseFiles
parseGlob
:template: pattern matches no files: %#q
→html/template: pattern matches no files: %#q
これは、html/template
パッケージから発生するエラーメッセージの一貫性を保つための小さな改善です。これにより、ユーザーはエラーがどのパッケージから発生したのかをより明確に識別できるようになります。
テストケースの追加
src/pkg/html/template/escape_test.go
に TestEmptyTemplate
という新しいテストケースが追加されています。
// 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で報告されたシナリオを再現します。
Must(New("page").ParseFiles(os.DevNull))
を使用して、実質的に空のテンプレートをパースします。os.DevNull
は内容がないため、page
テンプレートは作成されますが、その内部の構文木は構築されません。page.ExecuteTemplate(os.Stdout, "page", "nothing")
を呼び出し、この不完全なテンプレートを実行しようとします。- 変更前であればここでパニックが発生していましたが、変更後は
lookupAndEscapeTemplate
関数によってエラーが返されるようになります。 - テストは、
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.Tree
や tmpl.text.Root
といった構文木を表すフィールド)が不完全な場合に発生するnilポインタ参照を捕捉できませんでした。
変更後は、このチェックがより詳細かつ段階的に行われるようになりました。
-
if tmpl == nil
:- これは最も基本的なチェックで、指定された
name
のテンプレートがt.set
マップ(テンプレートの名前空間)に全く登録されていない場合に真となります。 - この場合、
fmt.Errorf("html/template: %q is undefined", name)
を返します。これは、テンプレートが存在しないことを明確に伝えるエラーメッセージであり、呼び出し元がこのエラーを適切に処理できるようになります。nilポインタパニックは発生しません。
- これは最も基本的なチェックで、指定された
-
if tmpl.text.Tree == nil || tmpl.text.Root == nil
:- この条件が今回のnilポインタバグ修正の核心です。
tmpl
自体はnil
ではないが、その内部のtext.Tree
またはtext.Root
がnil
である場合に真となります。これは、ParseFiles(os.DevNull)
のように、テンプレートがパースされたものの、その内容が空であるか、あるいは何らかの理由で構文木が正常に構築されなかった場合に発生します。- このような「不完全なテンプレート」に対して
ExecuteTemplate
が呼び出されると、以前はtmpl.text.Tree
やtmpl.text.Root
へのアクセスでnilポインタパニックが発生していました。 - この変更により、
fmt.Errorf("html/template: %q is an incomplete template", name)
というエラーが返され、パニックが回避されます。
-
if t.text.Lookup(name) == nil
:- これは元のコードの
panic
条件をほぼそのまま残したものです。 html/template
のtmpl
は存在するが、基盤となるtext/template
のLookup
がnil
を返すという状況は、html/template
とtext/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.Tree
やtext.Root
はnil
のままになります。Must
ヘルパー関数は、エラーが発生した場合にパニックを引き起こしますが、ParseFiles(os.DevNull)
はファイルが存在しないわけではないため、ここではエラーは発生しません。
-
if err := page.ExecuteTemplate(os.Stdout, "page", "nothing"); err == nil
page.ExecuteTemplate
を呼び出し、"page"
という名前のテンプレートを実行しようとします。- 修正前は、この呼び出しが
lookupAndEscapeTemplate
内でnilポインタパニックを引き起こしていました。 - 修正後は、
lookupAndEscapeTemplate
のif 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.go
とsrc/pkg/html/template/escape_test.go
) - Go言語のIssueトラッカー (Issue #3272に関する情報)
- Go言語のnilポインタとパニックに関する一般的な情報
os.DevNull
に関するGo言語のドキュメント