golang/go

text/template: stack overflow

dvyukov opened this issue · 13 comments

go version devel +149ac34 Mon May 9 17:50:29 2016 +0000 linux/amd64

The following program causes stack overflow. Documentation suggests that block calls itself rather than the top-level "" template, so there should be no recursion.

package main

import (
    "text/template"
    "io/ioutil"
)

func main() {
    template.Must(template.New("").Parse(`{{block "".}}{{end}}`)).Execute(ioutil.Discard, nil)
}
runtime: goroutine stack exceeds 1000000000-byte limit
fatal error: stack overflow

runtime stack:
runtime.throw(0x50efd4, 0xe)
    src/runtime/panic.go:566 +0x8b
runtime.newstack()
    src/runtime/stack.go:1035 +0x3ef
runtime.morestack()
    src/runtime/asm_amd64.s:366 +0x7f

goroutine 1 [running]:
text/template.(*state).evalCommand(0xc440100588, 0x0, 0x0, 0x0, 0xc4200ac300, 0x0, 0x0, 0x0, 0x0, 0x0, ...)
    src/text/template/exec.go:407 fp=0xc440100298 sp=0xc440100290
text/template.(*state).evalPipeline(0xc440100588, 0x0, 0x0, 0x0, 0xc4200ca0a0, 0x0, 0x0, 0x0)
    src/text/template/exec.go:389 +0xdd fp=0xc440100380 sp=0xc440100298
text/template.(*state).walkTemplate(0xc440100588, 0x0, 0x0, 0x0, 0xc4200c8180)
    src/text/template/exec.go:367 +0xe4 fp=0xc440100428 sp=0xc440100380
text/template.(*state).walk(0xc440100588, 0x0, 0x0, 0x0, 0x58f460, 0xc4200c8180)
    src/text/template/exec.go:232 +0x22f fp=0xc4401004a8 sp=0xc440100428
text/template.(*state).walk(0xc440100588, 0x0, 0x0, 0x0, 0x58f220, 0xc4200ac2d0)
    src/text/template/exec.go:227 +0x130 fp=0xc440100528 sp=0xc4401004a8
text/template.(*state).walkTemplate(0xc440100730, 0x0, 0x0, 0x0, 0xc4200c8180)
    src/text/template/exec.go:372 +0x1e4 fp=0xc4401005d0 sp=0xc440100528
text/template.(*state).walk(0xc440100730, 0x0, 0x0, 0x0, 0x58f460, 0xc4200c8180)
    src/text/template/exec.go:232 +0x22f fp=0xc440100650 sp=0xc4401005d0
text/template.(*state).walk(0xc440100730, 0x0, 0x0, 0x0, 0x58f220, 0xc4200ac2d0)
    src/text/template/exec.go:227 +0x130 fp=0xc4401006d0 sp=0xc440100650
text/template.(*state).walkTemplate(0xc4401008d8, 0x0, 0x0, 0x0, 0xc4200c8180)
    src/text/template/exec.go:372 +0x1e4 fp=0xc440100778 sp=0xc4401006d0
text/template.(*state).walk(0xc4401008d8, 0x0, 0x0, 0x0, 0x58f460, 0xc4200c8180)
    src/text/template/exec.go:232 +0x22f fp=0xc4401007f8 sp=0xc440100778
text/template.(*state).walk(0xc4401008d8, 0x0, 0x0, 0x0, 0x58f220, 0xc4200ac2d0)
    src/text/template/exec.go:227 +0x130 fp=0xc440100878 sp=0xc4401007f8
text/template.(*state).walkTemplate(0xc440100a80, 0x0, 0x0, 0x0, 0xc4200c8180)
    src/text/template/exec.go:372 +0x1e4 fp=0xc440100920 sp=0xc440100878
text/template.(*state).walk(0xc440100a80, 0x0, 0x0, 0x0, 0x58f460, 0xc4200c8180)
    src/text/template/exec.go:232 +0x22f fp=0xc4401009a0 sp=0xc440100920
text/template.(*state).walk(0xc440100a80, 0x0, 0x0, 0x0, 0x58f220, 0xc4200ac2d0)
    src/text/template/exec.go:227 +0x130 fp=0xc440100a20 sp=0xc4401009a0

Found with go-fuzz.

rsc commented

The dot in {{block "" .}} seems to be a red herring. Changing the dot to 1 still breaks. Similarly changing either "" to "x" (but not both) avoids the recursion. It's unclear to me whether the recursion is correct.

Leaving investigation for @robpike or @adg.

go 1.6.2

package main

import (
    "text/template"
    "io/ioutil"
)

func main() {
    template.Must(template.New("a").Parse(`{{template "a"}}`)).Execute(ioutil.Discard, nil)
}
package main

import (
    "text/template"
    "io/ioutil"
)

func main() {
    t := template.New("")
    template.Must(t.New("a").Parse(`{{template "b"}}`))
    template.Must(t.New("b").Parse(`{{template "a"}}`))
    t.ExecuteTemplate(ioutil.Discard, "a", nil)
}
runtime: goroutine stack exceeds 1000000000-byte limit
fatal error: stack overflow

These both look like legitimate recursions to me. The true question is whether the recursion should be detected and I'm undecided about that.

@robpike Why does {{block "".}}{{end}} call the top-level "" template rather than itself? If it would call itself, there would be no recursion since the block template is empty.

adg commented

{{block "" .}} is just wrong, so the code should fail somehow but I'm not sure exactly how or when. It's possible that this stack overflow is an acceptable failure mode.

A block defines a template, so we could make it so that it is not possible for a block to re-define the template in which the block appears. But even if that were done, it would still be possible to construct a cycle of nested templates if you try hard enough.

For context, go-fuzz constantly hits recursive template invocations. The simplest case is when template "foo" contains {{template "foo"}}. I had to filter out all inputs that contain string "template". It's a user input error, but it manifests as hard program crash. I hoped that I will be at least tests blocks. But now go-fuzz discovered a way to cause recursion using blocks as well.

Overriding an existing template with block sounds good.
But note that it not just overdefines, because it calls the top-level template which looks really bogus.

It is possible to trigger recursion using blocks? E.g. if block "foo" calls block "bar" and vise versa. Or it will fail because bar is not refined yet? If it is not possible to trigger recursion this way then I will be able to test at least blocks.

adg commented

You can create recursion with blocks:

{{define "foo"}}
{{block "bar" .}}{{end}}
{{end}}
{{define "bar"}}
{{block "foo" .}}{{end}}
{{end}}

But you can also trigger recursion with functions:

func foo() { bar() }
func bar() { foo() }

What does go-fuzz do to avoid that? Or has it just not run into it yet?

I don't know. Maybe it run into it. I looked at some reports, but I also instantly deleted lots of them.

adg commented

I'm going to close this because I don't think there's anything practical we should do about it. Thanks for the report.

Well, the evaluator could barf if the stack depth is unreasonably large. Maybe that's better than running the machine out of memory.

adg commented

Sent a CL

CL https://golang.org/cl/23091 mentions this issue.