Use newer mounting method (similar to fusermount) with macfuse 4.x

macfuse 4.x turns out to be incompatible with the old mounting method where
you open the device by yourself and only supports the newer method where you
receive a file descriptor from `mount_macfuse` through a unix socket.
geesefs-0-30-9
Vitaliy Filippov 2021-09-30 18:26:31 +03:00
parent 575b70f3fd
commit ec521aa7b7
3 changed files with 160 additions and 113 deletions

View File

@ -17,7 +17,10 @@ package fuse
import (
"context"
"fmt"
"net"
"os"
"os/exec"
"syscall"
)
// Server is an interface for any type that knows how to serve ops read from a
@ -93,3 +96,82 @@ func Mount(
return mfs, nil
}
func fusermount(binary string, argv []string, additionalEnv []string, wait bool) (*os.File, error) {
// Create a socket pair.
fds, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0)
if err != nil {
return nil, fmt.Errorf("Socketpair: %v", err)
}
// Wrap the sockets into os.File objects that we will pass off to fusermount.
writeFile := os.NewFile(uintptr(fds[0]), "fusermount-child-writes")
defer writeFile.Close()
readFile := os.NewFile(uintptr(fds[1]), "fusermount-parent-reads")
defer readFile.Close()
// Start fusermount/mount_macfuse/mount_osxfuse.
cmd := exec.Command(binary, argv...)
cmd.Env = append(os.Environ(), "_FUSE_COMMFD=3")
cmd.Env = append(cmd.Env, additionalEnv...)
cmd.ExtraFiles = []*os.File{writeFile}
cmd.Stderr = os.Stderr
// Run the command.
if wait {
err = cmd.Run()
} else {
err = cmd.Start()
}
if err != nil {
return nil, fmt.Errorf("running %v: %v", binary, err)
}
// Wrap the socket file in a connection.
c, err := net.FileConn(readFile)
if err != nil {
return nil, fmt.Errorf("FileConn: %v", err)
}
defer c.Close()
// We expect to have a Unix domain socket.
uc, ok := c.(*net.UnixConn)
if !ok {
return nil, fmt.Errorf("Expected UnixConn, got %T", c)
}
// Read a message.
buf := make([]byte, 32) // expect 1 byte
oob := make([]byte, 32) // expect 24 bytes
_, oobn, _, _, err := uc.ReadMsgUnix(buf, oob)
if err != nil {
return nil, fmt.Errorf("ReadMsgUnix: %v", err)
}
// Parse the message.
scms, err := syscall.ParseSocketControlMessage(oob[:oobn])
if err != nil {
return nil, fmt.Errorf("ParseSocketControlMessage: %v", err)
}
// We expect one message.
if len(scms) != 1 {
return nil, fmt.Errorf("expected 1 SocketControlMessage; got scms = %#v", scms)
}
scm := scms[0]
// Pull out the FD returned by fusermount
gotFds, err := syscall.ParseUnixRights(&scm)
if err != nil {
return nil, fmt.Errorf("syscall.ParseUnixRights: %v", err)
}
if len(gotFds) != 1 {
return nil, fmt.Errorf("wanted 1 fd; got %#v", gotFds)
}
// Turn the FD into an os.File.
return os.NewFile(uintptr(gotFds[0]), "/dev/fuse"), nil
}

View File

