Files
go-fuse/fuse/mount_linux_test.go

264 lines
7.2 KiB
Go

package fuse
import (
"fmt"
"os"
"syscall"
"testing"
"github.com/hanzoai/go-fuse/v2/internal/testutil"
"github.com/moby/sys/mountinfo"
)
// TestMountDevFd tests the special `/dev/fd/N` mountpoint syntax, where a
// privileged parent process opens /dev/fuse and calls mount() for us.
//
// In this test, we simulate a privileged parent by using the `fusermount` suid
// helper.
func TestMountDevFd(t *testing.T) {
realMountPoint := t.TempDir()
// Call the fusermount suid helper to obtain the file descriptor in place
// of a privileged parent.
var fuOpts MountOptions
fd, err := callFusermount(realMountPoint, &fuOpts)
if err != nil {
t.Fatal(err)
}
fdMountPoint := fmt.Sprintf("/dev/fd/%d", fd)
// Real test starts here:
// See if we can feed fdMountPoint to NewServer
fs := NewDefaultRawFileSystem()
opts := MountOptions{
Debug: testutil.VerboseTest(),
}
srv, err := NewServer(fs, fdMountPoint, &opts)
if err != nil {
t.Fatal(err)
}
go srv.Serve()
if err := srv.WaitMount(); err != nil {
t.Fatal(err)
}
// If we are actually mounted, we should get ENOSYS.
//
// This won't deadlock despite pollHack not working for `/dev/fd/N` mounts
// because functions in the syscall package don't use the poller.
var st syscall.Stat_t
err = syscall.Stat(realMountPoint, &st)
if err != syscall.ENOSYS {
t.Errorf("expected ENOSYS, got %v", err)
}
// Cleanup is somewhat tricky because `srv` does not know about
// `realMountPoint`, so `srv.Unmount()` cannot work.
//
// A normal user has to call `fusermount -u` for themselves to unmount.
// But in this test we can monkey-patch `srv.mountPoint`.
srv.mountPoint = realMountPoint
if err := srv.Unmount(); err != nil {
t.Error(err)
}
}
// TestMountMaxWrite makes sure that mounting works with all MaxWrite settings.
// We used to fail with EINVAL below 8k because readPool got too small.
func TestMountMaxWrite(t *testing.T) {
opts := []MountOptions{
{MaxWrite: 0}, // go-fuse default
{MaxWrite: 1},
{MaxWrite: 123},
{MaxWrite: 1 * 1024},
{MaxWrite: 4 * 1024},
{MaxWrite: 8 * 1024},
{MaxWrite: 64 * 1024}, // go-fuse default
{MaxWrite: 128 * 1024}, // limit in Linux v4.19 and older
{MaxWrite: 999 * 1024},
{MaxWrite: 1024 * 1024}, // limit in Linux v4.20+
}
for _, o := range opts {
name := fmt.Sprintf("MaxWrite%d", o.MaxWrite)
t.Run(name, func(t *testing.T) {
mnt := t.TempDir()
fs := NewDefaultRawFileSystem()
srv, err := NewServer(fs, mnt, &o)
if err != nil {
t.Error(err)
} else {
go srv.Serve()
srv.Unmount()
}
})
}
}
// mountCheckOptions mounts a defaultRawFileSystem and extracts the resulting effective
// mount options from /proc/self/mounts.
// The mount options are a comma-separated string like this:
// rw,nosuid,nodev,relatime,user_id=1026,group_id=1026
func mountCheckOptions(t *testing.T, opts MountOptions) (info mountinfo.Info) {
mnt := t.TempDir()
fs := NewDefaultRawFileSystem()
srv, err := NewServer(fs, mnt, &opts)
if err != nil {
t.Fatal(err)
}
// Check mount options
mounts, err := mountinfo.GetMounts(mountinfo.SingleEntryFilter(mnt))
if err != nil {
t.Error(err)
}
if len(mounts) != 1 {
t.Errorf("Could not find mountpoint %q in /proc/self/mountinfo", mnt)
}
orig := *mounts[0]
if testing.Verbose() {
t.Logf("full mountinfo: %#v", orig)
}
// We are only interested in some fields, as the others are arbitrary id numbers
// or contain random strings like "/tmp/TestDirectMount1126361240".
//
// What are all those fields: Look for "/proc/[pid]/mountinfo" in
// https://man7.org/linux/man-pages/man5/proc.5.html .
info = mountinfo.Info{
Options: orig.Options,
Source: orig.Source,
FSType: orig.FSType,
VFSOptions: orig.VFSOptions,
}
// server needs to run for Unmount to work
go srv.Serve()
err = srv.Unmount()
if err != nil {
t.Error(err)
}
return info
}
// TestDirectMount checks that DirectMount and DirectMountStrict work and show the
// same effective mount options in /proc/self/mounts
func TestDirectMount(t *testing.T) {
optsTable := []MountOptions{
{},
{AllowOther: true},
{MaxWrite: 9999},
{FsName: "aaa"},
{Name: "bbb"},
{FsName: "ccc", Name: "ddd"},
{FsName: "a,b"},
{FsName: `a\b`},
{FsName: `a\,b`},
{Options: []string{"exec"}},
{Options: []string{"noexec"}},
{Options: []string{"exec", "noexec"}},
{Options: []string{"noexec", "exec"}},
}
for _, opts := range optsTable {
opts.Debug = testutil.VerboseTest()
// Without DirectMount - i.e. using fusermount
o1 := mountCheckOptions(t, opts)
// With DirectMount
opts.DirectMount = true
o2 := mountCheckOptions(t, opts)
if o2 != o1 {
t.Errorf(`DirectMount effective mount options mismatch:
DirectMount: %#v
fusermount: %#v`, o2, o1)
// When this already fails then DirectMountStrict will fail the same way.
// Skip it for less noise in the logs.
continue
}
if os.Geteuid() == 0 {
// With DirectMountStrict
opts.DirectMountStrict = true
o3 := mountCheckOptions(t, opts)
if o3 != o1 {
t.Errorf(`DirectMountStrict effective mount options mismatch:
DirectMountStrict: %#v
fusermount: %#v`, o3, o1)
}
}
}
}
// TestDirectMountDevSuid checks that [no]dev/suid works with DirectMount and DirectMountStrict
// and show the same effective mount options in /proc/self/mounts
func TestDirectMountDevSuid(t *testing.T) {
if os.Geteuid() != 0 {
t.Skip("this test requires root permissions")
}
optsTable := []MountOptions{
{},
{Options: []string{"dev"}},
{Options: []string{"dev", "nodev"}},
{Options: []string{"nodev", "dev"}},
{Options: []string{"suid"}},
{Options: []string{"suid", "nosuid"}},
{Options: []string{"nosuid", "suid"}},
{Options: []string{"dev", "suid"}},
{Options: []string{"nodev", "nosuid"}},
}
for _, opts := range optsTable {
opts.Debug = testutil.VerboseTest()
// Without DirectMount - i.e. using fusermount
o1 := mountCheckOptions(t, opts)
// With DirectMount
opts.DirectMount = true
o2 := mountCheckOptions(t, opts)
if o2 != o1 {
t.Errorf(`DirectMount effective mount options mismatch:
DirectMount: %#v
fusermount: %#v`, o2, o1)
// When this already fails then DirectMountStrict will fail the same way.
// Skip it for less noise in the logs.
continue
}
// With DirectMountStrict
opts.DirectMountStrict = true
o3 := mountCheckOptions(t, opts)
if o3 != o1 {
t.Errorf(`DirectMountStrict effective mount options mismatch:
DirectMountStrict: %#v
fusermount: %#v`, o3, o1)
}
}
}
// TestEscapedMountOption tests that fusermount doesn't exit when when using commas or backslashs in options.
// It also tests that commas or backslashs in options are correctly propagated to /proc/mounts.
func TestEscapedMountOption(t *testing.T) {
fsname := `fsname,with\,many,comm\as,and\backsl\\ashs`
opts := &MountOptions{
FsName: fsname,
}
mnt := t.TempDir()
fs := NewDefaultRawFileSystem()
srv, err := NewServer(fs, mnt, opts)
if err != nil {
t.Error(err)
}
go srv.Serve()
defer srv.Unmount()
mounts, err := mountinfo.GetMounts(mountinfo.SingleEntryFilter(mnt))
if err != nil {
t.Fatal(err)
}
if len(mounts) != 1 {
t.Fatalf("Could not find mountpoint %q in /proc/self/mountinfo", mnt)
}
m := *mounts[0]
if m.Source != fsname {
t.Errorf("mountinfo(%q): got %q want %q", mnt, m.Source, fsname)
}
}