sockets: set socket permissions without umask hack - #158
Conversation
93648f9 to
a94d91f
Compare
| // 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 | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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-specificlistenUnix()implementation. - Implements manual socket creation on Unix (
socket→bind→ apply options →listen) to apply permissions/ownership before the socket becomes connectable. - Updates the Windows
listenUnix()helper to accept and applySockOptions (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.
58c1dd1 to
3e507d2
Compare
|
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 { |
| if ul, ok := l.(*net.UnixListener); ok { | ||
| ul.SetUnlinkOnClose(true) | ||
| } | ||
|
|
||
| return l, nil |
There was a problem hiding this comment.
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
}25a66bb to
c7ab054
Compare
| n, err := strconv.Atoi(strings.TrimSpace(string(b))) | ||
| if err != nil || n <= 0 { | ||
| return syscall.SOMAXCONN | ||
| } | ||
| return n | ||
| } |
There was a problem hiding this comment.
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
954e24a to
bd6073e
Compare
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>
|
Rebased; ready for review again, @vvoland |
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)