@ -40,6 +40,10 @@ type osxfuseInstallation struct {
// Environment variable used to pass the "called by library" flag.
LibVar string
// Open device manually (false) or receive the FD through a UNIX socket,
// like with fusermount (true)
UseCommFD bool
}
var (
@ -51,6 +55,7 @@ var (
Mount: "/Library/Filesystems/macfuse.fs/Contents/Resources/mount_macfuse",
DaemonVar: "_FUSE_DAEMON_PATH",
LibVar: "_FUSE_CALL_BY_LIB",
UseCommFD: true,
},
// v3
@ -106,6 +111,36 @@ func openOSXFUSEDev(devPrefix string) (dev *os.File, err error) {
}
}
func convertMountArgs(daemonVar string, libVar string,
cfg *MountConfig) ([]string, []string, error) {
// The mount helper doesn't understand any escaping.
for k, v := range cfg.toMap() {
if strings.Contains(k, ",") || strings.Contains(v, ",") {
return nil, nil, fmt.Errorf(
"mount options cannot contain commas on darwin: %q=%q",
k,
v)
}
}
env := []string{ libVar+"=" }
if daemonVar != "" {
env = append(env, daemonVar+"="+os.Args[0])
}
argv := []string{
"-o", cfg.toOptionsString(),
// Tell osxfuse-kext how large our buffer is. It must split
// writes larger than this into multiple writes.
//
// OSXFUSE seems to ignore InitResponse.MaxWrite, and uses
// this instead.
"-o", "iosize="+strconv.FormatUint(buffer.MaxWriteSize, 10),
}
return argv, env, nil
}
func callMount(
bin string,
daemonVar string,
@ -115,39 +150,21 @@ func callMount(
dev *os.File,
ready chan<- error) error {
// The mount helper doesn't understand any escaping.
for k, v := range cfg.toMap() {
if strings.Contains(k, ",") || strings.Contains(v, ",") {
return fmt.Errorf(
"mount options cannot contain commas on darwin: %q=%q",
k,
v)
}
argv, env, err := convertMountArgs(daemonVar, libVar, cfg)
if err != nil {
return err
}
// Call the mount helper, passing in the device file and saving output into a
// buffer.
cmd := exec.Command(
bin,
"-o", cfg.toOptionsString(),
// Tell osxfuse-kext how large our buffer is. It must split
// writes larger than this into multiple writes.
//
// OSXFUSE seems to ignore InitResponse.MaxWrite, and uses
// this instead.
"-o", "iosize="+strconv.FormatUint(buffer.MaxWriteSize, 10),
argv = append(argv,
// refers to fd passed in cmd.ExtraFiles
"3",
dir,
)
cmd := exec.Command(bin, argv...)
cmd.ExtraFiles = []*os.File{dev}
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, libVar+"=")
daemon := os.Args[0]
if daemonVar != "" {
cmd.Env = append(cmd.Env, daemonVar+"="+daemon)
}
cmd.Env = env
var buf bytes.Buffer
cmd.Stdout = &buf
@ -174,6 +191,23 @@ func callMount(
return nil
}
func callMountCommFD(
bin string,
daemonVar string,
libVar string,
dir string,
cfg *MountConfig) (*os.File, error) {
argv, env, err := convertMountArgs(daemonVar, libVar, cfg)
if err != nil {
return nil, err
}
env = append(env, "_FUSE_COMMVERS=2")
argv = append(argv, dir)
return fusermount(bin, argv, env, false)
}
// Begin the process of mounting at the given directory, returning a connection
// to the kernel. Mounting continues in the background, and is complete when an
// error is written to the supplied channel. The file system may need to
@ -189,6 +223,16 @@ func mount(
continue
}
if loc.UseCommFD {
// Call the mount binary with the device.
ready <- nil
dev, err = callMountCommFD(loc.Mount, loc.DaemonVar, loc.LibVar, dir, cfg)
if err != nil {
return nil, fmt.Errorf("callMount: %v", err)
}
return
}
// Open the device.
dev, err = openOSXFUSEDev(loc.DevicePrefix)

View File

@ -1,10 +1,8 @@
package fuse
import (
"bytes"
"errors"
"fmt"
"net"
"os"
"os/exec"
"syscall"
@ -23,92 +21,6 @@ func findFusermount() (string, error) {
return path, nil
}
func fusermount(dir string, cfg *MountConfig) (*os.File, error) {
// Create a socket pair.
fds, err := syscall.Socketpair(syscall.AF_FILE, syscall.SOCK_STREAM, 0)
if err != nil {
return nil, fmt.Errorf("Socketpair: %v", err)
}
// Wrap the sockets into os.File objects that we will pass off to fusermount.
writeFile := os.NewFile(uintptr(fds[0]), "fusermount-child-writes")
defer writeFile.Close()
readFile := os.NewFile(uintptr(fds[1]), "fusermount-parent-reads")
defer readFile.Close()
// Start fusermount, passing it a buffer in which to write stderr.
var stderr bytes.Buffer
fusermount, err := findFusermount()
if err != nil {
return nil, err
}
cmd := exec.Command(
fusermount,
"-o", cfg.toOptionsString(),
"--",
dir,
)
cmd.Env = append(os.Environ(), "_FUSE_COMMFD=3")
cmd.ExtraFiles = []*os.File{writeFile}
cmd.Stderr = &stderr
// Run the command.
err = cmd.Run()
if err != nil {
return nil, fmt.Errorf("running fusermount: %v\n\nstderr:\n%s", err, stderr.Bytes())
}
// Wrap the socket file in a connection.
c, err := net.FileConn(readFile)
if err != nil {
return nil, fmt.Errorf("FileConn: %v", err)
}
defer c.Close()
// We expect to have a Unix domain socket.
uc, ok := c.(*net.UnixConn)
if !ok {
return nil, fmt.Errorf("Expected UnixConn, got %T", c)
}
// Read a message.
buf := make([]byte, 32) // expect 1 byte
oob := make([]byte, 32) // expect 24 bytes
_, oobn, _, _, err := uc.ReadMsgUnix(buf, oob)
if err != nil {
return nil, fmt.Errorf("ReadMsgUnix: %v", err)
}
// Parse the message.
scms, err := syscall.ParseSocketControlMessage(oob[:oobn])
if err != nil {
return nil, fmt.Errorf("ParseSocketControlMessage: %v", err)
}
// We expect one message.
if len(scms) != 1 {
return nil, fmt.Errorf("expected 1 SocketControlMessage; got scms = %#v", scms)
}
scm := scms[0]
// Pull out the FD returned by fusermount
gotFds, err := syscall.ParseUnixRights(&scm)
if err != nil {
return nil, fmt.Errorf("syscall.ParseUnixRights: %v", err)
}
if len(gotFds) != 1 {
return nil, fmt.Errorf("wanted 1 fd; got %#v", gotFds)
}
// Turn the FD into an os.File.
return os.NewFile(uintptr(gotFds[0]), "/dev/fuse"), nil
}
func enableFunc(flag uintptr) func(uintptr) uintptr {
return func(v uintptr) uintptr {
return v | flag
@ -198,7 +110,16 @@ func mount(dir string, cfg *MountConfig, ready chan<- error) (*os.File, error) {
// have the CAP_SYS_ADMIN capability.
dev, err := directmount(dir, cfg)
if err == errFallback {
return fusermount(dir, cfg)
fusermountPath, err := findFusermount()
if err != nil {
return nil, err
}
argv := []string{
"-o", cfg.toOptionsString(),
"--",
dir,
}
return fusermount(fusermountPath, argv, []string{}, true)
}
return dev, err
}