From a773c36e59a47e048c8ebb01c168f863020510d6 Mon Sep 17 00:00:00 2001 From: F-WRunTime Date: Mon, 6 Apr 2026 15:30:04 -0600 Subject: [PATCH 1/3] html/template, net/http, cookiejar: add fuzz tests Add FuzzHTMLTemplateParse, FuzzReadRequest, and FuzzCookieJarSetCookies with anchor tests for local validation. Made-with: Cursor --- src/html/template/template_fuzz_test.go | 33 +++++++++++++++++ src/net/http/cookiejar/jar_fuzz_test.go | 45 +++++++++++++++++++++++ src/net/http/readrequest_fuzz_test.go | 47 +++++++++++++++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 src/html/template/template_fuzz_test.go create mode 100644 src/net/http/cookiejar/jar_fuzz_test.go create mode 100644 src/net/http/readrequest_fuzz_test.go 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..15b46829544b5f --- /dev/null +++ b/src/net/http/cookiejar/jar_fuzz_test.go @@ -0,0 +1,45 @@ +// 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" + "net/http" + "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}) +} + +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() + 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}) + } + }) +} 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() + } + }) +} From 9ed0bb78705ca697d357c4ea7d9925a8866e592c Mon Sep 17 00:00:00 2001 From: F-WRunTime Date: Tue, 7 Apr 2026 10:12:24 -0600 Subject: [PATCH 2/3] src: set go.mod to 1.26 for KaaS/CI stable toolchain Relax language version from 1.27 so remote fuzz jobs can use golang:1.26 images; local development can still use a newer toolchain. Made-with: Cursor --- src/go.mod | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From a846dc848dae3d1ce08c7295673d2eadb099ee3c Mon Sep 17 00:00:00 2001 From: F-WRunTime Date: Tue, 7 Apr 2026 10:21:04 -0600 Subject: [PATCH 3/3] New invariants tot he tests --- src/net/http/cookiejar/jar_fuzz_test.go | 63 +++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/net/http/cookiejar/jar_fuzz_test.go b/src/net/http/cookiejar/jar_fuzz_test.go index 15b46829544b5f..1365daa893fd47 100644 --- a/src/net/http/cookiejar/jar_fuzz_test.go +++ b/src/net/http/cookiejar/jar_fuzz_test.go @@ -6,7 +6,10 @@ package cookiejar import ( "bytes" + "cmp" "net/http" + "net/url" + "slices" "testing" ) @@ -20,6 +23,57 @@ func TestFuzzCookieJarSetCookiesAnchor(t *testing.T) { 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=/")) @@ -30,6 +84,7 @@ func FuzzCookieJarSetCookies(f *testing.F) { return } jar := newTestJar() + applied := 0 for _, line := range bytes.Split(data, []byte("\n")) { line = bytes.TrimSpace(line) if len(line) == 0 { @@ -40,6 +95,14 @@ func FuzzCookieJarSetCookies(f *testing.F) { 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) + } } }) }