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

[インデックス 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/templatehtml/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タグを入力した場合、エスケープ処理によってこれが &lt;script&gt;alert(&#39;XSS&#39;)&lt;/script&gt; のように変換され、ブラウザがスクリプトとして実行するのを防ぎます。このエスケープ処理は、テンプレートの実行時に動的に行われることがあり、その過程でテンプレートの内部状態(例えば、エスケープ済みかどうかのフラグや、コンテキスト情報など)が更新される可能性があります。

技術的詳細

このコミットの主要な目的は、html/templateTemplate オブジェクトが複数のゴルーチンから同時に実行された際に発生する可能性のある競合状態を解消し、スレッドセーフな操作を保証することです。

変更の核心は、テンプレートの関連付けられたセット(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 ミューテックスが使用され、共有状態へのアクセスが保護されています。

  1. 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)
    }
    
  2. 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)
    }
    
  3. 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
    }
    
  4. 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
    }
    
  5. 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 の変更点

  1. sync パッケージのインポート: import "sync" が追加され、sync.Mutex を使用できるようになりました。

  2. nameSpace 構造体の導入: type nameSpace struct { mu sync.Mutex; set map[string]*Template } が定義されました。これは、複数のテンプレート間で共有される状態(テンプレートのマップ set)と、その状態を保護するためのミューテックス mu をカプセル化します。

  3. Template 構造体の変更: text *template.Template の下に *nameSpace が埋め込まれました。これにより、Template オブジェクトは、関連するすべてのテンプレートが共有する nameSpace インスタンスにアクセスできるようになります。以前の set *map[string]*Template は削除されました。

  4. Template.Execute メソッドの変更:

    • メソッドシグネチャが (err error) を返すように変更されました。
    • t.nameSpace.mu.Lock()t.nameSpace.mu.Unlock() が追加され、テンプレートのエスケープ処理 (escapeTemplates) がミューテックスによって保護されるようになりました。これにより、複数のゴルーチンが同時に Execute を呼び出しても、エスケープ処理中のテンプレートの内部状態の変更が競合しないように保証されます。
    • 以前の t.escaped = true の行は、escapeTemplates がエラーを返した場合にのみ t.escaped = true となるように変更されました。
  5. Template.ExecuteTemplate メソッドの変更:

    • メソッドシグネチャが (err error) を返すように変更されました。
    • t.Lookup(name) の呼び出しが tmpl := t.set[name] に直接変更されました。これは、Lookup メソッド自体がミューテックスで保護されるようになったため、ExecuteTemplate 内で二重にロックを取得するのを避けるため、またはより直接的なアクセスを意図した変更と考えられます。
    • t.nameSpace.mu.Lock()t.nameSpace.mu.Unlock() が追加され、テンプレートのルックアップとエスケープ処理が保護されるようになりました。
  6. 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 メソッドを呼び出すように変更されたためです。
  7. New 関数と Template.New メソッドの変更:

    • New 関数では、nameSpace の新しいインスタンスが作成され、その中に set マップとミューテックスが初期化されます。
    • Template.New メソッドは、t.nameSpace.mu.Lock()defer t.nameSpace.mu.Unlock() を追加し、内部的にロックなしの t.new(name) メソッドを呼び出すように変更されました。これにより、新しいテンプレートの作成と既存のセットへの追加がスレッドセーフに行われます。
    • Template.new メソッドは、Template オブジェクトが既存の nameSpace を共有するように変更されました。
  8. Template.Lookup メソッドの変更:

    • t.nameSpace.mu.Lock()defer t.nameSpace.mu.Unlock() が追加され、テンプレートのルックアップがミューテックスで保護されるようになりました。

escape.go の変更点

  1. escapeTemplates 関数内のルックアップ変更: if t := tmpl.Lookup(name); t != nil { の行が if t := tmpl.set[name]; t != nil { に変更されました。これは、Lookup メソッドがミューテックスで保護されるようになったため、escapeTemplates 関数内で二重にロックを取得するのを避けるための最適化、またはより直接的なアクセスを意図した変更と考えられます。

  2. コメントの修正: // 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 パッケージは、テンプレートのパース、実行、ルックアップ、および関連するテンプレートの追加といった操作が、複数のゴルーチンから同時に行われても安全に動作するようになりました。

関連リンク

参考にした情報源リンク