mirror of
https://github.com/cirruslabs/tart-guest-agent.git
synced 2026-10-17 20:24:21 +02:00
140 lines
3.7 KiB
Go
140 lines
3.7 KiB
Go
//nolint:testpackage
|
|
package vsock
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
const socketTestTimeout = 5 * time.Second
|
|
|
|
func TestMain(m *testing.M) {
|
|
if path := os.Getenv("TART_GUEST_AGENT_TEST_SOCKET_PATH"); path != "" {
|
|
if err := checkInheritedSockets(path); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(0)
|
|
}
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
func checkInheritedSockets(path string) error {
|
|
for value := range strings.SplitSeq(os.Getenv("TART_GUEST_AGENT_TEST_SOCKET_FDS"), ",") {
|
|
socketFD, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid socket descriptor %q: %w", value, err)
|
|
}
|
|
|
|
address, err := unix.Getsockname(socketFD)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if address, ok := address.(*unix.SockaddrUnix); ok && address.Name == path {
|
|
return fmt.Errorf("socket descriptor %d was inherited", socketFD)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestSocketDescriptorsNotInherited(t *testing.T) {
|
|
// Keep the path below Darwin's Unix-domain socket path limit.
|
|
dir, err := os.MkdirTemp("", "vsock-") //nolint:usetesting
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, os.RemoveAll(dir)) })
|
|
path := filepath.Join(dir, "listener.sock")
|
|
socketFD, err := newSocket(unix.AF_UNIX)
|
|
require.NoError(t, err)
|
|
file := os.NewFile(uintptr(socketFD), "test listener")
|
|
t.Cleanup(func() { require.NoError(t, file.Close()) })
|
|
|
|
require.NoError(t, unix.Bind(socketFD, &unix.SockaddrUnix{Name: path}))
|
|
require.NoError(t, unix.Listen(socketFD, unix.SOMAXCONN))
|
|
requireSocketFlags(t, socketFD)
|
|
|
|
_, err = acceptSocket(file)
|
|
require.ErrorIs(t, err, unix.EAGAIN)
|
|
|
|
dialer := net.Dialer{Timeout: socketTestTimeout}
|
|
client, err := dialer.DialContext(t.Context(), "unix", path)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, client.Close()) })
|
|
|
|
acceptedFD := acceptAfterFork(t, file)
|
|
t.Cleanup(func() { require.NoError(t, unix.Close(acceptedFD)) })
|
|
requireSocketFlags(t, acceptedFD)
|
|
|
|
ctx, cancel := context.WithTimeout(t.Context(), socketTestTimeout)
|
|
defer cancel()
|
|
testExecutable, err := os.Executable()
|
|
require.NoError(t, err)
|
|
// #nosec G204 -- Re-execute the current test binary.
|
|
cmd := exec.CommandContext(ctx, testExecutable)
|
|
cmd.Env = append(os.Environ(),
|
|
"TART_GUEST_AGENT_TEST_SOCKET_PATH="+path,
|
|
fmt.Sprintf("TART_GUEST_AGENT_TEST_SOCKET_FDS=%d,%d", socketFD, acceptedFD),
|
|
)
|
|
output, err := cmd.CombinedOutput()
|
|
require.NoError(t, err, "child process: %s", output)
|
|
}
|
|
|
|
func acceptAfterFork(t *testing.T, file *os.File) int {
|
|
t.Helper()
|
|
|
|
type result struct {
|
|
socketFD int
|
|
err error
|
|
}
|
|
accepted := make(chan result, 1)
|
|
|
|
syscall.ForkLock.Lock()
|
|
go func() {
|
|
socketFD, err := acceptSocket(file)
|
|
accepted <- result{socketFD: socketFD, err: err}
|
|
}()
|
|
|
|
select {
|
|
case early := <-accepted:
|
|
syscall.ForkLock.Unlock()
|
|
if early.socketFD >= 0 {
|
|
require.NoError(t, unix.Close(early.socketFD))
|
|
}
|
|
t.Fatalf("accept returned while ForkLock was held: %v", early.err)
|
|
case <-time.After(100 * time.Millisecond):
|
|
syscall.ForkLock.Unlock()
|
|
}
|
|
|
|
select {
|
|
case accepted := <-accepted:
|
|
require.NoError(t, accepted.err)
|
|
return accepted.socketFD
|
|
case <-time.After(socketTestTimeout):
|
|
t.Fatal("accept did not finish after ForkLock was released")
|
|
return -1
|
|
}
|
|
}
|
|
|
|
func requireSocketFlags(t *testing.T, socketFD int) {
|
|
t.Helper()
|
|
|
|
flags, err := unix.FcntlInt(uintptr(socketFD), unix.F_GETFD, 0)
|
|
require.NoError(t, err)
|
|
require.NotZero(t, flags&unix.FD_CLOEXEC, "descriptor %d must be close-on-exec", socketFD)
|
|
|
|
flags, err = unix.FcntlInt(uintptr(socketFD), unix.F_GETFL, 0)
|
|
require.NoError(t, err)
|
|
require.NotZero(t, flags&unix.O_NONBLOCK, "descriptor %d must be nonblocking", socketFD)
|
|
}
|