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

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.

Co-authored-by: Vitaliy Filippov <vitalif@yourcmc.ru>
zerocopy-examples
Vitaliy Filippov 2021-10-19 19:50:09 +03:00 committed by GitHub
parent 95fc8d1181
commit c75d3f26fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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
}