diff --git a/vendor.mod b/vendor.mod index 9d3166571041..abbd3eb340c2 100644 --- a/vendor.mod +++ b/vendor.mod @@ -21,7 +21,7 @@ require ( github.com/docker/cli-docs-tool v0.11.0 github.com/docker/distribution v2.8.3+incompatible github.com/docker/docker-credential-helpers v0.9.8 - github.com/docker/go-connections v0.7.0 + github.com/docker/go-connections v0.8.0 github.com/docker/go-units v0.5.0 github.com/fvbommel/sortorder v1.1.0 github.com/go-jose/go-jose/v4 v4.1.4 diff --git a/vendor.sum b/vendor.sum index 4579e9350875..71730507f80b 100644 --- a/vendor.sum +++ b/vendor.sum @@ -42,8 +42,8 @@ github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBi github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker-credential-helpers v0.9.8 h1:bIREROb7So6PRlq6KTtdS9MPEjC29OQRkFNlvK2OX8Q= github.com/docker/docker-credential-helpers v0.9.8/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= -github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= -github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +github.com/docker/go-connections v0.8.0 h1:T9UlP76qPLA/HaLrcC+s4Doqqv5XsWMMUGPF5Aih/k0= +github.com/docker/go-connections v0.8.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-events v0.0.0-20260608200158-dbf6103125a4 h1:Bj+mzWc7MJqqD0UzTaPmwszW3ttOVjSFi84ZU5l+2I0= github.com/docker/go-events v0.0.0-20260608200158-dbf6103125a4/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= diff --git a/vendor/github.com/docker/go-connections/sockets/inmem_socket.go b/vendor/github.com/docker/go-connections/sockets/inmem_socket.go index 06fcf747ac4b..d35b105f37c6 100644 --- a/vendor/github.com/docker/go-connections/sockets/inmem_socket.go +++ b/vendor/github.com/docker/go-connections/sockets/inmem_socket.go @@ -1,21 +1,19 @@ package sockets import ( + "context" "net" "sync" ) -// dummyAddr is used to satisfy net.Addr for the in-mem socket -// it is just stored as a string and returns the string for all calls -type dummyAddr string +// inmemAddr is used to satisfy net.Addr for the in-memory socket. +type inmemAddr string // Network returns the addr string, satisfies net.Addr -func (a dummyAddr) Network() string { - return string(a) -} +func (a inmemAddr) Network() string { return "inmem" } // String returns the string form -func (a dummyAddr) String() string { +func (a inmemAddr) String() string { return string(a) } @@ -23,7 +21,7 @@ func (a dummyAddr) String() string { type InmemSocket struct { chConn chan net.Conn chClose chan struct{} - addr dummyAddr + addr inmemAddr mu sync.Mutex } @@ -34,7 +32,7 @@ func NewInmemSocket(addr string, bufSize int) *InmemSocket { return &InmemSocket{ chConn: make(chan net.Conn, bufSize), chClose: make(chan struct{}), - addr: dummyAddr(addr), + addr: inmemAddr(addr), } } @@ -67,15 +65,41 @@ func (s *InmemSocket) Close() error { return nil } -// Dial is used to establish a connection with the in-mem server. -// It returns a [net.ErrClosed] if the connection is already closed. +// Dial establishes a connection with the in-memory listener. +// +// The network and addr parameters are accepted for compatibility with +// conventional dialer APIs but are currently ignored. +// +// It is equivalent to calling DialContext with context.Background(). +// It returns [net.ErrClosed] if the listener has already been closed. func (s *InmemSocket) Dial(network, addr string) (net.Conn, error) { + return s.DialContext(context.Background(), network, addr) +} + +// DialContext establishes a connection with the in-memory listener. +// +// The network and addr parameters are accepted for compatibility with +// conventional dialer APIs but are currently ignored. +// +// If ctx is canceled before the connection is established, DialContext +// returns the context error. It returns [net.ErrClosed] if the listener +// has already been closed. +func (s *InmemSocket) DialContext(ctx context.Context, network, addr string) (net.Conn, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + srvConn, clientConn := net.Pipe() select { case s.chConn <- srvConn: + return clientConn, nil + case <-ctx.Done(): + _ = srvConn.Close() + _ = clientConn.Close() + return nil, ctx.Err() case <-s.chClose: + _ = srvConn.Close() + _ = clientConn.Close() return nil, net.ErrClosed } - - return clientConn, nil } diff --git a/vendor/github.com/docker/go-connections/sockets/unix_socket.go b/vendor/github.com/docker/go-connections/sockets/unix_socket.go index e736f71d38b1..1f7b7cd95eed 100644 --- a/vendor/github.com/docker/go-connections/sockets/unix_socket.go +++ b/vendor/github.com/docker/go-connections/sockets/unix_socket.go @@ -47,38 +47,69 @@ For example: package sockets import ( + "errors" + "fmt" "net" "os" + "runtime" "syscall" ) +const supportsAbstractSockets = runtime.GOOS == "linux" + // SockOption sets up socket file's creating option type SockOption func(string) error -// NewUnixSocketWithOpts creates a unix socket with the specified options. -// By default, socket permissions are 0000 (i.e.: no access for anyone); pass -// WithChmod() and WithChown() to set the desired ownership and permissions. +// NewUnixSocketWithOpts creates a Unix socket with the specified options. +// +// On Unix platforms, socket permissions are 0000 by default, i.e. no access +// for anyone. Pass WithChmod() and WithChown() to set the desired permissions +// and ownership. +// +// On Windows, the socket uses Windows ACLs. Pass WithBasePermissions() to allow +// Administrators and LocalSystem full access, or WithAdditionalUsersAndGroups() +// to also grant generic read and write access to additional users or groups. // -// This function temporarily changes the system's "umask" to 0777 to work around -// a race condition between creating the socket and setting its permissions. While -// this should only be for a short duration, it may affect other processes that -// create files/directories during that period. +// Abstract Unix sockets (Go's Linux-specific "@" shorthand and the native +// leading-NUL representation) are supported only on Linux. On other platforms, +// attempts to use abstract socket addresses return an error. Because abstract +// sockets have no filesystem representation, filesystem-specific socket +// options are not supported. +// +// On platforms without abstract Unix socket support, attempts to use abstract +// socket addresses return an error wrapping [errors.ErrUnsupported]. func NewUnixSocketWithOpts(path string, opts ...SockOption) (net.Listener, error) { - if err := syscall.Unlink(path); err != nil && !os.IsNotExist(err) { - return nil, err + if isAbstractSocket(path) { + if !supportsAbstractSockets { + return nil, fmt.Errorf("abstract Unix socket %q is not supported on %s: %w", path, runtime.GOOS, errors.ErrUnsupported) + } + for _, opt := range opts { + if err := opt(path); err != nil { + return nil, err + } + } + return net.Listen("unix", path) } - - l, err := listenUnix(path) - if err != nil { + if err := syscall.Unlink(path); err != nil && !os.IsNotExist(err) { return nil, err } - for _, op := range opts { - if err := op(path); err != nil { - _ = l.Close() - return nil, err - } - } + return listenUnix(path, opts...) +} - return l, nil +// isAbstractSocket reports whether path is an abstract Unix socket address. +// +// Go recognizes two representations of abstract socket addresses: +// +// - On Linux, a path beginning with '@' is translated by the standard library +// to the kernel's native leading-NUL representation. +// See https://pkg.go.dev/net@go1.27rc2#UnixAddr. +// +// - A path beginning with a NUL byte uses the kernel's native representation +// directly. See https://github.com/golang/go/issues/78615. +// +// The interpretation of these addresses is platform-dependent; this helper only +// recognizes the syntax. +func isAbstractSocket(path string) bool { + return len(path) > 0 && (path[0] == '@' || path[0] == 0) } diff --git a/vendor/github.com/docker/go-connections/sockets/unix_socket_linux.go b/vendor/github.com/docker/go-connections/sockets/unix_socket_linux.go new file mode 100644 index 000000000000..59860610c3a1 --- /dev/null +++ b/vendor/github.com/docker/go-connections/sockets/unix_socket_linux.go @@ -0,0 +1,25 @@ +package sockets + +import ( + "os" + "strconv" + "strings" + "syscall" +) + +// maxListenerBacklog returns the maximum length of the queue of pending +// connections for a listening socket. +// +// It is similar to in stdlib, but without the fallbacks for Kernel < 4.1.0; +// https://github.com/golang/go/blob/go1.26.3/src/net/sock_linux.go#L33-L53 +func maxListenerBacklog() int { + b, err := os.ReadFile("/proc/sys/net/core/somaxconn") + if err != nil { + return syscall.SOMAXCONN + } + n, err := strconv.Atoi(strings.TrimSpace(string(b))) + if err != nil || n <= 0 { + return syscall.SOMAXCONN + } + return n +} diff --git a/vendor/github.com/docker/go-connections/sockets/unix_socket_nolinux.go b/vendor/github.com/docker/go-connections/sockets/unix_socket_nolinux.go new file mode 100644 index 000000000000..e269c7d75b45 --- /dev/null +++ b/vendor/github.com/docker/go-connections/sockets/unix_socket_nolinux.go @@ -0,0 +1,39 @@ +//go:build !linux && !windows + +package sockets + +import ( + "runtime" + "syscall" +) + +// maxListenerBacklog is similar to the equivalent in stdlib; +// https://github.com/golang/go/blob/go1.26.3/src/net/sock_bsd.go#L14-L39 +func maxListenerBacklog() int { + var ( + n uint32 + err error + ) + switch runtime.GOOS { + case "darwin", "ios": + n, err = syscall.SysctlUint32("kern.ipc.somaxconn") + case "freebsd": + n, err = syscall.SysctlUint32("kern.ipc.soacceptqueue") + case "netbsd": + // NOTE: NetBSD has no somaxconn-like kernel state so far + case "openbsd": + n, err = syscall.SysctlUint32("kern.somaxconn") + default: + return syscall.SOMAXCONN + } + if n == 0 || err != nil { + return syscall.SOMAXCONN + } + // FreeBSD stores the backlog in a uint16, as does Linux. + // Assume the other BSDs do too. Truncate number to avoid wrapping. + // See issue 5030. + if n > 1<<16-1 { + n = 1<<16 - 1 + } + return int(n) +} diff --git a/vendor/github.com/docker/go-connections/sockets/unix_socket_unix.go b/vendor/github.com/docker/go-connections/sockets/unix_socket_unix.go index a41a71654742..2bc1c1b44bf3 100644 --- a/vendor/github.com/docker/go-connections/sockets/unix_socket_unix.go +++ b/vendor/github.com/docker/go-connections/sockets/unix_socket_unix.go @@ -3,14 +3,35 @@ package sockets import ( + "errors" + "fmt" "net" "os" + "sync" "syscall" ) -// WithChown modifies the socket file's uid and gid +// defaultSocketPerms is the default permission mode applied to newly created +// Unix sockets. Sockets are created inaccessible by default; callers can +// override this by passing [WithChmod]. +// +// TODO(thaJeztah): Consider changing the default to 0o600, making the socket usable by its owner by default. +const defaultSocketPerms os.FileMode = 0o000 + +// WithChown modifies the socket file's uid and gid. +// +// Abstract Unix sockets have no filesystem representation, so this option +// returns an error wrapping [errors.ErrUnsupported] when used with an abstract +// socket. func WithChown(uid, gid int) SockOption { return func(path string) error { + if isAbstractSocket(path) { + return &os.PathError{ + Op: "chown", + Path: path, + Err: fmt.Errorf("abstract Unix sockets do not support filesystem permissions: %w", errors.ErrUnsupported), + } + } if err := os.Chown(path, uid, gid); err != nil { return err } @@ -19,8 +40,19 @@ func WithChown(uid, gid int) SockOption { } // WithChmod modifies socket file's access mode. +// +// Abstract Unix sockets have no filesystem representation, so this option +// returns an error wrapping [errors.ErrUnsupported] when used with an abstract +// socket. func WithChmod(mask os.FileMode) SockOption { return func(path string) error { + if isAbstractSocket(path) { + return &os.PathError{ + Op: "chmod", + Path: path, + Err: fmt.Errorf("abstract Unix sockets do not support filesystem permissions: %w", errors.ErrUnsupported), + } + } if err := os.Chmod(path, mask); err != nil { return err } @@ -28,27 +60,90 @@ func WithChmod(mask os.FileMode) SockOption { } } -// NewUnixSocket creates a unix socket with the specified path and group. +// NewUnixSocket creates a Unix socket with the specified path and group. +// +// On Unix platforms, the socket is owned by root:gid and has permissions 0660. +// +// Abstract Unix sockets are not supported by this helper. Use [NewUnixSocketWithOpts] +// without filesystem permission options instead. func NewUnixSocket(path string, gid int) (net.Listener, error) { return NewUnixSocketWithOpts(path, WithChown(0, gid), WithChmod(0o660)) } -func listenUnix(path string) (net.Listener, error) { - // net.Listen does not allow for permissions to be set. As a result, when - // specifying custom permissions ("WithChmod()"), there is a short time - // between creating the socket and applying the permissions, during which - // the socket permissions are Less restrictive than desired. +func listenUnix(path string, opts ...SockOption) (_ net.Listener, retErr error) { + // net.Listen does not allow permissions or ownership to be set between + // bind(2), which creates the socket path, and listen(2), which makes it + // possible for clients to connect. // - // To work around this limitation of net.Listen(), we temporarily set the - // umask to 0777, which forces the socket to be created with 000 permissions - // (i.e.: no access for anyone). After that, WithChmod() must be used to set - // the desired permissions. + // Creating the socket manually lets us apply options after bind(2), but + // before listen(2). This avoids temporarily relaxing the process umask while + // still preventing a socket from becoming connectable before the requested + // permissions are applied. // - // We don't use "defer" here, to reset the umask to its original value as soon - // as possible. Ideally we'd be able to detect if WithChmod() was passed as - // an option, and skip changing umask if default permissions are used. - origUmask := syscall.Umask(0o777) - l, err := net.Listen("unix", path) - syscall.Umask(origUmask) - return l, err + // See https://github.com/golang/go/issues/11822 + + // Similar to sysSocket in stdlib, but without the fast path for Linux. + // https://github.com/golang/go/blob/go1.26.3/src/net/sys_cloexec.go#L18-L36 + syscall.ForkLock.RLock() + fd, err := syscall.Socket(syscall.AF_UNIX, syscall.SOCK_STREAM, 0) + if err == nil { + syscall.CloseOnExec(fd) // No syscall.SOCK_CLOEXEC on macOS. + } + syscall.ForkLock.RUnlock() + if err != nil { + return nil, os.NewSyscallError("socket", err) + } + + defer func() { + if fd >= 0 { + _ = syscall.Close(fd) + } + }() + + if err := syscall.Bind(fd, &syscall.SockaddrUnix{Name: path}); err != nil { + return nil, os.NewSyscallError("bind", err) + } + + defer func() { + if retErr != nil { + _ = syscall.Unlink(path) + } + }() + + // Secure by default: the socket is not accessible at all + // unless permission options are set through WithChmod. + if err := os.Chmod(path, defaultSocketPerms); err != nil { + return nil, err + } + + for _, op := range opts { + if err := op(path); err != nil { + return nil, err + } + } + + if err := syscall.Listen(fd, listenerBacklog()); err != nil { + return nil, os.NewSyscallError("listen", err) + } + + f := os.NewFile(uintptr(fd), "unix:"+path) + fd = -1 // f now owns the original fd; prevent the defer from closing it. + + // FileListener duplicates f, sets the duplicate close-on-exec and nonblocking, + // and returns a net.Listener backed by that duplicate. The temporary *os.File + // is no longer needed after this point. + l, err := net.FileListener(f) + _ = f.Close() + if err != nil { + return nil, err + } + + if ul, ok := l.(*net.UnixListener); ok { + ul.SetUnlinkOnClose(true) + } + + return l, nil } + +// listenerBacklog is a caching wrapper around maxListenerBacklog. +var listenerBacklog = sync.OnceValue(maxListenerBacklog) diff --git a/vendor/github.com/docker/go-connections/sockets/unix_socket_windows.go b/vendor/github.com/docker/go-connections/sockets/unix_socket_windows.go index 01aee5f11a5b..a9a9e80d5012 100644 --- a/vendor/github.com/docker/go-connections/sockets/unix_socket_windows.go +++ b/vendor/github.com/docker/go-connections/sockets/unix_socket_windows.go @@ -48,7 +48,7 @@ func WithAdditionalUsersAndGroups(additionalUsersAndGroups []string) SockOption } sd, err := getSecurityDescriptor(additionalUsersAndGroups...) if err != nil { - return fmt.Errorf("looking up SID: %w", err) + return err } return withSDDL(sd)(path) } @@ -85,12 +85,15 @@ func withSDDL(sddl string) SockOption { } } -// NewUnixSocket creates a new unix socket. +// NewUnixSocket creates a new Unix socket. // // It sets [BasePermissions] on the socket path and grants the given additional // users and groups to generic read (GR) and write (GW) access. It returns // an error when failing to resolve any of the additional users and groups, // or when failing to apply the ACL. +// +// Abstract Unix sockets are not supported by this helper. Attempts to use +// abstract socket addresses return an error wrapping [errors.ErrUnsupported]. func NewUnixSocket(path string, additionalUsersAndGroups []string) (net.Listener, error) { var opts []SockOption if len(additionalUsersAndGroups) > 0 { @@ -103,27 +106,38 @@ func NewUnixSocket(path string, additionalUsersAndGroups []string) (net.Listener // getSecurityDescriptor returns the DACL for the Unix socket. // -// By default, it grants [BasePermissions], but allows for additional -// users and groups to get generic read (GR) and write (GW) access. It -// returns an error when failing to resolve any of the additional users -// and groups. +// By default, it grants [BasePermissions]. Additional users and groups +// are granted generic read (GR) and write (GW) access. It returns an +// error if any name cannot be resolved to a SID. func getSecurityDescriptor(additionalUsersAndGroups ...string) (string, error) { sddl := BasePermissions // Grant generic read (GR) and write (GW) access to whatever // additional users or groups were specified. // - // TODO(thaJeztah): should we fail on, or remove duplicates? + // We keep duplicates; two identical allow ACEs are redundant, + // but they do not create conflicting permissions, so should not error. + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/20233ed8-a6c6-4097-aafa-dd545ed24428 for _, g := range additionalUsersAndGroups { sid, err := winio.LookupSidByName(strings.TrimSpace(g)) if err != nil { return "", fmt.Errorf("looking up SID: %w", err) } - sddl += fmt.Sprintf("(A;;GRGW;;;%s)", sid) + sddl += "(A;;GRGW;;;" + sid + ")" } return sddl, nil } -func listenUnix(path string) (net.Listener, error) { - return net.Listen("unix", path) +func listenUnix(path string, opts ...SockOption) (net.Listener, error) { + l, err := net.Listen("unix", path) + if err != nil { + return nil, err + } + for _, op := range opts { + if err := op(path); err != nil { + _ = l.Close() + return nil, err + } + } + return l, nil } diff --git a/vendor/modules.txt b/vendor/modules.txt index 3919a8d83571..b002666c21bb 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -72,7 +72,7 @@ github.com/docker/distribution/registry/storage/cache/memory ## explicit; go 1.21 github.com/docker/docker-credential-helpers/client github.com/docker/docker-credential-helpers/credentials -# github.com/docker/go-connections v0.7.0 +# github.com/docker/go-connections v0.8.0 ## explicit; go 1.23 github.com/docker/go-connections/nat github.com/docker/go-connections/sockets