Skip to content

text/template: limit pipeline command count to prevent pathological resource usage #75231

@thevilledev

Description

@thevilledev

Go version

go version go1.25.0 darwin/arm64

Output of go env in your module/workspace:

AR='ar'
CC='cc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='c++'
GCCGO='gccgo'
GO111MODULE=''
GOARCH='arm64'
GOARM64='v8.0'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/Users/ville/Library/Caches/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/Users/ville/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/cl/npk3dq855kxf2ns3qth9pv4m0000gn/T/go-build3480984197=/tmp/go-build -gno-record-gcc-switches -fno-common'
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMOD='/dev/null'
GOMODCACHE='/Users/ville/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='darwin'
GOPATH='/Users/ville/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/opt/homebrew/Cellar/go/1.25.0/libexec'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/Users/ville/Library/Application Support/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/opt/homebrew/Cellar/go/1.25.0/libexec/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.25.0'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

Constructed a template with a very long pipeline to observe parser/runtime behavior. See it on Go playground here.

package main

import (
	"fmt"
	"runtime"
	"strings"
	"text/template"
)

func main() {
	// Adjust to explore behavior at different sizes.
	cmds := 100000

	// Build: {{ printf "x" | printf "y" | ... }}
	parts := make([]string, cmds)
	for i := 0; i < cmds; i++ {
		parts[i] = `printf "x"`
	}
	expr := "{{ " + strings.Join(parts, " | ") + " }}"

	fmt.Printf("Input size: %d bytes, commands: %d\n", len(expr), cmds)

	var m1, m2 runtime.MemStats
	runtime.ReadMemStats(&m1)

	_, err := template.New("test").Parse(expr)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
	}

	runtime.ReadMemStats(&m2)
	allocated := m2.TotalAlloc - m1.TotalAlloc

	fmt.Printf("Memory allocated: %d bytes\n", allocated)
	if len(expr) > 0 {
		fmt.Printf("Amplification factor: %.2fx\n", float64(allocated)/float64(len(expr)))
	}
}

What did you see happen?

With an extreme pipeline length of 100k items the output is as follows:

Input size: 1300003 bytes, commands: 100000
Memory allocated: 25314928 bytes
Amplification factor: 19.47x

With a roughly 1 MB of input, Go consumes 24 MB of memory.

For larger command counts, parsing exhibits significant time and memory amplification relative to input size. At very high counts, this can lead to prolonged CPU usage and, in some cases, out-of-memory termination depending on runtime limits.

What did you expect to see?

A reasonable limit on the number of commands allowed in a single pipeline to prevent accidental or adversarial resource exhaustion during parse or execution. Similar to the existing maximum expression nesting depth, a pipeline command cap would bound resource usage for a single expression. See #71201 for more information about the expression nesting depth.

Metadata

Metadata

Assignees

No one assigned

    Labels

    LibraryProposalIssues describing a requested change to the Go standard library or x/ libraries, but not to a toolNeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions