Skip to content

sockets: set socket permissions without umask hack - #158

Merged
thaJeztah merged 2 commits into
docker:mainfrom
thaJeztah:no_umask
Jul 24, 2026
Merged

sockets: set socket permissions without umask hack#158
thaJeztah merged 2 commits into
docker:mainfrom
thaJeztah:no_umask

Conversation

@thaJeztah

@thaJeztah thaJeztah commented May 28, 2026

Copy link
Copy Markdown
Member

relates to:

Create Unix sockets manually to apply permissions and ownership after bind(2), but before listen(2), avoiding races in net.Listen and removing the need for temporary process-wide umask changes.

- What I did

- How I did it

- How to verify it

- Description for the changelog

- A picture of a cute animal (not mandatory but encouraged)

@thaJeztah
thaJeztah force-pushed the no_umask branch 3 times, most recently from 93648f9 to a94d91f Compare May 28, 2026 15:17
Comment thread sockets/unix_socket_unix.go Outdated
Comment on lines +68 to +74
// Preserve the previous secure-by-default behavior: the socket is not
// accessible at all unless permission options are set.
//
// TODO(thaJeztah): consider using "0600" as default; using "0000" could potentially mean we can't cleanup on error.
if err := os.Chmod(path, 0); err != nil {
return nil, err
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what's sensible here; the previous code set umask to 0777, so the socket would be created with 0000 permissions; this keeps that behavior, but maybe 0600 would still be sensible, and would avoid ending up with a socket that cannot be removed.

@thaJeztah
thaJeztah marked this pull request as ready for review May 29, 2026 10:30
@thaJeztah
thaJeztah requested a review from Copilot May 29, 2026 10:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates Unix-domain socket creation to avoid the previous process-wide umask manipulation and the permission/ownership race window inherent in net.Listen("unix", ...) by creating/binding the socket manually and applying options before calling listen(2).

Changes:

  • Refactors NewUnixSocketWithOpts() to pass socket options into a platform-specific listenUnix() implementation.
  • Implements manual socket creation on Unix (socketbind → apply options → listen) to apply permissions/ownership before the socket becomes connectable.
  • Updates the Windows listenUnix() helper to accept and apply SockOptions (preserving existing behavior on Windows).

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
sockets/unix_socket.go Refactors NewUnixSocketWithOpts() to delegate options handling to listenUnix(path, opts...).
sockets/unix_socket_unix.go Replaces the umask workaround with manual socket creation and applies options before listen(2).
sockets/unix_socket_windows.go Adjusts listenUnix() signature to accept options and applies them after net.Listen.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread sockets/unix_socket.go
Comment thread sockets/unix_socket_unix.go Outdated
Comment thread sockets/unix_socket_unix.go
Comment thread sockets/unix_socket_unix.go Outdated
vvoland
vvoland previously approved these changes May 29, 2026
@thaJeztah
thaJeztah force-pushed the no_umask branch 2 times, most recently from 58c1dd1 to 3e507d2 Compare June 1, 2026 08:27
@thaJeztah

thaJeztah commented Jun 1, 2026

Copy link
Copy Markdown
Member Author

Updated; removed outdated comments, and applied changes to use a similar approach as stdlib for setting SOCK_CLOEXEC and returning syscall errors; https://github.com/docker/go-connections/compare/a94d91f8b2a6ba9a09cc7562309afa30442d5057..3e507d2f764f96e8d0d93a9c68963ff862a73764

diff --git a/sockets/unix_socket.go b/sockets/unix_socket.go
index b8a7c5e..b705c85 100644
--- a/sockets/unix_socket.go
+++ b/sockets/unix_socket.go
@@ -58,11 +58,6 @@ 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.
-//
-// 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.
 func NewUnixSocketWithOpts(path string, opts ...SockOption) (net.Listener, error) {
 	if err := syscall.Unlink(path); err != nil && !os.IsNotExist(err) {
 		return nil, err
diff --git a/sockets/unix_socket_unix.go b/sockets/unix_socket_unix.go
index a77ebb4..4aee588 100644
--- a/sockets/unix_socket_unix.go
+++ b/sockets/unix_socket_unix.go
@@ -44,11 +44,19 @@ func listenUnix(path string, opts ...SockOption) (_ net.Listener, retErr error)
 	// permissions are applied.
 	//
 	// 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, err
+		return nil, os.NewSyscallError("socket", err)
 	}
-	syscall.CloseOnExec(fd) // No syscall.SOCK_CLOEXEC on macOS.
+
 	defer func() {
 		if fd >= 0 {
 			_ = syscall.Close(fd)
@@ -56,7 +64,7 @@ func listenUnix(path string, opts ...SockOption) (_ net.Listener, retErr error)
 	}()
 
 	if err := syscall.Bind(fd, &syscall.SockaddrUnix{Name: path}); err != nil {
-		return nil, err
+		return nil, os.NewSyscallError("bind", err)
 	}
 
 	defer func() {
@@ -67,8 +75,6 @@ func listenUnix(path string, opts ...SockOption) (_ net.Listener, retErr error)
 
 	// Preserve the previous secure-by-default behavior: the socket is not
 	// accessible at all unless permission options are set.
-	//
-	// TODO(thaJeztah): consider using "0600" as default; using "0000" could potentially mean we can't cleanup on error.
 	if err := os.Chmod(path, 0); err != nil {
 		return nil, err
 	}
@@ -80,14 +86,15 @@ func listenUnix(path string, opts ...SockOption) (_ net.Listener, retErr error)
 	}
 
 	if err := syscall.Listen(fd, syscall.SOMAXCONN); err != nil {
-		return nil, err
+		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 takes ownership of the socket; f is only a temporary wrapper,
-	// and the temporary *os.File is no longer needed after this point.
+	// 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 {

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment thread sockets/unix_socket.go Outdated
Comment on lines +104 to +108
if ul, ok := l.(*net.UnixListener); ok {
ul.SetUnlinkOnClose(true)
}

return l, nil

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is correct; unix sockets created through net.Listen would have it set;

// SetUnlinkOnClose sets whether the underlying socket file should be removed
// from the file system when the listener is closed.
//
// The default behavior is to unlink the socket file only when package net created it.
// That is, when the listener and the underlying socket file were created by a call to
// Listen or ListenUnix, then by default closing the listener will remove the socket file.
// but if the listener was created by a call to FileListener to use an already existing
// socket file, then by default closing the listener will not remove the socket file.
func (l *UnixListener) SetUnlinkOnClose(unlink bool) {
	l.unlink = unlink
}

func (sl *sysListener) listenUnix(ctx context.Context, laddr *UnixAddr) (*UnixListener, error) {
	var ctrlCtxFn func(ctx context.Context, network, address string, c syscall.RawConn) error
	if sl.ListenConfig.Control != nil {
		ctrlCtxFn = func(ctx context.Context, network, address string, c syscall.RawConn) error {
			return sl.ListenConfig.Control(network, address, c)
		}
	}
	fd, err := unixSocket(ctx, sl.network, laddr, nil, "listen", ctrlCtxFn)
	if err != nil {
		return nil, err
	}
	return &UnixListener{fd: fd, path: fd.laddr.String(), unlink: true}, nil
}

This comment was marked as outdated.

This comment was marked as outdated.

This comment was marked as outdated.

@thaJeztah
thaJeztah force-pushed the no_umask branch 2 times, most recently from 25a66bb to c7ab054 Compare June 1, 2026 11:01
@thaJeztah
thaJeztah requested a review from Copilot June 1, 2026 11:03

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 12 comments.

Comment on lines +20 to +25
n, err := strconv.Atoi(strings.TrimSpace(string(b)))
if err != nil || n <= 0 {
return syscall.SOMAXCONN
}
return n
}

@thaJeztah thaJeztah Jun 1, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stdlib has code to handle kernel < 4.1, which no currently supported distros use, so skipping that logic, and just consuming what's in /proc/sys/net/core/somaxconn

https://github.com/golang/go/blob/go1.26.3/src/net/sock_linux.go#L33-L53

Comment thread sockets/unix_socket_unix.go
Comment thread sockets/unix_socket_linux.go
Comment thread sockets/unix_socket_nolinux.go
Comment thread sockets/unix_socket_linux.go
Comment thread sockets/unix_socket_nolinux.go
Comment thread sockets/unix_socket_linux.go
Comment thread sockets/unix_socket_unix.go
Comment thread sockets/unix_socket_linux.go
Comment thread sockets/unix_socket_nolinux.go
@thaJeztah
thaJeztah requested a review from vvoland June 2, 2026 09:10
@thaJeztah
thaJeztah marked this pull request as draft July 24, 2026 09:27
@thaJeztah
thaJeztah marked this pull request as ready for review July 24, 2026 10:47
Comment thread sockets/unix_socket_unix.go
vvoland
vvoland previously approved these changes Jul 24, 2026
@thaJeztah
thaJeztah force-pushed the no_umask branch 3 times, most recently from 954e24a to bd6073e Compare July 24, 2026 17:17
@thaJeztah
thaJeztah marked this pull request as draft July 24, 2026 17:20
Create Unix sockets manually to apply permissions and ownership after
bind(2), but before listen(2), avoiding races in net.Listen and removing
the need for temporary process-wide umask changes.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Align with stdlib, which sets these dynamically.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
@thaJeztah
thaJeztah marked this pull request as ready for review July 24, 2026 18:09
@thaJeztah

Copy link
Copy Markdown
Member Author

Rebased; ready for review again, @vvoland

@thaJeztah
thaJeztah requested a review from vvoland July 24, 2026 18:10
@thaJeztah
thaJeztah merged commit 754f906 into docker:main Jul 24, 2026
10 checks passed
@thaJeztah
thaJeztah deleted the no_umask branch July 24, 2026 18:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants