All checks were successful
		
		
	
	Test / Hakurei (push) Successful in 48s
				
			Test / Create distribution (push) Successful in 39s
				
			Test / Hakurei (race detector) (push) Successful in 49s
				
			Test / Hpkg (push) Successful in 47s
				
			Test / Sandbox (push) Successful in 1m52s
				
			Test / Sandbox (race detector) (push) Successful in 2m54s
				
			Test / Flake checks (push) Successful in 1m21s
				
			This is not always obvious from mountinfo. Signed-off-by: Ophestra <cat@gensokyo.uk>
		
			
				
	
	
		
			273 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			273 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
//go:build testtool
 | 
						|
 | 
						|
/*
 | 
						|
Package sandbox provides utilities for checking sandbox outcome.
 | 
						|
 | 
						|
This package must never be used outside integration tests, there is a much better native implementation of mountinfo
 | 
						|
in the public sandbox/vfs package. Files in this package are excluded by the build system to prevent accidental misuse.
 | 
						|
*/
 | 
						|
package sandbox
 | 
						|
 | 
						|
import (
 | 
						|
	"crypto/sha512"
 | 
						|
	"encoding/hex"
 | 
						|
	"encoding/json"
 | 
						|
	"errors"
 | 
						|
	"io/fs"
 | 
						|
	"log"
 | 
						|
	"net"
 | 
						|
	"os"
 | 
						|
	"path"
 | 
						|
	"syscall"
 | 
						|
)
 | 
						|
 | 
						|
var (
 | 
						|
	assert     = log.New(os.Stderr, "sandbox: ", 0)
 | 
						|
	printfFunc = assert.Printf
 | 
						|
	fatalfFunc = assert.Fatalf
 | 
						|
)
 | 
						|
 | 
						|
func printf(format string, v ...any) { printfFunc(format, v...) }
 | 
						|
func fatalf(format string, v ...any) { fatalfFunc(format, v...) }
 | 
						|
 | 
						|
type TestCase struct {
 | 
						|
	Env     []string          `json:"env"`
 | 
						|
	FS      *FS               `json:"fs"`
 | 
						|
	Mount   []*MountinfoEntry `json:"mount"`
 | 
						|
	Seccomp bool              `json:"seccomp"`
 | 
						|
 | 
						|
	TrySocket      string `json:"try_socket,omitempty"`
 | 
						|
	SocketAbstract bool   `json:"socket_abstract,omitempty"`
 | 
						|
	SocketPathname bool   `json:"socket_pathname,omitempty"`
 | 
						|
}
 | 
						|
 | 
						|
type T struct {
 | 
						|
	FS fs.FS
 | 
						|
 | 
						|
	MountsPath string
 | 
						|
}
 | 
						|
 | 
						|
func (t *T) MustCheckFile(wantFilePath string) {
 | 
						|
	var want *TestCase
 | 
						|
	mustDecode(wantFilePath, &want)
 | 
						|
	t.MustCheck(want)
 | 
						|
}
 | 
						|
 | 
						|
func mustAbs(s string) string {
 | 
						|
	if !path.IsAbs(s) {
 | 
						|
		fatalf("[FAIL] %q is not absolute", s)
 | 
						|
		panic("unreachable")
 | 
						|
	}
 | 
						|
	return s
 | 
						|
}
 | 
						|
 | 
						|
