test: move package sandbox internal
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Hakurei (push) Successful in 43s
Test / Hpkg (push) Successful in 40s
Test / Hakurei (race detector) (push) Successful in 43s
Test / Sandbox (push) Successful in 1m56s
Test / Sandbox (race detector) (push) Successful in 2m39s
Test / Flake checks (push) Successful in 1m24s
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Hakurei (push) Successful in 43s
Test / Hpkg (push) Successful in 40s
Test / Hakurei (race detector) (push) Successful in 43s
Test / Sandbox (push) Successful in 1m56s
Test / Sandbox (race detector) (push) Successful in 2m39s
Test / Flake checks (push) Successful in 1m24s
This should never be used outside vm tests. Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
@@ -1,272 +0,0 @@
|
||||
//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
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
//go:build testtool
|
||||
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type F func(format string, v ...any)
|
||||
|
||||
func SwapPrint(f F) (old F) { old = printfFunc; printfFunc = f; return }
|
||||
func SwapFatal(f F) (old F) { old = fatalfFunc; fatalfFunc = f; return }
|
||||
|
||||
func MustWantFile(t *testing.T, v any) (wantFile string) {
|
||||
wantFile = path.Join(t.TempDir(), "want.json")
|
||||
if f, err := os.OpenFile(wantFile, os.O_CREATE|os.O_WRONLY, 0400); err != nil {
|
||||
t.Fatalf("cannot create %q: %v", wantFile, err)
|
||||
} else if err = json.NewEncoder(f).Encode(v); err != nil {
|
||||
t.Fatalf("cannot encode to %q: %v", wantFile, err)
|
||||
} else if err = f.Close(); err != nil {
|
||||
t.Fatalf("cannot close %q: %v", wantFile, err)
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
if err := os.Remove(wantFile); err != nil {
|
||||
t.Fatalf("cannot remove %q: %v", wantFile, err)
|
||||
}
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
//go:build testtool
|
||||
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrFSBadLength = errors.New("bad dir length")
|
||||
ErrFSBadData = errors.New("data differs")
|
||||
ErrFSBadMode = errors.New("mode differs")
|
||||
ErrFSInvalidEnt = errors.New("invalid entry condition")
|
||||
)
|
||||
|
||||
type FS struct {
|
||||
Mode fs.FileMode `json:"mode"`
|
||||
Dir map[string]*FS `json:"dir"`
|
||||
Data *string `json:"data"`
|
||||
}
|
||||
|
||||
func printDir(prefix string, dir []fs.DirEntry) {
|
||||
names := make([]string, len(dir))
|
||||
for i, ent := range dir {
|
||||
name := ent.Name()
|
||||
if ent.IsDir() {
|
||||
name += "/"
|
||||
}
|
||||
names[i] = fmt.Sprintf("%q", name)
|
||||
}
|
||||
printf("[FAIL] d %s: %s", prefix, strings.Join(names, " "))
|
||||
}
|
||||
|
||||
func (s *FS) Compare(prefix string, e fs.FS) error {
|
||||
if s.Data != nil {
|
||||
if s.Dir != nil {
|
||||
panic("invalid state")
|
||||
}
|
||||
panic("invalid compare call")
|
||||
}
|
||||
|
||||
if s.Dir == nil {
|
||||
printf("[ OK ] s %s", prefix)
|
||||
return nil
|
||||
}
|
||||
|
||||
var dir []fs.DirEntry
|
||||
if d, err := fs.ReadDir(e, prefix); err != nil {
|
||||
return err
|
||||
} else if len(d) != len(s.Dir) {
|
||||
printDir(prefix, d)
|
||||
return ErrFSBadLength
|
||||
} else {
|
||||
dir = d
|
||||
}
|
||||
|
||||
for _, got := range dir {
|
||||
name := got.Name()
|
||||
|
||||
if want, ok := s.Dir[name]; !ok {
|
||||
printDir(prefix, dir)
|
||||
return fs.ErrNotExist
|
||||
} else if want.Dir != nil && !got.IsDir() {
|
||||
printDir(prefix, dir)
|
||||
return ErrFSInvalidEnt
|
||||
} else {
|
||||
name = path.Join(prefix, name)
|
||||
|
||||
if fi, err := got.Info(); err != nil {
|
||||
return err
|
||||
} else if fi.Mode() != want.Mode {
|
||||
printf("[FAIL] m %s: %x, want %x",
|
||||
name, uint32(fi.Mode()), uint32(want.Mode))
|
||||
return ErrFSBadMode
|
||||
}
|
||||
|
||||
if want.Data != nil {
|
||||
if want.Dir != nil {
|
||||
panic("invalid state")
|
||||
}
|
||||
if v, err := fs.ReadFile(e, name); err != nil {
|
||||
return err
|
||||
} else if string(v) != *want.Data {
|
||||
printf("[FAIL] f %s", name)
|
||||
printf("got: %s", v)
|
||||
printf("want: %s", *want.Data)
|
||||
return ErrFSBadData
|
||||
}
|
||||
printf("[ OK ] f %s", name)
|
||||
} else if err := want.Compare(name, e); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
printf("[ OK ] d %s", prefix)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
//go:build testtool
|
||||
|
||||
package sandbox_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"hakurei.app/test/sandbox"
|
||||
)
|
||||
|
||||
var (
|
||||
fsPasswdSample = "u0_a20:x:65534:65534:Hakurei:/var/lib/persist/module/hakurei/u0/a20:/run/current-system/sw/bin/zsh"
|
||||
fsGroupSample = "hakurei:x:65534:"
|
||||
)
|
||||
|
||||
func TestCompare(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
||||
sample fstest.MapFS
|
||||
want *sandbox.FS
|
||||
wantOut string
|
||||
wantErr error
|
||||
}{
|
||||
{"skip", fstest.MapFS{}, &sandbox.FS{}, "[ OK ] s .\x00", nil},
|
||||
{"simple pass", fstest.MapFS{".hakurei": {Mode: 0x800001ed}},
|
||||
&sandbox.FS{Dir: map[string]*sandbox.FS{".hakurei": {Mode: 0x800001ed}}},
|
||||
"[ OK ] s .hakurei\x00[ OK ] d .\x00", nil},
|
||||
{"bad length", fstest.MapFS{".hakurei": {Mode: 0x800001ed}},
|
||||
&sandbox.FS{Dir: make(map[string]*sandbox.FS)},
|
||||
"[FAIL] d .: \".hakurei/\"\x00", sandbox.ErrFSBadLength},
|
||||
{"top level bad mode", fstest.MapFS{".hakurei": {Mode: 0x800001ed}},
|
||||
&sandbox.FS{Dir: map[string]*sandbox.FS{".hakurei": {Mode: 0xdeadbeef}}},
|
||||
"[FAIL] m .hakurei: 800001ed, want deadbeef\x00", sandbox.ErrFSBadMode},
|
||||
{"invalid entry condition", fstest.MapFS{"test": {Data: []byte{'0'}, Mode: 0644}},
|
||||
&sandbox.FS{Dir: map[string]*sandbox.FS{"test": {Dir: make(map[string]*sandbox.FS)}}},
|
||||
"[FAIL] d .: \"test\"\x00", sandbox.ErrFSInvalidEnt},
|
||||
{"nonexistent", fstest.MapFS{"test": {Data: []byte{'0'}, Mode: 0644}},
|
||||
&sandbox.FS{Dir: map[string]*sandbox.FS{".test": {}}},
|
||||
"[FAIL] d .: \"test\"\x00", fs.ErrNotExist},
|
||||
{"file", fstest.MapFS{"etc": {Mode: 0x800001c0},
|
||||
"etc/passwd": {Data: []byte(fsPasswdSample), Mode: 0644},
|
||||
"etc/group": {Data: []byte(fsGroupSample), Mode: 0644},
|
||||
}, &sandbox.FS{Dir: map[string]*sandbox.FS{"etc": {Mode: 0x800001c0, Dir: map[string]*sandbox.FS{
|
||||
"passwd": {Mode: 0x1a4, Data: &fsPasswdSample},
|
||||
"group": {Mode: 0x1a4, Data: &fsGroupSample},
|
||||
}}}}, "[ OK ] f etc/group\x00[ OK ] f etc/passwd\x00[ OK ] d etc\x00[ OK ] d .\x00", nil},
|
||||
{"file differ", fstest.MapFS{"etc": {Mode: 0x800001c0},
|
||||
"etc/passwd": {Data: []byte(fsPasswdSample), Mode: 0644},
|
||||
"etc/group": {Data: []byte(fsGroupSample), Mode: 0644},
|
||||
}, &sandbox.FS{Dir: map[string]*sandbox.FS{"etc": {Mode: 0x800001c0, Dir: map[string]*sandbox.FS{
|
||||
"passwd": {Mode: 0x1a4, Data: &fsGroupSample},
|
||||
"group": {Mode: 0x1a4, Data: &fsGroupSample},
|
||||
}}}}, "[ OK ] f etc/group\x00[FAIL] f etc/passwd\x00got: u0_a20:x:65534:65534:Hakurei:/var/lib/persist/module/hakurei/u0/a20:/run/current-system/sw/bin/zsh\x00want: hakurei:x:65534:\x00", sandbox.ErrFSBadData},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
gotOut := new(strings.Builder)
|
||||
oldPrint := sandbox.SwapPrint(func(format string, v ...any) { _, _ = fmt.Fprintf(gotOut, format+"\x00", v...) })
|
||||
t.Cleanup(func() { sandbox.SwapPrint(oldPrint) })
|
||||
|
||||
err := tc.want.Compare(".", tc.sample)
|
||||
if !errors.Is(err, tc.wantErr) {
|
||||
t.Errorf("Compare: error = %v; wantErr %v",
|
||||
err, tc.wantErr)
|
||||
}
|
||||
|
||||
if gotOut.String() != tc.wantOut {
|
||||
t.Errorf("Compare: output %q; want %q",
|
||||
gotOut, tc.wantOut)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
//go:build testtool
|
||||
|
||||
package sandbox
|
||||
|
||||
/*
|
||||
#cgo linux pkg-config: --static mount
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <libmount.h>
|
||||
|
||||
const char *HAKUREI_MOUNTINFO_PATH = "/proc/self/mountinfo";
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"iter"
|
||||
"runtime"
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMountinfoParse = errors.New("invalid mountinfo records")
|
||||
ErrMountinfoIter = errors.New("cannot allocate iterator")
|
||||
ErrMountinfoFault = errors.New("cannot iterate on filesystems")
|
||||
)
|
||||
|
||||
type (
|
||||
Mountinfo struct {
|
||||
mu sync.RWMutex
|
||||
p string
|
||||
err error
|
||||
|
||||
tb *C.struct_libmnt_table
|
||||
itr *C.struct_libmnt_iter
|
||||
|
||||
fs *C.struct_libmnt_fs
|
||||
}
|
||||
|
||||
// MountinfoEntry represents deterministic mountinfo parts of a libmnt_fs entry.
|
||||
MountinfoEntry struct {
|
||||
// mount ID: a unique ID for the mount (may be reused after umount(2)).
|
||||
ID int `json:"id"`
|
||||
// parent ID: the ID of the parent mount (or of self for the root of this mount namespace's mount tree).
|
||||
Parent int `json:"parent"`
|
||||
// root: the pathname of the directory in the filesystem which forms the root of this mount.
|
||||
Root string `json:"root"`
|
||||
// mount point: the pathname of the mount point relative to the process's root directory.
|
||||
Target string `json:"target"`
|
||||
// mount options: per-mount options (see mount(2)).
|
||||
VfsOptstr string `json:"vfs_optstr"`
|
||||
// filesystem type: the filesystem type in the form "type[.subtype]".
|
||||
FsType string `json:"fstype"`
|
||||
// mount source: filesystem-specific information or "none".
|
||||
Source string `json:"source"`
|
||||
// super options: per-superblock options (see mount(2)).
|
||||
FsOptstr string `json:"fs_optstr"`
|
||||
}
|
||||
)
|
||||
|
||||
func (m *Mountinfo) copy(v *MountinfoEntry) {
|
||||
if m.fs == nil {
|
||||
panic("invalid entry")
|
||||
}
|
||||
v.ID = int(C.mnt_fs_get_id(m.fs))
|
||||
v.Parent = int(C.mnt_fs_get_parent_id(m.fs))
|
||||
v.Root = C.GoString(C.mnt_fs_get_root(m.fs))
|
||||
v.Target = C.GoString(C.mnt_fs_get_target(m.fs))
|
||||
v.VfsOptstr = C.GoString(C.mnt_fs_get_vfs_options(m.fs))
|
||||
v.FsType = C.GoString(C.mnt_fs_get_fstype(m.fs))
|
||||
v.Source = C.GoString(C.mnt_fs_get_source(m.fs))
|
||||
v.FsOptstr = C.GoString(C.mnt_fs_get_fs_options(m.fs))
|
||||
}
|
||||
|
||||
func NewMountinfo(p string) *Mountinfo { m := new(Mountinfo); m.p = p; return m }
|
||||
|
||||
func (m *Mountinfo) Err() error { m.mu.RLock(); defer m.mu.RUnlock(); return m.err }
|
||||
|
||||
func (m *Mountinfo) Parse() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.tb != nil {
|
||||
panic("open called twice")
|
||||
}
|
||||
|
||||
if m.p == "" {
|
||||
m.tb = C.mnt_new_table_from_file(C.HAKUREI_MOUNTINFO_PATH)
|
||||
} else {
|
||||
name := C.CString(m.p)
|
||||
m.tb = C.mnt_new_table_from_file(name)
|
||||
C.free(unsafe.Pointer(name))
|
||||
}
|
||||
if m.tb == nil {
|
||||
return ErrMountinfoParse
|
||||
}
|
||||
m.itr = C.mnt_new_iter(C.MNT_ITER_FORWARD)
|
||||
if m.itr == nil {
|
||||
C.mnt_unref_table(m.tb)
|
||||
return ErrMountinfoIter
|
||||
}
|
||||
|
||||
runtime.SetFinalizer(m, (*Mountinfo).Unref)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Mountinfo) Unref() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.tb == nil {
|
||||
panic("unref called before parse")
|
||||
}
|
||||
|
||||
C.mnt_unref_table(m.tb)
|
||||
C.mnt_free_iter(m.itr)
|
||||
runtime.SetFinalizer(m, nil)
|
||||
}
|
||||
|
||||
func (m *Mountinfo) Entries() iter.Seq[*MountinfoEntry] {
|
||||
return func(yield func(*MountinfoEntry) bool) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
C.mnt_reset_iter(m.itr, -1)
|
||||
|
||||
var rc C.int
|
||||
ent := new(MountinfoEntry)
|
||||
for rc = C.mnt_table_next_fs(m.tb, m.itr, &m.fs); rc == 0; rc = C.mnt_table_next_fs(m.tb, m.itr, &m.fs) {
|
||||
m.copy(ent)
|
||||
if !yield(ent) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if rc < 0 {
|
||||
m.err = ErrMountinfoFault
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *MountinfoEntry) EqualWithIgnore(want *MountinfoEntry, ignore string) bool {
|
||||
return (e.ID == want.ID || want.ID == -1) &&
|
||||
(e.Parent == want.Parent || want.Parent == -1) &&
|
||||
(e.Root == want.Root || want.Root == ignore) &&
|
||||
(e.Target == want.Target || want.Target == ignore) &&
|
||||
(e.VfsOptstr == want.VfsOptstr || want.VfsOptstr == ignore) &&
|
||||
(e.FsType == want.FsType || want.FsType == ignore) &&
|
||||
(e.Source == want.Source || want.Source == ignore) &&
|
||||
(e.FsOptstr == want.FsOptstr || want.FsOptstr == ignore)
|
||||
}
|
||||
|
||||
func (e *MountinfoEntry) String() string {
|
||||
return fmt.Sprintf("%d %d %s %s %s %s %s %s",
|
||||
e.ID, e.Parent, e.Root, e.Target, e.VfsOptstr, e.FsType, e.Source, e.FsOptstr)
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
//go:build testtool
|
||||
|
||||
package sandbox_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/test/sandbox"
|
||||
)
|
||||
|
||||
func TestMountinfo(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
||||
sample string
|
||||
want []*sandbox.MountinfoEntry
|
||||
}{
|
||||
{"util-linux", `15 20 0:3 / /proc rw,relatime - proc /proc rw
|
||||
16 20 0:15 / /sys rw,relatime - sysfs /sys rw
|
||||
17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1983516k,nr_inodes=495879,mode=755
|
||||
18 17 0:10 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=000
|
||||
19 17 0:16 / /dev/shm rw,relatime - tmpfs tmpfs rw
|
||||
20 1 8:4 / / rw,noatime - ext3 /dev/sda4 rw,errors=continue,user_xattr,acl,barrier=0,data=ordered
|
||||
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755
|
||||
22 21 0:18 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd
|
||||
23 21 0:19 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuset
|
||||
24 21 0:20 / /sys/fs/cgroup/ns rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,ns
|
||||
25 21 0:21 / /sys/fs/cgroup/cpu rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpu
|
||||
26 21 0:22 / /sys/fs/cgroup/cpuacct rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuacct
|
||||
27 21 0:23 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,memory
|
||||
28 21 0:24 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,devices
|
||||
29 21 0:25 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,freezer
|
||||
30 21 0:26 / /sys/fs/cgroup/net_cls rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,net_cls
|
||||
31 21 0:27 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,blkio
|
||||
32 16 0:28 / /sys/kernel/security rw,relatime - autofs systemd-1 rw,fd=22,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
|
||||
33 17 0:29 / /dev/hugepages rw,relatime - autofs systemd-1 rw,fd=23,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
|
||||
34 16 0:30 / /sys/kernel/debug rw,relatime - autofs systemd-1 rw,fd=24,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
|
||||
35 15 0:31 / /proc/sys/fs/binfmt_misc rw,relatime - autofs systemd-1 rw,fd=25,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
|
||||
36 17 0:32 / /dev/mqueue rw,relatime - autofs systemd-1 rw,fd=26,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
|
||||
37 15 0:14 / /proc/bus/usb rw,relatime - usbfs /proc/bus/usb rw
|
||||
38 33 0:33 / /dev/hugepages rw,relatime - hugetlbfs hugetlbfs rw
|
||||
39 36 0:12 / /dev/mqueue rw,relatime - mqueue mqueue rw
|
||||
40 20 8:6 / /boot rw,noatime - ext3 /dev/sda6 rw,errors=continue,barrier=0,data=ordered
|
||||
41 20 253:0 / /home/kzak rw,noatime - ext4 /dev/mapper/kzak-home rw,barrier=1,data=ordered
|
||||
42 35 0:34 / /proc/sys/fs/binfmt_misc rw,relatime - binfmt_misc none rw
|
||||
43 16 0:35 / /sys/fs/fuse/connections rw,relatime - fusectl fusectl rw
|
||||
44 41 0:36 / /home/kzak/.gvfs rw,nosuid,nodev,relatime - fuse.gvfs-fuse-daemon gvfs-fuse-daemon rw,user_id=500,group_id=500
|
||||
45 20 0:37 / /var/lib/nfs/rpc_pipefs rw,relatime - rpc_pipefs sunrpc rw
|
||||
47 20 0:38 / /mnt/sounds rw,relatime - cifs //foo.home/bar/ rw,unc=\\foo.home\bar,username=kzak,domain=SRGROUP,uid=0,noforceuid,gid=0,noforcegid,addr=192.168.111.1,posixpaths,serverino,acl,rsize=16384,wsize=57344
|
||||
49 20 0:56 / /mnt/test/foobar rw,relatime,nosymfollow shared:323 - tmpfs tmpfs rw`, []*sandbox.MountinfoEntry{
|
||||
e(15, 20, "/", "/proc", "rw,relatime", "proc", "/proc", "rw"),
|
||||
e(16, 20, "/", "/sys", "rw,relatime", "sysfs", "/sys", "rw"),
|
||||
e(17, 20, "/", "/dev", "rw,relatime", "devtmpfs", "udev", "rw,size=1983516k,nr_inodes=495879,mode=755"),
|
||||
e(18, 17, "/", "/dev/pts", "rw,relatime", "devpts", "devpts", "rw,gid=5,mode=620,ptmxmode=000"),
|
||||
e(19, 17, "/", "/dev/shm", "rw,relatime", "tmpfs", "tmpfs", "rw"),
|
||||
e(20, 1, "/", "/", "rw,noatime", "ext3", "/dev/sda4", "rw,errors=continue,user_xattr,acl,barrier=0,data=ordered"),
|
||||
e(21, 16, "/", "/sys/fs/cgroup", "rw,nosuid,nodev,noexec,relatime", "tmpfs", "tmpfs", "rw,mode=755"),
|
||||
e(22, 21, "/", "/sys/fs/cgroup/systemd", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd"),
|
||||
e(23, 21, "/", "/sys/fs/cgroup/cpuset", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,cpuset"),
|
||||
e(24, 21, "/", "/sys/fs/cgroup/ns", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,ns"),
|
||||
e(25, 21, "/", "/sys/fs/cgroup/cpu", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,cpu"),
|
||||
e(26, 21, "/", "/sys/fs/cgroup/cpuacct", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,cpuacct"),
|
||||
e(27, 21, "/", "/sys/fs/cgroup/memory", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,memory"),
|
||||
e(28, 21, "/", "/sys/fs/cgroup/devices", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,devices"),
|
||||
e(29, 21, "/", "/sys/fs/cgroup/freezer", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,freezer"),
|
||||
e(30, 21, "/", "/sys/fs/cgroup/net_cls", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,net_cls"),
|
||||
e(31, 21, "/", "/sys/fs/cgroup/blkio", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,blkio"),
|
||||
e(32, 16, "/", "/sys/kernel/security", "rw,relatime", "autofs", "systemd-1", "rw,fd=22,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"),
|
||||
e(33, 17, "/", "/dev/hugepages", "rw,relatime", "autofs", "systemd-1", "rw,fd=23,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"),
|
||||
e(34, 16, "/", "/sys/kernel/debug", "rw,relatime", "autofs", "systemd-1", "rw,fd=24,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"),
|
||||
e(35, 15, "/", "/proc/sys/fs/binfmt_misc", "rw,relatime", "autofs", "systemd-1", "rw,fd=25,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"),
|
||||
e(36, 17, "/", "/dev/mqueue", "rw,relatime", "autofs", "systemd-1", "rw,fd=26,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"),
|
||||
e(37, 15, "/", "/proc/bus/usb", "rw,relatime", "usbfs", "/proc/bus/usb", "rw"),
|
||||
e(38, 33, "/", "/dev/hugepages", "rw,relatime", "hugetlbfs", "hugetlbfs", "rw"),
|
||||
e(39, 36, "/", "/dev/mqueue", "rw,relatime", "mqueue", "mqueue", "rw"),
|
||||
e(40, 20, "/", "/boot", "rw,noatime", "ext3", "/dev/sda6", "rw,errors=continue,barrier=0,data=ordered"),
|
||||
e(41, 20, "/", "/home/kzak", "rw,noatime", "ext4", "/dev/mapper/kzak-home", "rw,barrier=1,data=ordered"),
|
||||
e(42, 35, "/", "/proc/sys/fs/binfmt_misc", "rw,relatime", "binfmt_misc", "none", "rw"),
|
||||
e(43, 16, "/", "/sys/fs/fuse/connections", "rw,relatime", "fusectl", "fusectl", "rw"),
|
||||
e(44, 41, "/", "/home/kzak/.gvfs", "rw,nosuid,nodev,relatime", "fuse.gvfs-fuse-daemon", "gvfs-fuse-daemon", "rw,user_id=500,group_id=500"),
|
||||
e(45, 20, "/", "/var/lib/nfs/rpc_pipefs", "rw,relatime", "rpc_pipefs", "sunrpc", "rw"),
|
||||
e(47, 20, "/", "/mnt/sounds", "rw,relatime", "cifs", "//foo.home/bar/", "rw,unc=\\\\foo.home\\bar,username=kzak,domain=SRGROUP,uid=0,noforceuid,gid=0,noforcegid,addr=192.168.111.1,posixpaths,serverino,acl,rsize=16384,wsize=57344"),
|
||||
e(49, 20, "/", "/mnt/test/foobar", "rw,relatime,nosymfollow", "tmpfs", "tmpfs", "rw"),
|
||||
}},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
name := path.Join(t.TempDir(), "sample")
|
||||
if err := os.WriteFile(name, []byte(tc.sample), 0400); err != nil {
|
||||
t.Fatalf("cannot write sample: %v", err)
|
||||
}
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
m := sandbox.NewMountinfo(name)
|
||||
if err := m.Parse(); err != nil {
|
||||
t.Fatalf("Parse: error = %v", err)
|
||||
}
|
||||
|
||||
i := 0
|
||||
for ent := range m.Entries() {
|
||||
if i == len(tc.want) {
|
||||
t.Errorf("Entries: got more than %d entries", i)
|
||||
t.FailNow()
|
||||
}
|
||||
if !ent.EqualWithIgnore(tc.want[i], "\x00") {
|
||||
t.Errorf("Entries: entry %d\n got: %#v\nwant: %#v", i,
|
||||
ent, &tc.want[i])
|
||||
t.FailNow()
|
||||
} else {
|
||||
t.Logf("%s", ent)
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
if err := m.Err(); err != nil {
|
||||
t.Fatalf("Mountinfo: error = %v", err)
|
||||
}
|
||||
|
||||
m.Unref()
|
||||
})
|
||||
|
||||
if err := os.Remove(name); err != nil {
|
||||
t.Fatalf("cannot remove %q: %v", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func e(
|
||||
id, parent int, root, target, vfsOptstr string, fsType, source, fsOptstr string,
|
||||
) *sandbox.MountinfoEntry {
|
||||
return &sandbox.MountinfoEntry{
|
||||
ID: id,
|
||||
Parent: parent,
|
||||
Root: root,
|
||||
Target: target,
|
||||
VfsOptstr: vfsOptstr,
|
||||
FsType: fsType,
|
||||
Source: source,
|
||||
FsOptstr: fsOptstr,
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
//go:build testtool
|
||||
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
NULL = 0
|
||||
|
||||
PTRACE_ATTACH = 16
|
||||
PTRACE_DETACH = 17
|
||||
PTRACE_SECCOMP_GET_FILTER = 0x420c
|
||||
)
|
||||
|
||||
type ptraceError struct {
|
||||
op string
|
||||
errno syscall.Errno
|
||||
}
|
||||
|
||||
func (p *ptraceError) Error() string { return fmt.Sprintf("%s: %v", p.op, p.errno) }
|
||||
|
||||
func (p *ptraceError) Unwrap() error {
|
||||
if p.errno == 0 {
|
||||
return nil
|
||||
}
|
||||
return p.errno
|
||||
}
|
||||
|
||||
func ptrace(op uintptr, pid, addr int, data unsafe.Pointer) (r uintptr, errno syscall.Errno) {
|
||||
r, _, errno = syscall.Syscall6(syscall.SYS_PTRACE, op, uintptr(pid), uintptr(addr), uintptr(data), NULL, NULL)
|
||||
return
|
||||
}
|
||||
|
||||
func ptraceAttach(pid int) error {
|
||||
if _, errno := ptrace(PTRACE_ATTACH, pid, 0, nil); errno != 0 {
|
||||
return &ptraceError{"PTRACE_ATTACH", errno}
|
||||
}
|
||||
|
||||
var status syscall.WaitStatus
|
||||
for {
|
||||
if _, err := syscall.Wait4(pid, &status, syscall.WALL, nil); err != nil {
|
||||
if errors.Is(err, syscall.EINTR) {
|
||||
continue
|
||||
}
|
||||
fatalf("cannot waitpid: %v", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ptraceDetach(pid int) error {
|
||||
if _, errno := ptrace(PTRACE_DETACH, pid, 0, nil); errno != 0 {
|
||||
return &ptraceError{"PTRACE_DETACH", errno}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type sockFilter struct { /* Filter block */
|
||||
code uint16 /* Actual filter code */
|
||||
jt uint8 /* Jump true */
|
||||
jf uint8 /* Jump false */
|
||||
k uint32 /* Generic multiuse field */
|
||||
}
|
||||
|
||||
func getFilter[T comparable](pid, index int) ([]T, error) {
|
||||
if s := unsafe.Sizeof(*new(T)); s != 8 {
|
||||
panic(fmt.Sprintf("invalid filter block size %d", s))
|
||||
}
|
||||
|
||||
var buf []T
|
||||
if n, errno := ptrace(PTRACE_SECCOMP_GET_FILTER, pid, index, nil); errno != 0 {
|
||||
return nil, &ptraceError{"PTRACE_SECCOMP_GET_FILTER", errno}
|
||||
} else {
|
||||
buf = make([]T, n)
|
||||
}
|
||||
if _, errno := ptrace(PTRACE_SECCOMP_GET_FILTER, pid, index, unsafe.Pointer(&buf[0])); errno != 0 {
|
||||
return nil, &ptraceError{"PTRACE_SECCOMP_GET_FILTER", errno}
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
//go:build testtool
|
||||
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
/*
|
||||
#include <sys/quota.h>
|
||||
*/
|
||||
import "C"
|
||||
|
||||
func trySyscalls() error {
|
||||
testCases := []struct {
|
||||
name string
|
||||
errno syscall.Errno
|
||||
|
||||
trap, a1, a2, a3, a4, a5, a6 uintptr
|
||||
}{
|
||||
{"syslog", syscall.EPERM, syscall.SYS_SYSLOG, 0, NULL, NULL, NULL, NULL, NULL},
|
||||
{"acct", syscall.EPERM, syscall.SYS_ACCT, 0, NULL, NULL, NULL, NULL, NULL},
|
||||
{"quotactl", syscall.EPERM, syscall.SYS_QUOTACTL, C.Q_GETQUOTA, NULL, uintptr(os.Getuid()), NULL, NULL, NULL},
|
||||
{"add_key", syscall.EPERM, syscall.SYS_ADD_KEY, NULL, NULL, NULL, NULL, NULL, NULL},
|
||||
{"keyctl", syscall.EPERM, syscall.SYS_KEYCTL, NULL, NULL, NULL, NULL, NULL, NULL},
|
||||
{"request_key", syscall.EPERM, syscall.SYS_REQUEST_KEY, NULL, NULL, NULL, NULL, NULL, NULL},
|
||||
{"move_pages", syscall.EPERM, syscall.SYS_MOVE_PAGES, uintptr(os.Getpid()), NULL, NULL, NULL, NULL, NULL},
|
||||
{"mbind", syscall.EPERM, syscall.SYS_MBIND, NULL, NULL, NULL, NULL, NULL, NULL},
|
||||
{"get_mempolicy", syscall.EPERM, syscall.SYS_GET_MEMPOLICY, NULL, NULL, NULL, NULL, NULL, NULL},
|
||||
{"set_mempolicy", syscall.EPERM, syscall.SYS_SET_MEMPOLICY, NULL, NULL, NULL, NULL, NULL, NULL},
|
||||
{"migrate_pages", syscall.EPERM, syscall.SYS_MIGRATE_PAGES, NULL, NULL, NULL, NULL, NULL, NULL},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
if _, _, errno := syscall.Syscall6(tc.trap, tc.a1, tc.a2, tc.a3, tc.a4, tc.a5, tc.a6); errno != tc.errno {
|
||||
printf("[FAIL] %s: %v, want %v", tc.name, errno, tc.errno)
|
||||
return errno
|
||||
}
|
||||
printf("[ OK ] %s: %v", tc.name, tc.errno)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/test/sandbox"
|
||||
"hakurei.app/test/internal/sandbox"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -12,7 +12,7 @@ buildGoModule rec {
|
||||
|
||||
src = builtins.path {
|
||||
name = "${pname}-src";
|
||||
path = lib.cleanSource ../.;
|
||||
path = lib.cleanSource ../../.;
|
||||
filter = path: type: (type == "directory") || (type == "regular" && lib.hasSuffix ".go" path);
|
||||
};
|
||||
vendorHash = null;
|
||||
@@ -23,7 +23,7 @@ buildGoModule rec {
|
||||
nativeBuildInputs = [ pkg-config ];
|
||||
|
||||
preBuild = ''
|
||||
go mod init hakurei.app/test/sandbox >& /dev/null
|
||||
go mod init hakurei.app/test >& /dev/null
|
||||
'';
|
||||
|
||||
postInstall = ''
|
||||
|
||||
Reference in New Issue
Block a user