mirror of
https://github.com/hanzoai/go-fuse.git
synced 2026-10-30 14:46:18 +00:00
264 lines
7.2 KiB
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)
|
|
}
|
|
}
|