func (t *T) MustCheck(want *TestCase) {
 | 
						|
	checkWritableDirPaths := []string{
 | 
						|
		"/dev/shm",
 | 
						|
		"/tmp",
 | 
						|
		os.Getenv("XDG_RUNTIME_DIR"),
 | 
						|
	}
 | 
						|
	for _, a := range checkWritableDirPaths {
 | 
						|
		pathname := path.Join(mustAbs(a), ".hakurei-check")
 | 
						|
		if err := os.WriteFile(pathname, make([]byte, 1<<8), 0600); err != nil {
 | 
						|
			fatalf("[FAIL] %s", err)
 | 
						|
		} else if err = os.Remove(pathname); err != nil {
 | 
						|
			fatalf("[FAIL] %s", err)
 | 
						|
		} else {
 | 
						|
			printf("[ OK ] %s is writable", a)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if want.Env != nil {
 | 
						|
		var (
 | 
						|
			fail bool
 | 
						|
			i    int
 | 
						|
			got  string
 | 
						|
		)
 | 
						|
		for i, got = range os.Environ() {
 | 
						|
			if i == len(want.Env) {
 | 
						|
				fatalf("got more than %d environment variables", len(want.Env))
 | 
						|
			}
 | 
						|
			if got != want.Env[i] {
 | 
						|
				fail = true
 | 
						|
				printf("[FAIL] %s", got)
 | 
						|
			} else {
 | 
						|
				printf("[ OK ] %s", got)
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		i++
 | 
						|
		if i != len(want.Env) {
 | 
						|
			fatalf("got %d environment variables, want %d", i, len(want.Env))
 | 
						|
		}
 | 
						|
 | 
						|
		if fail {
 | 
						|
			fatalf("[FAIL] some environment variables did not match")
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		printf("[SKIP] skipping environ check")
 | 
						|
	}
 | 
						|
 | 
						|
	if want.FS != nil && t.FS != nil {
 | 
						|
		if err := want.FS.Compare(".", t.FS); err != nil {
 | 
						|
			fatalf("%v", err)
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		printf("[SKIP] skipping fs check")
 | 
						|
	}
 | 
						|
 | 
						|
	if want.Mount != nil {
 | 
						|
		var fail bool
 | 
						|
		m := mustParseMountinfo(t.MountsPath)
 | 
						|
		i := 0
 | 
						|
		for ent := range m.Entries() {
 | 
						|
			if i == len(want.Mount) {
 | 
						|
				fatalf("got more than %d entries", i)
 | 
						|
			}
 | 
						|
			if !ent.EqualWithIgnore(want.Mount[i], "//ignore") {
 | 
						|
				fail = true
 | 
						|
				printf("[FAIL] %s", ent)
 | 
						|
			} else {
 | 
						|
				printf("[ OK ] %s", ent)
 | 
						|
			}
 | 
						|
 | 
						|
			i++
 | 
						|
		}
 | 
						|
		if err := m.Err(); err != nil {
 | 
						|
			fatalf("%v", err)
 | 
						|
		}
 | 
						|
 | 
						|
		if i != len(want.Mount) {
 | 
						|
			fatalf("got %d entries, want %d", i, len(want.Mount))
 | 
						|
		}
 | 
						|
 | 
						|
		if fail {
 | 
						|
			fatalf("[FAIL] some mount points did not match")
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		printf("[SKIP] skipping mounts check")
 | 
						|
	}
 | 
						|
 | 
						|
	if want.Seccomp {
 | 
						|
		if trySyscalls() != nil {
 | 
						|
			os.Exit(1)
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		printf("[SKIP] skipping seccomp check")
 | 
						|
	}
 | 
						|
 | 
						|
	if want.TrySocket != "" {
 | 
						|
		abstractConn, abstractErr := net.Dial("unix", "@"+want.TrySocket)
 | 
						|
		pathnameConn, pathnameErr := net.Dial("unix", want.TrySocket)
 | 
						|
		ok := true
 | 
						|
 | 
						|
		if abstractErr == nil {
 | 
						|
			if err := abstractConn.Close(); err != nil {
 | 
						|
				ok = false
 | 
						|
				log.Printf("Close: %v", err)
 | 
						|
			}
 | 
						|
		}
 | 
						|
		if pathnameErr == nil {
 | 
						|
			if err := pathnameConn.Close(); err != nil {
 | 
						|
				ok = false
 | 
						|
				log.Printf("Close: %v", err)
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		abstractWantErr := error(syscall.EPERM)
 | 
						|
		pathnameWantErr := error(syscall.ENOENT)
 | 
						|
		if want.SocketAbstract {
 | 
						|
			abstractWantErr = nil
 | 
						|
		}
 | 
						|
		if want.SocketPathname {
 | 
						|
			pathnameWantErr = nil
 | 
						|
		}
 | 
						|
 | 
						|
		if !errors.Is(abstractErr, abstractWantErr) {
 | 
						|
			ok = false
 | 
						|
			log.Printf("abstractErr: %v, want %v", abstractErr, abstractWantErr)
 | 
						|
		}
 | 
						|
		if !errors.Is(pathnameErr, pathnameWantErr) {
 | 
						|
			ok = false
 | 
						|
			log.Printf("pathnameErr: %v, want %v", pathnameErr, pathnameWantErr)
 | 
						|
		}
 | 
						|
 | 
						|
		if !ok {
 | 
						|
			os.Exit(1)
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func MustCheckFilter(pid int, want string) {
 | 
						|
	err := CheckFilter(pid, want)
 | 
						|
	if err == nil {
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	var perr *ptraceError
 | 
						|
	if !errors.As(err, &perr) {
 | 
						|
		fatalf("%s", err)
 | 
						|
	}
 | 
						|
	switch perr.op {
 | 
						|
	case "PTRACE_ATTACH":
 | 
						|
		fatalf("cannot attach to process %d: %v", pid, err)
 | 
						|
	case "PTRACE_SECCOMP_GET_FILTER":
 | 
						|
		if perr.errno == syscall.ENOENT {
 | 
						|
			fatalf("seccomp filter not installed for process %d", pid)
 | 
						|
		}
 | 
						|
		fatalf("cannot get filter: %v", err)
 | 
						|
	default:
 | 
						|
		fatalf("cannot check filter: %v", err)
 | 
						|
	}
 | 
						|
 | 
						|
	*(*int)(nil) = 0 // not reached
 | 
						|
}
 | 
						|
 | 
						|
func CheckFilter(pid int, want string) error {
 | 
						|
	if err := ptraceAttach(pid); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	defer func() {
 | 
						|
		if err := ptraceDetach(pid); err != nil {
 | 
						|
			printf("cannot detach from process %d: %v", pid, err)
 | 
						|
		}
 | 
						|
	}()
 | 
						|
 | 
						|
	h := sha512.New()
 | 
						|
 | 
						|
	if buf, err := getFilter[[8]byte](pid, 0); err != nil {
 | 
						|
		return err
 | 
						|
	} else {
 | 
						|
		for _, b := range buf {
 | 
						|
			h.Write(b[:])
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if got := hex.EncodeToString(h.Sum(nil)); got != want {
 | 
						|
		printf("[FAIL] %s", got)
 | 
						|
		return syscall.ENOTRECOVERABLE
 | 
						|
	} else {
 | 
						|
		printf("[ OK ] %s", got)
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func mustDecode(wantFilePath string, v any) {
 | 
						|
	if f, err := os.Open(wantFilePath); err != nil {
 | 
						|
		fatalf("cannot open %q: %v", wantFilePath, err)
 | 
						|
	} else if err = json.NewDecoder(f).Decode(v); err != nil {
 | 
						|
		fatalf("cannot decode %q: %v", wantFilePath, err)
 | 
						|
	} else if err = f.Close(); err != nil {
 | 
						|
		fatalf("cannot close %q: %v", wantFilePath, err)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func mustParseMountinfo(name string) *Mountinfo {
 | 
						|
	m := NewMountinfo(name)
 | 
						|
	if err := m.Parse(); err != nil {
 | 
						|
		fatalf("%v", err)
 | 
						|
		panic("unreachable")
 | 
						|
	}
 | 
						|
	return m
 | 
						|
}
 |