mirror of
https://github.com/golang/go.git
synced 2026-01-31 16:12:04 +03:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28622c1959 | ||
|
|
e05b2c92d9 | ||
|
|
79ec0c94f3 | ||
|
|
bed6c81c2d | ||
|
|
2612dcfd3c | ||
|
|
90f72bd500 | ||
|
|
e0f655bf3f | ||
|
|
100c5a6680 | ||
|
|
f0c69db15a | ||
|
|
9fd3ac8a10 | ||
|
|
5d7a787aa2 | ||
|
|
930ce220d0 | ||
|
|
6a057327cf | ||
|
|
66f6feaa53 | ||
|
|
d6f2741248 | ||
|
|
28ac8d2104 | ||
|
|
06993c7721 | ||
|
|
0b53e410f8 | ||
|
|
7735dc90ed | ||
|
|
205d086595 | ||
|
|
16fdaac4b1 | ||
|
|
f3dc4aac0b | ||
|
|
79c3081b4b | ||
|
|
b816c79658 | ||
|
|
90de3b3399 | ||
|
|
bec452a3a2 | ||
|
|
57bd28ab7f | ||
|
|
f75bcffa4a | ||
|
|
7d570090a9 | ||
|
|
be61132165 | ||
|
|
a86792b169 | ||
|
|
879e3cb5f7 | ||
|
|
56ebf80e57 | ||
|
|
b1959cf6f7 | ||
|
|
cdd8cf4988 | ||
|
|
8995e84ac6 | ||
|
|
749dff880a | ||
|
|
21ac81c1e1 | ||
|
|
c72fcab6d6 | ||
|
|
6e676ab2b8 | ||
|
|
ac94297758 | ||
|
|
6961c3775f | ||
|
|
ebee011a54 | ||
|
|
84fb1b8253 | ||
|
|
c95d3093ca | ||
|
|
561964c9a8 | ||
|
|
e73dadc758 | ||
|
|
2899144b8d | ||
|
|
b062eb46e8 | ||
|
|
8ac5714ef2 | ||
|
|
9546293d22 | ||
|
|
4b3a0b9785 | ||
|
|
5abb1d84f8 |
@@ -1 +1,2 @@
|
||||
branch: master
|
||||
branch: release-branch.go1.25
|
||||
parent-branch: master
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!--{
|
||||
"Title": "The Go Programming Language Specification",
|
||||
"Subtitle": "Language version go1.25 (Feb 25, 2025)",
|
||||
"Subtitle": "Language version go1.25 (Aug 12, 2025)",
|
||||
"Path": "/ref/spec"
|
||||
}-->
|
||||
|
||||
|
||||
@@ -153,6 +153,16 @@ for example,
|
||||
see the [runtime documentation](/pkg/runtime#hdr-Environment_Variables)
|
||||
and the [go command documentation](/cmd/go#hdr-Build_and_test_caching).
|
||||
|
||||
### Go 1.26
|
||||
|
||||
Go 1.26 added a new `httpcookiemaxnum` setting that controls the maximum number
|
||||
of cookies that net/http will accept when parsing HTTP headers. If the number of
|
||||
cookie in a header exceeds the number set in `httpcookiemaxnum`, cookie parsing
|
||||
will fail early. The default value is `httpcookiemaxnum=3000`. Setting
|
||||
`httpcookiemaxnum=0` will allow the cookie parsing to accept an indefinite
|
||||
number of cookies. To avoid denial of service attacks, this setting and default
|
||||
was backported to Go 1.25.2 and Go 1.24.8.
|
||||
|
||||
### Go 1.25
|
||||
|
||||
Go 1.25 added a new `decoratemappings` setting that controls whether the Go
|
||||
|
||||
@@ -9,4 +9,4 @@
|
||||
#
|
||||
# go test cmd/go/internal/fips140 -update
|
||||
#
|
||||
v1.0.0.zip b50508feaeff05d22516b21e1fd210bbf5d6a1e422eaf2cfa23fe379342713b8
|
||||
v1.0.0-c2097c7c.zip daf3614e0406f67ae6323c902db3f953a1effb199142362a039e7526dfb9368b
|
||||
|
||||
@@ -1 +1 @@
|
||||
v1.0.0
|
||||
v1.0.0-c2097c7c
|
||||
|
||||
Binary file not shown.
1
lib/fips140/v1.0.0.txt
Normal file
1
lib/fips140/v1.0.0.txt
Normal file
@@ -0,0 +1 @@
|
||||
v1.0.0-c2097c7c
|
||||
@@ -39,6 +39,7 @@ var (
|
||||
errMissData = errors.New("archive/tar: sparse file references non-existent data")
|
||||
errUnrefData = errors.New("archive/tar: sparse file contains unreferenced data")
|
||||
errWriteHole = errors.New("archive/tar: write non-NUL byte in sparse hole")
|
||||
errSparseTooLong = errors.New("archive/tar: sparse map too long")
|
||||
)
|
||||
|
||||
type headerError []string
|
||||
|
||||
@@ -531,12 +531,17 @@ func readGNUSparseMap1x0(r io.Reader) (sparseDatas, error) {
|
||||
cntNewline int64
|
||||
buf bytes.Buffer
|
||||
blk block
|
||||
totalSize int
|
||||
)
|
||||
|
||||
// feedTokens copies data in blocks from r into buf until there are
|
||||
// at least cnt newlines in buf. It will not read more blocks than needed.
|
||||
feedTokens := func(n int64) error {
|
||||
for cntNewline < n {
|
||||
totalSize += len(blk)
|
||||
if totalSize > maxSpecialFileSize {
|
||||
return errSparseTooLong
|
||||
}
|
||||
if _, err := mustReadFull(r, blk[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -569,8 +574,8 @@ func readGNUSparseMap1x0(r io.Reader) (sparseDatas, error) {
|
||||
}
|
||||
|
||||
// Parse for all member entries.
|
||||
// numEntries is trusted after this since a potential attacker must have
|
||||
// committed resources proportional to what this library used.
|
||||
// numEntries is trusted after this since feedTokens limits the number of
|
||||
// tokens based on maxSpecialFileSize.
|
||||
if err := feedTokens(2 * numEntries); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -621,6 +621,11 @@ func TestReader(t *testing.T) {
|
||||
},
|
||||
Format: FormatPAX,
|
||||
}},
|
||||
}, {
|
||||
// Small compressed file that uncompresses to
|
||||
// a file with a very large GNU 1.0 sparse map.
|
||||
file: "testdata/gnu-sparse-many-zeros.tar.bz2",
|
||||
err: errSparseTooLong,
|
||||
}}
|
||||
|
||||
for _, v := range vectors {
|
||||
|
||||
BIN
src/archive/tar/testdata/gnu-sparse-many-zeros.tar.bz2
vendored
Normal file
BIN
src/archive/tar/testdata/gnu-sparse-many-zeros.tar.bz2
vendored
Normal file
Binary file not shown.
@@ -128,14 +128,29 @@ func Info(ctxt *obj.Link, fnsym *obj.LSym, infosym *obj.LSym, curfn obj.Func) (s
|
||||
// already referenced by a dwarf var, attach an R_USETYPE relocation to
|
||||
// the function symbol to insure that the type included in DWARF
|
||||
// processing during linking.
|
||||
// Do the same with R_USEIFACE relocations from the function symbol for the
|
||||
// same reason.
|
||||
// All these R_USETYPE relocations are only looked at if the function
|
||||
// survives deadcode elimination in the linker.
|
||||
typesyms := []*obj.LSym{}
|
||||
for t := range fnsym.Func().Autot {
|
||||
typesyms = append(typesyms, t)
|
||||
}
|
||||
for i := range fnsym.R {
|
||||
if fnsym.R[i].Type == objabi.R_USEIFACE && !strings.HasPrefix(fnsym.R[i].Sym.Name, "go:itab.") {
|
||||
// Types referenced through itab will be referenced from somewhere else
|
||||
typesyms = append(typesyms, fnsym.R[i].Sym)
|
||||
}
|
||||
}
|
||||
slices.SortFunc(typesyms, func(a, b *obj.LSym) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
var lastsym *obj.LSym
|
||||
for _, sym := range typesyms {
|
||||
if sym == lastsym {
|
||||
continue
|
||||
}
|
||||
lastsym = sym
|
||||
infosym.AddRel(ctxt, obj.Reloc{Type: objabi.R_USETYPE, Sym: sym})
|
||||
}
|
||||
fnsym.Func().Autot = nil
|
||||
|
||||
@@ -124,18 +124,21 @@ func tighten(f *Func) {
|
||||
|
||||
// If the target location is inside a loop,
|
||||
// move the target location up to just before the loop head.
|
||||
for _, b := range f.Blocks {
|
||||
origloop := loops.b2l[b.ID]
|
||||
for _, v := range b.Values {
|
||||
t := target[v.ID]
|
||||
if t == nil {
|
||||
continue
|
||||
}
|
||||
targetloop := loops.b2l[t.ID]
|
||||
for targetloop != nil && (origloop == nil || targetloop.depth > origloop.depth) {
|
||||
t = idom[targetloop.header.ID]
|
||||
target[v.ID] = t
|
||||
targetloop = loops.b2l[t.ID]
|
||||
if !loops.hasIrreducible {
|
||||
// Loop info might not be correct for irreducible loops. See issue 75569.
|
||||
for _, b := range f.Blocks {
|
||||
origloop := loops.b2l[b.ID]
|
||||
for _, v := range b.Values {
|
||||
t := target[v.ID]
|
||||
if t == nil {
|
||||
continue
|
||||
}
|
||||
targetloop := loops.b2l[t.ID]
|
||||
for targetloop != nil && (origloop == nil || targetloop.depth > origloop.depth) {
|
||||
t = idom[targetloop.header.ID]
|
||||
target[v.ID] = t
|
||||
targetloop = loops.b2l[t.ID]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,10 +27,10 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/mod/module"
|
||||
"golang.org/x/mod/semver"
|
||||
modzip "golang.org/x/mod/zip"
|
||||
)
|
||||
|
||||
@@ -61,7 +61,7 @@ func main() {
|
||||
|
||||
// Must have valid version, and must not overwrite existing file.
|
||||
version := flag.Arg(0)
|
||||
if !regexp.MustCompile(`^v\d+\.\d+\.\d+$`).MatchString(version) {
|
||||
if semver.Canonical(version) != version {
|
||||
log.Fatalf("invalid version %q; must be vX.Y.Z", version)
|
||||
}
|
||||
if _, err := os.Stat(version + ".zip"); err == nil {
|
||||
@@ -117,7 +117,9 @@ func main() {
|
||||
if !bytes.Contains(contents, []byte(returnLine)) {
|
||||
log.Fatalf("did not find %q in fips140.go", returnLine)
|
||||
}
|
||||
newLine := `return "` + version + `"`
|
||||
// Use only the vX.Y.Z part of a possible vX.Y.Z-hash version.
|
||||
v, _, _ := strings.Cut(version, "-")
|
||||
newLine := `return "` + v + `"`
|
||||
contents = bytes.ReplaceAll(contents, []byte(returnLine), []byte(newLine))
|
||||
wf, err := zw.Create(f.Name)
|
||||
if err != nil {
|
||||
|
||||
@@ -109,6 +109,9 @@ func ModIsPrefix(path, vers string) bool {
|
||||
// The caller is assumed to have checked that ModIsValid(path, vers) is true.
|
||||
func ModIsPrerelease(path, vers string) bool {
|
||||
if IsToolchain(path) {
|
||||
if path == "toolchain" {
|
||||
return IsPrerelease(FromToolchain(vers))
|
||||
}
|
||||
return IsPrerelease(vers)
|
||||
}
|
||||
return semver.Prerelease(vers) != ""
|
||||
|
||||
@@ -277,6 +277,29 @@ func loadModTool(ctx context.Context, name string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func builtTool(runAction *work.Action) string {
|
||||
linkAction := runAction.Deps[0]
|
||||
if toolN {
|
||||
// #72824: If -n is set, use the cached path if we can.
|
||||
// This is only necessary if the binary wasn't cached
|
||||
// before this invocation of the go command: if the binary
|
||||
// was cached, BuiltTarget() will be the cached executable.
|
||||
// It's only in the "first run", where we actually do the build
|
||||
// and save the result to the cache that BuiltTarget is not
|
||||
// the cached binary. Ideally, we would set BuiltTarget
|
||||
// to the cached path even in the first run, but if we
|
||||
// copy the binary to the cached path, and try to run it
|
||||
// in the same process, we'll run into the dreaded #22315
|
||||
// resulting in occasional ETXTBSYs. Instead of getting the
|
||||
// ETXTBSY and then retrying just don't use the cached path
|
||||
// on the first run if we're going to actually run the binary.
|
||||
if cached := linkAction.CachedExecutable(); cached != "" {
|
||||
return cached
|
||||
}
|
||||
}
|
||||
return linkAction.BuiltTarget()
|
||||
}
|
||||
|
||||
func buildAndRunBuiltinTool(ctx context.Context, toolName, tool string, args []string) {
|
||||
// Override GOOS and GOARCH for the build to build the tool using
|
||||
// the same GOOS and GOARCH as this go command.
|
||||
@@ -288,7 +311,7 @@ func buildAndRunBuiltinTool(ctx context.Context, toolName, tool string, args []s
|
||||
modload.RootMode = modload.NoRoot
|
||||
|
||||
runFunc := func(b *work.Builder, ctx context.Context, a *work.Action) error {
|
||||
cmdline := str.StringList(a.Deps[0].BuiltTarget(), a.Args)
|
||||
cmdline := str.StringList(builtTool(a), a.Args)
|
||||
return runBuiltTool(toolName, nil, cmdline)
|
||||
}
|
||||
|
||||
@@ -300,7 +323,7 @@ func buildAndRunModtool(ctx context.Context, toolName, tool string, args []strin
|
||||
// Use the ExecCmd to run the binary, as go run does. ExecCmd allows users
|
||||
// to provide a runner to run the binary, for example a simulator for binaries
|
||||
// that are cross-compiled to a different platform.
|
||||
cmdline := str.StringList(work.FindExecCmd(), a.Deps[0].BuiltTarget(), a.Args)
|
||||
cmdline := str.StringList(work.FindExecCmd(), builtTool(a), a.Args)
|
||||
// Use same environment go run uses to start the executable:
|
||||
// the original environment with cfg.GOROOTbin added to the path.
|
||||
env := slices.Clip(cfg.OrigEnv)
|
||||
|
||||
@@ -97,11 +97,12 @@ type Action struct {
|
||||
CacheExecutable bool // Whether to cache executables produced by link steps
|
||||
|
||||
// Generated files, directories.
|
||||
Objdir string // directory for intermediate objects
|
||||
Target string // goal of the action: the created package or executable
|
||||
built string // the actual created package or executable
|
||||
actionID cache.ActionID // cache ID of action input
|
||||
buildID string // build ID of action output
|
||||
Objdir string // directory for intermediate objects
|
||||
Target string // goal of the action: the created package or executable
|
||||
built string // the actual created package or executable
|
||||
cachedExecutable string // the cached executable, if CacheExecutable was set
|
||||
actionID cache.ActionID // cache ID of action input
|
||||
buildID string // build ID of action output
|
||||
|
||||
VetxOnly bool // Mode=="vet": only being called to supply info about dependencies
|
||||
needVet bool // Mode=="build": need to fill in vet config
|
||||
@@ -133,6 +134,10 @@ func (a *Action) BuildID() string { return a.buildID }
|
||||
// from Target when the result was cached.
|
||||
func (a *Action) BuiltTarget() string { return a.built }
|
||||
|
||||
// CachedExecutable returns the cached executable, if CacheExecutable
|
||||
// was set and the executable could be cached, and "" otherwise.
|
||||
func (a *Action) CachedExecutable() string { return a.cachedExecutable }
|
||||
|
||||
// An actionQueue is a priority queue of actions.
|
||||
type actionQueue []*Action
|
||||
|
||||
|
||||
@@ -745,8 +745,9 @@ func (b *Builder) updateBuildID(a *Action, target string) error {
|
||||
}
|
||||
outputID, _, err := c.PutExecutable(a.actionID, name+cfg.ExeSuffix, r)
|
||||
r.Close()
|
||||
a.cachedExecutable = c.OutputFile(outputID)
|
||||
if err == nil && cfg.BuildX {
|
||||
sh.ShowCmd("", "%s # internal", joinUnambiguously(str.StringList("cp", target, c.OutputFile(outputID))))
|
||||
sh.ShowCmd("", "%s # internal", joinUnambiguously(str.StringList("cp", target, a.cachedExecutable)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
5
src/cmd/go/testdata/script/fipssnap.txt
vendored
5
src/cmd/go/testdata/script/fipssnap.txt
vendored
@@ -1,4 +1,4 @@
|
||||
env snap=v1.0.0
|
||||
env snap=v1.0.0-c2097c7c
|
||||
env alias=inprocess
|
||||
|
||||
env GOFIPS140=$snap
|
||||
@@ -23,8 +23,7 @@ stdout crypto/internal/fips140/$snap/sha256
|
||||
! stdout crypto/internal/fips140/check
|
||||
|
||||
# again with GOFIPS140=$alias
|
||||
# TODO: enable when we add inprocess.txt
|
||||
# env GOFIPS140=$alias
|
||||
env GOFIPS140=$alias
|
||||
|
||||
# default GODEBUG includes fips140=on
|
||||
go list -f '{{.DefaultGODEBUG}}'
|
||||
|
||||
10
src/cmd/go/testdata/script/mod_get_toolchain.txt
vendored
10
src/cmd/go/testdata/script/mod_get_toolchain.txt
vendored
@@ -94,12 +94,14 @@ stderr '^go: added toolchain go1.24rc1$'
|
||||
grep 'go 1.22.9' go.mod # no longer implied
|
||||
grep 'toolchain go1.24rc1' go.mod
|
||||
|
||||
# go get toolchain@latest finds go1.999testmod.
|
||||
# go get toolchain@latest finds go1.23.9.
|
||||
cp go.mod.orig go.mod
|
||||
go get toolchain@latest
|
||||
stderr '^go: added toolchain go1.999testmod$'
|
||||
stderr '^go: added toolchain go1.23.9$'
|
||||
grep 'go 1.21' go.mod
|
||||
grep 'toolchain go1.999testmod' go.mod
|
||||
grep 'toolchain go1.23.9' go.mod
|
||||
|
||||
|
||||
|
||||
# Bug fixes.
|
||||
|
||||
@@ -115,7 +117,7 @@ stderr '^go: upgraded go 1.19 => 1.21.0'
|
||||
|
||||
# go get toolchain@1.24rc1 is OK too.
|
||||
go get toolchain@1.24rc1
|
||||
stderr '^go: downgraded toolchain go1.999testmod => go1.24rc1$'
|
||||
stderr '^go: upgraded toolchain go1.23.9 => go1.24rc1$'
|
||||
|
||||
# go get go@1.21 should work if we are the Go 1.21 language version,
|
||||
# even though there's no toolchain for it.
|
||||
|
||||
27
src/cmd/go/testdata/script/tool_n_issue72824.txt
vendored
Normal file
27
src/cmd/go/testdata/script/tool_n_issue72824.txt
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
[short] skip 'does a build in using an empty cache'
|
||||
|
||||
# Start with a fresh cache because we want to verify the behavior
|
||||
# when the tool hasn't been cached previously.
|
||||
env GOCACHE=$WORK${/}cache
|
||||
|
||||
# Even when the tool hasn't been previously cached but was built and
|
||||
# saved to the cache in the invocation of 'go tool -n' we should return
|
||||
# its cached location.
|
||||
go tool -n foo
|
||||
stdout $GOCACHE
|
||||
|
||||
# And of course we should also return the cached location on subsequent
|
||||
# runs.
|
||||
go tool -n foo
|
||||
stdout $GOCACHE
|
||||
|
||||
-- go.mod --
|
||||
module example.com/foo
|
||||
|
||||
go 1.25
|
||||
|
||||
tool example.com/foo
|
||||
-- main.go --
|
||||
package main
|
||||
|
||||
func main() {}
|
||||
@@ -463,6 +463,8 @@ func (c *cancelCtx) Done() <-chan struct{} {
|
||||
func (c *cancelCtx) Err() error {
|
||||
// An atomic load is ~5x faster than a mutex, which can matter in tight loops.
|
||||
if err := c.err.Load(); err != nil {
|
||||
// Ensure the done channel has been closed before returning a non-nil error.
|
||||
<-c.Done()
|
||||
return err.(error)
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -1177,3 +1177,23 @@ func (c *customContext) Err() error {
|
||||
func (c *customContext) Value(key any) any {
|
||||
return c.parent.Value(key)
|
||||
}
|
||||
|
||||
// Issue #75533.
|
||||
func TestContextErrDoneRace(t *testing.T) {
|
||||
// 4 iterations reliably reproduced #75533.
|
||||
for range 10 {
|
||||
ctx, cancel := WithCancel(Background())
|
||||
donec := ctx.Done()
|
||||
go cancel()
|
||||
for ctx.Err() == nil {
|
||||
if runtime.GOARCH == "wasm" {
|
||||
runtime.Gosched() // need to explicitly yield
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-donec:
|
||||
default:
|
||||
t.Fatalf("ctx.Err is non-nil, but ctx.Done is not closed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ type MakeHash func() hash.Hash
|
||||
// TestHash performs a set of tests on hash.Hash implementations, checking the
|
||||
// documented requirements of Write, Sum, Reset, Size, and BlockSize.
|
||||
func TestHash(t *testing.T, mh MakeHash) {
|
||||
if boring.Enabled || fips140.Version() == "v1.0" {
|
||||
if boring.Enabled || fips140.Version() == "v1.0.0" {
|
||||
testhash.TestHashWithoutClone(t, testhash.MakeHash(mh))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -56,9 +56,10 @@ func CAST(name string, f func() error) {
|
||||
}
|
||||
|
||||
// PCT runs the named Pairwise Consistency Test (if operated in FIPS mode) and
|
||||
// returns any errors. If an error is returned, the key must not be used.
|
||||
// aborts the program (stopping the module input/output and entering the "error
|
||||
// state") if the test fails.
|
||||
//
|
||||
// PCTs are mandatory for every key pair that is generated/imported, including
|
||||
// PCTs are mandatory for every generated (but not imported) key pair, including
|
||||
// ephemeral keys (which effectively doubles the cost of key establishment). See
|
||||
// Implementation Guidance 10.3.A Additional Comment 1.
|
||||
//
|
||||
@@ -66,17 +67,23 @@ func CAST(name string, f func() error) {
|
||||
//
|
||||
// If a package p calls PCT during key generation, an invocation of that
|
||||
// function should be added to fipstest.TestConditionals.
|
||||
func PCT(name string, f func() error) error {
|
||||
func PCT(name string, f func() error) {
|
||||
if strings.ContainsAny(name, ",#=:") {
|
||||
panic("fips: invalid self-test name: " + name)
|
||||
}
|
||||
if !Enabled {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
err := f()
|
||||
if name == failfipscast {
|
||||
err = errors.New("simulated PCT failure")
|
||||
}
|
||||
return err
|
||||
if err != nil {
|
||||
fatal("FIPS 140-3 self-test failed: " + name + ": " + err.Error())
|
||||
panic("unreachable")
|
||||
}
|
||||
if debug {
|
||||
println("FIPS 140-3 PCT passed:", name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,6 +161,27 @@ func GenerateKey[P Point[P]](c *Curve[P], rand io.Reader) (*PrivateKey, error) {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// A "Pairwise Consistency Test" makes no sense if we just generated the
|
||||
// public key from an ephemeral private key. Moreover, there is no way to
|
||||
// check it aside from redoing the exact same computation again. SP 800-56A
|
||||
// Rev. 3, Section 5.6.2.1.4 acknowledges that, and doesn't require it.
|
||||
// However, ISO 19790:2012, Section 7.10.3.3 has a blanket requirement for a
|
||||
// PCT for all generated keys (AS10.35) and FIPS 140-3 IG 10.3.A, Additional
|
||||
// Comment 1 goes out of its way to say that "the PCT shall be performed
|
||||
// consistent [...], even if the underlying standard does not require a
|
||||
// PCT". So we do it. And make ECDH nearly 50% slower (only) in FIPS mode.
|
||||
fips140.PCT("ECDH PCT", func() error {
|
||||
p1, err := c.newPoint().ScalarBaseMult(privateKey.d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !bytes.Equal(p1.Bytes(), privateKey.pub.q) {
|
||||
return errors.New("crypto/ecdh: public key does not match private key")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return privateKey, nil
|
||||
}
|
||||
}
|
||||
@@ -188,28 +209,6 @@ func NewPrivateKey[P Point[P]](c *Curve[P], key []byte) (*PrivateKey, error) {
|
||||
panic("crypto/ecdh: internal error: public key is the identity element")
|
||||
}
|
||||
|
||||
// A "Pairwise Consistency Test" makes no sense if we just generated the
|
||||
// public key from an ephemeral private key. Moreover, there is no way to
|
||||
// check it aside from redoing the exact same computation again. SP 800-56A
|
||||
// Rev. 3, Section 5.6.2.1.4 acknowledges that, and doesn't require it.
|
||||
// However, ISO 19790:2012, Section 7.10.3.3 has a blanket requirement for a
|
||||
// PCT for all generated keys (AS10.35) and FIPS 140-3 IG 10.3.A, Additional
|
||||
// Comment 1 goes out of its way to say that "the PCT shall be performed
|
||||
// consistent [...], even if the underlying standard does not require a
|
||||
// PCT". So we do it. And make ECDH nearly 50% slower (only) in FIPS mode.
|
||||
if err := fips140.PCT("ECDH PCT", func() error {
|
||||
p1, err := c.newPoint().ScalarBaseMult(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !bytes.Equal(p1.Bytes(), publicKey) {
|
||||
return errors.New("crypto/ecdh: public key does not match private key")
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
k := &PrivateKey{d: bytes.Clone(key), pub: PublicKey{curve: c.curve, q: publicKey}}
|
||||
return k, nil
|
||||
}
|
||||
|
||||
@@ -51,8 +51,8 @@ func testHash() []byte {
|
||||
}
|
||||
}
|
||||
|
||||
func fipsPCT[P Point[P]](c *Curve[P], k *PrivateKey) error {
|
||||
return fips140.PCT("ECDSA PCT", func() error {
|
||||
func fipsPCT[P Point[P]](c *Curve[P], k *PrivateKey) {
|
||||
fips140.PCT("ECDSA PCT", func() error {
|
||||
hash := testHash()
|
||||
drbg := newDRBG(sha512.New, k.d, bits2octets(P256(), hash), nil)
|
||||
sig, err := sign(c, k, drbg, hash)
|
||||
|
||||
@@ -167,11 +167,6 @@ func NewPrivateKey[P Point[P]](c *Curve[P], D, Q []byte) (*PrivateKey, error) {
|
||||
return nil, err
|
||||
}
|
||||
priv := &PrivateKey{pub: *pub, d: d.Bytes(c.N)}
|
||||
if err := fipsPCT(c, priv); err != nil {
|
||||
// This can happen if the application went out of its way to make an
|
||||
// ecdsa.PrivateKey with a mismatching PublicKey.
|
||||
return nil, err
|
||||
}
|
||||
return priv, nil
|
||||
}
|
||||
|
||||
@@ -204,10 +199,7 @@ func GenerateKey[P Point[P]](c *Curve[P], rand io.Reader) (*PrivateKey, error) {
|
||||
},
|
||||
d: k.Bytes(c.N),
|
||||
}
|
||||
if err := fipsPCT(c, priv); err != nil {
|
||||
// This clearly can't happen, but FIPS 140-3 mandates that we check it.
|
||||
panic(err)
|
||||
}
|
||||
fipsPCT(c, priv)
|
||||
return priv, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ func newDRBG[H hash.Hash](hash func() H, entropy, nonce []byte, s personalizatio
|
||||
//
|
||||
// This should only be used for ACVP testing. hmacDRBG is not intended to be
|
||||
// used directly.
|
||||
func TestingOnlyNewDRBG(hash func() hash.Hash, entropy, nonce []byte, s []byte) *hmacDRBG {
|
||||
func TestingOnlyNewDRBG[H hash.Hash](hash func() H, entropy, nonce []byte, s []byte) *hmacDRBG {
|
||||
return newDRBG(hash, entropy, nonce, plainPersonalizationString(s))
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
func fipsPCT(k *PrivateKey) error {
|
||||
return fips140.PCT("Ed25519 sign and verify PCT", func() error {
|
||||
func fipsPCT(k *PrivateKey) {
|
||||
fips140.PCT("Ed25519 sign and verify PCT", func() error {
|
||||
return pairwiseTest(k)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -69,10 +69,7 @@ func generateKey(priv *PrivateKey) (*PrivateKey, error) {
|
||||
fips140.RecordApproved()
|
||||
drbg.Read(priv.seed[:])
|
||||
precomputePrivateKey(priv)
|
||||
if err := fipsPCT(priv); err != nil {
|
||||
// This clearly can't happen, but FIPS 140-3 requires that we check.
|
||||
panic(err)
|
||||
}
|
||||
fipsPCT(priv)
|
||||
return priv, nil
|
||||
}
|
||||
|
||||
@@ -88,10 +85,6 @@ func newPrivateKeyFromSeed(priv *PrivateKey, seed []byte) (*PrivateKey, error) {
|
||||
}
|
||||
copy(priv.seed[:], seed)
|
||||
precomputePrivateKey(priv)
|
||||
if err := fipsPCT(priv); err != nil {
|
||||
// This clearly can't happen, but FIPS 140-3 requires that we check.
|
||||
panic(err)
|
||||
}
|
||||
return priv, nil
|
||||
}
|
||||
|
||||
@@ -137,12 +130,6 @@ func newPrivateKey(priv *PrivateKey, privBytes []byte) (*PrivateKey, error) {
|
||||
|
||||
copy(priv.prefix[:], h[32:])
|
||||
|
||||
if err := fipsPCT(priv); err != nil {
|
||||
// This can happen if the application messed with the private key
|
||||
// encoding, and the public key doesn't match the seed anymore.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return priv, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ package fips140
|
||||
import (
|
||||
"crypto/internal/fips140deps/godebug"
|
||||
"errors"
|
||||
"hash"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
@@ -63,16 +62,10 @@ func Name() string {
|
||||
return "Go Cryptographic Module"
|
||||
}
|
||||
|
||||
// Version returns the formal version (such as "v1.0") if building against a
|
||||
// Version returns the formal version (such as "v1.0.0") if building against a
|
||||
// frozen module with GOFIPS140. Otherwise, it returns "latest".
|
||||
func Version() string {
|
||||
// This return value is replaced by mkzip.go, it must not be changed or
|
||||
// moved to a different file.
|
||||
return "latest" //mkzip:version
|
||||
}
|
||||
|
||||
// Hash is a legacy compatibility alias for hash.Hash.
|
||||
//
|
||||
// It's only here because [crypto/internal/fips140/ecdsa.TestingOnlyNewDRBG]
|
||||
// takes a "func() fips140.Hash" in v1.0.0, instead of being generic.
|
||||
type Hash = hash.Hash
|
||||
|
||||
@@ -118,10 +118,7 @@ func generateKey1024(dk *DecapsulationKey1024) (*DecapsulationKey1024, error) {
|
||||
var z [32]byte
|
||||
drbg.Read(z[:])
|
||||
kemKeyGen1024(dk, &d, &z)
|
||||
if err := fips140.PCT("ML-KEM PCT", func() error { return kemPCT1024(dk) }); err != nil {
|
||||
// This clearly can't happen, but FIPS 140-3 requires us to check.
|
||||
panic(err)
|
||||
}
|
||||
fips140.PCT("ML-KEM PCT", func() error { return kemPCT1024(dk) })
|
||||
fips140.RecordApproved()
|
||||
return dk, nil
|
||||
}
|
||||
@@ -149,10 +146,6 @@ func newKeyFromSeed1024(dk *DecapsulationKey1024, seed []byte) (*DecapsulationKe
|
||||
d := (*[32]byte)(seed[:32])
|
||||
z := (*[32]byte)(seed[32:])
|
||||
kemKeyGen1024(dk, d, z)
|
||||
if err := fips140.PCT("ML-KEM PCT", func() error { return kemPCT1024(dk) }); err != nil {
|
||||
// This clearly can't happen, but FIPS 140-3 requires us to check.
|
||||
panic(err)
|
||||
}
|
||||
fips140.RecordApproved()
|
||||
return dk, nil
|
||||
}
|
||||
|
||||
@@ -177,10 +177,7 @@ func generateKey(dk *DecapsulationKey768) (*DecapsulationKey768, error) {
|
||||
var z [32]byte
|
||||
drbg.Read(z[:])
|
||||
kemKeyGen(dk, &d, &z)
|
||||
if err := fips140.PCT("ML-KEM PCT", func() error { return kemPCT(dk) }); err != nil {
|
||||
// This clearly can't happen, but FIPS 140-3 requires us to check.
|
||||
panic(err)
|
||||
}
|
||||
fips140.PCT("ML-KEM PCT", func() error { return kemPCT(dk) })
|
||||
fips140.RecordApproved()
|
||||
return dk, nil
|
||||
}
|
||||
@@ -208,10 +205,6 @@ func newKeyFromSeed(dk *DecapsulationKey768, seed []byte) (*DecapsulationKey768,
|
||||
d := (*[32]byte)(seed[:32])
|
||||
z := (*[32]byte)(seed[32:])
|
||||
kemKeyGen(dk, d, z)
|
||||
if err := fips140.PCT("ML-KEM PCT", func() error { return kemPCT(dk) }); err != nil {
|
||||
// This clearly can't happen, but FIPS 140-3 requires us to check.
|
||||
panic(err)
|
||||
}
|
||||
fips140.RecordApproved()
|
||||
return dk, nil
|
||||
}
|
||||
|
||||
@@ -105,7 +105,28 @@ func GenerateKey(rand io.Reader, bits int) (*PrivateKey, error) {
|
||||
// negligible chance of failure we can defer the check to the end of key
|
||||
// generation and return an error if it fails. See [checkPrivateKey].
|
||||
|
||||
return newPrivateKey(N, 65537, d, P, Q)
|
||||
k, err := newPrivateKey(N, 65537, d, P, Q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if k.fipsApproved {
|
||||
fips140.PCT("RSA sign and verify PCT", func() error {
|
||||
hash := []byte{
|
||||
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
||||
0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10,
|
||||
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
|
||||
0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
|
||||
}
|
||||
sig, err := signPKCS1v15(k, "SHA-256", hash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return verifyPKCS1v15(k.PublicKey(), "SHA-256", hash, sig)
|
||||
})
|
||||
}
|
||||
|
||||
return k, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -310,26 +310,6 @@ func checkPrivateKey(priv *PrivateKey) error {
|
||||
return errors.New("crypto/rsa: d too small")
|
||||
}
|
||||
|
||||
// If the key is still in scope for FIPS mode, perform a Pairwise
|
||||
// Consistency Test.
|
||||
if priv.fipsApproved {
|
||||
if err := fips140.PCT("RSA sign and verify PCT", func() error {
|
||||
hash := []byte{
|
||||
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
||||
0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10,
|
||||
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
|
||||
0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
|
||||
}
|
||||
sig, err := signPKCS1v15(priv, "SHA-256", hash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return verifyPKCS1v15(priv.PublicKey(), "SHA-256", hash, sig)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1624,7 +1624,7 @@ func cmdHmacDrbgAft(h func() hash.Hash) command {
|
||||
// * Uninstantiate
|
||||
// See Table 7 in draft-vassilev-acvp-drbg
|
||||
out := make([]byte, outLen)
|
||||
drbg := ecdsa.TestingOnlyNewDRBG(func() fips140.Hash { return h() }, entropy, nonce, personalization)
|
||||
drbg := ecdsa.TestingOnlyNewDRBG(h, entropy, nonce, personalization)
|
||||
drbg.Generate(out)
|
||||
drbg.Generate(out)
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
package fipstest
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/internal/fips140"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"internal/testenv"
|
||||
"io/fs"
|
||||
@@ -50,8 +50,6 @@ var allCASTs = []string{
|
||||
"KAS-ECC-SSC P-256",
|
||||
"ML-KEM PCT",
|
||||
"ML-KEM PCT",
|
||||
"ML-KEM PCT",
|
||||
"ML-KEM PCT",
|
||||
"ML-KEM-768",
|
||||
"PBKDF2",
|
||||
"RSA sign and verify PCT",
|
||||
@@ -107,60 +105,65 @@ func TestAllCASTs(t *testing.T) {
|
||||
// TestConditionals causes the conditional CASTs and PCTs to be invoked.
|
||||
func TestConditionals(t *testing.T) {
|
||||
mlkem.GenerateKey768()
|
||||
k, err := ecdh.GenerateKey(ecdh.P256(), rand.Reader)
|
||||
kDH, err := ecdh.GenerateKey(ecdh.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Error(err)
|
||||
} else {
|
||||
ecdh.ECDH(ecdh.P256(), kDH, kDH.PublicKey())
|
||||
}
|
||||
ecdh.ECDH(ecdh.P256(), k, k.PublicKey())
|
||||
kDSA, err := ecdsa.GenerateKey(ecdsa.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Error(err)
|
||||
} else {
|
||||
ecdsa.SignDeterministic(ecdsa.P256(), sha256.New, kDSA, make([]byte, 32))
|
||||
}
|
||||
ecdsa.SignDeterministic(ecdsa.P256(), sha256.New, kDSA, make([]byte, 32))
|
||||
k25519, err := ed25519.GenerateKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Error(err)
|
||||
} else {
|
||||
ed25519.Sign(k25519, make([]byte, 32))
|
||||
}
|
||||
ed25519.Sign(k25519, make([]byte, 32))
|
||||
rsa.VerifyPKCS1v15(&rsa.PublicKey{}, "", nil, nil)
|
||||
// Parse an RSA key to hit the PCT rather than generating one (which is slow).
|
||||
block, _ := pem.Decode([]byte(strings.ReplaceAll(
|
||||
`-----BEGIN RSA TESTING KEY-----
|
||||
MIIEowIBAAKCAQEAsPnoGUOnrpiSqt4XynxA+HRP7S+BSObI6qJ7fQAVSPtRkqso
|
||||
tWxQYLEYzNEx5ZSHTGypibVsJylvCfuToDTfMul8b/CZjP2Ob0LdpYrNH6l5hvFE
|
||||
89FU1nZQF15oVLOpUgA7wGiHuEVawrGfey92UE68mOyUVXGweJIVDdxqdMoPvNNU
|
||||
l86BU02vlBiESxOuox+dWmuVV7vfYZ79Toh/LUK43YvJh+rhv4nKuF7iHjVjBd9s
|
||||
B6iDjj70HFldzOQ9r8SRI+9NirupPTkF5AKNe6kUhKJ1luB7S27ZkvB3tSTT3P59
|
||||
3VVJvnzOjaA1z6Cz+4+eRvcysqhrRgFlwI9TEwIDAQABAoIBAEEYiyDP29vCzx/+
|
||||
dS3LqnI5BjUuJhXUnc6AWX/PCgVAO+8A+gZRgvct7PtZb0sM6P9ZcLrweomlGezI
|
||||
FrL0/6xQaa8bBr/ve/a8155OgcjFo6fZEw3Dz7ra5fbSiPmu4/b/kvrg+Br1l77J
|
||||
aun6uUAs1f5B9wW+vbR7tzbT/mxaUeDiBzKpe15GwcvbJtdIVMa2YErtRjc1/5B2
|
||||
BGVXyvlJv0SIlcIEMsHgnAFOp1ZgQ08aDzvilLq8XVMOahAhP1O2A3X8hKdXPyrx
|
||||
IVWE9bS9ptTo+eF6eNl+d7htpKGEZHUxinoQpWEBTv+iOoHsVunkEJ3vjLP3lyI/
|
||||
fY0NQ1ECgYEA3RBXAjgvIys2gfU3keImF8e/TprLge1I2vbWmV2j6rZCg5r/AS0u
|
||||
pii5CvJ5/T5vfJPNgPBy8B/yRDs+6PJO1GmnlhOkG9JAIPkv0RBZvR0PMBtbp6nT
|
||||
Y3yo1lwamBVBfY6rc0sLTzosZh2aGoLzrHNMQFMGaauORzBFpY5lU50CgYEAzPHl
|
||||
u5DI6Xgep1vr8QvCUuEesCOgJg8Yh1UqVoY/SmQh6MYAv1I9bLGwrb3WW/7kqIoD
|
||||
fj0aQV5buVZI2loMomtU9KY5SFIsPV+JuUpy7/+VE01ZQM5FdY8wiYCQiVZYju9X
|
||||
Wz5LxMNoz+gT7pwlLCsC4N+R8aoBk404aF1gum8CgYAJ7VTq7Zj4TFV7Soa/T1eE
|
||||
k9y8a+kdoYk3BASpCHJ29M5R2KEA7YV9wrBklHTz8VzSTFTbKHEQ5W5csAhoL5Fo
|
||||
qoHzFFi3Qx7MHESQb9qHyolHEMNx6QdsHUn7rlEnaTTyrXh3ifQtD6C0yTmFXUIS
|
||||
CW9wKApOrnyKJ9nI0HcuZQKBgQCMtoV6e9VGX4AEfpuHvAAnMYQFgeBiYTkBKltQ
|
||||
XwozhH63uMMomUmtSG87Sz1TmrXadjAhy8gsG6I0pWaN7QgBuFnzQ/HOkwTm+qKw
|
||||
AsrZt4zeXNwsH7QXHEJCFnCmqw9QzEoZTrNtHJHpNboBuVnYcoueZEJrP8OnUG3r
|
||||
UjmopwKBgAqB2KYYMUqAOvYcBnEfLDmyZv9BTVNHbR2lKkMYqv5LlvDaBxVfilE0
|
||||
2riO4p6BaAdvzXjKeRrGNEKoHNBpOSfYCOM16NjL8hIZB1CaV3WbT5oY+jp7Mzd5
|
||||
7d56RZOE+ERK2uz/7JX9VSsM/LbH9pJibd4e8mikDS9ntciqOH/3
|
||||
-----END RSA TESTING KEY-----`, "TESTING KEY", "PRIVATE KEY")))
|
||||
if _, err := x509.ParsePKCS1PrivateKey(block.Bytes); err != nil {
|
||||
t.Fatal(err)
|
||||
kRSA, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
rsa.SignPKCS1v15(kRSA, crypto.SHA256.String(), make([]byte, 32))
|
||||
}
|
||||
t.Log("completed successfully")
|
||||
}
|
||||
|
||||
func TestCASTPasses(t *testing.T) {
|
||||
moduleStatus(t)
|
||||
testenv.MustHaveExec(t)
|
||||
if err := fips140.Supported(); err != nil {
|
||||
t.Skipf("test requires FIPS 140 mode: %v", err)
|
||||
}
|
||||
|
||||
cmd := testenv.Command(t, testenv.Executable(t), "-test.run=^TestConditionals$", "-test.v")
|
||||
cmd.Env = append(cmd.Env, "GODEBUG=fips140=debug")
|
||||
out, err := cmd.CombinedOutput()
|
||||
t.Logf("%s", out)
|
||||
if err != nil || !strings.Contains(string(out), "completed successfully") {
|
||||
t.Errorf("TestConditionals did not complete successfully")
|
||||
}
|
||||
|
||||
for _, name := range allCASTs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if !strings.Contains(string(out), fmt.Sprintf("passed: %s\n", name)) {
|
||||
t.Errorf("CAST/PCT %s success was not logged", name)
|
||||
} else {
|
||||
t.Logf("CAST/PCT succeeded: %s", name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCASTFailures(t *testing.T) {
|
||||
moduleStatus(t)
|
||||
testenv.MustHaveExec(t)
|
||||
if err := fips140.Supported(); err != nil {
|
||||
t.Skipf("test requires FIPS 140 mode: %v", err)
|
||||
}
|
||||
|
||||
for _, name := range allCASTs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
@@ -169,7 +172,6 @@ func TestCASTFailures(t *testing.T) {
|
||||
if !testing.Verbose() {
|
||||
t.Parallel()
|
||||
}
|
||||
t.Logf("CAST/PCT succeeded: %s", name)
|
||||
t.Logf("Testing CAST/PCT failure...")
|
||||
cmd := testenv.Command(t, testenv.Executable(t), "-test.run=^TestConditionals$", "-test.v")
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("GODEBUG=failfipscast=%s,fips140=on", name))
|
||||
@@ -180,6 +182,8 @@ func TestCASTFailures(t *testing.T) {
|
||||
}
|
||||
if strings.Contains(string(out), "completed successfully") {
|
||||
t.Errorf("CAST/PCT %s failure did not stop the program", name)
|
||||
} else if !strings.Contains(string(out), "self-test failed: "+name) {
|
||||
t.Errorf("CAST/PCT %s failure did not log the expected message", name)
|
||||
} else {
|
||||
t.Logf("CAST/PCT %s failed as expected and caused the program to exit", name)
|
||||
}
|
||||
|
||||
@@ -74,11 +74,9 @@ func TestVersion(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
exp := setting.Value
|
||||
if exp == "v1.0.0" {
|
||||
// Unfortunately we enshrined the version of the first module as
|
||||
// v1.0 before deciding to go for full versions.
|
||||
exp = "v1.0"
|
||||
}
|
||||
// Remove the -hash suffix, if any.
|
||||
// The version from fips140.Version omits it.
|
||||
exp, _, _ = strings.Cut(exp, "-")
|
||||
if v := fips140.Version(); v != exp {
|
||||
t.Errorf("Version is %q, expected %q", v, exp)
|
||||
}
|
||||
|
||||
@@ -357,7 +357,7 @@ func negotiateALPN(serverProtos, clientProtos []string, quic bool) (string, erro
|
||||
if http11fallback {
|
||||
return "", nil
|
||||
}
|
||||
return "", fmt.Errorf("tls: client requested unsupported application protocols (%s)", clientProtos)
|
||||
return "", fmt.Errorf("tls: client requested unsupported application protocols (%q)", clientProtos)
|
||||
}
|
||||
|
||||
// supportsECDHE returns whether ECDHE key exchanges can be used with this
|
||||
|
||||
@@ -429,10 +429,8 @@ func parseSANExtension(der cryptobyte.String) (dnsNames, emailAddresses []string
|
||||
if err != nil {
|
||||
return fmt.Errorf("x509: cannot parse URI %q: %s", uriStr, err)
|
||||
}
|
||||
if len(uri.Host) > 0 {
|
||||
if _, ok := domainToReverseLabels(uri.Host); !ok {
|
||||
return fmt.Errorf("x509: cannot parse URI %q: invalid domain", uriStr)
|
||||
}
|
||||
if len(uri.Host) > 0 && !domainNameValid(uri.Host, false) {
|
||||
return fmt.Errorf("x509: cannot parse URI %q: invalid domain", uriStr)
|
||||
}
|
||||
uris = append(uris, uri)
|
||||
case nameTypeIP:
|
||||
@@ -598,15 +596,7 @@ func parseNameConstraintsExtension(out *Certificate, e pkix.Extension) (unhandle
|
||||
return nil, nil, nil, nil, errors.New("x509: invalid constraint value: " + err.Error())
|
||||
}
|
||||
|
||||
trimmedDomain := domain
|
||||
if len(trimmedDomain) > 0 && trimmedDomain[0] == '.' {
|
||||
// constraints can have a leading
|
||||
// period to exclude the domain
|
||||
// itself, but that's not valid in a
|
||||
// normal domain name.
|
||||
trimmedDomain = trimmedDomain[1:]
|
||||
}
|
||||
if _, ok := domainToReverseLabels(trimmedDomain); !ok {
|
||||
if !domainNameValid(domain, true) {
|
||||
return nil, nil, nil, nil, fmt.Errorf("x509: failed to parse dnsName constraint %q", domain)
|
||||
}
|
||||
dnsNames = append(dnsNames, domain)
|
||||
@@ -647,12 +637,7 @@ func parseNameConstraintsExtension(out *Certificate, e pkix.Extension) (unhandle
|
||||
return nil, nil, nil, nil, fmt.Errorf("x509: failed to parse rfc822Name constraint %q", constraint)
|
||||
}
|
||||
} else {
|
||||
// Otherwise it's a domain name.
|
||||
domain := constraint
|
||||
if len(domain) > 0 && domain[0] == '.' {
|
||||
domain = domain[1:]
|
||||
}
|
||||
if _, ok := domainToReverseLabels(domain); !ok {
|
||||
if !domainNameValid(constraint, true) {
|
||||
return nil, nil, nil, nil, fmt.Errorf("x509: failed to parse rfc822Name constraint %q", constraint)
|
||||
}
|
||||
}
|
||||
@@ -668,15 +653,7 @@ func parseNameConstraintsExtension(out *Certificate, e pkix.Extension) (unhandle
|
||||
return nil, nil, nil, nil, fmt.Errorf("x509: failed to parse URI constraint %q: cannot be IP address", domain)
|
||||
}
|
||||
|
||||
trimmedDomain := domain
|
||||
if len(trimmedDomain) > 0 && trimmedDomain[0] == '.' {
|
||||
// constraints can have a leading
|
||||
// period to exclude the domain itself,
|
||||
// but that's not valid in a normal
|
||||
// domain name.
|
||||
trimmedDomain = trimmedDomain[1:]
|
||||
}
|
||||
if _, ok := domainToReverseLabels(trimmedDomain); !ok {
|
||||
if !domainNameValid(domain, true) {
|
||||
return nil, nil, nil, nil, fmt.Errorf("x509: failed to parse URI constraint %q", domain)
|
||||
}
|
||||
uriDomains = append(uriDomains, domain)
|
||||
@@ -1317,3 +1294,62 @@ func ParseRevocationList(der []byte) (*RevocationList, error) {
|
||||
|
||||
return rl, nil
|
||||
}
|
||||
|
||||
// domainNameValid is an alloc-less version of the checks that
|
||||
// domainToReverseLabels does.
|
||||
func domainNameValid(s string, constraint bool) bool {
|
||||
// TODO(#75835): This function omits a number of checks which we
|
||||
// really should be doing to enforce that domain names are valid names per
|
||||
// RFC 1034. We previously enabled these checks, but this broke a
|
||||
// significant number of certificates we previously considered valid, and we
|
||||
// happily create via CreateCertificate (et al). We should enable these
|
||||
// checks, but will need to gate them behind a GODEBUG.
|
||||
//
|
||||
// I have left the checks we previously enabled, noted with "TODO(#75835)" so
|
||||
// that we can easily re-enable them once we unbreak everyone.
|
||||
|
||||
// TODO(#75835): this should only be true for constraints.
|
||||
if len(s) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// Do not allow trailing period (FQDN format is not allowed in SANs or
|
||||
// constraints).
|
||||
if s[len(s)-1] == '.' {
|
||||
return false
|
||||
}
|
||||
|
||||
// TODO(#75835): domains must have at least one label, cannot have
|
||||
// a leading empty label, and cannot be longer than 253 characters.
|
||||
// if len(s) == 0 || (!constraint && s[0] == '.') || len(s) > 253 {
|
||||
// return false
|
||||
// }
|
||||
|
||||
lastDot := -1
|
||||
if constraint && s[0] == '.' {
|
||||
s = s[1:]
|
||||
}
|
||||
|
||||
for i := 0; i <= len(s); i++ {
|
||||
if i < len(s) && (s[i] < 33 || s[i] > 126) {
|
||||
// Invalid character.
|
||||
return false
|
||||
}
|
||||
if i == len(s) || s[i] == '.' {
|
||||
labelLen := i
|
||||
if lastDot >= 0 {
|
||||
labelLen -= lastDot + 1
|
||||
}
|
||||
if labelLen == 0 {
|
||||
return false
|
||||
}
|
||||
// TODO(#75835): labels cannot be longer than 63 characters.
|
||||
// if labelLen > 63 {
|
||||
// return false
|
||||
// }
|
||||
lastDot = i
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -5,9 +5,13 @@
|
||||
package x509
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"encoding/asn1"
|
||||
"encoding/pem"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1"
|
||||
@@ -251,3 +255,106 @@ d5l1tRhScKu2NBgm74nYmJxJYgvuTA38wGhRrGU=
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainNameValid(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
dnsName string
|
||||
constraint bool
|
||||
valid bool
|
||||
}{
|
||||
// TODO(#75835): these tests are for stricter name validation, which we
|
||||
// had to disable. Once we reenable these strict checks, behind a
|
||||
// GODEBUG, we should add them back in.
|
||||
// {"empty name, name", "", false, false},
|
||||
// {"254 char label, name", strings.Repeat("a.a", 84) + "aaa", false, false},
|
||||
// {"254 char label, constraint", strings.Repeat("a.a", 84) + "aaa", true, false},
|
||||
// {"253 char label, name", strings.Repeat("a.a", 84) + "aa", false, false},
|
||||
// {"253 char label, constraint", strings.Repeat("a.a", 84) + "aa", true, false},
|
||||
// {"64 char single label, name", strings.Repeat("a", 64), false, false},
|
||||
// {"64 char single label, constraint", strings.Repeat("a", 64), true, false},
|
||||
// {"64 char label, name", "a." + strings.Repeat("a", 64), false, false},
|
||||
// {"64 char label, constraint", "a." + strings.Repeat("a", 64), true, false},
|
||||
|
||||
// TODO(#75835): these are the inverse of the tests above, they should be removed
|
||||
// once the strict checking is enabled.
|
||||
{"254 char label, name", strings.Repeat("a.a", 84) + "aaa", false, true},
|
||||
{"254 char label, constraint", strings.Repeat("a.a", 84) + "aaa", true, true},
|
||||
{"253 char label, name", strings.Repeat("a.a", 84) + "aa", false, true},
|
||||
{"253 char label, constraint", strings.Repeat("a.a", 84) + "aa", true, true},
|
||||
{"64 char single label, name", strings.Repeat("a", 64), false, true},
|
||||
{"64 char single label, constraint", strings.Repeat("a", 64), true, true},
|
||||
{"64 char label, name", "a." + strings.Repeat("a", 64), false, true},
|
||||
{"64 char label, constraint", "a." + strings.Repeat("a", 64), true, true},
|
||||
|
||||
// Check we properly enforce properties of domain names.
|
||||
{"empty name, constraint", "", true, true},
|
||||
{"empty label, name", "a..a", false, false},
|
||||
{"empty label, constraint", "a..a", true, false},
|
||||
{"period, name", ".", false, false},
|
||||
{"period, constraint", ".", true, false}, // TODO(roland): not entirely clear if this is a valid constraint (require at least one label?)
|
||||
{"valid, name", "a.b.c", false, true},
|
||||
{"valid, constraint", "a.b.c", true, true},
|
||||
{"leading period, name", ".a.b.c", false, false},
|
||||
{"leading period, constraint", ".a.b.c", true, true},
|
||||
{"trailing period, name", "a.", false, false},
|
||||
{"trailing period, constraint", "a.", true, false},
|
||||
{"bare label, name", "a", false, true},
|
||||
{"bare label, constraint", "a", true, true},
|
||||
{"63 char single label, name", strings.Repeat("a", 63), false, true},
|
||||
{"63 char single label, constraint", strings.Repeat("a", 63), true, true},
|
||||
{"63 char label, name", "a." + strings.Repeat("a", 63), false, true},
|
||||
{"63 char label, constraint", "a." + strings.Repeat("a", 63), true, true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
valid := domainNameValid(tc.dnsName, tc.constraint)
|
||||
if tc.valid != valid {
|
||||
t.Errorf("domainNameValid(%q, %t) = %v; want %v", tc.dnsName, tc.constraint, !tc.valid, tc.valid)
|
||||
}
|
||||
// Also check that we enforce the same properties as domainToReverseLabels
|
||||
trimmedName := tc.dnsName
|
||||
if tc.constraint && len(trimmedName) > 1 && trimmedName[0] == '.' {
|
||||
trimmedName = trimmedName[1:]
|
||||
}
|
||||
_, revValid := domainToReverseLabels(trimmedName)
|
||||
if valid != revValid {
|
||||
t.Errorf("domainNameValid(%q, %t) = %t != domainToReverseLabels(%q) = %t", tc.dnsName, tc.constraint, valid, trimmedName, revValid)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoundtripWeirdSANs(t *testing.T) {
|
||||
// TODO(#75835): check that certificates we create with CreateCertificate that have malformed SAN values
|
||||
// can be parsed by ParseCertificate. We should eventually restrict this, but for now we have to maintain
|
||||
// this property as people have been relying on it.
|
||||
k, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
badNames := []string{
|
||||
"baredomain",
|
||||
"baredomain.",
|
||||
strings.Repeat("a", 255),
|
||||
strings.Repeat("a", 65) + ".com",
|
||||
}
|
||||
tmpl := &Certificate{
|
||||
EmailAddresses: badNames,
|
||||
DNSNames: badNames,
|
||||
}
|
||||
b, err := CreateCertificate(rand.Reader, tmpl, tmpl, &k.PublicKey, k)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = ParseCertificate(b)
|
||||
if err != nil {
|
||||
t.Fatalf("Couldn't roundtrip certificate: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzDomainNameValid(f *testing.F) {
|
||||
f.Fuzz(func(t *testing.T, data string) {
|
||||
domainNameValid(data, false)
|
||||
domainNameValid(data, true)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -391,6 +391,7 @@ func parseRFC2821Mailbox(in string) (mailbox rfc2821Mailbox, ok bool) {
|
||||
// domainToReverseLabels converts a textual domain name like foo.example.com to
|
||||
// the list of labels in reverse order, e.g. ["com", "example", "foo"].
|
||||
func domainToReverseLabels(domain string) (reverseLabels []string, ok bool) {
|
||||
reverseLabels = make([]string, 0, strings.Count(domain, ".")+1)
|
||||
for len(domain) > 0 {
|
||||
if i := strings.LastIndexByte(domain, '.'); i == -1 {
|
||||
reverseLabels = append(reverseLabels, domain)
|
||||
@@ -428,7 +429,7 @@ func domainToReverseLabels(domain string) (reverseLabels []string, ok bool) {
|
||||
return reverseLabels, true
|
||||
}
|
||||
|
||||
func matchEmailConstraint(mailbox rfc2821Mailbox, constraint string) (bool, error) {
|
||||
func matchEmailConstraint(mailbox rfc2821Mailbox, constraint string, reversedDomainsCache map[string][]string, reversedConstraintsCache map[string][]string) (bool, error) {
|
||||
// If the constraint contains an @, then it specifies an exact mailbox
|
||||
// name.
|
||||
if strings.Contains(constraint, "@") {
|
||||
@@ -441,10 +442,10 @@ func matchEmailConstraint(mailbox rfc2821Mailbox, constraint string) (bool, erro
|
||||
|
||||
// Otherwise the constraint is like a DNS constraint of the domain part
|
||||
// of the mailbox.
|
||||
return matchDomainConstraint(mailbox.domain, constraint)
|
||||
return matchDomainConstraint(mailbox.domain, constraint, reversedDomainsCache, reversedConstraintsCache)
|
||||
}
|
||||
|
||||
func matchURIConstraint(uri *url.URL, constraint string) (bool, error) {
|
||||
func matchURIConstraint(uri *url.URL, constraint string, reversedDomainsCache map[string][]string, reversedConstraintsCache map[string][]string) (bool, error) {
|
||||
// From RFC 5280, Section 4.2.1.10:
|
||||
// “a uniformResourceIdentifier that does not include an authority
|
||||
// component with a host name specified as a fully qualified domain
|
||||
@@ -473,7 +474,7 @@ func matchURIConstraint(uri *url.URL, constraint string) (bool, error) {
|
||||
return false, fmt.Errorf("URI with IP (%q) cannot be matched against constraints", uri.String())
|
||||
}
|
||||
|
||||
return matchDomainConstraint(host, constraint)
|
||||
return matchDomainConstraint(host, constraint, reversedDomainsCache, reversedConstraintsCache)
|
||||
}
|
||||
|
||||
func matchIPConstraint(ip net.IP, constraint *net.IPNet) (bool, error) {
|
||||
@@ -490,16 +491,21 @@ func matchIPConstraint(ip net.IP, constraint *net.IPNet) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func matchDomainConstraint(domain, constraint string) (bool, error) {
|
||||
func matchDomainConstraint(domain, constraint string, reversedDomainsCache map[string][]string, reversedConstraintsCache map[string][]string) (bool, error) {
|
||||
// The meaning of zero length constraints is not specified, but this
|
||||
// code follows NSS and accepts them as matching everything.
|
||||
if len(constraint) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
domainLabels, ok := domainToReverseLabels(domain)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("x509: internal error: cannot parse domain %q", domain)
|
||||
domainLabels, found := reversedDomainsCache[domain]
|
||||
if !found {
|
||||
var ok bool
|
||||
domainLabels, ok = domainToReverseLabels(domain)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("x509: internal error: cannot parse domain %q", domain)
|
||||
}
|
||||
reversedDomainsCache[domain] = domainLabels
|
||||
}
|
||||
|
||||
// RFC 5280 says that a leading period in a domain name means that at
|
||||
@@ -513,9 +519,14 @@ func matchDomainConstraint(domain, constraint string) (bool, error) {
|
||||
constraint = constraint[1:]
|
||||
}
|
||||
|
||||
constraintLabels, ok := domainToReverseLabels(constraint)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("x509: internal error: cannot parse domain %q", constraint)
|
||||
constraintLabels, found := reversedConstraintsCache[constraint]
|
||||
if !found {
|
||||
var ok bool
|
||||
constraintLabels, ok = domainToReverseLabels(constraint)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("x509: internal error: cannot parse domain %q", constraint)
|
||||
}
|
||||
reversedConstraintsCache[constraint] = constraintLabels
|
||||
}
|
||||
|
||||
if len(domainLabels) < len(constraintLabels) ||
|
||||
@@ -636,6 +647,19 @@ func (c *Certificate) isValid(certType int, currentChain []*Certificate, opts *V
|
||||
}
|
||||
}
|
||||
|
||||
// Each time we do constraint checking, we need to check the constraints in
|
||||
// the current certificate against all of the names that preceded it. We
|
||||
// reverse these names using domainToReverseLabels, which is a relatively
|
||||
// expensive operation. Since we check each name against each constraint,
|
||||
// this requires us to do N*C calls to domainToReverseLabels (where N is the
|
||||
// total number of names that preceed the certificate, and C is the total
|
||||
// number of constraints in the certificate). By caching the results of
|
||||
// calling domainToReverseLabels, we can reduce that to N+C calls at the
|
||||
// cost of keeping all of the parsed names and constraints in memory until
|
||||
// we return from isValid.
|
||||
reversedDomainsCache := map[string][]string{}
|
||||
reversedConstraintsCache := map[string][]string{}
|
||||
|
||||
if (certType == intermediateCertificate || certType == rootCertificate) &&
|
||||
c.hasNameConstraints() {
|
||||
toCheck := []*Certificate{}
|
||||
@@ -656,20 +680,20 @@ func (c *Certificate) isValid(certType int, currentChain []*Certificate, opts *V
|
||||
|
||||
if err := c.checkNameConstraints(&comparisonCount, maxConstraintComparisons, "email address", name, mailbox,
|
||||
func(parsedName, constraint any) (bool, error) {
|
||||
return matchEmailConstraint(parsedName.(rfc2821Mailbox), constraint.(string))
|
||||
return matchEmailConstraint(parsedName.(rfc2821Mailbox), constraint.(string), reversedDomainsCache, reversedConstraintsCache)
|
||||
}, c.PermittedEmailAddresses, c.ExcludedEmailAddresses); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case nameTypeDNS:
|
||||
name := string(data)
|
||||
if _, ok := domainToReverseLabels(name); !ok {
|
||||
if !domainNameValid(name, false) {
|
||||
return fmt.Errorf("x509: cannot parse dnsName %q", name)
|
||||
}
|
||||
|
||||
if err := c.checkNameConstraints(&comparisonCount, maxConstraintComparisons, "DNS name", name, name,
|
||||
func(parsedName, constraint any) (bool, error) {
|
||||
return matchDomainConstraint(parsedName.(string), constraint.(string))
|
||||
return matchDomainConstraint(parsedName.(string), constraint.(string), reversedDomainsCache, reversedConstraintsCache)
|
||||
}, c.PermittedDNSDomains, c.ExcludedDNSDomains); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -683,7 +707,7 @@ func (c *Certificate) isValid(certType int, currentChain []*Certificate, opts *V
|
||||
|
||||
if err := c.checkNameConstraints(&comparisonCount, maxConstraintComparisons, "URI", name, uri,
|
||||
func(parsedName, constraint any) (bool, error) {
|
||||
return matchURIConstraint(parsedName.(*url.URL), constraint.(string))
|
||||
return matchURIConstraint(parsedName.(*url.URL), constraint.(string), reversedDomainsCache, reversedConstraintsCache)
|
||||
}, c.PermittedURIDomains, c.ExcludedURIDomains); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -927,7 +951,10 @@ func alreadyInChain(candidate *Certificate, chain []*Certificate) bool {
|
||||
if !bytes.Equal(candidate.RawSubject, cert.RawSubject) {
|
||||
continue
|
||||
}
|
||||
if !candidate.PublicKey.(pubKeyEqual).Equal(cert.PublicKey) {
|
||||
// We enforce the canonical encoding of SPKI (by only allowing the
|
||||
// correct AI paremeter encodings in parseCertificate), so it's safe to
|
||||
// directly compare the raw bytes.
|
||||
if !bytes.Equal(candidate.RawSubjectPublicKeyInfo, cert.RawSubjectPublicKeyInfo) {
|
||||
continue
|
||||
}
|
||||
var certSAN *pkix.Extension
|
||||
|
||||
@@ -6,6 +6,7 @@ package x509
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/dsa"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
@@ -1351,7 +1352,7 @@ var nameConstraintTests = []struct {
|
||||
|
||||
func TestNameConstraints(t *testing.T) {
|
||||
for i, test := range nameConstraintTests {
|
||||
result, err := matchDomainConstraint(test.domain, test.constraint)
|
||||
result, err := matchDomainConstraint(test.domain, test.constraint, map[string][]string{}, map[string][]string{})
|
||||
|
||||
if err != nil && !test.expectError {
|
||||
t.Errorf("unexpected error for test #%d: domain=%s, constraint=%s, err=%s", i, test.domain, test.constraint, err)
|
||||
@@ -3048,3 +3049,129 @@ func TestInvalidPolicyWithAnyKeyUsage(t *testing.T) {
|
||||
t.Fatalf("unexpected error, got %q, want %q", err, expectedErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCertificateChainSignedByECDSA(t *testing.T) {
|
||||
caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
root := &Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{CommonName: "X"},
|
||||
NotBefore: time.Now().Add(-time.Hour),
|
||||
NotAfter: time.Now().Add(365 * 24 * time.Hour),
|
||||
IsCA: true,
|
||||
KeyUsage: KeyUsageCertSign | KeyUsageCRLSign,
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
caDER, err := CreateCertificate(rand.Reader, root, root, &caKey.PublicKey, caKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
root, err = ParseCertificate(caDER)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
leafKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
leaf := &Certificate{
|
||||
SerialNumber: big.NewInt(42),
|
||||
Subject: pkix.Name{CommonName: "leaf"},
|
||||
NotBefore: time.Now().Add(-10 * time.Minute),
|
||||
NotAfter: time.Now().Add(24 * time.Hour),
|
||||
KeyUsage: KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []ExtKeyUsage{ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
leafDER, err := CreateCertificate(rand.Reader, leaf, root, &leafKey.PublicKey, caKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
leaf, err = ParseCertificate(leafDER)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
inter, err := ParseCertificate(dsaSelfSignedCNX(t))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
inters := NewCertPool()
|
||||
inters.AddCert(root)
|
||||
inters.AddCert(inter)
|
||||
|
||||
wantErr := "certificate signed by unknown authority"
|
||||
_, err = leaf.Verify(VerifyOptions{Intermediates: inters, Roots: NewCertPool()})
|
||||
if !strings.Contains(err.Error(), wantErr) {
|
||||
t.Errorf("got %v, want %q", err, wantErr)
|
||||
}
|
||||
}
|
||||
|
||||
// dsaSelfSignedCNX produces DER-encoded
|
||||
// certificate with the properties:
|
||||
//
|
||||
// Subject=Issuer=CN=X
|
||||
// DSA SPKI
|
||||
// Matching inner/outer signature OIDs
|
||||
// Dummy ECDSA signature
|
||||
func dsaSelfSignedCNX(t *testing.T) []byte {
|
||||
t.Helper()
|
||||
var params dsa.Parameters
|
||||
if err := dsa.GenerateParameters(¶ms, rand.Reader, dsa.L1024N160); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var dsaPriv dsa.PrivateKey
|
||||
dsaPriv.Parameters = params
|
||||
if err := dsa.GenerateKey(&dsaPriv, rand.Reader); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dsaPub := &dsaPriv.PublicKey
|
||||
|
||||
type dsaParams struct{ P, Q, G *big.Int }
|
||||
paramDER, err := asn1.Marshal(dsaParams{dsaPub.P, dsaPub.Q, dsaPub.G})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
yDER, err := asn1.Marshal(dsaPub.Y)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
spki := publicKeyInfo{
|
||||
Algorithm: pkix.AlgorithmIdentifier{
|
||||
Algorithm: oidPublicKeyDSA,
|
||||
Parameters: asn1.RawValue{FullBytes: paramDER},
|
||||
},
|
||||
PublicKey: asn1.BitString{Bytes: yDER, BitLength: 8 * len(yDER)},
|
||||
}
|
||||
|
||||
rdn := pkix.Name{CommonName: "X"}.ToRDNSequence()
|
||||
b, err := asn1.Marshal(rdn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rawName := asn1.RawValue{FullBytes: b}
|
||||
|
||||
algoIdent := pkix.AlgorithmIdentifier{Algorithm: oidSignatureDSAWithSHA256}
|
||||
tbs := tbsCertificate{
|
||||
Version: 0,
|
||||
SerialNumber: big.NewInt(1002),
|
||||
SignatureAlgorithm: algoIdent,
|
||||
Issuer: rawName,
|
||||
Validity: validity{NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(24 * time.Hour)},
|
||||
Subject: rawName,
|
||||
PublicKey: spki,
|
||||
}
|
||||
c := certificate{
|
||||
TBSCertificate: tbs,
|
||||
SignatureAlgorithm: algoIdent,
|
||||
SignatureValue: asn1.BitString{Bytes: []byte{0}, BitLength: 8},
|
||||
}
|
||||
dsaDER, err := asn1.Marshal(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return dsaDER
|
||||
}
|
||||
|
||||
@@ -335,7 +335,6 @@ func convertAssignRows(dest, src any, rows *Rows) error {
|
||||
if rows == nil {
|
||||
return errors.New("invalid context to convert cursor rows, missing parent *Rows")
|
||||
}
|
||||
rows.closemu.Lock()
|
||||
*d = Rows{
|
||||
dc: rows.dc,
|
||||
releaseConn: func(error) {},
|
||||
@@ -351,7 +350,6 @@ func convertAssignRows(dest, src any, rows *Rows) error {
|
||||
parentCancel()
|
||||
}
|
||||
}
|
||||
rows.closemu.Unlock()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package sql
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
@@ -15,7 +16,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -91,8 +91,6 @@ func (cc *fakeDriverCtx) OpenConnector(name string) (driver.Connector, error) {
|
||||
type fakeDB struct {
|
||||
name string
|
||||
|
||||
useRawBytes atomic.Bool
|
||||
|
||||
mu sync.Mutex
|
||||
tables map[string]*table
|
||||
badConn bool
|
||||
@@ -684,8 +682,6 @@ func (c *fakeConn) PrepareContext(ctx context.Context, query string) (driver.Stm
|
||||
switch cmd {
|
||||
case "WIPE":
|
||||
// Nothing
|
||||
case "USE_RAWBYTES":
|
||||
c.db.useRawBytes.Store(true)
|
||||
case "SELECT":
|
||||
stmt, err = c.prepareSelect(stmt, parts)
|
||||
case "CREATE":
|
||||
@@ -789,9 +785,6 @@ func (s *fakeStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (d
|
||||
case "WIPE":
|
||||
db.wipe()
|
||||
return driver.ResultNoRows, nil
|
||||
case "USE_RAWBYTES":
|
||||
s.c.db.useRawBytes.Store(true)
|
||||
return driver.ResultNoRows, nil
|
||||
case "CREATE":
|
||||
if err := db.createTable(s.table, s.colName, s.colType); err != nil {
|
||||
return nil, err
|
||||
@@ -1076,10 +1069,9 @@ type rowsCursor struct {
|
||||
errPos int
|
||||
err error
|
||||
|
||||
// a clone of slices to give out to clients, indexed by the
|
||||
// original slice's first byte address. we clone them
|
||||
// just so we're able to corrupt them on close.
|
||||
bytesClone map[*byte][]byte
|
||||
// Data returned to clients.
|
||||
// We clone and stash it here so it can be invalidated by Close and Next.
|
||||
driverOwnedMemory [][]byte
|
||||
|
||||
// Every operation writes to line to enable the race detector
|
||||
// check for data races.
|
||||
@@ -1096,9 +1088,19 @@ func (rc *rowsCursor) touchMem() {
|
||||
rc.line++
|
||||
}
|
||||
|
||||
func (rc *rowsCursor) invalidateDriverOwnedMemory() {
|
||||
for _, buf := range rc.driverOwnedMemory {
|
||||
for i := range buf {
|
||||
buf[i] = 'x'
|
||||
}
|
||||
}
|
||||
rc.driverOwnedMemory = nil
|
||||
}
|
||||
|
||||
func (rc *rowsCursor) Close() error {
|
||||
rc.touchMem()
|
||||
rc.parentMem.touchMem()
|
||||
rc.invalidateDriverOwnedMemory()
|
||||
rc.closed = true
|
||||
return rc.closeErr
|
||||
}
|
||||
@@ -1129,6 +1131,8 @@ func (rc *rowsCursor) Next(dest []driver.Value) error {
|
||||
if rc.posRow >= len(rc.rows[rc.posSet]) {
|
||||
return io.EOF // per interface spec
|
||||
}
|
||||
// Corrupt any previously returned bytes.
|
||||
rc.invalidateDriverOwnedMemory()
|
||||
for i, v := range rc.rows[rc.posSet][rc.posRow].cols {
|
||||
// TODO(bradfitz): convert to subset types? naah, I
|
||||
// think the subset types should only be input to
|
||||
@@ -1136,20 +1140,13 @@ func (rc *rowsCursor) Next(dest []driver.Value) error {
|
||||
// a wider range of types coming out of drivers. all
|
||||
// for ease of drivers, and to prevent drivers from
|
||||
// messing up conversions or doing them differently.
|
||||
dest[i] = v
|
||||
|
||||
if bs, ok := v.([]byte); ok && !rc.db.useRawBytes.Load() {
|
||||
if rc.bytesClone == nil {
|
||||
rc.bytesClone = make(map[*byte][]byte)
|
||||
}
|
||||
clone, ok := rc.bytesClone[&bs[0]]
|
||||
if !ok {
|
||||
clone = make([]byte, len(bs))
|
||||
copy(clone, bs)
|
||||
rc.bytesClone[&bs[0]] = clone
|
||||
}
|
||||
dest[i] = clone
|
||||
if bs, ok := v.([]byte); ok {
|
||||
// Clone []bytes and stash for later invalidation.
|
||||
bs = bytes.Clone(bs)
|
||||
rc.driverOwnedMemory = append(rc.driverOwnedMemory, bs)
|
||||
v = bs
|
||||
}
|
||||
dest[i] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3368,38 +3368,36 @@ func (rs *Rows) Scan(dest ...any) error {
|
||||
// without calling Next.
|
||||
return fmt.Errorf("sql: Scan called without calling Next (closemuScanHold)")
|
||||
}
|
||||
|
||||
rs.closemu.RLock()
|
||||
|
||||
if rs.lasterr != nil && rs.lasterr != io.EOF {
|
||||
rs.closemu.RUnlock()
|
||||
return rs.lasterr
|
||||
}
|
||||
if rs.closed {
|
||||
err := rs.lasterrOrErrLocked(errRowsClosed)
|
||||
rs.closemu.RUnlock()
|
||||
return err
|
||||
}
|
||||
|
||||
if scanArgsContainRawBytes(dest) {
|
||||
rs.raw = rs.raw[:0]
|
||||
err := rs.scanLocked(dest...)
|
||||
if err == nil && scanArgsContainRawBytes(dest) {
|
||||
rs.closemuScanHold = true
|
||||
rs.raw = rs.raw[:0]
|
||||
} else {
|
||||
rs.closemu.RUnlock()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (rs *Rows) scanLocked(dest ...any) error {
|
||||
if rs.lasterr != nil && rs.lasterr != io.EOF {
|
||||
return rs.lasterr
|
||||
}
|
||||
if rs.closed {
|
||||
return rs.lasterrOrErrLocked(errRowsClosed)
|
||||
}
|
||||
|
||||
if rs.lastcols == nil {
|
||||
rs.closemuRUnlockIfHeldByScan()
|
||||
return errors.New("sql: Scan called without calling Next")
|
||||
}
|
||||
if len(dest) != len(rs.lastcols) {
|
||||
rs.closemuRUnlockIfHeldByScan()
|
||||
return fmt.Errorf("sql: expected %d destination arguments in Scan, not %d", len(rs.lastcols), len(dest))
|
||||
}
|
||||
|
||||
for i, sv := range rs.lastcols {
|
||||
err := convertAssignRows(dest[i], sv, rs)
|
||||
if err != nil {
|
||||
rs.closemuRUnlockIfHeldByScan()
|
||||
return fmt.Errorf(`sql: Scan error on column index %d, name %q: %w`, i, rs.rowsi.Columns()[i], err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package sql
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
@@ -4434,10 +4435,6 @@ func testContextCancelDuringRawBytesScan(t *testing.T, mode string) {
|
||||
db := newTestDB(t, "people")
|
||||
defer closeDB(t, db)
|
||||
|
||||
if _, err := db.Exec("USE_RAWBYTES"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// cancel used to call close asynchronously.
|
||||
// This test checks that it waits so as not to interfere with RawBytes.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@@ -4529,6 +4526,61 @@ func TestContextCancelBetweenNextAndErr(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type testScanner struct {
|
||||
scanf func(src any) error
|
||||
}
|
||||
|
||||
func (ts testScanner) Scan(src any) error { return ts.scanf(src) }
|
||||
|
||||
func TestContextCancelDuringScan(t *testing.T) {
|
||||
db := newTestDB(t, "people")
|
||||
defer closeDB(t, db)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
scanStart := make(chan any)
|
||||
scanEnd := make(chan error)
|
||||
scanner := &testScanner{
|
||||
scanf: func(src any) error {
|
||||
scanStart <- src
|
||||
return <-scanEnd
|
||||
},
|
||||
}
|
||||
|
||||
// Start a query, and pause it mid-scan.
|
||||
want := []byte("Alice")
|
||||
r, err := db.QueryContext(ctx, "SELECT|people|name|name=?", string(want))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !r.Next() {
|
||||
t.Fatalf("r.Next() = false, want true")
|
||||
}
|
||||
go func() {
|
||||
r.Scan(scanner)
|
||||
}()
|
||||
got := <-scanStart
|
||||
defer close(scanEnd)
|
||||
gotBytes, ok := got.([]byte)
|
||||
if !ok {
|
||||
t.Fatalf("r.Scan returned %T, want []byte", got)
|
||||
}
|
||||
if !bytes.Equal(gotBytes, want) {
|
||||
t.Fatalf("before cancel: r.Scan returned %q, want %q", gotBytes, want)
|
||||
}
|
||||
|
||||
// Cancel the query.
|
||||
// Sleep to give it a chance to finish canceling.
|
||||
cancel()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
// Cancelling the query should not have changed the result.
|
||||
if !bytes.Equal(gotBytes, want) {
|
||||
t.Fatalf("after cancel: r.Scan result is now %q, want %q", gotBytes, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNilErrorAfterClose(t *testing.T) {
|
||||
db := newTestDB(t, "people")
|
||||
defer closeDB(t, db)
|
||||
@@ -4562,10 +4614,6 @@ func TestRawBytesReuse(t *testing.T) {
|
||||
db := newTestDB(t, "people")
|
||||
defer closeDB(t, db)
|
||||
|
||||
if _, err := db.Exec("USE_RAWBYTES"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var raw RawBytes
|
||||
|
||||
// The RawBytes in this query aliases driver-owned memory.
|
||||
|
||||
@@ -98,7 +98,12 @@ func readCOFFSymbols(fh *FileHeader, r io.ReadSeeker) ([]COFFSymbol, error) {
|
||||
// isSymNameOffset checks symbol name if it is encoded as offset into string table.
|
||||
func isSymNameOffset(name [8]byte) (bool, uint32) {
|
||||
if name[0] == 0 && name[1] == 0 && name[2] == 0 && name[3] == 0 {
|
||||
return true, binary.LittleEndian.Uint32(name[4:])
|
||||
offset := binary.LittleEndian.Uint32(name[4:])
|
||||
if offset == 0 {
|
||||
// symbol has no name
|
||||
return false, 0
|
||||
}
|
||||
return true, offset
|
||||
}
|
||||
return false, 0
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ package asn1
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"internal/saferio"
|
||||
"math"
|
||||
"math/big"
|
||||
"reflect"
|
||||
@@ -666,10 +667,17 @@ func parseSequenceOf(bytes []byte, sliceType reflect.Type, elemType reflect.Type
|
||||
offset += t.length
|
||||
numElements++
|
||||
}
|
||||
ret = reflect.MakeSlice(sliceType, numElements, numElements)
|
||||
elemSize := uint64(elemType.Size())
|
||||
safeCap := saferio.SliceCapWithSize(elemSize, uint64(numElements))
|
||||
if safeCap < 0 {
|
||||
err = SyntaxError{fmt.Sprintf("%s slice too big: %d elements of %d bytes", elemType.Kind(), numElements, elemSize)}
|
||||
return
|
||||
}
|
||||
ret = reflect.MakeSlice(sliceType, 0, safeCap)
|
||||
params := fieldParameters{}
|
||||
offset := 0
|
||||
for i := 0; i < numElements; i++ {
|
||||
ret = reflect.Append(ret, reflect.Zero(elemType))
|
||||
offset, err = parseField(ret.Index(i), bytes, offset, params)
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
@@ -7,10 +7,12 @@ package asn1
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -1216,3 +1218,39 @@ func TestImplicitTypeRoundtrip(t *testing.T) {
|
||||
t.Fatalf("Unexpected diff after roundtripping struct\na: %#v\nb: %#v", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsingMemoryConsumption(t *testing.T) {
|
||||
// Craft a syntatically valid, but empty, ~10 MB DER bomb. A successful
|
||||
// unmarshal of this bomb should yield ~280 MB. However, the parsing should
|
||||
// fail due to the empty content; and, in such cases, we want to make sure
|
||||
// that we do not unnecessarily allocate memories.
|
||||
derBomb := make([]byte, 10_000_000)
|
||||
for i := range derBomb {
|
||||
derBomb[i] = 0x30
|
||||
}
|
||||
derBomb = append([]byte{0x30, 0x83, 0x98, 0x96, 0x80}, derBomb...)
|
||||
|
||||
var m runtime.MemStats
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m)
|
||||
memBefore := m.TotalAlloc
|
||||
|
||||
var out []struct {
|
||||
Id []int
|
||||
Critical bool `asn1:"optional"`
|
||||
Value []byte
|
||||
}
|
||||
_, err := Unmarshal(derBomb, &out)
|
||||
if !errors.As(err, &SyntaxError{}) {
|
||||
t.Fatalf("Incorrect error result: want (%v), but got (%v) instead", &SyntaxError{}, err)
|
||||
}
|
||||
|
||||
runtime.ReadMemStats(&m)
|
||||
memDiff := m.TotalAlloc - memBefore
|
||||
|
||||
// Ensure that the memory allocated does not exceed 10<<21 (~20 MB) when
|
||||
// the parsing fails.
|
||||
if memDiff > 10<<21 {
|
||||
t.Errorf("Too much memory allocated while parsing DER: %v MiB", memDiff/1024/1024)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ type Block struct {
|
||||
// line bytes. The remainder of the byte array (also not including the new line
|
||||
// bytes) is also returned and this will always be smaller than the original
|
||||
// argument.
|
||||
func getLine(data []byte) (line, rest []byte) {
|
||||
func getLine(data []byte) (line, rest []byte, consumed int) {
|
||||
i := bytes.IndexByte(data, '\n')
|
||||
var j int
|
||||
if i < 0 {
|
||||
@@ -49,7 +49,7 @@ func getLine(data []byte) (line, rest []byte) {
|
||||
i--
|
||||
}
|
||||
}
|
||||
return bytes.TrimRight(data[0:i], " \t"), data[j:]
|
||||
return bytes.TrimRight(data[0:i], " \t"), data[j:], j
|
||||
}
|
||||
|
||||
// removeSpacesAndTabs returns a copy of its input with all spaces and tabs
|
||||
@@ -90,20 +90,32 @@ func Decode(data []byte) (p *Block, rest []byte) {
|
||||
// pemStart begins with a newline. However, at the very beginning of
|
||||
// the byte array, we'll accept the start string without it.
|
||||
rest = data
|
||||
|
||||
for {
|
||||
if bytes.HasPrefix(rest, pemStart[1:]) {
|
||||
rest = rest[len(pemStart)-1:]
|
||||
} else if _, after, ok := bytes.Cut(rest, pemStart); ok {
|
||||
rest = after
|
||||
} else {
|
||||
// Find the first END line, and then find the last BEGIN line before
|
||||
// the end line. This lets us skip any repeated BEGIN lines that don't
|
||||
// have a matching END.
|
||||
endIndex := bytes.Index(rest, pemEnd)
|
||||
if endIndex < 0 {
|
||||
return nil, data
|
||||
}
|
||||
endTrailerIndex := endIndex + len(pemEnd)
|
||||
beginIndex := bytes.LastIndex(rest[:endIndex], pemStart[1:])
|
||||
if beginIndex < 0 || beginIndex > 0 && rest[beginIndex-1] != '\n' {
|
||||
return nil, data
|
||||
}
|
||||
rest = rest[beginIndex+len(pemStart)-1:]
|
||||
endIndex -= beginIndex + len(pemStart) - 1
|
||||
endTrailerIndex -= beginIndex + len(pemStart) - 1
|
||||
|
||||
var typeLine []byte
|
||||
typeLine, rest = getLine(rest)
|
||||
var consumed int
|
||||
typeLine, rest, consumed = getLine(rest)
|
||||
if !bytes.HasSuffix(typeLine, pemEndOfLine) {
|
||||
continue
|
||||
}
|
||||
endIndex -= consumed
|
||||
endTrailerIndex -= consumed
|
||||
typeLine = typeLine[0 : len(typeLine)-len(pemEndOfLine)]
|
||||
|
||||
p = &Block{
|
||||
@@ -117,7 +129,7 @@ func Decode(data []byte) (p *Block, rest []byte) {
|
||||
if len(rest) == 0 {
|
||||
return nil, data
|
||||
}
|
||||
line, next := getLine(rest)
|
||||
line, next, consumed := getLine(rest)
|
||||
|
||||
key, val, ok := bytes.Cut(line, colon)
|
||||
if !ok {
|
||||
@@ -129,21 +141,13 @@ func Decode(data []byte) (p *Block, rest []byte) {
|
||||
val = bytes.TrimSpace(val)
|
||||
p.Headers[string(key)] = string(val)
|
||||
rest = next
|
||||
endIndex -= consumed
|
||||
endTrailerIndex -= consumed
|
||||
}
|
||||
|
||||
var endIndex, endTrailerIndex int
|
||||
|
||||
// If there were no headers, the END line might occur
|
||||
// immediately, without a leading newline.
|
||||
if len(p.Headers) == 0 && bytes.HasPrefix(rest, pemEnd[1:]) {
|
||||
endIndex = 0
|
||||
endTrailerIndex = len(pemEnd) - 1
|
||||
} else {
|
||||
endIndex = bytes.Index(rest, pemEnd)
|
||||
endTrailerIndex = endIndex + len(pemEnd)
|
||||
}
|
||||
|
||||
if endIndex < 0 {
|
||||
// If there were headers, there must be a newline between the headers
|
||||
// and the END line, so endIndex should be >= 0.
|
||||
if len(p.Headers) > 0 && endIndex < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -163,21 +167,24 @@ func Decode(data []byte) (p *Block, rest []byte) {
|
||||
}
|
||||
|
||||
// The line must end with only whitespace.
|
||||
if s, _ := getLine(restOfEndLine); len(s) != 0 {
|
||||
if s, _, _ := getLine(restOfEndLine); len(s) != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
base64Data := removeSpacesAndTabs(rest[:endIndex])
|
||||
p.Bytes = make([]byte, base64.StdEncoding.DecodedLen(len(base64Data)))
|
||||
n, err := base64.StdEncoding.Decode(p.Bytes, base64Data)
|
||||
if err != nil {
|
||||
continue
|
||||
p.Bytes = []byte{}
|
||||
if endIndex > 0 {
|
||||
base64Data := removeSpacesAndTabs(rest[:endIndex])
|
||||
p.Bytes = make([]byte, base64.StdEncoding.DecodedLen(len(base64Data)))
|
||||
n, err := base64.StdEncoding.Decode(p.Bytes, base64Data)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
p.Bytes = p.Bytes[:n]
|
||||
}
|
||||
p.Bytes = p.Bytes[:n]
|
||||
|
||||
// the -1 is because we might have only matched pemEnd without the
|
||||
// leading newline if the PEM block was empty.
|
||||
_, rest = getLine(rest[endIndex+len(pemEnd)-1:])
|
||||
_, rest, _ = getLine(rest[endIndex+len(pemEnd)-1:])
|
||||
return p, rest
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ var getLineTests = []GetLineTest{
|
||||
|
||||
func TestGetLine(t *testing.T) {
|
||||
for i, test := range getLineTests {
|
||||
x, y := getLine([]byte(test.in))
|
||||
x, y, _ := getLine([]byte(test.in))
|
||||
if string(x) != test.out1 || string(y) != test.out2 {
|
||||
t.Errorf("#%d got:%+v,%+v want:%s,%s", i, x, y, test.out1, test.out2)
|
||||
}
|
||||
@@ -46,6 +46,7 @@ func TestDecode(t *testing.T) {
|
||||
if !reflect.DeepEqual(result, certificate) {
|
||||
t.Errorf("#0 got:%#v want:%#v", result, certificate)
|
||||
}
|
||||
|
||||
result, remainder = Decode(remainder)
|
||||
if !reflect.DeepEqual(result, privateKey) {
|
||||
t.Errorf("#1 got:%#v want:%#v", result, privateKey)
|
||||
@@ -68,7 +69,7 @@ func TestDecode(t *testing.T) {
|
||||
}
|
||||
|
||||
result, remainder = Decode(remainder)
|
||||
if result == nil || result.Type != "HEADERS" || len(result.Headers) != 1 {
|
||||
if result == nil || result.Type != "VALID HEADERS" || len(result.Headers) != 1 {
|
||||
t.Errorf("#5 expected single header block but got :%v", result)
|
||||
}
|
||||
|
||||
@@ -381,15 +382,15 @@ ZWAaUoVtWIQ52aKS0p19G99hhb+IVANC4akkdHV4SP8i7MVNZhfUmg==
|
||||
|
||||
# This shouldn't be recognised because of the missing newline after the
|
||||
headers.
|
||||
-----BEGIN HEADERS-----
|
||||
-----BEGIN INVALID HEADERS-----
|
||||
Header: 1
|
||||
-----END HEADERS-----
|
||||
-----END INVALID HEADERS-----
|
||||
|
||||
# This should be valid, however.
|
||||
-----BEGIN HEADERS-----
|
||||
-----BEGIN VALID HEADERS-----
|
||||
Header: 1
|
||||
|
||||
-----END HEADERS-----`)
|
||||
-----END VALID HEADERS-----`)
|
||||
|
||||
var certificate = &Block{Type: "CERTIFICATE",
|
||||
Headers: map[string]string{},
|
||||
|
||||
@@ -235,7 +235,6 @@ var depsRules = `
|
||||
internal/types/errors,
|
||||
mime/quotedprintable,
|
||||
net/internal/socktest,
|
||||
net/url,
|
||||
runtime/trace,
|
||||
text/scanner,
|
||||
text/tabwriter;
|
||||
@@ -298,6 +297,12 @@ var depsRules = `
|
||||
FMT
|
||||
< text/template/parse;
|
||||
|
||||
internal/bytealg, internal/itoa, math/bits, slices, strconv, unique
|
||||
< net/netip;
|
||||
|
||||
FMT, net/netip
|
||||
< net/url;
|
||||
|
||||
net/url, text/template/parse
|
||||
< text/template
|
||||
< internal/lazytemplate;
|
||||
@@ -412,9 +417,6 @@ var depsRules = `
|
||||
< golang.org/x/net/dns/dnsmessage,
|
||||
golang.org/x/net/lif;
|
||||
|
||||
internal/bytealg, internal/itoa, math/bits, slices, strconv, unique
|
||||
< net/netip;
|
||||
|
||||
os, net/netip
|
||||
< internal/routebsd;
|
||||
|
||||
@@ -557,7 +559,7 @@ var depsRules = `
|
||||
|
||||
# CRYPTO-MATH is crypto that exposes math/big APIs - no cgo, net; fmt now ok.
|
||||
|
||||
CRYPTO, FMT, math/big
|
||||
CRYPTO, FMT, math/big, internal/saferio
|
||||
< crypto/internal/boring/bbig
|
||||
< crypto/internal/fips140cache
|
||||
< crypto/rand
|
||||
|
||||
@@ -85,7 +85,7 @@ func gofips140() string {
|
||||
}
|
||||
|
||||
// isFIPSVersion reports whether v is a valid FIPS version,
|
||||
// of the form vX.Y.Z.
|
||||
// of the form vX.Y.Z or vX.Y.Z-hash.
|
||||
func isFIPSVersion(v string) bool {
|
||||
if !strings.HasPrefix(v, "v") {
|
||||
return false
|
||||
@@ -99,7 +99,8 @@ func isFIPSVersion(v string) bool {
|
||||
return false
|
||||
}
|
||||
v, ok = skipNum(v[len("."):])
|
||||
return ok && v == ""
|
||||
hasHash := strings.HasPrefix(v, "-") && len(v) == len("-")+8
|
||||
return ok && (v == "" || hasHash)
|
||||
}
|
||||
|
||||
// skipNum skips the leading text matching [0-9]+
|
||||
|
||||
@@ -42,6 +42,7 @@ var All = []Info{
|
||||
{Name: "http2client", Package: "net/http"},
|
||||
{Name: "http2debug", Package: "net/http", Opaque: true},
|
||||
{Name: "http2server", Package: "net/http"},
|
||||
{Name: "httpcookiemaxnum", Package: "net/http", Changed: 24, Old: "0"},
|
||||
{Name: "httplaxcontentlength", Package: "net/http", Changed: 22, Old: "1"},
|
||||
{Name: "httpmuxgo121", Package: "net/http", Changed: 22, Old: "1"},
|
||||
{Name: "httpservecontentkeepheaders", Package: "net/http", Changed: 23, Old: "1"},
|
||||
|
||||
@@ -636,12 +636,22 @@ func (fd *FD) Pread(b []byte, off int64) (int, error) {
|
||||
|
||||
fd.l.Lock()
|
||||
defer fd.l.Unlock()
|
||||
curoffset, err := syscall.Seek(fd.Sysfd, 0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
if fd.isBlocking {
|
||||
curoffset, err := syscall.Seek(fd.Sysfd, 0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer syscall.Seek(fd.Sysfd, curoffset, io.SeekStart)
|
||||
defer fd.setOffset(curoffset)
|
||||
} else {
|
||||
// Overlapped handles don't have the file pointer updated
|
||||
// when performing I/O operations, so there is no need to
|
||||
// call Seek to reset the file pointer.
|
||||
// Also, some overlapped file handles don't support seeking.
|
||||
// See https://go.dev/issues/74951.
|
||||
curoffset := fd.offset
|
||||
defer fd.setOffset(curoffset)
|
||||
}
|
||||
defer syscall.Seek(fd.Sysfd, curoffset, io.SeekStart)
|
||||
defer fd.setOffset(curoffset)
|
||||
o := &fd.rop
|
||||
o.InitBuf(b)
|
||||
fd.setOffset(off)
|
||||
@@ -852,12 +862,22 @@ func (fd *FD) Pwrite(buf []byte, off int64) (int, error) {
|
||||
|
||||
fd.l.Lock()
|
||||
defer fd.l.Unlock()
|
||||
curoffset, err := syscall.Seek(fd.Sysfd, 0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
if fd.isBlocking {
|
||||
curoffset, err := syscall.Seek(fd.Sysfd, 0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer syscall.Seek(fd.Sysfd, curoffset, io.SeekStart)
|
||||
defer fd.setOffset(curoffset)
|
||||
} else {
|
||||
// Overlapped handles don't have the file pointer updated
|
||||
// when performing I/O operations, so there is no need to
|
||||
// call Seek to reset the file pointer.
|
||||
// Also, some overlapped file handles don't support seeking.
|
||||
// See https://go.dev/issues/74951.
|
||||
curoffset := fd.offset
|
||||
defer fd.setOffset(curoffset)
|
||||
}
|
||||
defer syscall.Seek(fd.Sysfd, curoffset, io.SeekStart)
|
||||
defer fd.setOffset(curoffset)
|
||||
|
||||
var ntotal int
|
||||
for {
|
||||
@@ -1106,6 +1126,12 @@ func (fd *FD) Seek(offset int64, whence int) (int64, error) {
|
||||
fd.l.Lock()
|
||||
defer fd.l.Unlock()
|
||||
|
||||
if !fd.isBlocking && whence == io.SeekCurrent {
|
||||
// Windows doesn't keep the file pointer for overlapped file handles.
|
||||
// We do it ourselves in case to account for any read or write
|
||||
// operations that may have occurred.
|
||||
offset += fd.offset
|
||||
}
|
||||
n, err := syscall.Seek(fd.Sysfd, offset, whence)
|
||||
fd.setOffset(n)
|
||||
return n, err
|
||||
|
||||
@@ -383,57 +383,59 @@ func TestChannelMovedOutOfBubble(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
desc string
|
||||
f func(chan struct{})
|
||||
wantPanic string
|
||||
wantFatal string
|
||||
}{{
|
||||
desc: "receive",
|
||||
f: func(ch chan struct{}) {
|
||||
<-ch
|
||||
},
|
||||
wantPanic: "receive on synctest channel from outside bubble",
|
||||
wantFatal: "receive on synctest channel from outside bubble",
|
||||
}, {
|
||||
desc: "send",
|
||||
f: func(ch chan struct{}) {
|
||||
ch <- struct{}{}
|
||||
},
|
||||
wantPanic: "send on synctest channel from outside bubble",
|
||||
wantFatal: "send on synctest channel from outside bubble",
|
||||
}, {
|
||||
desc: "close",
|
||||
f: func(ch chan struct{}) {
|
||||
close(ch)
|
||||
},
|
||||
wantPanic: "close of synctest channel from outside bubble",
|
||||
wantFatal: "close of synctest channel from outside bubble",
|
||||
}} {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
// Bubbled channel accessed from outside any bubble.
|
||||
t.Run("outside_bubble", func(t *testing.T) {
|
||||
donec := make(chan struct{})
|
||||
ch := make(chan chan struct{})
|
||||
go func() {
|
||||
defer close(donec)
|
||||
defer wantPanic(t, test.wantPanic)
|
||||
test.f(<-ch)
|
||||
}()
|
||||
synctest.Run(func() {
|
||||
ch <- make(chan struct{})
|
||||
wantFatal(t, test.wantFatal, func() {
|
||||
donec := make(chan struct{})
|
||||
ch := make(chan chan struct{})
|
||||
go func() {
|
||||
defer close(donec)
|
||||
test.f(<-ch)
|
||||
}()
|
||||
synctest.Run(func() {
|
||||
ch <- make(chan struct{})
|
||||
})
|
||||
<-donec
|
||||
})
|
||||
<-donec
|
||||
})
|
||||
// Bubbled channel accessed from a different bubble.
|
||||
t.Run("different_bubble", func(t *testing.T) {
|
||||
donec := make(chan struct{})
|
||||
ch := make(chan chan struct{})
|
||||
go func() {
|
||||
defer close(donec)
|
||||
c := <-ch
|
||||
wantFatal(t, test.wantFatal, func() {
|
||||
donec := make(chan struct{})
|
||||
ch := make(chan chan struct{})
|
||||
go func() {
|
||||
defer close(donec)
|
||||
c := <-ch
|
||||
synctest.Run(func() {
|
||||
test.f(c)
|
||||
})
|
||||
}()
|
||||
synctest.Run(func() {
|
||||
defer wantPanic(t, test.wantPanic)
|
||||
test.f(c)
|
||||
ch <- make(chan struct{})
|
||||
})
|
||||
}()
|
||||
synctest.Run(func() {
|
||||
ch <- make(chan struct{})
|
||||
<-donec
|
||||
})
|
||||
<-donec
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -443,39 +445,40 @@ func TestTimerFromInsideBubble(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
desc string
|
||||
f func(tm *time.Timer)
|
||||
wantPanic string
|
||||
wantFatal string
|
||||
}{{
|
||||
desc: "read channel",
|
||||
f: func(tm *time.Timer) {
|
||||
<-tm.C
|
||||
},
|
||||
wantPanic: "receive on synctest channel from outside bubble",
|
||||
wantFatal: "receive on synctest channel from outside bubble",
|
||||
}, {
|
||||
desc: "Reset",
|
||||
f: func(tm *time.Timer) {
|
||||
tm.Reset(1 * time.Second)
|
||||
},
|
||||
wantPanic: "reset of synctest timer from outside bubble",
|
||||
wantFatal: "reset of synctest timer from outside bubble",
|
||||
}, {
|
||||
desc: "Stop",
|
||||
f: func(tm *time.Timer) {
|
||||
tm.Stop()
|
||||
},
|
||||
wantPanic: "stop of synctest timer from outside bubble",
|
||||
wantFatal: "stop of synctest timer from outside bubble",
|
||||
}} {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
donec := make(chan struct{})
|
||||
ch := make(chan *time.Timer)
|
||||
go func() {
|
||||
defer close(donec)
|
||||
defer wantPanic(t, test.wantPanic)
|
||||
test.f(<-ch)
|
||||
}()
|
||||
synctest.Run(func() {
|
||||
tm := time.NewTimer(1 * time.Second)
|
||||
ch <- tm
|
||||
wantFatal(t, test.wantFatal, func() {
|
||||
donec := make(chan struct{})
|
||||
ch := make(chan *time.Timer)
|
||||
go func() {
|
||||
defer close(donec)
|
||||
test.f(<-ch)
|
||||
}()
|
||||
synctest.Run(func() {
|
||||
tm := time.NewTimer(1 * time.Second)
|
||||
ch <- tm
|
||||
})
|
||||
<-donec
|
||||
})
|
||||
<-donec
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -776,6 +779,28 @@ func TestWaitGroupHeapAllocated(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// Issue #75134: Many racing bubble associations.
|
||||
func TestWaitGroupManyBubbles(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
for range 100 {
|
||||
wg.Go(func() {
|
||||
synctest.Run(func() {
|
||||
cancelc := make(chan struct{})
|
||||
var wg2 sync.WaitGroup
|
||||
for range 100 {
|
||||
wg2.Go(func() {
|
||||
<-cancelc
|
||||
})
|
||||
}
|
||||
synctest.Wait()
|
||||
close(cancelc)
|
||||
wg2.Wait()
|
||||
})
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestHappensBefore(t *testing.T) {
|
||||
// Use two parallel goroutines accessing different vars to ensure that
|
||||
// we correctly account for multiple goroutines in the bubble.
|
||||
|
||||
@@ -7,6 +7,7 @@ package http
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"internal/godebug"
|
||||
"log"
|
||||
"net"
|
||||
"net/http/internal/ascii"
|
||||
@@ -16,6 +17,8 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var httpcookiemaxnum = godebug.New("httpcookiemaxnum")
|
||||
|
||||
// A Cookie represents an HTTP cookie as sent in the Set-Cookie header of an
|
||||
// HTTP response or the Cookie header of an HTTP request.
|
||||
//
|
||||
@@ -58,16 +61,37 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
errBlankCookie = errors.New("http: blank cookie")
|
||||
errEqualNotFoundInCookie = errors.New("http: '=' not found in cookie")
|
||||
errInvalidCookieName = errors.New("http: invalid cookie name")
|
||||
errInvalidCookieValue = errors.New("http: invalid cookie value")
|
||||
errBlankCookie = errors.New("http: blank cookie")
|
||||
errEqualNotFoundInCookie = errors.New("http: '=' not found in cookie")
|
||||
errInvalidCookieName = errors.New("http: invalid cookie name")
|
||||
errInvalidCookieValue = errors.New("http: invalid cookie value")
|
||||
errCookieNumLimitExceeded = errors.New("http: number of cookies exceeded limit")
|
||||
)
|
||||
|
||||
const defaultCookieMaxNum = 3000
|
||||
|
||||
func cookieNumWithinMax(cookieNum int) bool {
|
||||
withinDefaultMax := cookieNum <= defaultCookieMaxNum
|
||||
if httpcookiemaxnum.Value() == "" {
|
||||
return withinDefaultMax
|
||||
}
|
||||
if customMax, err := strconv.Atoi(httpcookiemaxnum.Value()); err == nil {
|
||||
withinCustomMax := customMax == 0 || cookieNum <= customMax
|
||||
if withinDefaultMax != withinCustomMax {
|
||||
httpcookiemaxnum.IncNonDefault()
|
||||
}
|
||||
return withinCustomMax
|
||||
}
|
||||
return withinDefaultMax
|
||||
}
|
||||
|
||||
// ParseCookie parses a Cookie header value and returns all the cookies
|
||||
// which were set in it. Since the same cookie name can appear multiple times
|
||||
// the returned Values can contain more than one value for a given key.
|
||||
func ParseCookie(line string) ([]*Cookie, error) {
|
||||
if !cookieNumWithinMax(strings.Count(line, ";") + 1) {
|
||||
return nil, errCookieNumLimitExceeded
|
||||
}
|
||||
parts := strings.Split(textproto.TrimString(line), ";")
|
||||
if len(parts) == 1 && parts[0] == "" {
|
||||
return nil, errBlankCookie
|
||||
@@ -197,11 +221,21 @@ func ParseSetCookie(line string) (*Cookie, error) {
|
||||
|
||||
// readSetCookies parses all "Set-Cookie" values from
|
||||
// the header h and returns the successfully parsed Cookies.
|
||||
//
|
||||
// If the amount of cookies exceeds CookieNumLimit, and httpcookielimitnum
|
||||
// GODEBUG option is not explicitly turned off, this function will silently
|
||||
// fail and return an empty slice.
|
||||
func readSetCookies(h Header) []*Cookie {
|
||||
cookieCount := len(h["Set-Cookie"])
|
||||
if cookieCount == 0 {
|
||||
return []*Cookie{}
|
||||
}
|
||||
// Cookie limit was unfortunately introduced at a later point in time.
|
||||
// As such, we can only fail by returning an empty slice rather than
|
||||
// explicit error.
|
||||
if !cookieNumWithinMax(cookieCount) {
|
||||
return []*Cookie{}
|
||||
}
|
||||
cookies := make([]*Cookie, 0, cookieCount)
|
||||
for _, line := range h["Set-Cookie"] {
|
||||
if cookie, err := ParseSetCookie(line); err == nil {
|
||||
@@ -329,13 +363,28 @@ func (c *Cookie) Valid() error {
|
||||
// readCookies parses all "Cookie" values from the header h and
|
||||
// returns the successfully parsed Cookies.
|
||||
//
|
||||
// if filter isn't empty, only cookies of that name are returned.
|
||||
// If filter isn't empty, only cookies of that name are returned.
|
||||
//
|
||||
// If the amount of cookies exceeds CookieNumLimit, and httpcookielimitnum
|
||||
// GODEBUG option is not explicitly turned off, this function will silently
|
||||
// fail and return an empty slice.
|
||||
func readCookies(h Header, filter string) []*Cookie {
|
||||
lines := h["Cookie"]
|
||||
if len(lines) == 0 {
|
||||
return []*Cookie{}
|
||||
}
|
||||
|
||||
// Cookie limit was unfortunately introduced at a later point in time.
|
||||
// As such, we can only fail by returning an empty slice rather than
|
||||
// explicit error.
|
||||
cookieCount := 0
|
||||
for _, line := range lines {
|
||||
cookieCount += strings.Count(line, ";") + 1
|
||||
}
|
||||
if !cookieNumWithinMax(cookieCount) {
|
||||
return []*Cookie{}
|
||||
}
|
||||
|
||||
cookies := make([]*Cookie, 0, len(lines)+strings.Count(lines[0], ";"))
|
||||
for _, line := range lines {
|
||||
line = textproto.TrimString(line)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -255,16 +256,17 @@ func TestAddCookie(t *testing.T) {
|
||||
}
|
||||
|
||||
var readSetCookiesTests = []struct {
|
||||
Header Header
|
||||
Cookies []*Cookie
|
||||
header Header
|
||||
cookies []*Cookie
|
||||
godebug string
|
||||
}{
|
||||
{
|
||||
Header{"Set-Cookie": {"Cookie-1=v$1"}},
|
||||
[]*Cookie{{Name: "Cookie-1", Value: "v$1", Raw: "Cookie-1=v$1"}},
|
||||
header: Header{"Set-Cookie": {"Cookie-1=v$1"}},
|
||||
cookies: []*Cookie{{Name: "Cookie-1", Value: "v$1", Raw: "Cookie-1=v$1"}},
|
||||
},
|
||||
{
|
||||
Header{"Set-Cookie": {"NID=99=YsDT5i3E-CXax-; expires=Wed, 23-Nov-2011 01:05:03 GMT; path=/; domain=.google.ch; HttpOnly"}},
|
||||
[]*Cookie{{
|
||||
header: Header{"Set-Cookie": {"NID=99=YsDT5i3E-CXax-; expires=Wed, 23-Nov-2011 01:05:03 GMT; path=/; domain=.google.ch; HttpOnly"}},
|
||||
cookies: []*Cookie{{
|
||||
Name: "NID",
|
||||
Value: "99=YsDT5i3E-CXax-",
|
||||
Path: "/",
|
||||
@@ -276,8 +278,8 @@ var readSetCookiesTests = []struct {
|
||||
}},
|
||||
},
|
||||
{
|
||||
Header{"Set-Cookie": {".ASPXAUTH=7E3AA; expires=Wed, 07-Mar-2012 14:25:06 GMT; path=/; HttpOnly"}},
|
||||
[]*Cookie{{
|
||||
header: Header{"Set-Cookie": {".ASPXAUTH=7E3AA; expires=Wed, 07-Mar-2012 14:25:06 GMT; path=/; HttpOnly"}},
|
||||
cookies: []*Cookie{{
|
||||
Name: ".ASPXAUTH",
|
||||
Value: "7E3AA",
|
||||
Path: "/",
|
||||
@@ -288,8 +290,8 @@ var readSetCookiesTests = []struct {
|
||||
}},
|
||||
},
|
||||
{
|
||||
Header{"Set-Cookie": {"ASP.NET_SessionId=foo; path=/; HttpOnly"}},
|
||||
[]*Cookie{{
|
||||
header: Header{"Set-Cookie": {"ASP.NET_SessionId=foo; path=/; HttpOnly"}},
|
||||
cookies: []*Cookie{{
|
||||
Name: "ASP.NET_SessionId",
|
||||
Value: "foo",
|
||||
Path: "/",
|
||||
@@ -298,8 +300,8 @@ var readSetCookiesTests = []struct {
|
||||
}},
|
||||
},
|
||||
{
|
||||
Header{"Set-Cookie": {"samesitedefault=foo; SameSite"}},
|
||||
[]*Cookie{{
|
||||
header: Header{"Set-Cookie": {"samesitedefault=foo; SameSite"}},
|
||||
cookies: []*Cookie{{
|
||||
Name: "samesitedefault",
|
||||
Value: "foo",
|
||||
SameSite: SameSiteDefaultMode,
|
||||
@@ -307,8 +309,8 @@ var readSetCookiesTests = []struct {
|
||||
}},
|
||||
},
|
||||
{
|
||||
Header{"Set-Cookie": {"samesiteinvalidisdefault=foo; SameSite=invalid"}},
|
||||
[]*Cookie{{
|
||||
header: Header{"Set-Cookie": {"samesiteinvalidisdefault=foo; SameSite=invalid"}},
|
||||
cookies: []*Cookie{{
|
||||
Name: "samesiteinvalidisdefault",
|
||||
Value: "foo",
|
||||
SameSite: SameSiteDefaultMode,
|
||||
@@ -316,8 +318,8 @@ var readSetCookiesTests = []struct {
|
||||
}},
|
||||
},
|
||||
{
|
||||
Header{"Set-Cookie": {"samesitelax=foo; SameSite=Lax"}},
|
||||
[]*Cookie{{
|
||||
header: Header{"Set-Cookie": {"samesitelax=foo; SameSite=Lax"}},
|
||||
cookies: []*Cookie{{
|
||||
Name: "samesitelax",
|
||||
Value: "foo",
|
||||
SameSite: SameSiteLaxMode,
|
||||
@@ -325,8 +327,8 @@ var readSetCookiesTests = []struct {
|
||||
}},
|
||||
},
|
||||
{
|
||||
Header{"Set-Cookie": {"samesitestrict=foo; SameSite=Strict"}},
|
||||
[]*Cookie{{
|
||||
header: Header{"Set-Cookie": {"samesitestrict=foo; SameSite=Strict"}},
|
||||
cookies: []*Cookie{{
|
||||
Name: "samesitestrict",
|
||||
Value: "foo",
|
||||
SameSite: SameSiteStrictMode,
|
||||
@@ -334,8 +336,8 @@ var readSetCookiesTests = []struct {
|
||||
}},
|
||||
},
|
||||
{
|
||||
Header{"Set-Cookie": {"samesitenone=foo; SameSite=None"}},
|
||||
[]*Cookie{{
|
||||
header: Header{"Set-Cookie": {"samesitenone=foo; SameSite=None"}},
|
||||
cookies: []*Cookie{{
|
||||
Name: "samesitenone",
|
||||
Value: "foo",
|
||||
SameSite: SameSiteNoneMode,
|
||||
@@ -345,47 +347,66 @@ var readSetCookiesTests = []struct {
|
||||
// Make sure we can properly read back the Set-Cookie headers we create
|
||||
// for values containing spaces or commas:
|
||||
{
|
||||
Header{"Set-Cookie": {`special-1=a z`}},
|
||||
[]*Cookie{{Name: "special-1", Value: "a z", Raw: `special-1=a z`}},
|
||||
header: Header{"Set-Cookie": {`special-1=a z`}},
|
||||
cookies: []*Cookie{{Name: "special-1", Value: "a z", Raw: `special-1=a z`}},
|
||||
},
|
||||
{
|
||||
Header{"Set-Cookie": {`special-2=" z"`}},
|
||||
[]*Cookie{{Name: "special-2", Value: " z", Quoted: true, Raw: `special-2=" z"`}},
|
||||
header: Header{"Set-Cookie": {`special-2=" z"`}},
|
||||
cookies: []*Cookie{{Name: "special-2", Value: " z", Quoted: true, Raw: `special-2=" z"`}},
|
||||
},
|
||||
{
|
||||
Header{"Set-Cookie": {`special-3="a "`}},
|
||||
[]*Cookie{{Name: "special-3", Value: "a ", Quoted: true, Raw: `special-3="a "`}},
|
||||
header: Header{"Set-Cookie": {`special-3="a "`}},
|
||||
cookies: []*Cookie{{Name: "special-3", Value: "a ", Quoted: true, Raw: `special-3="a "`}},
|
||||
},
|
||||
{
|
||||
Header{"Set-Cookie": {`special-4=" "`}},
|
||||
[]*Cookie{{Name: "special-4", Value: " ", Quoted: true, Raw: `special-4=" "`}},
|
||||
header: Header{"Set-Cookie": {`special-4=" "`}},
|
||||
cookies: []*Cookie{{Name: "special-4", Value: " ", Quoted: true, Raw: `special-4=" "`}},
|
||||
},
|
||||
{
|
||||
Header{"Set-Cookie": {`special-5=a,z`}},
|
||||
[]*Cookie{{Name: "special-5", Value: "a,z", Raw: `special-5=a,z`}},
|
||||
header: Header{"Set-Cookie": {`special-5=a,z`}},
|
||||
cookies: []*Cookie{{Name: "special-5", Value: "a,z", Raw: `special-5=a,z`}},
|
||||
},
|
||||
{
|
||||
Header{"Set-Cookie": {`special-6=",z"`}},
|
||||
[]*Cookie{{Name: "special-6", Value: ",z", Quoted: true, Raw: `special-6=",z"`}},
|
||||
header: Header{"Set-Cookie": {`special-6=",z"`}},
|
||||
cookies: []*Cookie{{Name: "special-6", Value: ",z", Quoted: true, Raw: `special-6=",z"`}},
|
||||
},
|
||||
{
|
||||
Header{"Set-Cookie": {`special-7=a,`}},
|
||||
[]*Cookie{{Name: "special-7", Value: "a,", Raw: `special-7=a,`}},
|
||||
header: Header{"Set-Cookie": {`special-7=a,`}},
|
||||
cookies: []*Cookie{{Name: "special-7", Value: "a,", Raw: `special-7=a,`}},
|
||||
},
|
||||
{
|
||||
Header{"Set-Cookie": {`special-8=","`}},
|
||||
[]*Cookie{{Name: "special-8", Value: ",", Quoted: true, Raw: `special-8=","`}},
|
||||
header: Header{"Set-Cookie": {`special-8=","`}},
|
||||
cookies: []*Cookie{{Name: "special-8", Value: ",", Quoted: true, Raw: `special-8=","`}},
|
||||
},
|
||||
// Make sure we can properly read back the Set-Cookie headers
|
||||
// for names containing spaces:
|
||||
{
|
||||
Header{"Set-Cookie": {`special-9 =","`}},
|
||||
[]*Cookie{{Name: "special-9", Value: ",", Quoted: true, Raw: `special-9 =","`}},
|
||||
header: Header{"Set-Cookie": {`special-9 =","`}},
|
||||
cookies: []*Cookie{{Name: "special-9", Value: ",", Quoted: true, Raw: `special-9 =","`}},
|
||||
},
|
||||
// Quoted values (issue #46443)
|
||||
{
|
||||
Header{"Set-Cookie": {`cookie="quoted"`}},
|
||||
[]*Cookie{{Name: "cookie", Value: "quoted", Quoted: true, Raw: `cookie="quoted"`}},
|
||||
header: Header{"Set-Cookie": {`cookie="quoted"`}},
|
||||
cookies: []*Cookie{{Name: "cookie", Value: "quoted", Quoted: true, Raw: `cookie="quoted"`}},
|
||||
},
|
||||
{
|
||||
header: Header{"Set-Cookie": slices.Repeat([]string{"a="}, defaultCookieMaxNum+1)},
|
||||
cookies: []*Cookie{},
|
||||
},
|
||||
{
|
||||
header: Header{"Set-Cookie": slices.Repeat([]string{"a="}, 10)},
|
||||
cookies: []*Cookie{},
|
||||
godebug: "httpcookiemaxnum=5",
|
||||
},
|
||||
{
|
||||
header: Header{"Set-Cookie": strings.Split(strings.Repeat(";a=", defaultCookieMaxNum+1)[1:], ";")},
|
||||
cookies: slices.Repeat([]*Cookie{{Name: "a", Value: "", Quoted: false, Raw: "a="}}, defaultCookieMaxNum+1),
|
||||
godebug: "httpcookiemaxnum=0",
|
||||
},
|
||||
{
|
||||
header: Header{"Set-Cookie": strings.Split(strings.Repeat(";a=", defaultCookieMaxNum+1)[1:], ";")},
|
||||
cookies: slices.Repeat([]*Cookie{{Name: "a", Value: "", Quoted: false, Raw: "a="}}, defaultCookieMaxNum+1),
|
||||
godebug: fmt.Sprintf("httpcookiemaxnum=%v", defaultCookieMaxNum+1),
|
||||
},
|
||||
|
||||
// TODO(bradfitz): users have reported seeing this in the
|
||||
@@ -405,79 +426,103 @@ func toJSON(v any) string {
|
||||
|
||||
func TestReadSetCookies(t *testing.T) {
|
||||
for i, tt := range readSetCookiesTests {
|
||||
t.Setenv("GODEBUG", tt.godebug)
|
||||
for n := 0; n < 2; n++ { // to verify readSetCookies doesn't mutate its input
|
||||
c := readSetCookies(tt.Header)
|
||||
if !reflect.DeepEqual(c, tt.Cookies) {
|
||||
t.Errorf("#%d readSetCookies: have\n%s\nwant\n%s\n", i, toJSON(c), toJSON(tt.Cookies))
|
||||
c := readSetCookies(tt.header)
|
||||
if !reflect.DeepEqual(c, tt.cookies) {
|
||||
t.Errorf("#%d readSetCookies: have\n%s\nwant\n%s\n", i, toJSON(c), toJSON(tt.cookies))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var readCookiesTests = []struct {
|
||||
Header Header
|
||||
Filter string
|
||||
Cookies []*Cookie
|
||||
header Header
|
||||
filter string
|
||||
cookies []*Cookie
|
||||
godebug string
|
||||
}{
|
||||
{
|
||||
Header{"Cookie": {"Cookie-1=v$1", "c2=v2"}},
|
||||
"",
|
||||
[]*Cookie{
|
||||
header: Header{"Cookie": {"Cookie-1=v$1", "c2=v2"}},
|
||||
filter: "",
|
||||
cookies: []*Cookie{
|
||||
{Name: "Cookie-1", Value: "v$1"},
|
||||
{Name: "c2", Value: "v2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Header{"Cookie": {"Cookie-1=v$1", "c2=v2"}},
|
||||
"c2",
|
||||
[]*Cookie{
|
||||
header: Header{"Cookie": {"Cookie-1=v$1", "c2=v2"}},
|
||||
filter: "c2",
|
||||
cookies: []*Cookie{
|
||||
{Name: "c2", Value: "v2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Header{"Cookie": {"Cookie-1=v$1; c2=v2"}},
|
||||
"",
|
||||
[]*Cookie{
|
||||
header: Header{"Cookie": {"Cookie-1=v$1; c2=v2"}},
|
||||
filter: "",
|
||||
cookies: []*Cookie{
|
||||
{Name: "Cookie-1", Value: "v$1"},
|
||||
{Name: "c2", Value: "v2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Header{"Cookie": {"Cookie-1=v$1; c2=v2"}},
|
||||
"c2",
|
||||
[]*Cookie{
|
||||
header: Header{"Cookie": {"Cookie-1=v$1; c2=v2"}},
|
||||
filter: "c2",
|
||||
cookies: []*Cookie{
|
||||
{Name: "c2", Value: "v2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Header{"Cookie": {`Cookie-1="v$1"; c2="v2"`}},
|
||||
"",
|
||||
[]*Cookie{
|
||||
header: Header{"Cookie": {`Cookie-1="v$1"; c2="v2"`}},
|
||||
filter: "",
|
||||
cookies: []*Cookie{
|
||||
{Name: "Cookie-1", Value: "v$1", Quoted: true},
|
||||
{Name: "c2", Value: "v2", Quoted: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
Header{"Cookie": {`Cookie-1="v$1"; c2=v2;`}},
|
||||
"",
|
||||
[]*Cookie{
|
||||
header: Header{"Cookie": {`Cookie-1="v$1"; c2=v2;`}},
|
||||
filter: "",
|
||||
cookies: []*Cookie{
|
||||
{Name: "Cookie-1", Value: "v$1", Quoted: true},
|
||||
{Name: "c2", Value: "v2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Header{"Cookie": {``}},
|
||||
"",
|
||||
[]*Cookie{},
|
||||
header: Header{"Cookie": {``}},
|
||||
filter: "",
|
||||
cookies: []*Cookie{},
|
||||
},
|
||||
// GODEBUG=httpcookiemaxnum should work regardless if all cookies are sent
|
||||
// via one "Cookie" field, or multiple fields.
|
||||
{
|
||||
header: Header{"Cookie": {strings.Repeat(";a=", defaultCookieMaxNum+1)[1:]}},
|
||||
cookies: []*Cookie{},
|
||||
},
|
||||
{
|
||||
header: Header{"Cookie": slices.Repeat([]string{"a="}, 10)},
|
||||
cookies: []*Cookie{},
|
||||
godebug: "httpcookiemaxnum=5",
|
||||
},
|
||||
{
|
||||
header: Header{"Cookie": {strings.Repeat(";a=", defaultCookieMaxNum+1)[1:]}},
|
||||
cookies: slices.Repeat([]*Cookie{{Name: "a", Value: "", Quoted: false}}, defaultCookieMaxNum+1),
|
||||
godebug: "httpcookiemaxnum=0",
|
||||
},
|
||||
{
|
||||
header: Header{"Cookie": slices.Repeat([]string{"a="}, defaultCookieMaxNum+1)},
|
||||
cookies: slices.Repeat([]*Cookie{{Name: "a", Value: "", Quoted: false}}, defaultCookieMaxNum+1),
|
||||
godebug: fmt.Sprintf("httpcookiemaxnum=%v", defaultCookieMaxNum+1),
|
||||
},
|
||||
}
|
||||
|
||||
func TestReadCookies(t *testing.T) {
|
||||
for i, tt := range readCookiesTests {
|
||||
t.Setenv("GODEBUG", tt.godebug)
|
||||
for n := 0; n < 2; n++ { // to verify readCookies doesn't mutate its input
|
||||
c := readCookies(tt.Header, tt.Filter)
|
||||
if !reflect.DeepEqual(c, tt.Cookies) {
|
||||
t.Errorf("#%d readCookies:\nhave: %s\nwant: %s\n", i, toJSON(c), toJSON(tt.Cookies))
|
||||
c := readCookies(tt.header, tt.filter)
|
||||
if !reflect.DeepEqual(c, tt.cookies) {
|
||||
t.Errorf("#%d readCookies:\nhave: %s\nwant: %s\n", i, toJSON(c), toJSON(tt.cookies))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -689,6 +734,7 @@ func TestParseCookie(t *testing.T) {
|
||||
line string
|
||||
cookies []*Cookie
|
||||
err error
|
||||
godebug string
|
||||
}{
|
||||
{
|
||||
line: "Cookie-1=v$1",
|
||||
@@ -722,8 +768,28 @@ func TestParseCookie(t *testing.T) {
|
||||
line: "k1=\\",
|
||||
err: errInvalidCookieValue,
|
||||
},
|
||||
{
|
||||
line: strings.Repeat(";a=", defaultCookieMaxNum+1)[1:],
|
||||
err: errCookieNumLimitExceeded,
|
||||
},
|
||||
{
|
||||
line: strings.Repeat(";a=", 10)[1:],
|
||||
err: errCookieNumLimitExceeded,
|
||||
godebug: "httpcookiemaxnum=5",
|
||||
},
|
||||
{
|
||||
line: strings.Repeat(";a=", defaultCookieMaxNum+1)[1:],
|
||||
cookies: slices.Repeat([]*Cookie{{Name: "a", Value: "", Quoted: false}}, defaultCookieMaxNum+1),
|
||||
godebug: "httpcookiemaxnum=0",
|
||||
},
|
||||
{
|
||||
line: strings.Repeat(";a=", defaultCookieMaxNum+1)[1:],
|
||||
cookies: slices.Repeat([]*Cookie{{Name: "a", Value: "", Quoted: false}}, defaultCookieMaxNum+1),
|
||||
godebug: fmt.Sprintf("httpcookiemaxnum=%v", defaultCookieMaxNum+1),
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
t.Setenv("GODEBUG", tt.godebug)
|
||||
gotCookies, gotErr := ParseCookie(tt.line)
|
||||
if !errors.Is(gotErr, tt.err) {
|
||||
t.Errorf("#%d ParseCookie got error %v, want error %v", i, gotErr, tt.err)
|
||||
|
||||
@@ -77,13 +77,21 @@ func (c *CrossOriginProtection) AddTrustedOrigin(origin string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var noopHandler = HandlerFunc(func(w ResponseWriter, r *Request) {})
|
||||
type noopHandler struct{}
|
||||
|
||||
func (noopHandler) ServeHTTP(ResponseWriter, *Request) {}
|
||||
|
||||
var sentinelHandler Handler = &noopHandler{}
|
||||
|
||||
// AddInsecureBypassPattern permits all requests that match the given pattern.
|
||||
// The pattern syntax and precedence rules are the same as [ServeMux].
|
||||
//
|
||||
// AddInsecureBypassPattern can be called concurrently with other methods
|
||||
// or request handling, and applies to future requests.
|
||||
// The pattern syntax and precedence rules are the same as [ServeMux]. Only
|
||||
// requests that match the pattern directly are permitted. Those that ServeMux
|
||||
// would redirect to a pattern (e.g. after cleaning the path or adding a
|
||||
// trailing slash) are not.
|
||||
//
|
||||
// AddInsecureBypassPattern can be called concurrently with other methods or
|
||||
// request handling, and applies to future requests.
|
||||
func (c *CrossOriginProtection) AddInsecureBypassPattern(pattern string) {
|
||||
var bypass *ServeMux
|
||||
|
||||
@@ -99,7 +107,7 @@ func (c *CrossOriginProtection) AddInsecureBypassPattern(pattern string) {
|
||||
}
|
||||
}
|
||||
|
||||
bypass.Handle(pattern, noopHandler)
|
||||
bypass.Handle(pattern, sentinelHandler)
|
||||
}
|
||||
|
||||
// SetDenyHandler sets a handler to invoke when a request is rejected.
|
||||
@@ -172,7 +180,7 @@ var (
|
||||
// be deferred until the last moment.
|
||||
func (c *CrossOriginProtection) isRequestExempt(req *Request) bool {
|
||||
if bypass := c.bypass.Load(); bypass != nil {
|
||||
if _, pattern := bypass.Handler(req); pattern != "" {
|
||||
if h, _ := bypass.Handler(req); h == sentinelHandler {
|
||||
// The request matches a bypass pattern.
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -113,6 +113,11 @@ func TestCrossOriginProtectionPatternBypass(t *testing.T) {
|
||||
protection := http.NewCrossOriginProtection()
|
||||
protection.AddInsecureBypassPattern("/bypass/")
|
||||
protection.AddInsecureBypassPattern("/only/{foo}")
|
||||
protection.AddInsecureBypassPattern("/no-trailing")
|
||||
protection.AddInsecureBypassPattern("/yes-trailing/")
|
||||
protection.AddInsecureBypassPattern("PUT /put-only/")
|
||||
protection.AddInsecureBypassPattern("GET /get-only/")
|
||||
protection.AddInsecureBypassPattern("POST /post-only/")
|
||||
handler := protection.Handler(okHandler)
|
||||
|
||||
tests := []struct {
|
||||
@@ -126,13 +131,23 @@ func TestCrossOriginProtectionPatternBypass(t *testing.T) {
|
||||
{"non-bypass path without sec-fetch-site", "/api/", "", http.StatusForbidden},
|
||||
{"non-bypass path with cross-site", "/api/", "cross-site", http.StatusForbidden},
|
||||
|
||||
{"redirect to bypass path without ..", "/foo/../bypass/bar", "", http.StatusOK},
|
||||
{"redirect to bypass path with trailing slash", "/bypass", "", http.StatusOK},
|
||||
{"redirect to bypass path without ..", "/foo/../bypass/bar", "", http.StatusForbidden},
|
||||
{"redirect to bypass path with trailing slash", "/bypass", "", http.StatusForbidden},
|
||||
{"redirect to non-bypass path with ..", "/foo/../api/bar", "", http.StatusForbidden},
|
||||
{"redirect to non-bypass path with trailing slash", "/api", "", http.StatusForbidden},
|
||||
|
||||
{"wildcard bypass", "/only/123", "", http.StatusOK},
|
||||
{"non-wildcard", "/only/123/foo", "", http.StatusForbidden},
|
||||
|
||||
// https://go.dev/issue/75054
|
||||
{"no trailing slash exact match", "/no-trailing", "", http.StatusOK},
|
||||
{"no trailing slash with slash", "/no-trailing/", "", http.StatusForbidden},
|
||||
{"yes trailing slash exact match", "/yes-trailing/", "", http.StatusOK},
|
||||
{"yes trailing slash without slash", "/yes-trailing", "", http.StatusForbidden},
|
||||
|
||||
{"method-specific hit", "/post-only/", "", http.StatusOK},
|
||||
{"method-specific miss (PUT)", "/put-only/", "", http.StatusForbidden},
|
||||
{"method-specific miss (GET)", "/get-only/", "", http.StatusForbidden},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
|
||||
@@ -1372,7 +1372,10 @@ func (w *wantConn) cancel(t *Transport) {
|
||||
w.done = true
|
||||
w.mu.Unlock()
|
||||
|
||||
if pc != nil {
|
||||
// HTTP/2 connections (pc.alt != nil) aren't removed from the idle pool on use,
|
||||
// and should not be added back here. If the pconn isn't in the idle pool,
|
||||
// it's because we removed it due to an error.
|
||||
if pc != nil && pc.alt == nil {
|
||||
t.putOrCloseIdleConn(pc)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7559,3 +7559,35 @@ func TestTransportServerProtocols(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssue61474(t *testing.T) {
|
||||
run(t, testIssue61474, []testMode{http2Mode})
|
||||
}
|
||||
func testIssue61474(t *testing.T, mode testMode) {
|
||||
if testing.Short() {
|
||||
return
|
||||
}
|
||||
|
||||
// This test reliably exercises the condition causing #61474,
|
||||
// but requires many iterations to do so.
|
||||
// Keep the test around for now, but don't run it by default.
|
||||
t.Skip("test is too large")
|
||||
|
||||
cst := newClientServerTest(t, mode, HandlerFunc(func(rw ResponseWriter, req *Request) {
|
||||
}), func(tr *Transport) {
|
||||
tr.MaxConnsPerHost = 1
|
||||
})
|
||||
var wg sync.WaitGroup
|
||||
defer wg.Wait()
|
||||
for range 100000 {
|
||||
wg.Go(func() {
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 1*time.Millisecond)
|
||||
defer cancel()
|
||||
req, _ := NewRequestWithContext(ctx, "GET", cst.ts.URL, nil)
|
||||
resp, err := cst.c.Do(req)
|
||||
if err == nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,8 +237,12 @@ func ipToSockaddr(family int, ip IP, port int, zone string) (syscall.Sockaddr, e
|
||||
func addrPortToSockaddrInet4(ap netip.AddrPort) (syscall.SockaddrInet4, error) {
|
||||
// ipToSockaddrInet4 has special handling here for zero length slices.
|
||||
// We do not, because netip has no concept of a generic zero IP address.
|
||||
//
|
||||
// addr is allowed to be an IPv4-mapped IPv6 address.
|
||||
// As4 will unmap it to an IPv4 address.
|
||||
// The error message is kept consistent with ipToSockaddrInet4.
|
||||
addr := ap.Addr()
|
||||
if !addr.Is4() {
|
||||
if !addr.Is4() && !addr.Is4In6() {
|
||||
return syscall.SockaddrInet4{}, &AddrError{Err: "non-IPv4 address", Addr: addr.String()}
|
||||
}
|
||||
sa := syscall.SockaddrInet4{
|
||||
|
||||
@@ -724,7 +724,8 @@ func (p *addrParser) consumeDomainLiteral() (string, error) {
|
||||
}
|
||||
|
||||
// Parse the dtext
|
||||
var dtext string
|
||||
dtext := p.s
|
||||
dtextLen := 0
|
||||
for {
|
||||
if p.empty() {
|
||||
return "", errors.New("mail: unclosed domain-literal")
|
||||
@@ -741,9 +742,10 @@ func (p *addrParser) consumeDomainLiteral() (string, error) {
|
||||
return "", fmt.Errorf("mail: bad character in domain-literal: %q", r)
|
||||
}
|
||||
|
||||
dtext += p.s[:size]
|
||||
dtextLen += size
|
||||
p.s = p.s[size:]
|
||||
}
|
||||
dtext = dtext[:dtextLen]
|
||||
|
||||
// Skip the trailing ]
|
||||
if !p.consume(']') {
|
||||
|
||||
@@ -284,8 +284,10 @@ func (r *Reader) ReadCodeLine(expectCode int) (code int, message string, err err
|
||||
//
|
||||
// An expectCode <= 0 disables the check of the status code.
|
||||
func (r *Reader) ReadResponse(expectCode int) (code int, message string, err error) {
|
||||
code, continued, message, err := r.readCodeLine(expectCode)
|
||||
code, continued, first, err := r.readCodeLine(expectCode)
|
||||
multi := continued
|
||||
var messageBuilder strings.Builder
|
||||
messageBuilder.WriteString(first)
|
||||
for continued {
|
||||
line, err := r.ReadLine()
|
||||
if err != nil {
|
||||
@@ -296,12 +298,15 @@ func (r *Reader) ReadResponse(expectCode int) (code int, message string, err err
|
||||
var moreMessage string
|
||||
code2, continued, moreMessage, err = parseCodeLine(line, 0)
|
||||
if err != nil || code2 != code {
|
||||
message += "\n" + strings.TrimRight(line, "\r\n")
|
||||
messageBuilder.WriteByte('\n')
|
||||
messageBuilder.WriteString(strings.TrimRight(line, "\r\n"))
|
||||
continued = true
|
||||
continue
|
||||
}
|
||||
message += "\n" + moreMessage
|
||||
messageBuilder.WriteByte('\n')
|
||||
messageBuilder.WriteString(moreMessage)
|
||||
}
|
||||
message = messageBuilder.String()
|
||||
if err != nil && multi && message != "" {
|
||||
// replace one line error message with all lines (full message)
|
||||
err = &Error{code, message}
|
||||
|
||||
@@ -705,3 +705,40 @@ func TestIPv6WriteMsgUDPAddrPortTargetAddrIPVersion(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIPv4WriteMsgUDPAddrPortTargetAddrIPVersion verifies that
|
||||
// WriteMsgUDPAddrPort accepts IPv4 and IPv4-mapped IPv6 destination addresses,
|
||||
// and rejects IPv6 destination addresses on a "udp4" connection.
|
||||
func TestIPv4WriteMsgUDPAddrPortTargetAddrIPVersion(t *testing.T) {
|
||||
switch runtime.GOOS {
|
||||
case "plan9":
|
||||
t.Skipf("not supported on %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
if !testableNetwork("udp4") {
|
||||
t.Skipf("skipping: udp4 not available")
|
||||
}
|
||||
|
||||
conn, err := ListenUDP("udp4", &UDPAddr{IP: IPv4(127, 0, 0, 1)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
daddr4 := netip.AddrPortFrom(netip.MustParseAddr("127.0.0.1"), 12345)
|
||||
daddr4in6 := netip.AddrPortFrom(netip.MustParseAddr("::ffff:127.0.0.1"), 12345)
|
||||
daddr6 := netip.AddrPortFrom(netip.MustParseAddr("::1"), 12345)
|
||||
buf := make([]byte, 8)
|
||||
|
||||
if _, _, err = conn.WriteMsgUDPAddrPort(buf, nil, daddr4); err != nil {
|
||||
t.Errorf("conn.WriteMsgUDPAddrPort(buf, nil, daddr4) failed: %v", err)
|
||||
}
|
||||
|
||||
if _, _, err = conn.WriteMsgUDPAddrPort(buf, nil, daddr4in6); err != nil {
|
||||
t.Errorf("conn.WriteMsgUDPAddrPort(buf, nil, daddr4in6) failed: %v", err)
|
||||
}
|
||||
|
||||
if _, _, err = conn.WriteMsgUDPAddrPort(buf, nil, daddr6); err == nil {
|
||||
t.Errorf("conn.WriteMsgUDPAddrPort(buf, nil, daddr6) should have failed, but got no error")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/netip"
|
||||
"path"
|
||||
"slices"
|
||||
"strconv"
|
||||
@@ -626,40 +627,61 @@ func parseAuthority(authority string) (user *Userinfo, host string, err error) {
|
||||
// parseHost parses host as an authority without user
|
||||
// information. That is, as host[:port].
|
||||
func parseHost(host string) (string, error) {
|
||||
if strings.HasPrefix(host, "[") {
|
||||
if openBracketIdx := strings.LastIndex(host, "["); openBracketIdx != -1 {
|
||||
// Parse an IP-Literal in RFC 3986 and RFC 6874.
|
||||
// E.g., "[fe80::1]", "[fe80::1%25en0]", "[fe80::1]:80".
|
||||
i := strings.LastIndex(host, "]")
|
||||
if i < 0 {
|
||||
closeBracketIdx := strings.LastIndex(host, "]")
|
||||
if closeBracketIdx < 0 {
|
||||
return "", errors.New("missing ']' in host")
|
||||
}
|
||||
colonPort := host[i+1:]
|
||||
|
||||
colonPort := host[closeBracketIdx+1:]
|
||||
if !validOptionalPort(colonPort) {
|
||||
return "", fmt.Errorf("invalid port %q after host", colonPort)
|
||||
}
|
||||
unescapedColonPort, err := unescape(colonPort, encodeHost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hostname := host[openBracketIdx+1 : closeBracketIdx]
|
||||
var unescapedHostname string
|
||||
// RFC 6874 defines that %25 (%-encoded percent) introduces
|
||||
// the zone identifier, and the zone identifier can use basically
|
||||
// any %-encoding it likes. That's different from the host, which
|
||||
// can only %-encode non-ASCII bytes.
|
||||
// We do impose some restrictions on the zone, to avoid stupidity
|
||||
// like newlines.
|
||||
zone := strings.Index(host[:i], "%25")
|
||||
if zone >= 0 {
|
||||
host1, err := unescape(host[:zone], encodeHost)
|
||||
zoneIdx := strings.Index(hostname, "%25")
|
||||
if zoneIdx >= 0 {
|
||||
hostPart, err := unescape(hostname[:zoneIdx], encodeHost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
host2, err := unescape(host[zone:i], encodeZone)
|
||||
zonePart, err := unescape(hostname[zoneIdx:], encodeZone)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
host3, err := unescape(host[i:], encodeHost)
|
||||
unescapedHostname = hostPart + zonePart
|
||||
} else {
|
||||
var err error
|
||||
unescapedHostname, err = unescape(hostname, encodeHost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return host1 + host2 + host3, nil
|
||||
}
|
||||
|
||||
// Per RFC 3986, only a host identified by a valid
|
||||
// IPv6 address can be enclosed by square brackets.
|
||||
// This excludes any IPv4 or IPv4-mapped addresses.
|
||||
addr, err := netip.ParseAddr(unescapedHostname)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid host: %w", err)
|
||||
}
|
||||
if addr.Is4() || addr.Is4In6() {
|
||||
return "", errors.New("invalid IPv6 host")
|
||||
}
|
||||
return "[" + unescapedHostname + "]" + unescapedColonPort, nil
|
||||
} else if i := strings.LastIndex(host, ":"); i != -1 {
|
||||
colonPort := host[i:]
|
||||
if !validOptionalPort(colonPort) {
|
||||
|
||||
@@ -383,6 +383,16 @@ var urltests = []URLTest{
|
||||
},
|
||||
"",
|
||||
},
|
||||
// valid IPv6 host with port and path
|
||||
{
|
||||
"https://[2001:db8::1]:8443/test/path",
|
||||
&URL{
|
||||
Scheme: "https",
|
||||
Host: "[2001:db8::1]:8443",
|
||||
Path: "/test/path",
|
||||
},
|
||||
"",
|
||||
},
|
||||
// host subcomponent; IPv6 address with zone identifier in RFC 6874
|
||||
{
|
||||
"http://[fe80::1%25en0]/", // alphanum zone identifier
|
||||
@@ -707,6 +717,24 @@ var parseRequestURLTests = []struct {
|
||||
// RFC 6874.
|
||||
{"http://[fe80::1%en0]/", false},
|
||||
{"http://[fe80::1%en0]:8080/", false},
|
||||
|
||||
// Tests exercising RFC 3986 compliance
|
||||
{"https://[1:2:3:4:5:6:7:8]", true}, // full IPv6 address
|
||||
{"https://[2001:db8::a:b:c:d]", true}, // compressed IPv6 address
|
||||
{"https://[fe80::1%25eth0]", true}, // link-local address with zone ID (interface name)
|
||||
{"https://[fe80::abc:def%254]", true}, // link-local address with zone ID (interface index)
|
||||
{"https://[2001:db8::1]/path", true}, // compressed IPv6 address with path
|
||||
{"https://[fe80::1%25eth0]/path?query=1", true}, // link-local with zone, path, and query
|
||||
|
||||
{"https://[::ffff:192.0.2.1]", false},
|
||||
{"https://[:1] ", false},
|
||||
{"https://[1:2:3:4:5:6:7:8:9]", false},
|
||||
{"https://[1::1::1]", false},
|
||||
{"https://[1:2:3:]", false},
|
||||
{"https://[ffff::127.0.0.4000]", false},
|
||||
{"https://[0:0::test.com]:80", false},
|
||||
{"https://[2001:db8::test.com]", false},
|
||||
{"https://[test.com]", false},
|
||||
}
|
||||
|
||||
func TestParseRequestURI(t *testing.T) {
|
||||
@@ -1643,6 +1671,17 @@ func TestParseErrors(t *testing.T) {
|
||||
{"cache_object:foo", true},
|
||||
{"cache_object:foo/bar", true},
|
||||
{"cache_object/:foo/bar", false},
|
||||
|
||||
{"http://[192.168.0.1]/", true}, // IPv4 in brackets
|
||||
{"http://[192.168.0.1]:8080/", true}, // IPv4 in brackets with port
|
||||
{"http://[::ffff:192.168.0.1]/", true}, // IPv4-mapped IPv6 in brackets
|
||||
{"http://[::ffff:192.168.0.1]:8080/", true}, // IPv4-mapped IPv6 in brackets with port
|
||||
{"http://[::ffff:c0a8:1]/", true}, // IPv4-mapped IPv6 in brackets (hex)
|
||||
{"http://[not-an-ip]/", true}, // invalid IP string in brackets
|
||||
{"http://[fe80::1%foo]/", true}, // invalid zone format in brackets
|
||||
{"http://[fe80::1", true}, // missing closing bracket
|
||||
{"http://fe80::1]/", true}, // missing opening bracket
|
||||
{"http://[test.com]/", true}, // domain name in brackets
|
||||
}
|
||||
for _, tt := range tests {
|
||||
u, err := Parse(tt.in)
|
||||
|
||||
@@ -177,4 +177,48 @@ func TestLookPath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
checker := func(test string) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
t.Helper()
|
||||
t.Logf("PATH=%s", os.Getenv("PATH"))
|
||||
p, err := LookPath(test)
|
||||
if err == nil {
|
||||
t.Errorf("%q: error expected, got nil", test)
|
||||
}
|
||||
if p != "" {
|
||||
t.Errorf("%q: path returned should be \"\". Got %q", test, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reference behavior for the next test
|
||||
t.Run(pathVar+"=$OTHER2", func(t *testing.T) {
|
||||
t.Run("empty", checker(""))
|
||||
t.Run("dot", checker("."))
|
||||
t.Run("dotdot1", checker("abc/.."))
|
||||
t.Run("dotdot2", checker(".."))
|
||||
})
|
||||
|
||||
// Test the behavior when PATH contains an executable file which is not a directory
|
||||
t.Run(pathVar+"=exe", func(t *testing.T) {
|
||||
// Inject an executable file (not a directory) in PATH.
|
||||
// Use our own binary os.Args[0].
|
||||
t.Setenv(pathVar, testenv.Executable(t))
|
||||
t.Run("empty", checker(""))
|
||||
t.Run("dot", checker("."))
|
||||
t.Run("dotdot1", checker("abc/.."))
|
||||
t.Run("dotdot2", checker(".."))
|
||||
})
|
||||
|
||||
// Test the behavior when PATH contains an executable file which is not a directory
|
||||
t.Run(pathVar+"=exe/xx", func(t *testing.T) {
|
||||
// Inject an executable file (not a directory) in PATH.
|
||||
// Use our own binary os.Args[0].
|
||||
t.Setenv(pathVar, filepath.Join(testenv.Executable(t), "xx"))
|
||||
t.Run("empty", checker(""))
|
||||
t.Run("dot", checker("."))
|
||||
t.Run("dotdot1", checker("abc/.."))
|
||||
t.Run("dotdot2", checker(".."))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1328,3 +1328,13 @@ func addCriticalEnv(env []string) []string {
|
||||
// Code should use errors.Is(err, ErrDot), not err == ErrDot,
|
||||
// to test whether a returned error err is due to this condition.
|
||||
var ErrDot = errors.New("cannot run executable found relative to current directory")
|
||||
|
||||
// validateLookPath excludes paths that can't be valid
|
||||
// executable names. See issue #74466 and CVE-2025-47906.
|
||||
func validateLookPath(s string) error {
|
||||
switch s {
|
||||
case "", ".", "..":
|
||||
return ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -36,6 +36,10 @@ func findExecutable(file string) error {
|
||||
// As of Go 1.19, LookPath will instead return that path along with an error satisfying
|
||||
// [errors.Is](err, [ErrDot]). See the package documentation for more details.
|
||||
func LookPath(file string) (string, error) {
|
||||
if err := validateLookPath(filepath.Clean(file)); err != nil {
|
||||
return "", &Error{file, err}
|
||||
}
|
||||
|
||||
// skip the path lookup for these prefixes
|
||||
skip := []string{"/", "#", "./", "../"}
|
||||
|
||||
|
||||
@@ -54,6 +54,10 @@ func LookPath(file string) (string, error) {
|
||||
// (only bypass the path if file begins with / or ./ or ../)
|
||||
// but that would not match all the Unix shells.
|
||||
|
||||
if err := validateLookPath(file); err != nil {
|
||||
return "", &Error{file, err}
|
||||
}
|
||||
|
||||
if strings.Contains(file, "/") {
|
||||
err := findExecutable(file)
|
||||
if err == nil {
|
||||
|
||||
@@ -67,6 +67,10 @@ func findExecutable(file string, exts []string) (string, error) {
|
||||
// As of Go 1.19, LookPath will instead return that path along with an error satisfying
|
||||
// [errors.Is](err, [ErrDot]). See the package documentation for more details.
|
||||
func LookPath(file string) (string, error) {
|
||||
if err := validateLookPath(file); err != nil {
|
||||
return "", &Error{file, err}
|
||||
}
|
||||
|
||||
return lookPath(file, pathExt())
|
||||
}
|
||||
|
||||
@@ -80,6 +84,10 @@ func LookPath(file string) (string, error) {
|
||||
// "C:\foo\example.com" would be returned as-is even if the
|
||||
// program is actually "C:\foo\example.com.exe".
|
||||
func lookExtensions(path, dir string) (string, error) {
|
||||
if err := validateLookPath(path); err != nil {
|
||||
return "", &Error{path, err}
|
||||
}
|
||||
|
||||
if filepath.Base(path) == path {
|
||||
path = "." + string(filepath.Separator) + path
|
||||
}
|
||||
|
||||
@@ -1845,6 +1845,72 @@ func TestFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileOverlappedSeek(t *testing.T) {
|
||||
t.Parallel()
|
||||
name := filepath.Join(t.TempDir(), "foo")
|
||||
f := newFileOverlapped(t, name, true)
|
||||
content := []byte("foo")
|
||||
if _, err := f.Write(content); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Check that the file pointer is at the expected offset.
|
||||
n, err := f.Seek(0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n != int64(len(content)) {
|
||||
t.Errorf("expected file pointer to be at offset %d, got %d", len(content), n)
|
||||
}
|
||||
// Set the file pointer to the start of the file.
|
||||
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Read the first byte.
|
||||
var buf [1]byte
|
||||
if _, err := f.Read(buf[:]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(buf[:], content[:len(buf)]) {
|
||||
t.Errorf("expected %q, got %q", content[:len(buf)], buf[:])
|
||||
}
|
||||
// Check that the file pointer is at the expected offset.
|
||||
n, err = f.Seek(0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n != int64(len(buf)) {
|
||||
t.Errorf("expected file pointer to be at offset %d, got %d", len(buf), n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileOverlappedReadAtVolume(t *testing.T) {
|
||||
// Test that we can use File.ReadAt with an overlapped volume handle.
|
||||
// See https://go.dev/issues/74951.
|
||||
t.Parallel()
|
||||
name := `\\.\` + filepath.VolumeName(t.TempDir())
|
||||
namep, err := syscall.UTF16PtrFromString(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h, err := syscall.CreateFile(namep,
|
||||
syscall.GENERIC_READ|syscall.GENERIC_WRITE,
|
||||
syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_READ,
|
||||
nil, syscall.OPEN_ALWAYS, syscall.FILE_FLAG_OVERLAPPED, 0)
|
||||
if err != nil {
|
||||
if errors.Is(err, syscall.ERROR_ACCESS_DENIED) {
|
||||
t.Skip("skipping test: access denied")
|
||||
}
|
||||
t.Fatal(err)
|
||||
}
|
||||
f := os.NewFile(uintptr(h), name)
|
||||
defer f.Close()
|
||||
|
||||
var buf [0]byte
|
||||
if _, err := f.ReadAt(buf[:], 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipe(t *testing.T) {
|
||||
t.Parallel()
|
||||
r, w, err := os.Pipe()
|
||||
|
||||
@@ -131,7 +131,9 @@ func rootMkdirAll(r *Root, fullname string, perm FileMode) error {
|
||||
if try > 0 || !IsNotExist(err) {
|
||||
return 0, &PathError{Op: "openat", Err: err}
|
||||
}
|
||||
if err := mkdirat(parent, name, perm); err != nil {
|
||||
// Try again on EEXIST, because the directory may have been created
|
||||
// by another process or thread between the rootOpenDir and mkdirat calls.
|
||||
if err := mkdirat(parent, name, perm); err != nil && err != syscall.EEXIST {
|
||||
return 0, &PathError{Op: "mkdirat", Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1919,3 +1919,36 @@ func TestRootWriteReadFile(t *testing.T) {
|
||||
t.Fatalf("root.ReadFile(%q) = %q, %v; want %q, nil", name, got, err, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootName(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
root, err := os.OpenRoot(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer root.Close()
|
||||
if got, want := root.Name(), dir; got != want {
|
||||
t.Errorf("root.Name() = %q, want %q", got, want)
|
||||
}
|
||||
|
||||
f, err := root.Create("file")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
if got, want := f.Name(), filepath.Join(dir, "file"); got != want {
|
||||
t.Errorf(`root.Create("file").Name() = %q, want %q`, got, want)
|
||||
}
|
||||
|
||||
if err := root.Mkdir("dir", 0o777); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
subroot, err := root.OpenRoot("dir")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer subroot.Close()
|
||||
if got, want := subroot.Name(), filepath.Join(dir, "dir"); got != want {
|
||||
t.Errorf(`root.OpenRoot("dir").Name() = %q, want %q`, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ func openRootInRoot(r *Root, name string) (*Root, error) {
|
||||
if err != nil {
|
||||
return nil, &PathError{Op: "openat", Path: name, Err: err}
|
||||
}
|
||||
return newRoot(fd, name)
|
||||
return newRoot(fd, joinPath(r.Name(), name))
|
||||
}
|
||||
|
||||
// rootOpenFileNolog is Root.OpenFile.
|
||||
|
||||
@@ -123,7 +123,7 @@ func openRootInRoot(r *Root, name string) (*Root, error) {
|
||||
if err != nil {
|
||||
return nil, &PathError{Op: "openat", Path: name, Err: err}
|
||||
}
|
||||
return newRoot(fd, name)
|
||||
return newRoot(fd, joinPath(r.Name(), name))
|
||||
}
|
||||
|
||||
// rootOpenFileNolog is Root.OpenFile.
|
||||
|
||||
@@ -7,6 +7,7 @@ package user
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"internal/syscall/windows"
|
||||
@@ -16,11 +17,92 @@ import (
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// addUserAccount creates a local user account.
|
||||
// It returns the name and password of the new account.
|
||||
// Multiple programs or goroutines calling addUserAccount simultaneously will not choose the same directory.
|
||||
func addUserAccount(t *testing.T) (name, password string) {
|
||||
t.TempDir()
|
||||
pattern := t.Name()
|
||||
// Windows limits the user name to 20 characters,
|
||||
// leave space for a 4 digits random suffix.
|
||||
const maxNameLen, suffixLen = 20, 4
|
||||
pattern = pattern[:min(len(pattern), maxNameLen-suffixLen)]
|
||||
// Drop unusual characters from the account name.
|
||||
mapper := func(r rune) rune {
|
||||
if r < utf8.RuneSelf {
|
||||
if '0' <= r && r <= '9' ||
|
||||
'a' <= r && r <= 'z' ||
|
||||
'A' <= r && r <= 'Z' {
|
||||
return r
|
||||
}
|
||||
} else if unicode.IsLetter(r) || unicode.IsNumber(r) {
|
||||
return r
|
||||
}
|
||||
return -1
|
||||
}
|
||||
pattern = strings.Map(mapper, pattern)
|
||||
|
||||
// Generate a long random password.
|
||||
var pwd [33]byte
|
||||
rand.Read(pwd[:])
|
||||
// Add special chars to ensure it satisfies password requirements.
|
||||
password = base64.StdEncoding.EncodeToString(pwd[:]) + "_-As@!%*(1)4#2"
|
||||
password16, err := syscall.UTF16PtrFromString(password)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
try := 0
|
||||
for {
|
||||
// Calculate a random suffix to append to the user name.
|
||||
var suffix [2]byte
|
||||
rand.Read(suffix[:])
|
||||
suffixStr := strconv.FormatUint(uint64(binary.LittleEndian.Uint16(suffix[:])), 10)
|
||||
name := pattern + suffixStr[:min(len(suffixStr), suffixLen)]
|
||||
name16, err := syscall.UTF16PtrFromString(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Create user.
|
||||
userInfo := windows.UserInfo1{
|
||||
Name: name16,
|
||||
Password: password16,
|
||||
Priv: windows.USER_PRIV_USER,
|
||||
}
|
||||
err = windows.NetUserAdd(nil, 1, (*byte)(unsafe.Pointer(&userInfo)), nil)
|
||||
if errors.Is(err, syscall.ERROR_ACCESS_DENIED) {
|
||||
t.Skip("skipping test; don't have permission to create user")
|
||||
}
|
||||
// If the user already exists, try again with a different name.
|
||||
if errors.Is(err, windows.NERR_UserExists) {
|
||||
if try++; try < 1000 {
|
||||
t.Log("user already exists, trying again with a different name")
|
||||
continue
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("NetUserAdd failed: %v", err)
|
||||
}
|
||||
// Delete the user when the test is done.
|
||||
t.Cleanup(func() {
|
||||
if err := windows.NetUserDel(nil, name16); err != nil {
|
||||
if !errors.Is(err, windows.NERR_UserNotFound) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
return name, password
|
||||
}
|
||||
}
|
||||
|
||||
// windowsTestAccount creates a test user and returns a token for that user.
|
||||
// If the user already exists, it will be deleted and recreated.
|
||||
// The caller is responsible for closing the token.
|
||||
@@ -32,47 +114,15 @@ func windowsTestAccount(t *testing.T) (syscall.Token, *User) {
|
||||
// See https://dev.go/issue/70396.
|
||||
t.Skip("skipping non-hermetic test outside of Go builders")
|
||||
}
|
||||
const testUserName = "GoStdTestUser01"
|
||||
var password [33]byte
|
||||
rand.Read(password[:])
|
||||
// Add special chars to ensure it satisfies password requirements.
|
||||
pwd := base64.StdEncoding.EncodeToString(password[:]) + "_-As@!%*(1)4#2"
|
||||
name, err := syscall.UTF16PtrFromString(testUserName)
|
||||
name, password := addUserAccount(t)
|
||||
name16, err := syscall.UTF16PtrFromString(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pwd16, err := syscall.UTF16PtrFromString(pwd)
|
||||
pwd16, err := syscall.UTF16PtrFromString(password)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
userInfo := windows.UserInfo1{
|
||||
Name: name,
|
||||
Password: pwd16,
|
||||
Priv: windows.USER_PRIV_USER,
|
||||
}
|
||||
// Create user.
|
||||
err = windows.NetUserAdd(nil, 1, (*byte)(unsafe.Pointer(&userInfo)), nil)
|
||||
if errors.Is(err, syscall.ERROR_ACCESS_DENIED) {
|
||||
t.Skip("skipping test; don't have permission to create user")
|
||||
}
|
||||
if errors.Is(err, windows.NERR_UserExists) {
|
||||
// User already exists, delete and recreate.
|
||||
if err = windows.NetUserDel(nil, name); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = windows.NetUserAdd(nil, 1, (*byte)(unsafe.Pointer(&userInfo)), nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err = windows.NetUserDel(nil, name); err != nil {
|
||||
if !errors.Is(err, windows.NERR_UserNotFound) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
domain, err := syscall.UTF16PtrFromString(".")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -80,13 +130,13 @@ func windowsTestAccount(t *testing.T) (syscall.Token, *User) {
|
||||
const LOGON32_PROVIDER_DEFAULT = 0
|
||||
const LOGON32_LOGON_INTERACTIVE = 2
|
||||
var token syscall.Token
|
||||
if err = windows.LogonUser(name, domain, pwd16, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &token); err != nil {
|
||||
if err = windows.LogonUser(name16, domain, pwd16, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &token); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
token.Close()
|
||||
})
|
||||
usr, err := Lookup(testUserName)
|
||||
usr, err := Lookup(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
|
||||
}
|
||||
|
||||
if c.bubble != nil && getg().bubble != c.bubble {
|
||||
panic(plainError("send on synctest channel from outside bubble"))
|
||||
fatal("send on synctest channel from outside bubble")
|
||||
}
|
||||
|
||||
// Fast path: check for failed non-blocking operation without acquiring the lock.
|
||||
@@ -318,7 +318,7 @@ func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
|
||||
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
|
||||
if c.bubble != nil && getg().bubble != c.bubble {
|
||||
unlockf()
|
||||
panic(plainError("send on synctest channel from outside bubble"))
|
||||
fatal("send on synctest channel from outside bubble")
|
||||
}
|
||||
if raceenabled {
|
||||
if c.dataqsiz == 0 {
|
||||
@@ -416,7 +416,7 @@ func closechan(c *hchan) {
|
||||
panic(plainError("close of nil channel"))
|
||||
}
|
||||
if c.bubble != nil && getg().bubble != c.bubble {
|
||||
panic(plainError("close of synctest channel from outside bubble"))
|
||||
fatal("close of synctest channel from outside bubble")
|
||||
}
|
||||
|
||||
lock(&c.lock)
|
||||
@@ -538,7 +538,7 @@ func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool)
|
||||
}
|
||||
|
||||
if c.bubble != nil && getg().bubble != c.bubble {
|
||||
panic(plainError("receive on synctest channel from outside bubble"))
|
||||
fatal("receive on synctest channel from outside bubble")
|
||||
}
|
||||
|
||||
if c.timer != nil {
|
||||
@@ -702,7 +702,7 @@ func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool)
|
||||
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
|
||||
if c.bubble != nil && getg().bubble != c.bubble {
|
||||
unlockf()
|
||||
panic(plainError("receive on synctest channel from outside bubble"))
|
||||
fatal("receive on synctest channel from outside bubble")
|
||||
}
|
||||
if c.dataqsiz == 0 {
|
||||
if raceenabled {
|
||||
|
||||
72
src/runtime/decoratemappings_test.go
Normal file
72
src/runtime/decoratemappings_test.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright 2025 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 runtime_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func validateMapLabels(t *testing.T, labels []string) {
|
||||
// These are the specific region labels that need get added during the
|
||||
// runtime phase. Hence they are the ones that need to be confirmed as
|
||||
// present at the time the test reads its own region labels, which
|
||||
// is sufficient to validate that the default `decoratemappings` value
|
||||
// (enabled) was set early enough in the init process.
|
||||
regions := map[string]bool{
|
||||
"allspans array": false,
|
||||
"gc bits": false,
|
||||
"heap": false,
|
||||
"heap index": false,
|
||||
"heap reservation": false,
|
||||
"immortal metadata": false,
|
||||
"page alloc": false,
|
||||
"page alloc index": false,
|
||||
"page summary": false,
|
||||
"scavenge index": false,
|
||||
}
|
||||
for _, label := range labels {
|
||||
if _, ok := regions[label]; !ok {
|
||||
t.Logf("unexpected region label found: \"%s\"", label)
|
||||
}
|
||||
regions[label] = true
|
||||
}
|
||||
for label, found := range regions {
|
||||
if !found {
|
||||
t.Logf("region label missing: \"%s\"", label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecorateMappings(t *testing.T) {
|
||||
if runtime.GOOS != "linux" {
|
||||
t.Skip("decoratemappings is only supported on Linux")
|
||||
// /proc/self/maps is also Linux-specific
|
||||
}
|
||||
|
||||
var labels []string
|
||||
if rawMaps, err := os.ReadFile("/proc/self/maps"); err != nil {
|
||||
t.Fatalf("failed to read /proc/self/maps: %v", err)
|
||||
} else {
|
||||
t.Logf("maps:%s\n", string(rawMaps))
|
||||
matches := regexp.MustCompile("[^[]+ \\[anon: Go: (.+)\\]\n").FindAllSubmatch(rawMaps, -1)
|
||||
for _, match_pair := range matches {
|
||||
// match_pair consists of the matching substring and the parenthesized group
|
||||
labels = append(labels, string(match_pair[1]))
|
||||
}
|
||||
}
|
||||
t.Logf("DebugDecorateMappings: %v", *runtime.DebugDecorateMappings)
|
||||
if *runtime.DebugDecorateMappings != 0 && runtime.SetVMANameSupported() {
|
||||
validateMapLabels(t, labels)
|
||||
} else {
|
||||
if len(labels) > 0 {
|
||||
t.Errorf("unexpected mapping labels present: %v", labels)
|
||||
} else {
|
||||
t.Skip("mapping labels absent as expected")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1927,3 +1927,7 @@ func (t *TraceStackTable) Reset() {
|
||||
func TraceStack(gp *G, tab *TraceStackTable) {
|
||||
traceStack(0, gp, (*traceStackTable)(tab))
|
||||
}
|
||||
|
||||
var DebugDecorateMappings = &debug.decoratemappings
|
||||
|
||||
func SetVMANameSupported() bool { return setVMANameSupported() }
|
||||
|
||||
@@ -282,6 +282,11 @@ Below is the full list of supported metrics, ordered lexicographically.
|
||||
The number of non-default behaviors executed by the net/http
|
||||
package due to a non-default GODEBUG=http2server=... setting.
|
||||
|
||||
/godebug/non-default-behavior/httpcookiemaxnum:events
|
||||
The number of non-default behaviors executed by the net/http
|
||||
package due to a non-default GODEBUG=httpcookiemaxnum=...
|
||||
setting.
|
||||
|
||||
/godebug/non-default-behavior/httplaxcontentlength:events
|
||||
The number of non-default behaviors executed by the net/http
|
||||
package due to a non-default GODEBUG=httplaxcontentlength=...
|
||||
|
||||
@@ -789,7 +789,9 @@ func cpuinit(env string) {
|
||||
// getGodebugEarly extracts the environment variable GODEBUG from the environment on
|
||||
// Unix-like operating systems and returns it. This function exists to extract GODEBUG
|
||||
// early before much of the runtime is initialized.
|
||||
func getGodebugEarly() string {
|
||||
//
|
||||
// Returns nil, false if OS doesn't provide env vars early in the init sequence.
|
||||
func getGodebugEarly() (string, bool) {
|
||||
const prefix = "GODEBUG="
|
||||
var env string
|
||||
switch GOOS {
|
||||
@@ -807,12 +809,16 @@ func getGodebugEarly() string {
|
||||
s := unsafe.String(p, findnull(p))
|
||||
|
||||
if stringslite.HasPrefix(s, prefix) {
|
||||
env = gostring(p)[len(prefix):]
|
||||
env = gostringnocopy(p)[len(prefix):]
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
return env
|
||||
return env, true
|
||||
}
|
||||
|
||||
// The bootstrap sequence is:
|
||||
@@ -859,11 +865,14 @@ func schedinit() {
|
||||
// The world starts stopped.
|
||||
worldStopped()
|
||||
|
||||
godebug, parsedGodebug := getGodebugEarly()
|
||||
if parsedGodebug {
|
||||
parseRuntimeDebugVars(godebug)
|
||||
}
|
||||
ticks.init() // run as early as possible
|
||||
moduledataverify()
|
||||
stackinit()
|
||||
mallocinit()
|
||||
godebug := getGodebugEarly()
|
||||
cpuinit(godebug) // must run before alginit
|
||||
randinit() // must run before alginit, mcommoninit
|
||||
alginit() // maps, hash, rand must not be used before this call
|
||||
@@ -880,7 +889,12 @@ func schedinit() {
|
||||
goenvs()
|
||||
secure()
|
||||
checkfds()
|
||||
parsedebugvars()
|
||||
if !parsedGodebug {
|
||||
// Some platforms, e.g., Windows, didn't make env vars available "early",
|
||||
// so try again now.
|
||||
parseRuntimeDebugVars(gogetenv("GODEBUG"))
|
||||
}
|
||||
finishDebugVarsSetup()
|
||||
gcinit()
|
||||
|
||||
// Allocate stack space that can be used when crashing due to bad stack
|
||||
|
||||
@@ -402,7 +402,7 @@ var dbgvars = []*dbgVar{
|
||||
{name: "updatemaxprocs", value: &debug.updatemaxprocs, def: 1},
|
||||
}
|
||||
|
||||
func parsedebugvars() {
|
||||
func parseRuntimeDebugVars(godebug string) {
|
||||
// defaults
|
||||
debug.cgocheck = 1
|
||||
debug.invalidptr = 1
|
||||
@@ -420,12 +420,6 @@ func parsedebugvars() {
|
||||
}
|
||||
debug.traceadvanceperiod = defaultTraceAdvancePeriod
|
||||
|
||||
godebug := gogetenv("GODEBUG")
|
||||
|
||||
p := new(string)
|
||||
*p = godebug
|
||||
godebugEnv.Store(p)
|
||||
|
||||
// apply runtime defaults, if any
|
||||
for _, v := range dbgvars {
|
||||
if v.def != 0 {
|
||||
@@ -437,7 +431,6 @@ func parsedebugvars() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// apply compile-time GODEBUG settings
|
||||
parsegodebug(godebugDefault, nil)
|
||||
|
||||
@@ -463,6 +456,12 @@ func parsedebugvars() {
|
||||
if debug.gccheckmark > 0 {
|
||||
debug.asyncpreemptoff = 1
|
||||
}
|
||||
}
|
||||
|
||||
func finishDebugVarsSetup() {
|
||||
p := new(string)
|
||||
*p = gogetenv("GODEBUG")
|
||||
godebugEnv.Store(p)
|
||||
|
||||
setTraceback(gogetenv("GOTRACEBACK"))
|
||||
traceback_env = traceback_cache
|
||||
|
||||
@@ -178,7 +178,7 @@ func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, blo
|
||||
|
||||
if cas.c.bubble != nil {
|
||||
if getg().bubble != cas.c.bubble {
|
||||
panic(plainError("select on synctest channel from outside bubble"))
|
||||
fatal("select on synctest channel from outside bubble")
|
||||
}
|
||||
} else {
|
||||
allSynctest = false
|
||||
|
||||
@@ -14,9 +14,13 @@ import (
|
||||
|
||||
var prSetVMAUnsupported atomic.Bool
|
||||
|
||||
func setVMANameSupported() bool {
|
||||
return !prSetVMAUnsupported.Load()
|
||||
}
|
||||
|
||||
// setVMAName calls prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, start, len, name)
|
||||
func setVMAName(start unsafe.Pointer, length uintptr, name string) {
|
||||
if debug.decoratemappings == 0 || prSetVMAUnsupported.Load() {
|
||||
if debug.decoratemappings == 0 || !setVMANameSupported() {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -10,3 +10,5 @@ import "unsafe"
|
||||
|
||||
// setVMAName isn’t implemented
|
||||
func setVMAName(start unsafe.Pointer, len uintptr, name string) {}
|
||||
|
||||
func setVMANameSupported() bool { return false }
|
||||
|
||||
@@ -410,7 +410,9 @@ func getOrSetBubbleSpecial(p unsafe.Pointer, bubbleid uint64, add bool) (assoc i
|
||||
} else if add {
|
||||
// p is not associated with a bubble,
|
||||
// and we've been asked to add an association.
|
||||
lock(&mheap_.speciallock)
|
||||
s := (*specialBubble)(mheap_.specialBubbleAlloc.alloc())
|
||||
unlock(&mheap_.speciallock)
|
||||
s.bubbleid = bubbleid
|
||||
s.special.kind = _KindSpecialBubble
|
||||
s.special.offset = offset
|
||||
|
||||
@@ -415,7 +415,7 @@ func newTimer(when, period int64, f func(arg any, seq uintptr, delay int64), arg
|
||||
//go:linkname stopTimer time.stopTimer
|
||||
func stopTimer(t *timeTimer) bool {
|
||||
if t.isFake && getg().bubble == nil {
|
||||
panic("stop of synctest timer from outside bubble")
|
||||
fatal("stop of synctest timer from outside bubble")
|
||||
}
|
||||
return t.stop()
|
||||
}
|
||||
@@ -430,7 +430,7 @@ func resetTimer(t *timeTimer, when, period int64) bool {
|
||||
racerelease(unsafe.Pointer(&t.timer))
|
||||
}
|
||||
if t.isFake && getg().bubble == nil {
|
||||
panic("reset of synctest timer from outside bubble")
|
||||
fatal("reset of synctest timer from outside bubble")
|
||||
}
|
||||
return t.reset(when, period)
|
||||
}
|
||||
|
||||
@@ -232,7 +232,7 @@ func (x *Uintptr) Add(delta uintptr) (new uintptr) { return AddUintptr(&x.v, del
|
||||
func (x *Uintptr) And(mask uintptr) (old uintptr) { return AndUintptr(&x.v, mask) }
|
||||
|
||||
// Or atomically performs a bitwise OR operation on x using the bitmask
|
||||
// provided as mask and returns the updated value after the OR operation.
|
||||
// provided as mask and returns the old value.
|
||||
func (x *Uintptr) Or(mask uintptr) (old uintptr) { return OrUintptr(&x.v, mask) }
|
||||
|
||||
// noCopy may be added to structs which must not be copied
|
||||
|
||||
77
test/fixedbugs/issue75569.go
Normal file
77
test/fixedbugs/issue75569.go
Normal file
@@ -0,0 +1,77 @@
|
||||
// run
|
||||
|
||||
// Copyright 2025 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 main
|
||||
|
||||
func fff(a []int, b bool, p, q *int) {
|
||||
outer:
|
||||
n := a[0]
|
||||
a = a[1:]
|
||||
switch n {
|
||||
case 1:
|
||||
goto one
|
||||
case 2:
|
||||
goto two
|
||||
case 3:
|
||||
goto three
|
||||
case 4:
|
||||
goto four
|
||||
}
|
||||
|
||||
one:
|
||||
goto inner
|
||||
two:
|
||||
goto outer
|
||||
three:
|
||||
goto inner
|
||||
four:
|
||||
goto innerSideEntry
|
||||
|
||||
inner:
|
||||
n = a[0]
|
||||
a = a[1:]
|
||||
switch n {
|
||||
case 1:
|
||||
goto outer
|
||||
case 2:
|
||||
goto inner
|
||||
case 3:
|
||||
goto innerSideEntry
|
||||
default:
|
||||
return
|
||||
}
|
||||
innerSideEntry:
|
||||
n = a[0]
|
||||
a = a[1:]
|
||||
switch n {
|
||||
case 1:
|
||||
goto outer
|
||||
case 2:
|
||||
goto inner
|
||||
case 3:
|
||||
goto inner
|
||||
}
|
||||
ggg(p, q)
|
||||
goto inner
|
||||
}
|
||||
|
||||
var b bool
|
||||
|
||||
func ggg(p, q *int) {
|
||||
n := *p + 5 // this +5 ends up in the entry block, well before the *p load
|
||||
if b {
|
||||
*q = 0
|
||||
}
|
||||
*p = n
|
||||
}
|
||||
|
||||
func main() {
|
||||
var x, y int
|
||||
fff([]int{4, 4, 4}, false, &x, &y)
|
||||
if x != 5 {
|
||||
panic(x)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user