diff --git a/mount.go b/mount.go index dce72a8..543f55b 100644 --- a/mount.go +++ b/mount.go @@ -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 +} diff --git a/mount_darwin.go b/mount_darwin.go index 362cf94..8558acf 100644 --- a/mount_darwin.go +++ b/mount_darwin.go @@ -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) diff --git a/mount_linux.go b/mount_linux.go index 182a370..34eefb2 100644 --- a/mount_linux.go +++ b/mount_linux.go @@ -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 }