Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

net/http: unix socket does not get removed when application exits, which makes restart fail #70985

Closed
XANi opened this issue Dec 24, 2024 · 9 comments

Comments

@XANi
Copy link

XANi commented Dec 24, 2024

Go version

go version go1.23.4 linux/amd64

Output of go env in your module/workspace:

env really doesn't matter here, this bug have years.

I initially thought it was Gin bug ( https://github.com/gin-gonic/gin/issues/3817 ) but I digged deeper

What did you do?

Running this code

func main() {
	u, err := net.Listen("unix", "sock_raw")
	fmt.Println(err)
	if err == nil {
		defer u.Close()
	}
}

over and over will work every time unless killed by SIGKILL because u.Close() cleans up the socket.

but adding http.Serve in the mix:

package main

import (
	"fmt"
	"net"
	"net/http"
)

type H struct{}

func (h *H) ServeHTTP(w http.ResponseWriter, r *http.Request) {

}

func main() {
	u, err := net.Listen("unix", "sock_http")
	fmt.Println(err)
	if err == nil {
		defer u.Close()
		fmt.Println("press C-c and run me again")
		fmt.Println(http.Serve(u, &H{}))
	}

}

will fail after first run because of leftover socket that now for some reason is not cleared by u.Close(). Even if cleanup is added manually by defer os.Remove()

...
func main() {
	u, err := net.Listen("unix", "sock_http")
	defer os.Remove("sock_http") # <-- here
	fmt.Println(err)
	if err == nil {
		defer u.Close()
		fmt.Println("press C-c and run me again")
		fmt.Println(http.Serve(u, &H{}))
	}
}

the result is still the same:

-> ᛯ go run main.go                                                                                                                                                     1
<nil>
press C-c and run me again
^Csignal: interrupt
-> ᛯ go run main.go                                                                                                                                                     1
listen unix sock_http: bind: address already in use
[20:44:53] ^ [/tmp/g] 
-> ᛯ 

I even tried adding

		defer func() {
			c := exec.Command("/bin/rm", "sock_http")
			c.Run()
		}()

but that didn't remove it either. Also tried adding signal handler so it gets clean os.Exit:

package main

import (
	"fmt"
	"net"
	"net/http"
	"os"
	"os/signal"
)

func main() {

	c := make(chan os.Signal, 1)
	signal.Notify(c, os.Interrupt)
	go func() {
		for _ = range c {
			fmt.Println("exiting")
			os.Exit(1)
		}
	}()
	u, err := net.Listen("unix", "sock_http")
	fmt.Println(err)
	if err == nil {
		defer u.Close()
		fmt.Println("press C-c and run me again")
		fmt.Println(http.Serve(u, nil))
	}

with same result:

-> ᛯ go run main.go
<nil>
press C-c and run me again
^Cexiting
exit status 1
-> ᛯ go run main.go                                                                                                                                                     1
listen unix sock_http: bind: address already in use

What did you see happen?

Socket not being removed when http.Serve uses it

What did you expect to see?

I'd expect one of 2 things to happen:

  • net.Listen user pre-created socket if it can, which allows to, for example, set the permissions to the socket before listening on it which might desirable security-wise
  • the socket is correctly cleared if removed when http.Serve is using it
@XANi XANi changed the title import/path: issue title net/http: unix socket does not get removed when application exits, which makes restart fail Dec 24, 2024
@rittneje
Copy link

This is how Unix domain sockets work - you have to delete the file before you can use the same address again. See https://man7.org/linux/man-pages/man7/unix.7.html

Binding to a socket with a filename creates a socket in the
filesystem that must be deleted by the caller when it is no
longer needed (using unlink(2)). The usual UNIX close-behind
semantics apply; the socket can be unlinked at any time and will
be finally removed from the filesystem when the last reference to
it is closed.

You should either do os.Remove first (and ignore os.ErrNotExist), or consider using the abstract namespace instead if you only need to support Linux.

@XANi
Copy link
Author

XANi commented Dec 25, 2024

@rittneje net.Listen does that cleanup on its own when unix socket is closed.

The problem is really that most of the apps that use http.Serve() will handle it incorrectly because what you'd need to do is delete the socket in sigint/sigterm signal handler coz defer is not executed when app gets SIGINT. i.e most obvious use case is also wrong by default because most people don't expect defer being skipped when getting SIGINT or even running os.Exit

@seankhliao
Copy link
Member

I didn't think there much we can do if you choose to use os.Exit or not allow cleanup to run by handling signals

@seankhliao seankhliao closed this as not planned Won't fix, can't repro, duplicate, stale Dec 25, 2024
@XANi
Copy link
Author

XANi commented Dec 25, 2024

@seankhliao the first examples do not handle signals in any way and cleanup does not happen. That's why I'm reporting it, I actually found it out by just using gin that uses standard http.Serve with nothing special and cleanup doesn't happen on Ctrl+c

@rittneje
Copy link

@XANi It is not possible for any application to handle SIGKILL or sudden loss of power. Therefore, any robust application must arrange for the file to be deleted before calling Listen.

Also, if you want SIGINT/SIGTERM to run any defers, then you need to handle these signals yourself instead of relying on the default behavior, which cannot know about any of your defers since it would be in a completely different goroutine.

@XANi
Copy link
Author

XANi commented Dec 25, 2024

Also, if you want SIGINT/SIGTERM to run any defers, then you need to handle these signals yourself instead of relying on the default behavior, which cannot know about any of your defers since it would be in a completely different goroutine.

Okay, I didn't knew that is possible, how you can exit Go while triggering all defers ? Coz that would solve my problem.

@rittneje
Copy link

See signal.Notify or signal.NotifyContext.

@XANi
Copy link
Author

XANi commented Dec 25, 2024

@rittneje I asked about triggering defers (that might be dug 2 dependencies deep, like in case where I originally found that problem inside gin framework), I know how to handle signals

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

No branches or pull requests

4 participants