diff --git a/src/go.mod b/src/go.mod index 2ac54d7b35e011..f387c2b7483b59 100644 --- a/src/go.mod +++ b/src/go.mod @@ -1,6 +1,7 @@ module std -go 1.27 +// Relaxed below 1.27 so KaaS / CI can use stable golang:1.26 images (Option C). +go 1.26 require ( golang.org/x/crypto v0.47.1-0.20260113154411-7d0074ccc6f1 diff --git a/src/html/template/template_fuzz_test.go b/src/html/template/template_fuzz_test.go new file mode 100644 index 00000000000000..ca6025f630d806 --- /dev/null +++ b/src/html/template/template_fuzz_test.go @@ -0,0 +1,33 @@ +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package template_test + +import ( + "html/template" + "testing" +) + +func TestFuzzHTMLTemplateParseAnchor(t *testing.T) { + _, err := template.New("anchor").Parse("{{.}}") + if err != nil { + t.Fatal(err) + } +} + +func FuzzHTMLTemplateParse(f *testing.F) { + f.Add([]byte("{{.}}")) + f.Add([]byte("")) + f.Add([]byte("{{if .X}}{{.Y}}{{end}}")) + + f.Fuzz(func(t *testing.T, data []byte) { + if len(data) > 256*1024 { + return + } + _, err := template.New("fuzz").Parse(string(data)) + if err != nil { + return + } + }) +} diff --git a/src/net/http/cookiejar/jar_fuzz_test.go b/src/net/http/cookiejar/jar_fuzz_test.go new file mode 100644 index 00000000000000..1365daa893fd47 --- /dev/null +++ b/src/net/http/cookiejar/jar_fuzz_test.go @@ -0,0 +1,108 @@ +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cookiejar + +import ( + "bytes" + "cmp" + "net/http" + "net/url" + "slices" + "testing" +) + +func TestFuzzCookieJarSetCookiesAnchor(t *testing.T) { + u := mustParseURL("https://example.org/path") + j := newTestJar() + c, err := http.ParseSetCookie("a=b; Path=/") + if err != nil { + t.Fatal(err) + } + j.SetCookies(u, []*http.Cookie{c}) +} + +// fuzzNormalizeCookies returns a sorted copy for (Name, Value, Quoted) comparison. +// jar.Cookies only populates those fields (see jar.go). +func fuzzNormalizeCookies(cs []*http.Cookie) []http.Cookie { + out := make([]http.Cookie, len(cs)) + for i, c := range cs { + if c != nil { + out[i] = *c + } + } + slices.SortFunc(out, func(a, b http.Cookie) int { + if r := cmp.Compare(a.Name, b.Name); r != 0 { + return r + } + if r := cmp.Compare(a.Value, b.Value); r != 0 { + return r + } + if a.Quoted == b.Quoted { + return 0 + } + if !a.Quoted { + return -1 + } + return 1 + }) + return out +} + +// fuzzAssertJarCookiesSemanticallyConsistent checks that Cookies is idempotent +// and returns only well-formed names — stronger than “no panic” alone. +func fuzzAssertJarCookiesSemanticallyConsistent(t *testing.T, jar *Jar, u *url.URL) { + t.Helper() + got1 := jar.Cookies(u) + got2 := jar.Cookies(u) + n1 := fuzzNormalizeCookies(got1) + n2 := fuzzNormalizeCookies(got2) + if len(n1) != len(n2) { + t.Fatalf("Cookies length unstable between reads: %d vs %d", len(n1), len(n2)) + } + for i := range n1 { + a, b := n1[i], n2[i] + if a.Name != b.Name || a.Value != b.Value || a.Quoted != b.Quoted { + t.Fatalf("Cookies mismatch on re-read at %d: %+v vs %+v", i, a, b) + } + } + for _, c := range got1 { + if c.Name == "" { + t.Fatalf("jar returned cookie with empty Name") + } + } +} + +func FuzzCookieJarSetCookies(f *testing.F) { + u := mustParseURL("https://example.org/path") + f.Add([]byte("a=b; Path=/")) + f.Add([]byte("session=xyz; Path=/; HttpOnly\nlang=en; Path=/")) + + f.Fuzz(func(t *testing.T, data []byte) { + if len(data) > 8*1024 { + return + } + jar := newTestJar() + applied := 0 + for _, line := range bytes.Split(data, []byte("\n")) { + line = bytes.TrimSpace(line) + if len(line) == 0 { + continue + } + c, err := http.ParseSetCookie(string(line)) + if err != nil { + continue + } + jar.SetCookies(u, []*http.Cookie{c}) + applied++ + } + fuzzAssertJarCookiesSemanticallyConsistent(t, jar, u) + // Trivial bound: jar cannot return more cookies than Set-Cookie lines we applied. + if applied > 0 { + if n := len(jar.Cookies(u)); n > applied { + t.Fatalf("jar returned %d cookies but only %d Set-Cookie lines applied", n, applied) + } + } + }) +} diff --git a/src/net/http/readrequest_fuzz_test.go b/src/net/http/readrequest_fuzz_test.go new file mode 100644 index 00000000000000..cf8aa55888cd4a --- /dev/null +++ b/src/net/http/readrequest_fuzz_test.go @@ -0,0 +1,47 @@ +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package http + +import ( + "bufio" + "bytes" + "io" + "strings" + "testing" +) + +func TestFuzzReadRequestAnchor(t *testing.T) { + const raw = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n" + req, err := ReadRequest(bufio.NewReader(strings.NewReader(raw))) + if err != nil { + t.Fatal(err) + } + if req.Method != "GET" { + t.Fatalf("Method = %q", req.Method) + } + if req.Body != nil { + io.Copy(io.Discard, req.Body) + req.Body.Close() + } +} + +func FuzzReadRequest(f *testing.F) { + f.Add([]byte("GET / HTTP/1.1\r\nHost: x\r\n\r\n")) + f.Add([]byte("GET http://x/ HTTP/1.1\r\nHost: x\r\n\r\n")) + + f.Fuzz(func(t *testing.T, data []byte) { + if len(data) > 64*1024 { + return + } + req, err := ReadRequest(bufio.NewReader(bytes.NewReader(data))) + if err != nil { + return + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + req.Body.Close() + } + }) +}