[インデックス 10559] ファイルの概要
このコミットは、Go言語の標準ライブラリである html/template
パッケージにおいて、テンプレートの実行がスレッドセーフになるように修正を加えるものです。具体的には、テンプレートの実行中にテンプレート自体が変更される可能性があるという問題に対処し、text/template
パッケージと同様のスレッドセーフな保証を提供することを目指しています。
コミット
commit 9a86e244bf9041926e03610319474a149356fa2d
Author: Rob Pike <r@golang.org>
Date: Wed Nov 30 20:11:57 2011 -0800
html/template: make execution thread-safe
The problem is that execution can modify the template, so it needs
interlocking to have the same thread-safe guarantee as text/template.
Fixes #2439.
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/5450056
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9a86e244bf9041926e03610319474a149356fa2d
元コミット内容
html/template: make execution thread-safe
The problem is that execution can modify the template, so it needs
interlocking to have the same thread-safe guarantee as text/template.
Fixes #2439.
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/5450056
変更の背景
Go言語の html/template
パッケージは、HTML出力の生成を安全に行うためのテンプレートエンジンです。このパッケージは、クロスサイトスクリプティング(XSS)などの脆弱性を防ぐために、自動エスケープ機能を提供します。
このコミットがなされた背景には、html/template
の実行がスレッドセーフではないという問題がありました。具体的には、複数のゴルーチン(Goにおける軽量スレッド)が同時に同じテンプレートを実行しようとした際に、テンプレートの内部状態が変更される可能性があり、これにより競合状態(Race Condition)が発生し、予期せぬ動作やデータ破損につながる恐れがありました。
コミットメッセージにある「execution can modify the template」という記述は、テンプレートがパースされた後も、実行時に内部的なエスケープ処理やコンテキスト計算などが行われ、その結果としてテンプレートオブジェクト自体が変更される可能性があることを示唆しています。このような変更が複数のゴルーチンから同時に行われると、一貫性のない状態に陥る危険性があります。
この問題は、Issue #2439として報告されており、このコミットはその問題を解決するために導入されました。目標は、text/template
パッケージが提供するスレッドセーフな保証を html/template
でも実現することでした。text/template
は、より汎用的なテキスト出力のためのテンプレートエンジンであり、その設計はスレッドセーフであることを前提としていました。
前提知識の解説
Go言語のテンプレートパッケージ (text/template
と html/template
)
text/template
: Go言語の標準ライブラリで提供される汎用的なテキストテンプレートエンジンです。任意のテキスト形式の出力を生成するために使用されます。例えば、設定ファイル、コード生成、プレーンテキストのレポートなどに利用できます。html/template
:text/template
をベースにしており、特にHTML出力を安全に生成するために設計されています。主な特徴は、自動エスケープ機能です。これにより、テンプレートに挿入されるデータが自動的にエスケープされ、クロスサイトスクリプティング(XSS)攻撃などのWebセキュリティ脆弱性を防ぎます。例えば、ユーザーが入力した文字列をそのままHTMLに出力すると、悪意のあるスクリプトが埋め込まれる可能性がありますが、html/template
はこれを自動的に無害化します。
スレッドセーフティと競合状態 (Race Condition)
- スレッドセーフティ (Thread Safety): 複数のスレッド(またはゴルーチン)から同時にアクセスされたり、操作されたりしても、プログラムが正しく動作し、データの一貫性が保たれる性質を指します。
- 競合状態 (Race Condition): 複数のスレッドが共有リソース(この場合はテンプレートオブジェクトの内部状態)に同時にアクセスし、そのアクセス順序によって結果が変わってしまう状態を指します。競合状態は、プログラムの予測不能な動作やバグの原因となります。
- ミューテックス (Mutex): Mutual Exclusion(相互排他)の略で、共有リソースへのアクセスを制御するための同期プリミティブです。ミューテックスは、一度に一つのスレッドだけが特定のコードセクション(クリティカルセクション)を実行できるようにロックをかけます。これにより、複数のスレッドが同時に共有リソースを変更することを防ぎ、競合状態を回避してスレッドセーフティを確保します。Go言語では
sync.Mutex
型として提供されます。
エスケープ処理
html/template
におけるエスケープ処理は、テンプレートが実行される際に、出力されるデータがHTMLの文脈において安全であることを保証するためのプロセスです。例えば、{{.UserComment}}
のようにユーザー入力を表示する際に、ユーザーが <script>alert('XSS')</script>
のような悪意のあるHTMLタグを入力した場合、エスケープ処理によってこれが <script>alert('XSS')</script>
のように変換され、ブラウザがスクリプトとして実行するのを防ぎます。このエスケープ処理は、テンプレートの実行時に動的に行われることがあり、その過程でテンプレートの内部状態(例えば、エスケープ済みかどうかのフラグや、コンテキスト情報など)が更新される可能性があります。
技術的詳細
このコミットの主要な目的は、html/template
の Template
オブジェクトが複数のゴルーチンから同時に実行された際に発生する可能性のある競合状態を解消し、スレッドセーフな操作を保証することです。
変更の核心は、テンプレートの関連付けられたセット(set
フィールド)と、エスケープ状態(escaped
フィールド)へのアクセスを同期させるためのミューテックス (sync.Mutex
) の導入です。
nameSpace
構造体の導入
以前は、Template
構造体は set *map[string]*Template
という形で、関連するテンプレートのマップへのポインタを持っていました。このマップは、複数の Template
オブジェクト間で共有され、テンプレートのルックアップや追加が行われる際に使用されていました。
このコミットでは、この共有されるマップと、それに付随する同期メカニズムをカプセル化するために、新たに nameSpace
構造体が導入されました。
type nameSpace struct {
mu sync.Mutex
set map[string]*Template
}
mu sync.Mutex
: このミューテックスが、set
マップへのアクセスを保護します。これにより、複数のゴルーチンが同時にset
マップを読み書きしようとした際に、競合状態が発生するのを防ぎます。set map[string]*Template
: 関連するテンプレートの名前とTemplate
オブジェクトのマッピングを保持します。
Template
構造体は、*nameSpace
を埋め込む形に変更されました。
type Template struct {
// ...
text *template.Template
*nameSpace // common to all associated templates
}
これにより、同じテンプレートセットに属するすべての Template
オブジェクトが、共通の nameSpace
インスタンスを共有し、その中のミューテックスを使って同期を取ることができるようになります。
ミューテックスによる同期の適用
以下のメソッドで nameSpace.mu
ミューテックスが使用され、共有状態へのアクセスが保護されています。
-
Template.Execute
メソッド: テンプレートの実行前にt.nameSpace.mu.Lock()
でロックを取得し、エスケープ処理 (escapeTemplates
) が完了した後にロックを解放します。これにより、エスケープ処理中にテンプレートの内部状態が変更されることを防ぎます。func (t *Template) Execute(wr io.Writer, data interface{}) (err error) { t.nameSpace.mu.Lock() if !t.escaped { if err = escapeTemplates(t, t.Name()); err != nil { t.escaped = true } } t.nameSpace.mu.Unlock() if err != nil { return } return t.text.Execute(wr, data) }
-
Template.ExecuteTemplate
メソッド: 指定された名前のテンプレートを実行する前にロックを取得し、テンプレートのルックアップとエスケープ処理が完了した後にロックを解放します。func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) (err error) { t.nameSpace.mu.Lock() tmpl := t.set[name] // Access to shared 'set' map if tmpl == nil { t.nameSpace.mu.Unlock() return fmt.Errorf("template: no template %q associated with template %q", name, t.Name()) } if !tmpl.escaped { err = escapeTemplates(tmpl, name) } t.nameSpace.mu.Unlock() if err != nil { return } return tmpl.text.ExecuteTemplate(wr, name, data) }
-
Template.Parse
メソッド: 新しいテンプレートをパースし、既存のテンプレートセットを更新する際にロックを取得します。これにより、set
マップの変更がスレッドセーフに行われます。func (t *Template) Parse(src string) (*Template, error) { t.nameSpace.mu.Lock() // Lock for 'escaped' field t.escaped = false t.nameSpace.mu.Unlock() ret, err := t.text.Parse(src) // ... t.nameSpace.mu.Lock() // Lock for 'set' map updates defer t.nameSpace.mu.Unlock() for _, v := range ret.Templates() { name := v.Name() tmpl := t.set[name] // Access to shared 'set' map if tmpl == nil { tmpl = t.new(name) // Calls internal 'new' which also uses the lock } tmpl.escaped = false tmpl.text = v } return t, nil }
-
Template.New
およびTemplate.new
メソッド: 新しいテンプレートを作成し、現在のテンプレートセットに追加する際にロックを取得します。Template.New
は外部から呼び出されるメソッドでロックを取得し、内部のTemplate.new
はロックなしで実際の処理を行います。func (t *Template) New(name string) *Template { t.nameSpace.mu.Lock() defer t.nameSpace.mu.Unlock() return t.new(name) } func (t *Template) new(name string) *Template { tmpl := &Template{ false, t.text.New(name), t.nameSpace, // Shares the same nameSpace } tmpl.set[name] = tmpl // Access to shared 'set' map return tmpl }
-
Template.Lookup
メソッド: 名前でテンプレートをルックアップする際にロックを取得します。func (t *Template) Lookup(name string) *Template { t.nameSpace.mu.Lock() defer t.nameSpace.mu.Unlock() return t.set[name] // Access to shared 'set' map }
escape.go
の変更
escape.go
では、escapeTemplates
関数内でテンプレートをルックアップする際に、tmpl.Lookup(name)
から tmpl.set[name]
へと直接 set
マップにアクセスするように変更されています。これは、Lookup
メソッド自体がミューテックスを使用するように変更されたため、escapeTemplates
内で二重にロックを取得するのを避けるため、またはより直接的なアクセスを意図した変更と考えられます。
また、コメントの修正も行われています。
--- a/src/pkg/html/template/escape.go
+++ b/src/pkg/html/template/escape.go
@@ -32,7 +32,7 @@ func escapeTemplates(tmpl *Template, names ...string) error {
if err != nil {
// Prevent execution of unsafe templates.
for _, name := range names {
- if t := tmpl.Lookup(name); t != nil {
+ if t := tmpl.set[name]; t != nil {
t.text.Tree = nil
}
}
@@ -520,7 +520,7 @@ func (e *escaper) computeOutCtx(c context, t *template.Template) context {
if !ok && c1.state != stateError {
return context{
state: stateError,
- // TODO: Find the first node with a line in t.Tree.Root
+ // TODO: Find the first node with a line in t.text.Tree.Root
err: errorf(ErrOutputContext, 0, "cannot compute output context for template %s", t.Name()),
}
}
template.go
の変更
template.go
は、html/template
パッケージの主要なロジックが含まれるファイルです。
sync
パッケージのインポート: ミューテックスを使用するためにsync
パッケージがインポートされました。nameSpace
構造体の定義と埋め込み: 前述の通り、nameSpace
構造体が定義され、Template
構造体に埋め込まれました。Execute
メソッドの変更: 以前はTemplate
構造体自身がescaped
フラグを持っていましたが、スレッドセーフなエスケープ処理のためにミューテックスが追加されました。ExecuteTemplate
メソッドの変更:Lookup
の代わりにt.set[name]
を直接使用し、ミューテックスで保護されるようになりました。Parse
メソッドの変更: テンプレートのパースとセットの更新がミューテックスで保護されるようになりました。New
関数とTemplate.New
メソッドの変更: 新しいテンプレートの作成とセットへの追加がミューテックスで保護されるようになりました。特にTemplate.New
は、内部的にロックなしのTemplate.new
を呼び出す形に変更されました。Lookup
メソッドの変更:Lookup
メソッド自体がミューテックスで保護されるようになりました。
これらの変更により、html/template
パッケージは、複数のゴルーチンから同時にテンプレートのパース、実行、ルックアップ、および関連するテンプレートの追加が行われても、内部状態の一貫性が保たれるようになりました。
コアとなるコードの変更箇所
src/pkg/html/template/template.go
--- a/src/pkg/html/template/template.go
+++ b/src/pkg/html/template/template.go
@@ -9,6 +9,7 @@ import (
"io"
"io/ioutil"
"path/filepath"
+ "sync"
"text/template"
)
@@ -19,22 +20,47 @@ type Template struct {
// We could embed the text/template field, but it's safer not to because
// we need to keep our version of the name space and the underlying
// template's in sync.
- text *template.Template
- // Templates are grouped by sharing the set, a pointer.
- set *map[string]*Template
+ text *template.Template
+ *nameSpace // common to all associated templates
+}
+
+// nameSpace is the data structure shared by all templates in an association.
+type nameSpace struct {
+ mu sync.Mutex
+ set map[string]*Template
+}
+
+// Execute applies a parsed template to the specified data object,
+// writing the output to wr.
+func (t *Template) Execute(wr io.Writer, data interface{}) (err error) {
+ t.nameSpace.mu.Lock()
+ if !t.escaped {
+ if err = escapeTemplates(t, t.Name()); err != nil {
+ t.escaped = true
+ }
+ }
+ t.nameSpace.mu.Unlock()
+ if err != nil {
+ return
+ }
+ return t.text.Execute(wr, data)
}
// ExecuteTemplate applies the template associated with t that has the given name
// to the specified data object and writes the output to wr.
-func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error {
- tmpl := t.Lookup(name)
+func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) (err error) {
+ t.nameSpace.mu.Lock()
+ tmpl := t.set[name]
if tmpl == nil {
+ t.nameSpace.mu.Unlock()
return fmt.Errorf("template: no template %q associated with template %q", name, t.Name())
}
if !tmpl.escaped {
- if err := escapeTemplates(tmpl, name); err != nil { // TODO: make a method of set?
- return err
- }
+ err = escapeTemplates(tmpl, name)
+ }
+ t.nameSpace.mu.Unlock()
+ if err != nil {
+ return
}
return tmpl.text.ExecuteTemplate(wr, name, data)
}
@@ -44,7 +70,9 @@ func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{})
// to the set. If a template is redefined, the element in the set is
// overwritten with the new definition.\n func (t *Template) Parse(src string) (*Template, error) {
+\tt.nameSpace.mu.Lock()\n \tt.escaped = false
+\tt.nameSpace.mu.Unlock()\n \tret, err := t.text.Parse(src)
\tif err != nil {
\t\treturn nil, err
@@ -52,11 +80,13 @@ func (t *Template) Parse(src string) (*Template, error) {
\t// In general, all the named templates might have changed underfoot.\n \t// Regardless, some new ones may have been defined.\n \t// The template.Template set has been updated; update ours.\n+\tt.nameSpace.mu.Lock()\n+\tdefer t.nameSpace.mu.Unlock()\n \tfor _, v := range ret.Templates() {\n \t\tname := v.Name()\n-\t\ttmpl := t.Lookup(name)\n+\t\ttmpl := t.set[name]\n \t\tif tmpl == nil {\n-\t\t\ttmpl = t.New(name)\n+\t\t\ttmpl = t.new(name)\n \t\t}\n \t\ttmpl.escaped = false
\t\ttmpl.text = v
@@ -64,18 +94,6 @@ func (t *Template) Parse(src string) (*Template, error) {
\treturn t, nil
}\n \n-// Execute applies a parsed template to the specified data object,\n-// writing the output to wr.\n-func (t *Template) Execute(wr io.Writer, data interface{}) error {\n-\tif !t.escaped {\n-\t\tif err := escapeTemplates(t, t.Name()); err != nil {\n-\t\t\treturn err\n-\t\t}\n-\t\tt.escaped = true\n-\t}\n-\treturn t.text.Execute(wr, data)\n-}\n-\n // Add is unimplemented.\n func (t *Template) Add(*Template) error {\n \treturn fmt.Errorf("html/template: Add unimplemented")\n@@ -88,13 +106,14 @@ func (t *Template) Clone(name string) error {\n \n // New allocates a new HTML template with the given name.\n func New(name string) *Template {\n-\tset := make(map[string]*Template)\n \ttmpl := &Template{\n \t\tfalse,\n \t\ttemplate.New(name),\n-\t\t&set,\n+\t\t&nameSpace{\n+\t\t\tset: make(map[string]*Template),\n+\t\t},\n \t}\n-\t(*tmpl.set)[name] = tmpl\n+\ttmpl.set[name] = tmpl\n \treturn tmpl\n }\n \n@@ -102,12 +121,19 @@ func New(name string) *Template {\n // and with the same delimiters. The association, which is transitive,\n // allows one template to invoke another with a {{template}} action.\n func (t *Template) New(name string) *Template {\n+\tt.nameSpace.mu.Lock()\n+\tdefer t.nameSpace.mu.Unlock()\n+\treturn t.new(name)\n+}\n+\n+// new is the implementation of New, without the lock.\n+func (t *Template) new(name string) *Template {\n \ttmpl := &Template{\n \t\tfalse,\n \t\tt.text.New(name),\n-\t\tt.set,\n+\t\tt.nameSpace,\n \t}\n-\t(*tmpl.set)[name] = tmpl\n+\ttmpl.set[name] = tmpl\n \treturn tmpl\n }\n \n@@ -138,7 +164,9 @@ func (t *Template) Delims(left, right string) *Template {\n // Lookup returns the template with the given name that is associated with t,\n // or nil if there is no such template.\n func (t *Template) Lookup(name string) *Template {\n-\treturn (*t.set)[name]\n+\tt.nameSpace.mu.Lock()\n+\tdefer t.nameSpace.mu.Unlock()\n+\treturn t.set[name]\n }\n \n // Must panics if err is non-nil in the same way as template.Must.\n```
### `src/pkg/html/template/escape.go`
```diff
--- a/src/pkg/html/template/escape.go
+++ b/src/pkg/html/template/escape.go
@@ -32,7 +32,7 @@ func escapeTemplates(tmpl *Template, names ...string) error {
if err != nil {
// Prevent execution of unsafe templates.
for _, name := range names {
- if t := tmpl.Lookup(name); t != nil {
+ if t := tmpl.set[name]; t != nil {
t.text.Tree = nil
}
}
@@ -520,7 +520,7 @@ func (e *escaper) computeOutCtx(c context, t *template.Template) context {
if !ok && c1.state != stateError {
return context{
state: stateError,
- // TODO: Find the first node with a line in t.Tree.Root
+ // TODO: Find the first node with a line in t.text.Tree.Root
err: errorf(ErrOutputContext, 0, "cannot compute output context for template %s", t.Name()),
}
}
コアとなるコードの解説
template.go
の変更点
-
sync
パッケージのインポート:import "sync"
が追加され、sync.Mutex
を使用できるようになりました。 -
nameSpace
構造体の導入:type nameSpace struct { mu sync.Mutex; set map[string]*Template }
が定義されました。これは、複数のテンプレート間で共有される状態(テンプレートのマップset
)と、その状態を保護するためのミューテックスmu
をカプセル化します。 -
Template
構造体の変更:text *template.Template
の下に*nameSpace
が埋め込まれました。これにより、Template
オブジェクトは、関連するすべてのテンプレートが共有するnameSpace
インスタンスにアクセスできるようになります。以前のset *map[string]*Template
は削除されました。 -
Template.Execute
メソッドの変更:- メソッドシグネチャが
(err error)
を返すように変更されました。 t.nameSpace.mu.Lock()
とt.nameSpace.mu.Unlock()
が追加され、テンプレートのエスケープ処理 (escapeTemplates
) がミューテックスによって保護されるようになりました。これにより、複数のゴルーチンが同時にExecute
を呼び出しても、エスケープ処理中のテンプレートの内部状態の変更が競合しないように保証されます。- 以前の
t.escaped = true
の行は、escapeTemplates
がエラーを返した場合にのみt.escaped = true
となるように変更されました。
- メソッドシグネチャが
-
Template.ExecuteTemplate
メソッドの変更:- メソッドシグネチャが
(err error)
を返すように変更されました。 t.Lookup(name)
の呼び出しがtmpl := t.set[name]
に直接変更されました。これは、Lookup
メソッド自体がミューテックスで保護されるようになったため、ExecuteTemplate
内で二重にロックを取得するのを避けるため、またはより直接的なアクセスを意図した変更と考えられます。t.nameSpace.mu.Lock()
とt.nameSpace.mu.Unlock()
が追加され、テンプレートのルックアップとエスケープ処理が保護されるようになりました。
- メソッドシグネチャが
-
Template.Parse
メソッドの変更:t.nameSpace.mu.Lock()
とt.nameSpace.mu.Unlock()
が追加され、t.escaped
フラグの更新が保護されるようになりました。ret.Templates()
から取得したテンプレートをt.set
に追加するループ全体がt.nameSpace.mu.Lock()
とdefer t.nameSpace.mu.Unlock()
で保護されるようになりました。これにより、テンプレートセットの更新がスレッドセーフに行われます。tmpl = t.New(name)
の呼び出しがtmpl = t.new(name)
に変更されました。これは、Template.New
がミューテックスを持つため、内部的にロックなしのnew
メソッドを呼び出すように変更されたためです。
-
New
関数とTemplate.New
メソッドの変更:New
関数では、nameSpace
の新しいインスタンスが作成され、その中にset
マップとミューテックスが初期化されます。Template.New
メソッドは、t.nameSpace.mu.Lock()
とdefer t.nameSpace.mu.Unlock()
を追加し、内部的にロックなしのt.new(name)
メソッドを呼び出すように変更されました。これにより、新しいテンプレートの作成と既存のセットへの追加がスレッドセーフに行われます。Template.new
メソッドは、Template
オブジェクトが既存のnameSpace
を共有するように変更されました。
-
Template.Lookup
メソッドの変更:t.nameSpace.mu.Lock()
とdefer t.nameSpace.mu.Unlock()
が追加され、テンプレートのルックアップがミューテックスで保護されるようになりました。
escape.go
の変更点
-
escapeTemplates
関数内のルックアップ変更:if t := tmpl.Lookup(name); t != nil {
の行がif t := tmpl.set[name]; t != nil {
に変更されました。これは、Lookup
メソッドがミューテックスで保護されるようになったため、escapeTemplates
関数内で二重にロックを取得するのを避けるための最適化、またはより直接的なアクセスを意図した変更と考えられます。 -
コメントの修正:
// TODO: Find the first node with a line in t.Tree.Root
が// TODO: Find the first node with a line in t.text.Tree.Root
に修正されました。これは、Template
構造体のtext
フィールドがtext/template.Template
型であり、そのTree
フィールドにアクセスする必要があることを明確にするための修正です。
これらの変更により、html/template
パッケージは、テンプレートのパース、実行、ルックアップ、および関連するテンプレートの追加といった操作が、複数のゴルーチンから同時に行われても安全に動作するようになりました。
関連リンク
- Go Issue #2439: html/template: make execution thread-safe
- Go CL 5450056: html/template: make execution thread-safe
参考にした情報源リンク
- Go Documentation:
text/template
package: https://pkg.go.dev/text/template - Go Documentation:
html/template
package: https://pkg.go.dev/html/template - Go Documentation:
sync
package: https://pkg.go.dev/sync - Wikipedia: Race condition: https://en.wikipedia.org/wiki/Race_condition
- Wikipedia: Mutual exclusion: https://en.wikipedia.org/wiki/Mutual_exclusion