Skip to content

Commit

Permalink
text/template: add lock for Template.tmpl to fix data race
Browse files Browse the repository at this point in the history
This adds a new lock protecting "tmpl".

This is a copy of https://golang.org/cl/257817 by Andreas Fleig,
updated for current tip, and updated to start running the
html/template TestEscapeRace test.

Thanks to @bep for providing the test case.

Fixes #39807

Change-Id: Ic8874484290283a49116812eeaffb8608346dc70
Reviewed-on: https://go-review.googlesource.com/c/go/+/316669
Trust: Ian Lance Taylor <[email protected]>
Run-TryBot: Ian Lance Taylor <[email protected]>
TryBot-Result: Go Bot <[email protected]>
Reviewed-by: Dmitri Shuralyov <[email protected]>
  • Loading branch information
ianlancetaylor committed May 4, 2021
1 parent 731a015 commit 496d7c6
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 7 deletions.
2 changes: 0 additions & 2 deletions src/html/template/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1720,8 +1720,6 @@ var v = "v";
`

func TestEscapeRace(t *testing.T) {
t.Skip("this test currently fails with -race; see issue #39807")

tmpl := New("")
_, err := tmpl.New("templ.html").Parse(raceText)
if err != nil {
Expand Down
9 changes: 4 additions & 5 deletions src/text/template/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,10 +179,7 @@ func errRecover(errp *error) {
// A template may be executed safely in parallel, although if parallel
// executions share a Writer the output may be interleaved.
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error {
var tmpl *Template
if t.common != nil {
tmpl = t.tmpl[name]
}
tmpl := t.Lookup(name)
if tmpl == nil {
return fmt.Errorf("template: no template %q associated with template %q", name, t.name)
}
Expand Down Expand Up @@ -230,6 +227,8 @@ func (t *Template) DefinedTemplates() string {
return ""
}
var b strings.Builder
t.muTmpl.RLock()
defer t.muTmpl.RUnlock()
for name, tmpl := range t.tmpl {
if tmpl.Tree == nil || tmpl.Root == nil {
continue
Expand Down Expand Up @@ -401,7 +400,7 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {

func (s *state) walkTemplate(dot reflect.Value, t *parse.TemplateNode) {
s.at(t)
tmpl := s.tmpl.tmpl[t.Name]
tmpl := s.tmpl.Lookup(t.Name)
if tmpl == nil {
s.errorf("template %q not defined", t.Name)
}
Expand Down
38 changes: 38 additions & 0 deletions src/text/template/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"io"
"reflect"
"strings"
"sync"
"testing"
)

Expand Down Expand Up @@ -1732,3 +1733,40 @@ func TestIssue43065(t *testing.T) {
t.Errorf("%s", err)
}
}

// Issue 39807: data race in html/template & text/template
func TestIssue39807(t *testing.T) {
var wg sync.WaitGroup

tplFoo, err := New("foo").Parse(`{{ template "bar" . }}`)
if err != nil {
t.Error(err)
}

tplBar, err := New("bar").Parse("bar")
if err != nil {
t.Error(err)
}

gofuncs := 10
numTemplates := 10

for i := 1; i <= gofuncs; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < numTemplates; j++ {
_, err := tplFoo.AddParseTree(tplBar.Name(), tplBar.Tree)
if err != nil {
t.Error(err)
}
err = tplFoo.Execute(io.Discard, nil)
if err != nil {
t.Error(err)
}
}
}()
}

wg.Wait()
}
9 changes: 9 additions & 0 deletions src/text/template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
// common holds the information shared by related templates.
type common struct {
tmpl map[string]*Template // Map from name to defined templates.
muTmpl sync.RWMutex // protects tmpl
option option
// We use two maps, one for parsing and one for execution.
// This separation makes the API cleaner since it doesn't
Expand Down Expand Up @@ -88,6 +89,8 @@ func (t *Template) Clone() (*Template, error) {
if t.common == nil {
return nt, nil
}
t.muTmpl.RLock()
defer t.muTmpl.RUnlock()
for k, v := range t.tmpl {
if k == t.name {
nt.tmpl[t.name] = nt
Expand Down Expand Up @@ -124,6 +127,8 @@ func (t *Template) copy(c *common) *Template {
// its definition. If it has been defined and already has that name, the existing
// definition is replaced; otherwise a new template is created, defined, and returned.
func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error) {
t.muTmpl.Lock()
defer t.muTmpl.Unlock()
t.init()
nt := t
if name != t.name {
Expand All @@ -142,6 +147,8 @@ func (t *Template) Templates() []*Template {
return nil
}
// Return a slice so we don't expose the map.
t.muTmpl.RLock()
defer t.muTmpl.RUnlock()
m := make([]*Template, 0, len(t.tmpl))
for _, v := range t.tmpl {
m = append(m, v)
Expand Down Expand Up @@ -182,6 +189,8 @@ func (t *Template) Lookup(name string) *Template {
if t.common == nil {
return nil
}
t.muTmpl.RLock()
defer t.muTmpl.RUnlock()
return t.tmpl[name]
}

Expand Down

0 comments on commit 496d7c6

Please sign in to comment.