~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
177 lines · 4.2 KB Raw History
//go:build linux

package devsup

import (
	"bytes"
	"cmp"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"golang.org/x/sys/unix"
)

type storedFD struct {
	name string
	f    *os.File
}

type fdStore struct {
	path string
	conn *net.UnixConn
	fds  []storedFD
}

func (st *fdStore) socket() string { return st.path }

func (s *server) notifyPath(name string) string {
	sum := sha256.Sum256([]byte(s.id + "/" + name))
	return filepath.Join(filepath.Dir(s.o.Socket), "n-"+hex.EncodeToString(sum[:])[:12]+".sock")
}

func (s *server) openFDStore(u *unit) error {
	if u.spec.FDStore <= 0 || u.store != nil {
		return nil
	}
	path := s.notifyPath(u.spec.Name)
	if len(path) > 107 {
		return fmt.Errorf("devsup: the notify socket %s of %s is over the 107-byte sun_path limit", path, u.spec.Name)
	}
	os.Remove(path)
	conn, err := net.ListenUnixgram("unixgram", &net.UnixAddr{Name: path, Net: "unixgram"})
	if err != nil {
		return fmt.Errorf("devsup: the notify socket of %s: %w", u.spec.Name, err)
	}
	os.Chmod(path, 0o600)
	raw, err := conn.SyscallConn()
	if err == nil {
		var serr error
		err = raw.Control(func(fd uintptr) { serr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_PASSCRED, 1) })
		if err == nil {
			err = serr
		}
	}
	if err != nil {
		conn.Close()
		os.Remove(path)
		return fmt.Errorf("devsup: SO_PASSCRED on the notify socket of %s: %w", u.spec.Name, err)
	}
	st := &fdStore{path: path, conn: conn}
	u.store = st
	s.wg.Go(func() { s.readNotify(u, st) })
	return nil
}

func (s *server) readNotify(u *unit, st *fdStore) {
	buf := make([]byte, 4096)
	oob := make([]byte, unix.CmsgSpace(253*4)+unix.CmsgSpace(unix.SizeofUcred))
	for {
		n, oobn, _, _, err := st.conn.ReadMsgUnix(buf, oob)
		if err != nil {
			return
		}
		var fds []int
		pid := 0
		if msgs, err := unix.ParseSocketControlMessage(oob[:oobn]); err == nil {
			for i := range msgs {
				switch {
				case msgs[i].Header.Level == unix.SOL_SOCKET && msgs[i].Header.Type == unix.SCM_RIGHTS:
					if got, err := unix.ParseUnixRights(&msgs[i]); err == nil {
						fds = append(fds, got...)
					}
				case msgs[i].Header.Level == unix.SOL_SOCKET && msgs[i].Header.Type == unix.SCM_CREDENTIALS:
					if cred, err := unix.ParseUnixCredentials(&msgs[i]); err == nil {
						pid = int(cred.Pid)
					}
				}
			}
		}
		s.notified(u, st, pid, buf[:n], fds)
	}
}

func notifyFields(msg []byte) map[string]string {
	out := map[string]string{}
	for line := range bytes.SplitSeq(msg, []byte{'\n'}) {
		k, v, ok := strings.Cut(string(line), "=")
		if ok {
			out[k] = v
		}
	}
	return out
}

func (s *server) notified(u *unit, st *fdStore, pid int, msg []byte, fds []int) {
	closeAll := func() {
		for _, fd := range fds {
			unix.Close(fd)
		}
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	if u.store != st || pid <= 0 || !(pid == u.st.MainPID || s.belongs(u.spec.Name, u.st.InvocationID, u.pgid, pid)) {
		s.log.Warn("a notify message from outside the unit was dropped", "unit", u.spec.Name, "pid", pid)
		closeAll()
		return
	}
	f := notifyFields(msg)
	name := f["FDNAME"]
	name = cmp.Or(name, "stored")
	if f["FDSTOREREMOVE"] == "1" {
		keep := st.fds[:0]
		for _, sfd := range st.fds {
			if sfd.name == name {
				sfd.f.Close()
				continue
			}
			keep = append(keep, sfd)
		}
		st.fds = keep
		closeAll()
		return
	}
	if f["FDSTORE"] != "1" {
		closeAll()
		return
	}
	for _, fd := range fds {
		if len(st.fds) >= u.spec.FDStore {
			s.log.Warn("the fd store is full; an fd was dropped", "unit", u.spec.Name, "max", u.spec.FDStore)
			unix.Close(fd)
			continue
		}
		st.fds = append(st.fds, storedFD{name: name, f: os.NewFile(uintptr(fd), name)})
		s.log.Info("fd stored", "unit", u.spec.Name, "name", name, "pid", pid, "held", len(st.fds))
	}
}

func (st *fdStore) launchFiles() ([]*os.File, []string) {
	if st == nil || len(st.fds) == 0 {
		return nil, nil
	}
	var files []*os.File
	var names []string
	for _, sfd := range st.fds {
		files = append(files, sfd.f)
		names = append(names, sfd.name)
	}
	return files, []string{EnvListenFDs + "=" + strconv.Itoa(len(files)), EnvListenNames + "=" + strings.Join(names, ":")}
}

func (st *fdStore) release() {
	if st == nil {
		return
	}
	st.conn.Close()
	os.Remove(st.path)
	for _, sfd := range st.fds {
		sfd.f.Close()
	}
	st.fds = nil
}