From 1f578f160cbed384db0f8dbe708e990fc2235da9 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Wed, 11 Mar 2026 12:36:13 +0100 Subject: [PATCH] Upgrade to to Go 1.26.1 (#14597) (note) This fixes a security issue: https://www.cve.org/CVERecord?id=CVE-2026-27142 Closes #14595 --- .circleci/config.yml | 4 +- common/hreflect/helpers.go | 2 +- scripts/fork_go_templates/main.go | 2 +- .../go_templates/htmltemplate/attr_string.go | 5 +- .../go_templates/htmltemplate/context.go | 8 ++ tpl/internal/go_templates/htmltemplate/doc.go | 5 +- .../htmltemplate/element_string.go | 5 +- .../go_templates/htmltemplate/error.go | 7 +- .../go_templates/htmltemplate/escape.go | 14 ++- .../go_templates/htmltemplate/escape_test.go | 20 +++- .../go_templates/htmltemplate/exec_test.go | 62 ++++++---- .../go_templates/htmltemplate/state_string.go | 8 +- .../go_templates/htmltemplate/template.go | 2 +- .../htmltemplate/template_test.go | 4 +- .../go_templates/htmltemplate/transition.go | 47 +++++++- tpl/internal/go_templates/testenv/testenv.go | 108 +++++++++++++----- .../go_templates/testenv/testenv_test.go | 50 ++------ .../go_templates/testenv/testenv_unix.go | 24 +--- tpl/internal/go_templates/texttemplate/doc.go | 5 + .../go_templates/texttemplate/example_test.go | 2 +- .../texttemplate/examplefunc_test.go | 69 +++++++++++ .../go_templates/texttemplate/exec.go | 8 +- .../go_templates/texttemplate/exec_test.go | 92 ++++++++++----- .../go_templates/texttemplate/funcs.go | 39 +++---- .../go_templates/texttemplate/multi_test.go | 11 ++ .../texttemplate/parse/lex_test.go | 12 -- .../go_templates/texttemplate/parse/parse.go | 29 ++++- .../texttemplate/parse/parse_test.go | 14 +++ .../go_templates/texttemplate/template.go | 1 + 29 files changed, 447 insertions(+), 212 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 18047d6e8..cc15e71e0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,7 +4,7 @@ parameters: defaults: &defaults resource_class: large docker: - - image: bepsays/ci-hugoreleaser:1.22600.20000 + - image: bepsays/ci-hugoreleaser:1.22600.20100 environment: &buildenv GOMODCACHE: /root/project/gomodcache version: 2 @@ -58,7 +58,7 @@ jobs: environment: <<: [*buildenv] docker: - - image: bepsays/ci-hugoreleaser-linux-arm64:1.22600.20000 + - image: bepsays/ci-hugoreleaser-linux-arm64:1.22600.20100 steps: - *restore-cache - &attach-workspace diff --git a/common/hreflect/helpers.go b/common/hreflect/helpers.go index fe11d2b9f..802f0e59c 100644 --- a/common/hreflect/helpers.go +++ b/common/hreflect/helpers.go @@ -136,7 +136,7 @@ func IsTruthfulValue(val reflect.Value) (truth bool) { truth = val.Bool() case reflect.Complex64, reflect.Complex128: truth = val.Complex() != 0 - case reflect.Chan, reflect.Func, reflect.Pointer, reflect.Interface: + case reflect.Chan, reflect.Func, reflect.Pointer, reflect.UnsafePointer, reflect.Interface: truth = !val.IsNil() case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: truth = val.Int() != 0 diff --git a/scripts/fork_go_templates/main.go b/scripts/fork_go_templates/main.go index e4895c87a..3ea3d6c32 100644 --- a/scripts/fork_go_templates/main.go +++ b/scripts/fork_go_templates/main.go @@ -15,7 +15,7 @@ import ( ) func main() { - // The current is built with 3901409b5d [release-branch.go1.24] go1.24.0 + // The current is built with e87b10ea2a2c6c65b80c4374af42b9c02ac9fb20 go1.26.1 // TODO(bep) preserve the staticcheck.conf file. fmt.Println("Forking ...") defer fmt.Println("Done ...") diff --git a/tpl/internal/go_templates/htmltemplate/attr_string.go b/tpl/internal/go_templates/htmltemplate/attr_string.go index 51c3f2620..7159fa9cb 100644 --- a/tpl/internal/go_templates/htmltemplate/attr_string.go +++ b/tpl/internal/go_templates/htmltemplate/attr_string.go @@ -14,11 +14,12 @@ func _() { _ = x[attrStyle-3] _ = x[attrURL-4] _ = x[attrSrcset-5] + _ = x[attrMetaContent-6] } -const _attr_name = "attrNoneattrScriptattrScriptTypeattrStyleattrURLattrSrcset" +const _attr_name = "attrNoneattrScriptattrScriptTypeattrStyleattrURLattrSrcsetattrMetaContent" -var _attr_index = [...]uint8{0, 8, 18, 32, 41, 48, 58} +var _attr_index = [...]uint8{0, 8, 18, 32, 41, 48, 58, 73} func (i attr) String() string { if i >= attr(len(_attr_index)-1) { diff --git a/tpl/internal/go_templates/htmltemplate/context.go b/tpl/internal/go_templates/htmltemplate/context.go index 3acb9bdf0..5bb2253eb 100644 --- a/tpl/internal/go_templates/htmltemplate/context.go +++ b/tpl/internal/go_templates/htmltemplate/context.go @@ -157,6 +157,10 @@ const ( // stateError is an infectious error state outside any valid // HTML/CSS/JS construct. stateError + // stateMetaContent occurs inside a HTML meta element content attribute. + stateMetaContent + // stateMetaContentURL occurs inside a "url=" tag in a HTML meta element content attribute. + stateMetaContentURL // stateDead marks unreachable code after a {{break}} or {{continue}}. stateDead ) @@ -268,6 +272,8 @@ const ( elementTextarea // elementTitle corresponds to the RCDATA element. elementTitle + // elementMeta corresponds to the HTML <meta> element. + elementMeta ) //go:generate stringer -type attr @@ -289,4 +295,6 @@ const ( attrURL // attrSrcset corresponds to a srcset attribute. attrSrcset + // attrMetaContent corresponds to the content attribute in meta HTML element. + attrMetaContent ) diff --git a/tpl/internal/go_templates/htmltemplate/doc.go b/tpl/internal/go_templates/htmltemplate/doc.go index 7442194f7..f2d896b29 100644 --- a/tpl/internal/go_templates/htmltemplate/doc.go +++ b/tpl/internal/go_templates/htmltemplate/doc.go @@ -28,6 +28,9 @@ HTML templates treat data values as plain text which should be encoded so they can be safely embedded in an HTML document. The escaping is contextual, so actions can appear within JavaScript, CSS, and URI contexts. +Comments are stripped from output, except for those passed in via the +[HTML], [CSS], and [JS] types for their respective contexts. + The security model used by this package assumes that template authors are trusted, while Execute's data parameter is not. More details are provided below. @@ -210,7 +213,7 @@ that would have been produced if {{.}} was a regular string. # Security Model -https://rawgit.com/mikesamuel/sanitized-jquery-templates/trunk/safetemplate.html#problem_definition defines "safe" as used by this package. +https://web.archive.org/web/20160501113828/http://js-quasis-libraries-and-repl.googlecode.com/svn/trunk/safetemplate.html#problem_definition defines "safe" as used by this package. This package assumes that template authors are trusted, that Execute's data parameter is not, and seeks to preserve the properties below in the face diff --git a/tpl/internal/go_templates/htmltemplate/element_string.go b/tpl/internal/go_templates/htmltemplate/element_string.go index db286655a..bdf9da7b9 100644 --- a/tpl/internal/go_templates/htmltemplate/element_string.go +++ b/tpl/internal/go_templates/htmltemplate/element_string.go @@ -13,11 +13,12 @@ func _() { _ = x[elementStyle-2] _ = x[elementTextarea-3] _ = x[elementTitle-4] + _ = x[elementMeta-5] } -const _element_name = "elementNoneelementScriptelementStyleelementTextareaelementTitle" +const _element_name = "elementNoneelementScriptelementStyleelementTextareaelementTitleelementMeta" -var _element_index = [...]uint8{0, 11, 24, 36, 51, 63} +var _element_index = [...]uint8{0, 11, 24, 36, 51, 63, 74} func (i element) String() string { if i >= element(len(_element_index)-1) { diff --git a/tpl/internal/go_templates/htmltemplate/error.go b/tpl/internal/go_templates/htmltemplate/error.go index de4b4de1e..f8ad0081f 100644 --- a/tpl/internal/go_templates/htmltemplate/error.go +++ b/tpl/internal/go_templates/htmltemplate/error.go @@ -80,15 +80,18 @@ const ( ErrBadHTML // ErrBranchEnd: "{{if}} branches end in different contexts" - // Example: + // Examples: // {{if .C}}<a href="{{end}}{{.X}} + // <script {{with .T}}type="{{.}}"{{end}}> // Discussion: // Package html/template statically examines each path through an // {{if}}, {{range}}, or {{with}} to escape any following pipelines. - // The example is ambiguous since {{.X}} might be an HTML text node, + // The first example is ambiguous since {{.X}} might be an HTML text node, // or a URL prefix in an HTML attribute. The context of {{.X}} is // used to figure out how to escape it, but that context depends on // the run-time value of {{.C}} which is not statically known. + // The second example is ambiguous as the script type attribute + // can change the type of escaping needed for the script contents. // // The problem is usually something like missing quotes or angle // brackets, or can be avoided by refactoring to put the two contexts diff --git a/tpl/internal/go_templates/htmltemplate/escape.go b/tpl/internal/go_templates/htmltemplate/escape.go index f5d5674fa..67c515273 100644 --- a/tpl/internal/go_templates/htmltemplate/escape.go +++ b/tpl/internal/go_templates/htmltemplate/escape.go @@ -164,7 +164,7 @@ func (e *escaper) escape(c context, n parse.Node) context { panic("escaping " + n.String() + " is unimplemented") } -// var debugAllowActionJSTmpl = godebug.New("jstmpllitinterp") +var htmlmetacontenturlescape = true // godebug.New("htmlmetacontenturlescape") // escapeAction escapes an action template node. func (e *escaper) escapeAction(c context, n *parse.ActionNode) context { @@ -223,6 +223,18 @@ func (e *escaper) escapeAction(c context, n *parse.ActionNode) context { default: panic(c.urlPart.String()) } + case stateMetaContent: + // Handled below in delim check. + case stateMetaContentURL: + if htmlmetacontenturlescape { + s = append(s, "_html_template_urlfilter") + } else { + // We don't have a great place to increment this, since it's hard to + // know if we actually escape any urls in _html_template_urlfilter, + // since it has no information about what context it is being + // executed in etc. This is probably the best we can do. + // htmlmetacontenturlescape.IncNonDefault() + } case stateJS: s = append(s, "_html_template_jsvalescaper") // A slash after a value starts a div operator. diff --git a/tpl/internal/go_templates/htmltemplate/escape_test.go b/tpl/internal/go_templates/htmltemplate/escape_test.go index a5a810ffc..7ea1c7224 100644 --- a/tpl/internal/go_templates/htmltemplate/escape_test.go +++ b/tpl/internal/go_templates/htmltemplate/escape_test.go @@ -739,6 +739,16 @@ func TestEscape(t *testing.T) { "<script>var a = `${ var a = \"{{\"a \\\" d\"}}\" }`</script>", "<script>var a = `${ var a = \"a \\u0022 d\" }`</script>", }, + { + "meta content attribute url", + `<meta http-equiv="refresh" content="asd; url={{"javascript:alert(1)"}}; asd; url={{"vbscript:alert(1)"}}; asd">`, + `<meta http-equiv="refresh" content="asd; url=#ZgotmplZ; asd; url=#ZgotmplZ; asd">`, + }, + { + "meta content string", + `<meta http-equiv="refresh" content="{{"asd: 123"}}">`, + `<meta http-equiv="refresh" content="asd: 123">`, + }, } for _, test := range tests { @@ -944,7 +954,6 @@ func TestEscapeSet(t *testing.T) { t.Errorf("want\n\t%q\ngot\n\t%q", test.want, got) } } - } func TestErrors(t *testing.T) { @@ -1021,6 +1030,14 @@ func TestErrors(t *testing.T) { "<script>var tmpl = `asd ${return \"{\"}`;</script>", ``, }, + { + `{{if eq "" ""}}<meta>{{end}}`, + ``, + }, + { + `{{if eq "" ""}}<meta content="url={{"asd"}}">{{end}}`, + ``, + }, // Error cases. { @@ -1198,7 +1215,6 @@ func TestErrors(t *testing.T) { // Check that we get the same error if we call Execute again. if err := tmpl.Execute(buf, nil); err == nil || err.Error() != got { t.Errorf("input=%q: unexpected error on second call %q", test.input, err) - } } } diff --git a/tpl/internal/go_templates/htmltemplate/exec_test.go b/tpl/internal/go_templates/htmltemplate/exec_test.go index 2790470fd..e01813e68 100644 --- a/tpl/internal/go_templates/htmltemplate/exec_test.go +++ b/tpl/internal/go_templates/htmltemplate/exec_test.go @@ -325,12 +325,16 @@ var execTests = []execTest{ {"$.U.V", "{{$.U.V}}", "v", tVal, true}, {"declare in action", "{{$x := $.U.V}}{{$x}}", "v", tVal, true}, {"simple assignment", "{{$x := 2}}{{$x = 3}}{{$x}}", "3", tVal, true}, - {"nested assignment", + { + "nested assignment", "{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{$x}}", - "3", tVal, true}, - {"nested assignment changes the last declaration", + "3", tVal, true, + }, + { + "nested assignment changes the last declaration", "{{$x := 1}}{{if true}}{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{end}}{{$x}}", - "1", tVal, true}, + "1", tVal, true, + }, // Type with String method. {"V{6666}.String()", "-{{.V0}}-", "-{6666}-", tVal, true}, // NOTE: -<6666>- in text/template @@ -377,15 +381,21 @@ var execTests = []execTest{ {".Method3(nil constant)", "-{{.Method3 nil}}-", "-Method3: <nil>-", tVal, true}, {".Method3(nil value)", "-{{.Method3 .MXI.unset}}-", "-Method3: <nil>-", tVal, true}, {"method on var", "{{if $x := .}}-{{$x.Method2 .U16 $x.X}}{{end}}-", "-Method2: 16 x-", tVal, true}, - {"method on chained var", + { + "method on chained var", "{{range .MSIone}}{{if $.U.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}", - "true", tVal, true}, - {"chained method", + "true", tVal, true, + }, + { + "chained method", "{{range .MSIone}}{{if $.GetU.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}", - "true", tVal, true}, - {"chained method on variable", + "true", tVal, true, + }, + { + "chained method on variable", "{{with $x := .}}{{with .SI}}{{$.GetU.TrueFalse $.True}}{{end}}{{end}}", - "true", tVal, true}, + "true", tVal, true, + }, {".NilOKFunc not nil", "{{call .NilOKFunc .PI}}", "false", tVal, true}, {".NilOKFunc nil", "{{call .NilOKFunc nil}}", "true", tVal, true}, {"method on nil value from slice", "-{{range .}}{{.Method1 1234}}{{end}}-", "-1234-", tSliceOfNil, true}, @@ -471,10 +481,14 @@ var execTests = []execTest{ {"printf lots", `{{printf "%d %s %g %s" 127 "hello" 7-3i .Method0}}`, "127 hello (7-3i) M0", tVal, true}, // HTML. - {"html", `{{html "<script>alert(\"XSS\");</script>"}}`, - "<script>alert("XSS");</script>", nil, true}, - {"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`, - "<script>alert("XSS");</script>", nil, true}, + { + "html", `{{html "<script>alert(\"XSS\");</script>"}}`, + "<script>alert("XSS");</script>", nil, true, + }, + { + "html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`, + "<script>alert("XSS");</script>", nil, true, + }, {"html", `{{html .PS}}`, "a string", tVal, true}, {"html typed nil", `{{html .NIL}}`, "<nil>", tVal, true}, {"html untyped nil", `{{html .Empty0}}`, "<nil>", tVal, true}, // NOTE: "<no value>" in text/template @@ -716,7 +730,7 @@ func count(n int) chan string { } c := make(chan string) go func() { - for i := range n { + for i := 0; i < n; i++ { c <- "abcdefghijklmnop"[i : i+1] } close(c) @@ -840,7 +854,7 @@ var delimPairs = []string{ func TestDelims(t *testing.T) { const hello = "Hello, world" - var value = struct{ Str string }{hello} + value := struct{ Str string }{hello} for i := 0; i < len(delimPairs); i += 2 { text := ".Str" left := delimPairs[i+0] @@ -863,7 +877,7 @@ func TestDelims(t *testing.T) { if err != nil { t.Fatalf("delim %q text %q parse err %s", left, text, err) } - var b = new(strings.Builder) + b := new(strings.Builder) err = tmpl.Execute(b, value) if err != nil { t.Fatalf("delim %q exec err %s", left, err) @@ -964,7 +978,7 @@ const treeTemplate = ` ` func TestTree(t *testing.T) { - var tree = &Tree{ + tree := &Tree{ 1, &Tree{ 2, &Tree{ @@ -1215,7 +1229,7 @@ var cmpTests = []cmpTest{ func TestComparison(t *testing.T) { b := new(strings.Builder) - var cmpStruct = struct { + cmpStruct := struct { Uthree, Ufour uint NegOne, Three int Ptr, NilPtr *int @@ -1604,8 +1618,8 @@ func TestInterfaceValues(t *testing.T) { "Nil": nil, "Zero": 0, }) - if after, ok := strings.CutPrefix(tt.out, "ERROR:"); ok { - e := strings.TrimSpace(after) + if strings.HasPrefix(tt.out, "ERROR:") { + e := strings.TrimSpace(strings.TrimPrefix(tt.out, "ERROR:")) if err == nil || !strings.Contains(err.Error(), e) { t.Errorf("%s: Execute: %v, want error %q", tt.text, err, e) } @@ -1741,7 +1755,7 @@ func TestEscapeRace(t *testing.T) { t.Fatal(err) } const count = 20 - for i := range count { + for i := 0; i < count; i++ { _, err := tmpl.New(fmt.Sprintf("x%d.html", i)).Parse(`{{ template "templ.html" .}}`) if err != nil { t.Fatal(err) @@ -1749,11 +1763,11 @@ func TestEscapeRace(t *testing.T) { } var wg sync.WaitGroup - for range 10 { + for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() - for j := range count { + for j := 0; j < count; j++ { sub := tmpl.Lookup(fmt.Sprintf("x%d.html", j)) if err := sub.Execute(io.Discard, nil); err != nil { t.Error(err) diff --git a/tpl/internal/go_templates/htmltemplate/state_string.go b/tpl/internal/go_templates/htmltemplate/state_string.go index eed1e8bcc..f5a70b223 100644 --- a/tpl/internal/go_templates/htmltemplate/state_string.go +++ b/tpl/internal/go_templates/htmltemplate/state_string.go @@ -36,12 +36,14 @@ func _() { _ = x[stateCSSBlockCmt-25] _ = x[stateCSSLineCmt-26] _ = x[stateError-27] - _ = x[stateDead-28] + _ = x[stateMetaContent-28] + _ = x[stateMetaContentURL-29] + _ = x[stateDead-30] } -const _state_name = "stateTextstateTagstateAttrNamestateAfterNamestateBeforeValuestateHTMLCmtstateRCDATAstateAttrstateURLstateSrcsetstateJSstateJSDqStrstateJSSqStrstateJSTmplLitstateJSRegexpstateJSBlockCmtstateJSLineCmtstateJSHTMLOpenCmtstateJSHTMLCloseCmtstateCSSstateCSSDqStrstateCSSSqStrstateCSSDqURLstateCSSSqURLstateCSSURLstateCSSBlockCmtstateCSSLineCmtstateErrorstateDead" +const _state_name = "stateTextstateTagstateAttrNamestateAfterNamestateBeforeValuestateHTMLCmtstateRCDATAstateAttrstateURLstateSrcsetstateJSstateJSDqStrstateJSSqStrstateJSTmplLitstateJSRegexpstateJSBlockCmtstateJSLineCmtstateJSHTMLOpenCmtstateJSHTMLCloseCmtstateCSSstateCSSDqStrstateCSSSqStrstateCSSDqURLstateCSSSqURLstateCSSURLstateCSSBlockCmtstateCSSLineCmtstateErrorstateMetaContentstateMetaContentURLstateDead" -var _state_index = [...]uint16{0, 9, 17, 30, 44, 60, 72, 83, 92, 100, 111, 118, 130, 142, 156, 169, 184, 198, 216, 235, 243, 256, 269, 282, 295, 306, 322, 337, 347, 356} +var _state_index = [...]uint16{0, 9, 17, 30, 44, 60, 72, 83, 92, 100, 111, 118, 130, 142, 156, 169, 184, 198, 216, 235, 243, 256, 269, 282, 295, 306, 322, 337, 347, 363, 382, 391} func (i state) String() string { if i >= state(len(_state_index)-1) { diff --git a/tpl/internal/go_templates/htmltemplate/template.go b/tpl/internal/go_templates/htmltemplate/template.go index cd80e0183..4582ddd5f 100644 --- a/tpl/internal/go_templates/htmltemplate/template.go +++ b/tpl/internal/go_templates/htmltemplate/template.go @@ -267,7 +267,7 @@ func (t *Template) Clone() (*Template, error) { name := x.Name() src := t.set[name] if src == nil || src.escapeErr != nil { - return nil, fmt.Errorf("html/template: cannot Clone %q after it has executed, %q not found", t.Name(), name) + return nil, fmt.Errorf("html/template: cannot Clone %q after it has executed", t.Name()) } x.Tree = x.Tree.Copy() ret.set[name] = &Template{ diff --git a/tpl/internal/go_templates/htmltemplate/template_test.go b/tpl/internal/go_templates/htmltemplate/template_test.go index 0838308a1..c5a5df437 100644 --- a/tpl/internal/go_templates/htmltemplate/template_test.go +++ b/tpl/internal/go_templates/htmltemplate/template_test.go @@ -14,11 +14,11 @@ import ( "testing" . "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate" - "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse" // https://golang.org/issue/12996 + "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse" ) func TestTemplateClone(t *testing.T) { - + // https://golang.org/issue/12996 orig := New("name") clone, err := orig.Clone() if err != nil { diff --git a/tpl/internal/go_templates/htmltemplate/transition.go b/tpl/internal/go_templates/htmltemplate/transition.go index c430389a3..7fbab1df7 100644 --- a/tpl/internal/go_templates/htmltemplate/transition.go +++ b/tpl/internal/go_templates/htmltemplate/transition.go @@ -23,6 +23,8 @@ var transitionFunc = [...]func(context, []byte) (context, int){ stateRCDATA: tSpecialTagEnd, stateAttr: tAttr, stateURL: tURL, + stateMetaContent: tMetaContent, + stateMetaContentURL: tMetaContentURL, stateSrcset: tURL, stateJS: tJS, stateJSDqStr: tJSDelimited, @@ -83,6 +85,7 @@ var elementContentType = [...]state{ elementStyle: stateCSS, elementTextarea: stateRCDATA, elementTitle: stateRCDATA, + elementMeta: stateText, } // tTag is the context transition function for the tag state. @@ -93,6 +96,11 @@ func tTag(c context, s []byte) (context, int) { return c, len(s) } if s[i] == '>' { + // Treat <meta> specially, because it doesn't have an end tag, and we + // want to transition into the correct state/element for it. + if c.element == elementMeta { + return context{state: stateText, element: elementNone}, i + 1 + } return context{ state: elementContentType[c.element], element: c.element, @@ -113,6 +121,8 @@ func tTag(c context, s []byte) (context, int) { attrName := strings.ToLower(string(s[i:j])) if c.element == elementScript && attrName == "type" { attr = attrScriptType + } else if c.element == elementMeta && attrName == "content" { + attr = attrMetaContent } else { switch attrType(attrName) { case contentTypeURL: @@ -162,12 +172,13 @@ func tAfterName(c context, s []byte) (context, int) { } var attrStartStates = [...]state{ - attrNone: stateAttr, - attrScript: stateJS, - attrScriptType: stateAttr, - attrStyle: stateCSS, - attrURL: stateURL, - attrSrcset: stateSrcset, + attrNone: stateAttr, + attrScript: stateJS, + attrScriptType: stateAttr, + attrStyle: stateCSS, + attrURL: stateURL, + attrSrcset: stateSrcset, + attrMetaContent: stateMetaContent, } // tBeforeValue is the context transition function for stateBeforeValue. @@ -203,6 +214,7 @@ var specialTagEndMarkers = [...][]byte{ elementStyle: []byte("style"), elementTextarea: []byte("textarea"), elementTitle: []byte("title"), + elementMeta: []byte(""), } var ( @@ -612,6 +624,28 @@ func tError(c context, s []byte) (context, int) { return c, len(s) } +// tMetaContent is the context transition function for the meta content attribute state. +func tMetaContent(c context, s []byte) (context, int) { + for i := 0; i < len(s); i++ { + if i+3 <= len(s)-1 && bytes.Equal(bytes.ToLower(s[i:i+4]), []byte("url=")) { + c.state = stateMetaContentURL + return c, i + 4 + } + } + return c, len(s) +} + +// tMetaContentURL is the context transition function for the "url=" part of a meta content attribute state. +func tMetaContentURL(c context, s []byte) (context, int) { + for i := 0; i < len(s); i++ { + if s[i] == ';' { + c.state = stateMetaContent + return c, i + 1 + } + } + return c, len(s) +} + // eatAttrName returns the largest j such that s[i:j] is an attribute name. // It returns an error if s[i:] does not look like it begins with an // attribute name, such as encountering a quote mark without a preceding @@ -638,6 +672,7 @@ var elementNameMap = map[string]element{ "style": elementStyle, "textarea": elementTextarea, "title": elementTitle, + "meta": elementMeta, } // asciiAlpha reports whether c is an ASCII letter. diff --git a/tpl/internal/go_templates/testenv/testenv.go b/tpl/internal/go_templates/testenv/testenv.go index e87ed1c9b..d19d09cd4 100644 --- a/tpl/internal/go_templates/testenv/testenv.go +++ b/tpl/internal/go_templates/testenv/testenv.go @@ -12,6 +12,7 @@ package testenv import ( "bytes" + "errors" "flag" "fmt" "os" @@ -22,8 +23,6 @@ import ( "strings" "sync" "testing" - - "github.com/gohugoio/hugo/tpl/internal/go_templates/cfg" ) // Save the original environment during init for use in checks. A test @@ -32,8 +31,13 @@ import ( // environment might cause environment checks to behave erratically. var origEnv = os.Environ() -// Builder reports the name of the builder running this test -// (for example, "linux-amd64" or "windows-386-gce"). +// Builder reports the name of the builder running this test. For example, +// "gotip-linux-amd64_avx512-test_only" or "go1.24-windows-arm64" on LUCI, +// or "linux-amd64" on our old infrastructure. Prefer using runtime.GOOS, +// runtime.GOARCH, race.Enabled, reading the OS version, checking CPU +// feature flags with internal/cpu, etc. over parsing builder names when +// possible. When matching builder names, prefer a fuzzy match instead +// of a strict comparison. // If the test is not running on the build infrastructure, // Builder returns the empty string. func Builder() string { @@ -55,7 +59,31 @@ func HasGoBuild() bool { } var tryGoBuild = sync.OnceValue(func() error { - // Removed by Hugo, not used. + // To run 'go build', we need to be able to exec a 'go' command. + // We somewhat arbitrarily choose to exec 'go tool -n compile' because that + // also confirms that cmd/go can find the compiler. (Before CL 472096, + // we sometimes ended up with cmd/go installed in the test environment + // without a cmd/compile it could use to actually build things.) + goTool, err := goTool() + if err != nil { + return err + } + cmd := exec.Command(goTool, "tool", "-n", "compile") + cmd.Env = origEnv + out, err := cmd.Output() + if err != nil { + return fmt.Errorf("%v: %w", cmd, err) + } + out = bytes.TrimSpace(out) + if len(out) == 0 { + return fmt.Errorf("%v: no tool reported", cmd) + } + if _, err := exec.LookPath(string(out)); err != nil { + return err + } + + // Removed by Hugo (not used) + return nil }) @@ -113,18 +141,9 @@ func MustHaveParallelism(t testing.TB) { // If the tool is unavailable GoToolPath calls t.Skip. // If the tool should be available and isn't, GoToolPath calls t.Fatal. func GoToolPath(t testing.TB) string { - MustHaveGoBuild(t) - path, err := GoTool() - if err != nil { - t.Fatal(err) - } - // Add all environment variables that affect the Go command to test metadata. - // Cached test results will be invalidate when these variables change. - // See golang.org/issue/32285. - for envVar := range strings.FieldsSeq(cfg.KnownEnv) { - os.Getenv(envVar) - } - return path + t.Skip() + // Removed by Hugo (not used) + return "" } var findGOROOT = sync.OnceValues(func() (path string, err error) { @@ -210,8 +229,8 @@ func GOROOT(t testing.TB) string { // GoTool reports the path to the Go tool. func GoTool() (string, error) { - // Removed by Hugo, not used. - return "", nil + // Removed by Hugo (not used) + return "", errors.New("GoTool is not implemented") } var goTool = sync.OnceValues(func() (string, error) { @@ -282,17 +301,31 @@ func MustHaveCGO(t testing.TB) { // CanInternalLink reports whether the current system can link programs with // internal linking. func CanInternalLink(withCgo bool) bool { - // Removed by Hugo, not used. + // Removed by Hugo (not used) return false } +// SpecialBuildTypes are interesting build types that may affect linking. +type SpecialBuildTypes struct { + Cgo bool + Asan bool + Msan bool + Race bool +} + +// NoSpecialBuildTypes indicates a standard, no cgo go build. +var NoSpecialBuildTypes SpecialBuildTypes + // MustInternalLink checks that the current system can link programs with internal // linking. // If not, MustInternalLink calls t.Skip with an explanation. -func MustInternalLink(t testing.TB, withCgo bool) { - if !CanInternalLink(withCgo) { +func MustInternalLink(t testing.TB, with SpecialBuildTypes) { + if with.Asan || with.Msan || with.Race { + t.Skipf("skipping test: internal linking with sanitizers is not supported") + } + if !CanInternalLink(with.Cgo) { t.Helper() - if withCgo && CanInternalLink(false) { + if with.Cgo && CanInternalLink(false) { t.Skipf("skipping test: internal linking on %s/%s is not supported with cgo", runtime.GOOS, runtime.GOARCH) } t.Skipf("skipping test: internal linking on %s/%s is not supported", runtime.GOOS, runtime.GOARCH) @@ -303,14 +336,14 @@ func MustInternalLink(t testing.TB, withCgo bool) { // internal linking. // If not, MustInternalLinkPIE calls t.Skip with an explanation. func MustInternalLinkPIE(t testing.TB) { - // Removed by Hugo, not used. + // Removed by Hugo (not used) } // MustHaveBuildMode reports whether the current system can build programs in // the given build mode. // If not, MustHaveBuildMode calls t.Skip with an explanation. func MustHaveBuildMode(t testing.TB, buildmode string) { - // Removed by Hugo, not used. + // Removed by Hugo (not used) } // HasSymlink reports whether the current system can use os.Symlink. @@ -444,5 +477,28 @@ func SyscallIsNotSupported(err error) bool { // This function should be used when it is necessary to avoid t.Parallel on // 32-bit machines, typically because the test uses lots of memory. func ParallelOn64Bit(t *testing.T) { - // Removed by Hugo, not used. + // Removed by Hugo (not used) +} + +// CPUProfilingBroken returns true if CPU profiling has known issues on this +// platform. +func CPUProfilingBroken() bool { + switch runtime.GOOS { + case "plan9": + // Profiling unimplemented. + return true + case "aix": + // See https://golang.org/issue/45170. + return true + case "ios", "dragonfly", "netbsd", "illumos", "solaris": + // See https://golang.org/issue/13841. + return true + case "openbsd": + if runtime.GOARCH == "arm" || runtime.GOARCH == "arm64" { + // See https://golang.org/issue/13841. + return true + } + } + + return false } diff --git a/tpl/internal/go_templates/testenv/testenv_test.go b/tpl/internal/go_templates/testenv/testenv_test.go index 87bcb1c71..d1f0970d6 100644 --- a/tpl/internal/go_templates/testenv/testenv_test.go +++ b/tpl/internal/go_templates/testenv/testenv_test.go @@ -15,44 +15,7 @@ import ( ) func TestGoToolLocation(t *testing.T) { - t.Skip("This test is not relevant for Hugo") - testenv.MustHaveGoBuild(t) - - var exeSuffix string - if runtime.GOOS == "windows" { - exeSuffix = ".exe" - } - - // Tests are defined to run within their package source directory, - // and this package's source directory is $GOROOT/src/internal/testenv. - // The 'go' command is installed at $GOROOT/bin/go, so if the environment - // is correct then testenv.GoTool() should be identical to ../../../bin/go. - - relWant := "../../../bin/go" + exeSuffix - absWant, err := filepath.Abs(relWant) - if err != nil { - t.Fatal(err) - } - - wantInfo, err := os.Stat(absWant) - if err != nil { - t.Fatal(err) - } - t.Logf("found go tool at %q (%q)", relWant, absWant) - - goTool, err := testenv.GoTool() - if err != nil { - t.Fatalf("testenv.GoTool(): %v", err) - } - t.Logf("testenv.GoTool() = %q", goTool) - - gotInfo, err := os.Stat(goTool) - if err != nil { - t.Fatal(err) - } - if !os.SameFile(wantInfo, gotInfo) { - t.Fatalf("%q is not the same file as %q", absWant, goTool) - } + // Removed by Hugo. } func TestHasGoBuild(t *testing.T) { @@ -90,7 +53,12 @@ func TestHasGoBuild(t *testing.T) { return } case "android": - panic("Removed by Hugo, should not be used") + if isEmulatedBuilder(b) { + // As of 2023-05-02, the test environment on the emulated builders is + // missing a C linker. + t.Logf("HasGoBuild is false on %s", b) + return + } } if strings.Contains(b, "-noopt") { @@ -172,8 +140,8 @@ func TestCleanCmdEnvPWD(t *testing.T) { cmd = testenv.CleanCmdEnv(cmd) for _, env := range cmd.Env { - if after, ok := strings.CutPrefix(env, "PWD="); ok { - pwd := after + if strings.HasPrefix(env, "PWD=") { + pwd := strings.TrimPrefix(env, "PWD=") if pwd != dir { t.Errorf("unexpected PWD: want %s, got %s", dir, pwd) } diff --git a/tpl/internal/go_templates/testenv/testenv_unix.go b/tpl/internal/go_templates/testenv/testenv_unix.go index a62907884..af806fb13 100644 --- a/tpl/internal/go_templates/testenv/testenv_unix.go +++ b/tpl/internal/go_templates/testenv/testenv_unix.go @@ -7,8 +7,6 @@ package testenv import ( - "errors" - "io/fs" "syscall" ) @@ -17,27 +15,7 @@ import ( var Sigquit = syscall.SIGQUIT func syscallIsNotSupported(err error) bool { - if err == nil { - return false - } - - var errno syscall.Errno - if errors.As(err, &errno) { - switch errno { - case syscall.EPERM, syscall.EROFS: - // User lacks permission: either the call requires root permission and the - // user is not root, or the call is denied by a container security policy. - return true - case syscall.EINVAL: - // Some containers return EINVAL instead of EPERM if a system call is - // denied by security policy. - return true - } - } - - if errors.Is(err, fs.ErrPermission) || errors.Is(err, errors.ErrUnsupported) { - return true - } + // Removed by Hugo. return false } diff --git a/tpl/internal/go_templates/texttemplate/doc.go b/tpl/internal/go_templates/texttemplate/doc.go index 7b63bb76a..01b99f0e2 100644 --- a/tpl/internal/go_templates/texttemplate/doc.go +++ b/tpl/internal/go_templates/texttemplate/doc.go @@ -15,6 +15,11 @@ Execution of the template walks the structure and sets the cursor, represented by a period '.' and called "dot", to the value at the current location in the structure as execution proceeds. +The security model used by this package assumes that template authors are +trusted. The package does not auto-escape output, so injecting code into +a template can lead to arbitrary code execution if the template is executed +by an untrusted source. + The input text for a template is UTF-8-encoded text in any format. "Actions"--data evaluations or control structures--are delimited by "{{" and "}}"; all text outside actions is copied to the output unchanged. diff --git a/tpl/internal/go_templates/texttemplate/example_test.go b/tpl/internal/go_templates/texttemplate/example_test.go index 975ceea93..295a810b8 100644 --- a/tpl/internal/go_templates/texttemplate/example_test.go +++ b/tpl/internal/go_templates/texttemplate/example_test.go @@ -35,7 +35,7 @@ Josie Name, Gift string Attended bool } - recipients := []Recipient{ + var recipients = []Recipient{ {"Aunt Mildred", "bone china tea set", true}, {"Uncle John", "moleskin pants", false}, {"Cousin Rodney", "", false}, diff --git a/tpl/internal/go_templates/texttemplate/examplefunc_test.go b/tpl/internal/go_templates/texttemplate/examplefunc_test.go index 4a13b1f9a..0c745bdd4 100644 --- a/tpl/internal/go_templates/texttemplate/examplefunc_test.go +++ b/tpl/internal/go_templates/texttemplate/examplefunc_test.go @@ -55,3 +55,72 @@ Output 2: {{printf "%q" . | title}} // Output 1: "The Go Programming Language" // Output 2: "The Go Programming Language" } + +// This example demonstrates registering two custom template functions +// and how to overwite one of the functions after the template has been +// parsed. Overwriting can be used, for example, to alter the operation +// of cloned templates. +func ExampleTemplate_funcs() { + + // Define a simple template to test the functions. + const tmpl = `{{ . | lower | repeat }}` + + // Define the template funcMap with two functions. + var funcMap = template.FuncMap{ + "lower": strings.ToLower, + "repeat": func(s string) string { return strings.Repeat(s, 2) }, + } + + // Define a New template, add the funcMap using Funcs and then Parse + // the template. + parsedTmpl, err := template.New("t").Funcs(funcMap).Parse(tmpl) + if err != nil { + log.Fatal(err) + } + if err := parsedTmpl.Execute(os.Stdout, "ABC\n"); err != nil { + log.Fatal(err) + } + + // [Funcs] must be called before a template is parsed to add + // functions to the template. [Funcs] can also be used after a + // template is parsed to overwrite template functions. + // + // Here the function identified by "repeat" is overwritten. + parsedTmpl.Funcs(template.FuncMap{ + "repeat": func(s string) string { return strings.Repeat(s, 3) }, + }) + if err := parsedTmpl.Execute(os.Stdout, "DEF\n"); err != nil { + log.Fatal(err) + } + // Output: + // abc + // abc + // def + // def + // def +} + +// This example demonstrates how to use "if". +func ExampleTemplate_if() { + type book struct { + Stars float32 + Name string + } + + tpl, err := template.New("book").Parse(`{{ if (gt .Stars 4.0) }}"{{.Name }}" is a great book.{{ else }}"{{.Name}}" is not a great book.{{ end }}`) + if err != nil { + log.Fatalf("failed to parse template: %s", err) + } + + b := &book{ + Stars: 4.9, + Name: "Good Night, Gopher", + } + err = tpl.Execute(os.Stdout, b) + if err != nil { + log.Fatalf("failed to execute template: %s", err) + } + + // Output: + // "Good Night, Gopher" is a great book. +} diff --git a/tpl/internal/go_templates/texttemplate/exec.go b/tpl/internal/go_templates/texttemplate/exec.go index a61fcde19..d20c7bba7 100644 --- a/tpl/internal/go_templates/texttemplate/exec.go +++ b/tpl/internal/go_templates/texttemplate/exec.go @@ -7,12 +7,13 @@ package template import ( "errors" "fmt" - "github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort" - "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse" "io" "reflect" "runtime" "strings" + + "github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort" + "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse" ) // maxExecDepth specifies the maximum stack depth of templates within @@ -326,6 +327,7 @@ func isTrueOld(val reflect.Value) (truth, ok bool) { // Something like var x interface{}, never set. It's a form of nil. return false, true } + switch val.Kind() { case reflect.Array, reflect.Map, reflect.Slice, reflect.String: truth = val.Len() > 0 @@ -333,7 +335,7 @@ func isTrueOld(val reflect.Value) (truth, ok bool) { truth = val.Bool() case reflect.Complex64, reflect.Complex128: truth = val.Complex() != 0 - case reflect.Chan, reflect.Func, reflect.Pointer, reflect.Interface: + case reflect.Chan, reflect.Func, reflect.Pointer, reflect.UnsafePointer, reflect.Interface: truth = !val.IsNil() case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: truth = val.Int() != 0 diff --git a/tpl/internal/go_templates/texttemplate/exec_test.go b/tpl/internal/go_templates/texttemplate/exec_test.go index b2cf3e7c6..5f3e0e902 100644 --- a/tpl/internal/go_templates/texttemplate/exec_test.go +++ b/tpl/internal/go_templates/texttemplate/exec_test.go @@ -18,6 +18,7 @@ import ( "strings" "sync" "testing" + "unsafe" ) var debug = flag.Bool("debug", false, "show the errors produced by the tests") @@ -45,7 +46,8 @@ type T struct { SIEmpty []int SB []bool // Arrays - AI [3]int + AI [3]int + PAI *[3]int // pointer to array // Maps MSI map[string]int MSIone map[string]int // one element, for deterministic output @@ -74,10 +76,12 @@ type T struct { Str fmt.Stringer Err error // Pointers - PI *int - PS *string - PSI *[]int - NIL *int + PI *int + PS *string + PSI *[]int + NIL *int + UPI unsafe.Pointer + EmptyUPI unsafe.Pointer // Function (not method) BinaryFunc func(string, string) string VariadicFunc func(...string) string @@ -142,6 +146,7 @@ var tVal = &T{ SI: []int{3, 4, 5}, SICap: make([]int, 5, 10), AI: [3]int{3, 4, 5}, + PAI: &[3]int{3, 4, 5}, SB: []bool{true, false}, MSI: map[string]int{"one": 1, "two": 2, "three": 3}, MSIone: map[string]int{"one": 1}, @@ -169,6 +174,7 @@ var tVal = &T{ PI: newInt(23), PS: newString("a string"), PSI: newIntSlice(21, 22, 23), + UPI: newUnsafePointer(23), BinaryFunc: func(a, b string) string { return fmt.Sprintf("[%s=%s]", a, b) }, VariadicFunc: func(s ...string) string { return fmt.Sprint("<", strings.Join(s, "+"), ">") }, VariadicFuncInt: func(a int, s ...string) string { return fmt.Sprint(a, "=<", strings.Join(s, "+"), ">") }, @@ -195,6 +201,10 @@ func newInt(n int) *int { return &n } +func newUnsafePointer(n int) unsafe.Pointer { + return unsafe.Pointer(&n) +} + func newString(s string) *string { return &s } @@ -456,6 +466,10 @@ var execTests = []execTest{ {"if 0.0", "{{if .FloatZero}}NON-ZERO{{else}}ZERO{{end}}", "ZERO", tVal, true}, {"if 1.5i", "{{if 1.5i}}NON-ZERO{{else}}ZERO{{end}}", "NON-ZERO", tVal, true}, {"if 0.0i", "{{if .ComplexZero}}NON-ZERO{{else}}ZERO{{end}}", "ZERO", tVal, true}, + {"if nonNilPointer", "{{if .PI}}NON-ZERO{{else}}ZERO{{end}}", "NON-ZERO", tVal, true}, + {"if nilPointer", "{{if .NIL}}NON-ZERO{{else}}ZERO{{end}}", "ZERO", tVal, true}, + {"if UPI", "{{if .UPI}}NON-ZERO{{else}}ZERO{{end}}", "NON-ZERO", tVal, true}, + {"if EmptyUPI", "{{if .EmptyUPI}}NON-ZERO{{else}}ZERO{{end}}", "ZERO", tVal, true}, {"if emptystring", "{{if ``}}NON-EMPTY{{else}}EMPTY{{end}}", "EMPTY", tVal, true}, {"if string", "{{if `notempty`}}NON-EMPTY{{else}}EMPTY{{end}}", "NON-EMPTY", tVal, true}, {"if emptyslice", "{{if .SIEmpty}}NON-EMPTY{{else}}EMPTY{{end}}", "EMPTY", tVal, true}, @@ -561,6 +575,9 @@ var execTests = []execTest{ {"array[:]", "{{slice .AI}}", "[3 4 5]", tVal, true}, {"array[1:]", "{{slice .AI 1}}", "[4 5]", tVal, true}, {"array[1:2]", "{{slice .AI 1 2}}", "[4]", tVal, true}, + {"pointer to array[:]", "{{slice .PAI}}", "[3 4 5]", tVal, true}, + {"pointer to array[1:]", "{{slice .PAI 1}}", "[4 5]", tVal, true}, + {"pointer to array[1:2]", "{{slice .PAI 1 2}}", "[4]", tVal, true}, {"string[:]", "{{slice .S}}", "xyz", tVal, true}, {"string[0:1]", "{{slice .S 0 1}}", "x", tVal, true}, {"string[1:]", "{{slice .S 1}}", "yz", tVal, true}, @@ -804,7 +821,7 @@ func count(n int) chan string { } c := make(chan string) go func() { - for i := range n { + for i := 0; i < n; i++ { c <- "abcdefghijklmnop"[i : i+1] } close(c) @@ -1008,24 +1025,6 @@ type CustomError struct{} func (*CustomError) Error() string { return "heyo !" } -// Check that a custom error can be returned. -func TestExecError_CustomError(t *testing.T) { - failingFunc := func() (string, error) { - return "", &CustomError{} - } - tmpl := Must(New("top").Funcs(FuncMap{ - "err": failingFunc, - }).Parse("{{ err }}")) - - var b bytes.Buffer - err := tmpl.Execute(&b, nil) - - var e *CustomError - if !errors.As(err, &e) { - t.Fatalf("expected custom error; got %s", err) - } -} - func TestJSEscaping(t *testing.T) { testCases := []struct { in, exp string @@ -1510,6 +1509,45 @@ func TestBadFuncNames(t *testing.T) { } } +func TestIsTrue(t *testing.T) { + var nil_ptr *int + var nil_chan chan int + tests := []struct { + v any + want bool + }{ + {1, true}, + {0, false}, + {uint8(1), true}, + {uint8(0), false}, + {float64(1.0), true}, + {float64(0.0), false}, + {complex64(1.0), true}, + {complex64(0.0), false}, + {true, true}, + {false, false}, + {[2]int{1, 2}, true}, + {[0]int{}, false}, + {[]byte("abc"), true}, + {[]byte(""), false}, + {map[string]int{"a": 1, "b": 2}, true}, + {map[string]int{}, false}, + {make(chan int), true}, + {nil_chan, false}, + //{new(int), true}, // Commented out for Hugo. We have a slightly different view on ... the truth. + {nil_ptr, false}, + {unsafe.Pointer(new(int)), true}, + {unsafe.Pointer(nil_ptr), false}, + } + for i, test_case := range tests { + + got, _ := IsTrue(test_case.v) + if got != test_case.want { + t.Fatalf("[%d] expect result %v, got %v", i, test_case.want, got) + } + } +} + func testBadFuncName(name string, t *testing.T) { t.Helper() defer func() { @@ -1714,8 +1752,8 @@ func TestInterfaceValues(t *testing.T) { "Nil": nil, "Zero": 0, }) - if after, ok := strings.CutPrefix(tt.out, "ERROR:"); ok { - e := strings.TrimSpace(after) + if strings.HasPrefix(tt.out, "ERROR:") { + e := strings.TrimSpace(strings.TrimPrefix(tt.out, "ERROR:")) if err == nil || !strings.Contains(err.Error(), e) { t.Errorf("%s: Execute: %v, want error %q", tt.text, err, e) } @@ -1940,7 +1978,7 @@ func TestIssue39807(t *testing.T) { wg.Add(1) go func() { defer wg.Done() - for range numTemplates { + for j := 0; j < numTemplates; j++ { _, err := tplFoo.AddParseTree(tplBar.Name(), tplBar.Tree) if err != nil { t.Error(err) diff --git a/tpl/internal/go_templates/texttemplate/funcs.go b/tpl/internal/go_templates/texttemplate/funcs.go index 5318798ad..ed9c7ea5d 100644 --- a/tpl/internal/go_templates/texttemplate/funcs.go +++ b/tpl/internal/go_templates/texttemplate/funcs.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "io" - "maps" "net/url" "reflect" "strings" @@ -23,7 +22,7 @@ import ( // return value evaluates to non-nil during execution, execution terminates and // Execute returns that error. // -// Errors returned by Execute wrap the underlying error; call [errors.As] to +// Errors returned by Execute wrap the underlying error; call [errors.AsType] to // unwrap them. // // When template execution invokes a function with an argument list, that list @@ -63,26 +62,13 @@ func builtins() FuncMap { } } -var builtinFuncsOnce struct { - sync.Once - v map[string]reflect.Value -} - -// builtinFuncsOnce lazily computes & caches the builtinFuncs map. -// TODO: revert this back to a global map once golang.org/issue/2559 is fixed. -func builtinFuncs() map[string]reflect.Value { - builtinFuncsOnce.Do(func() { - builtinFuncsOnce.v = createValueFuncs(builtins()) - }) - return builtinFuncsOnce.v -} - -// createValueFuncs turns a FuncMap into a map[string]reflect.Value -func createValueFuncs(funcMap FuncMap) map[string]reflect.Value { - m := make(map[string]reflect.Value) +// builtinFuncs lazily computes & caches the builtinFuncs map. +var builtinFuncs = sync.OnceValue(func() map[string]reflect.Value { + funcMap := builtins() + m := make(map[string]reflect.Value, len(funcMap)) addValueFuncs(m, funcMap) return m -} +}) // addValueFuncs adds to values the functions in funcs, converting them to reflect.Values. func addValueFuncs(out map[string]reflect.Value, in FuncMap) { @@ -104,7 +90,9 @@ func addValueFuncs(out map[string]reflect.Value, in FuncMap) { // addFuncs adds to values the functions in funcs. It does no checking of the input - // call addValueFuncs first. func addFuncs(out, in FuncMap) { - maps.Copy(out, in) + for name, fn := range in { + out[name] = fn + } } // goodFunc reports whether the function or method has the right result signature. @@ -256,6 +244,10 @@ func slice(item reflect.Value, indexes ...reflect.Value) (reflect.Value, error) if !item.IsValid() { return reflect.Value{}, fmt.Errorf("slice of untyped nil") } + var isNil bool + if item, isNil = indirect(item); isNil { + return reflect.Value{}, fmt.Errorf("slice of nil pointer") + } if len(indexes) > 3 { return reflect.Value{}, fmt.Errorf("too many slice indexes: %d", len(indexes)) } @@ -408,7 +400,6 @@ func not(arg reflect.Value) bool { var ( errBadComparisonType = errors.New("invalid type for comparison") - errBadComparison = errors.New("incompatible types for comparison") errNoComparison = errors.New("missing argument for comparison") ) @@ -486,7 +477,7 @@ func eq(arg1 reflect.Value, arg2 ...reflect.Value) (bool, error) { truth = arg.Int() >= 0 && arg1.Uint() == uint64(arg.Int()) default: if arg1.IsValid() && arg.IsValid() { - return false, errBadComparison + return false, fmt.Errorf("incompatible types for comparison: %v and %v", arg1.Type(), arg.Type()) } } } else { @@ -552,7 +543,7 @@ func lt(arg1, arg2 reflect.Value) (bool, error) { case k1 == uintKind && k2 == intKind: truth = arg2.Int() >= 0 && arg1.Uint() < uint64(arg2.Int()) default: - return false, errBadComparison + return false, fmt.Errorf("incompatible types for comparison: %v and %v", arg1.Type(), arg2.Type()) } } else { switch k1 { diff --git a/tpl/internal/go_templates/texttemplate/multi_test.go b/tpl/internal/go_templates/texttemplate/multi_test.go index 6e383c131..869a443fa 100644 --- a/tpl/internal/go_templates/texttemplate/multi_test.go +++ b/tpl/internal/go_templates/texttemplate/multi_test.go @@ -213,6 +213,7 @@ const ( cloneText2 = `{{define "b"}}b{{end}}` cloneText3 = `{{define "c"}}root{{end}}` cloneText4 = `{{define "c"}}clone{{end}}` + cloneText5 = `{{define "e"}}{{.Foo}}{{end}}` ) func TestClone(t *testing.T) { @@ -225,6 +226,8 @@ func TestClone(t *testing.T) { if err != nil { t.Fatal(err) } + root.Parse(cloneText5) + root.Option("missingkey=error") clone := Must(root.Clone()) // Add variants to both. _, err = root.Parse(cloneText3) @@ -262,6 +265,14 @@ func TestClone(t *testing.T) { if b.String() != "bclone" { t.Errorf("expected %q got %q", "bclone", b.String()) } + b.Reset() + rootErr := root.ExecuteTemplate(&b, "e", map[string]any{}) + cloneErr := clone.ExecuteTemplate(&b, "e", map[string]any{}) + if cloneErr == nil { + t.Errorf("expected error from missing key in cloned template") + } else if got, want := cloneErr.Error(), rootErr.Error(); got != want { + t.Errorf("got %q, wan t %q", got, want) + } } func TestAddParseTree(t *testing.T) { diff --git a/tpl/internal/go_templates/texttemplate/parse/lex_test.go b/tpl/internal/go_templates/texttemplate/parse/lex_test.go index b1ea69967..633ffb055 100644 --- a/tpl/internal/go_templates/texttemplate/parse/lex_test.go +++ b/tpl/internal/go_templates/texttemplate/parse/lex_test.go @@ -581,15 +581,3 @@ func TestPos(t *testing.T) { } } } - -// parseLexer is a local version of parse that lets us pass in the lexer instead of building it. -// We expect an error, so the tree set and funcs list are explicitly nil. -func (t *Tree) parseLexer(lex *lexer) (tree *Tree, err error) { - defer t.recover(&err) - t.ParseName = t.Name - t.startParse(nil, lex, map[string]*Tree{}) - t.parse() - t.add() - t.stopParse() - return t, nil -} diff --git a/tpl/internal/go_templates/texttemplate/parse/parse.go b/tpl/internal/go_templates/texttemplate/parse/parse.go index cc8a93360..b74dfb7f4 100644 --- a/tpl/internal/go_templates/texttemplate/parse/parse.go +++ b/tpl/internal/go_templates/texttemplate/parse/parse.go @@ -12,7 +12,6 @@ import ( "bytes" "fmt" "runtime" - "slices" "strconv" "strings" ) @@ -33,9 +32,10 @@ type Tree struct { treeSet map[string]*Tree actionLine int // line of left delim starting action rangeDepth int + stackDepth int // depth of nested parenthesized expressions } -// A mode value is a set of flags (or 0). Modes control parser behavior. +// A Mode value is a set of flags (or 0). Modes control parser behavior. type Mode uint const ( @@ -43,6 +43,17 @@ const ( SkipFuncCheck // do not check that functions are defined ) +// maxStackDepth is the maximum depth permitted for nested +// parenthesized expressions. +var maxStackDepth = 10000 + +// init reduces maxStackDepth for WebAssembly due to its smaller stack size. +func init() { + if runtime.GOARCH == "wasm" { + maxStackDepth = 1000 + } +} + // Copy returns a copy of the [Tree]. Any parsing state is discarded. func (t *Tree) Copy() *Tree { if t == nil { @@ -224,6 +235,7 @@ func (t *Tree) startParse(funcs []map[string]any, lex *lexer, treeSet map[string t.vars = []string{"$"} t.funcs = funcs t.treeSet = treeSet + t.stackDepth = 0 lex.options = lexOptions{ emitComment: t.Mode&ParseComments != 0, breakOK: !t.hasFunction("break"), @@ -534,7 +546,7 @@ func (t *Tree) parseControl(context string) (pos Pos, line int, pipe *PipeNode, t.rangeDepth-- } switch next.Type() { - case nodeEnd: // done + case nodeEnd: //done case nodeElse: // Special case for "else if" and "else with". // If the "else" is followed immediately by an "if" or "with", @@ -788,6 +800,11 @@ func (t *Tree) term() Node { } return number case itemLeftParen: + if t.stackDepth >= maxStackDepth { + t.errorf("max expression depth exceeded") + } + t.stackDepth++ + defer func() { t.stackDepth-- }() return t.pipeline("parenthesized pipeline", itemRightParen) case itemString, itemRawString: s, err := strconv.Unquote(token.val) @@ -822,8 +839,10 @@ func (t *Tree) popVars(n int) { // variable is not defined. func (t *Tree) useVar(pos Pos, name string) Node { v := t.newVariable(pos, name) - if slices.Contains(t.vars, v.Ident[0]) { - return v + for _, varName := range t.vars { + if varName == v.Ident[0] { + return v + } } t.errorf("undefined variable %q", v.Ident[0]) return nil diff --git a/tpl/internal/go_templates/texttemplate/parse/parse_test.go b/tpl/internal/go_templates/texttemplate/parse/parse_test.go index 47951a9c9..90ec9852b 100644 --- a/tpl/internal/go_templates/texttemplate/parse/parse_test.go +++ b/tpl/internal/go_templates/texttemplate/parse/parse_test.go @@ -89,6 +89,11 @@ var numberTests = []numberTest{ {"0xef", true, true, true, false, 0xef, 0xef, 0xef, 0}, } +func init() { + // Use a small stack limit for testing to avoid creating huge expressions. + maxStackDepth = 3 +} + func TestNumberParse(t *testing.T) { for _, test := range numberTests { // If fmt.Sscan thinks it's complex, it's complex. We can't trust the output @@ -330,6 +335,15 @@ var parseTests = []parseTest{ {"empty pipeline", `{{printf "%d" ( ) }}`, hasError, ""}, // Missing pipeline in block {"block definition", `{{block "foo"}}hello{{end}}`, hasError, ""}, + + // Expression nested depth tests. + {"paren nesting normal", "{{ (( 1 )) }}", noError, "{{((1))}}"}, + {"paren nesting at limit", "{{ ((( 1 ))) }}", noError, "{{(((1)))}}"}, + {"paren nesting exceeds limit", "{{ (((( 1 )))) }}", hasError, "template: test:1: max expression depth exceeded"}, + {"paren nesting in pipeline", "{{ ((( 1 ))) | printf }}", noError, "{{(((1))) | printf}}"}, + {"paren nesting in pipeline exceeds limit", "{{ (((( 1 )))) | printf }}", hasError, "template: test:1: max expression depth exceeded"}, + {"paren nesting with other constructs", "{{ if ((( true ))) }}YES{{ end }}", noError, "{{if (((true)))}}\"YES\"{{end}}"}, + {"paren nesting with other constructs exceeds limit", "{{ if (((( true )))) }}YES{{ end }}", hasError, "template: test:1: max expression depth exceeded"}, } var builtins = map[string]any{ diff --git a/tpl/internal/go_templates/texttemplate/template.go b/tpl/internal/go_templates/texttemplate/template.go index 0be64eee2..e66c01fd4 100644 --- a/tpl/internal/go_templates/texttemplate/template.go +++ b/tpl/internal/go_templates/texttemplate/template.go @@ -90,6 +90,7 @@ func (t *Template) Clone() (*Template, error) { if t.common == nil { return nt, nil } + nt.option = t.option t.muTmpl.RLock() defer t.muTmpl.RUnlock() for k, v := range t.tmpl { -- 2.39.5