Commit e6ee85fd authored by Kirill Smelkov's avatar Kirill Smelkov

Merge branch 'master' into y/nodefs-cancel

* master:
  Replace defunct travis ci with github actions
  Rewrite defunct all.bash
  fuse: move parseFuseFd() to unbreak darwin build
  fuse: support special /dev/fd/N mountpoint
  fuse: refer to fs instead of nodefs & pathfs
  fuse: move fusermount logic to new callFusermount() helper
  fuse: add `SyncRead` flag to MountOptions to ensure in-order reads
  fs: allow inode number 1
  Run "go mod tidy"
  fs: fix Inode.IsDir logic
  fuse: fix deadlock in TestDeleteNotify
  fs: recreate kernelNodeIds & stableAttrs maps when they shrink
  fs: fix outdated comment in removeRef
parents 7e0334c3 3ab5d95a
name: CI
on:
push:
pull_request:
schedule:
- cron: '0 12 * * *' # Every day noon UTC
jobs:
build:
strategy:
matrix:
go:
- "1.13.x" # Ubuntu 20.04 LTS "focal"
- "1.15.x" # Debian 11 "Bullseye"
- "1.16.x" # Golang upstream stable
- "1.17.x" # Golang upstream stable
# Don't cancel everything when one Go version fails
fail-fast: false
runs-on: ubuntu-latest
steps:
- name: Install Go ${{ matrix.go }}
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go }}
- uses: actions/checkout@v2
with:
fetch-depth: 0 # Make "git describe" work
# CI platform specific setup steps happen here
- run: sudo apt-get install -qq fuse3 libssl-dev libfuse-dev
# Actual test steps are in all.bash
- run: ./all.bash
......@@ -14,3 +14,6 @@ example/bulkstat/bulkstat
example/zipfs/zipfs
# test binaries
*.test
benchmark/bulkstat.bin
benchmark/bulkstat/main
benchmark/cstatfs
sudo: required
# Ubuntu 18.04 "Bionic", https://docs.travis-ci.com/user/reference/bionic/
# Kernel 5.0.0-1026-gcp
dist: bionic
language: go
go_import_path: github.com/hanwen/go-fuse
go:
- 1.10.x
- 1.11.x
- 1.12.x
- 1.13.x
- 1.14.x
- 1.15.x
- master
env:
- GOMAXPROCS=1
# vvv = native `nproc`
- GOMAXPROCS=
matrix:
fast_finish: true
allow_failures:
- go: master
before_install:
- sudo apt-get install -qq pkg-config fuse
- sudo modprobe fuse
- sudo chmod 666 /dev/fuse
- sudo chown root:$USER /etc/fuse.conf
install:
- go get -t ./...
- go get -t -race ./...
# Travis CI has a no-output-timeout of 10 minutes.
# Set "go test -timeout" lower so we get proper backtraces
# on a hung test.
# The tests sometimes hang in a way that "go test -timeout"
# does not work anymore. Use the external "timeout" command
# as backup, triggering 1 minute later.
script:
- set -e # fail fast
- timeout -s QUIT -k 10s 90s go test -failfast -timeout 1m -p 1 -v ./fs
- timeout -s QUIT -k 10s 6m go test -failfast -timeout 5m -p 1 -v ./...
- set +e # restore
# GO-FUSE
[![Build Status](https://travis-ci.org/hanwen/go-fuse.svg?branch=master)](https://travis-ci.org/hanwen/go-fuse)
[![CI](https://github.com/hanwen/go-fuse/actions/workflows/ci.yml/badge.svg)](https://github.com/hanwen/go-fuse/actions/workflows/ci.yml)
[![GoDoc](https://godoc.org/github.com/hanwen/go-fuse?status.svg)](https://godoc.org/github.com/hanwen/go-fuse)
Go native bindings for the FUSE kernel module.
......
#!/bin/sh
set -eu
set -eux
for d in fuse fuse/nodefs fuse/pathfs fuse/test zipfs unionfs \
example/hello example/loopback example/zipfs \
example/multizip example/unionfs example/memfs \
example/autounionfs example/statfs ; \
do
go build -o /dev/null github.com/hanwen/go-fuse/${d}
done
# Everything must compile on Linux
go build ./...
# Not everything compiles on MacOS (try GOOS=darwin go build ./...).
# But our key packages should.
GOOS=darwin go build ./fuse/... ./fs/... ./example/loopback/...
for d in fuse zipfs unionfs fuse/test
do
(
cd $d
# Make sure it compiles on all platforms.
for GOOS in darwin linux ; do
export GOOS
go test -c -i github.com/hanwen/go-fuse/$d
done
echo "go test github.com/hanwen/go-fuse/$d"
go test github.com/hanwen/go-fuse/$d
echo "go test -race github.com/hanwen/go-fuse/$d"
go test -race github.com/hanwen/go-fuse/$d
)
done
for target in "clean" "install" ; do
for d in fuse fuse/nodefs fuse/pathfs fuse/test zipfs unionfs \
example/hello example/loopback example/zipfs \
example/multizip example/unionfs example/memfs \
example/autounionfs example/statfs ; \
do
if test "${target}" = "install" && test "${d}" = "fuse/test"; then
continue
fi
echo "go ${target} github.com/hanwen/go-fuse/${d}"
go ${target} github.com/hanwen/go-fuse/${d}
done
done
# Run the tests. Why the flags:
# -timeout 5m ... Get a backtrace on a hung test before the CI system kills us
# -p 1 .......... Run tests serially, which also means we get live output
# instead of per-package buffering.
# -count 1 ...... Disable result caching, so we can see flakey tests
go test -timeout 5m -p 1 -count 1 ./...
make -C benchmark
for d in benchmark
do
go test github.com/hanwen/go-fuse/benchmark -test.bench '.*' -test.cpu 1,2
done
go test ./benchmark -test.bench '.*' -test.cpu 1,2
......@@ -7,6 +7,7 @@ package fs
import (
"context"
"log"
"runtime/debug"
"sync"
"syscall"
"time"
......@@ -81,6 +82,12 @@ type rawBridge struct {
kernelNodeIds map[uint64]*Inode
// nextNodeID is the next free NodeID. Increment after copying the value.
nextNodeId uint64
// nodeCountHigh records the highest number of entries we had in the
// kernelNodeIds map.
// As the size of stableAttrs tracks kernelNodeIds (+- a few entries due to
// concurrent FORGETs, LOOKUPs, and the fixed NodeID 1), this is also a good
// estimate for stableAttrs.
nodeCountHigh int
files []*fileEntry
freeFiles []uint32
......@@ -201,6 +208,9 @@ func (b *rawBridge) addNewChild(parent *Inode, name string, child *Inode, file F
child.changeCounter++
b.kernelNodeIds[child.nodeId] = child
if len(b.kernelNodeIds) > b.nodeCountHigh {
b.nodeCountHigh = len(b.kernelNodeIds)
}
// Any node that might be there is overwritten - it is obsolete now
b.stableAttrs[id] = child
if file != nil {
......@@ -260,9 +270,6 @@ func NewNodeFS(root InodeEmbedder, opts *Options) fuse.RawFileSystem {
nextNodeId: 2, // the root node has nodeid 1
stableAttrs: make(map[StableAttr]*Inode),
}
if bridge.automaticIno == 1 {
bridge.automaticIno++
}
if bridge.automaticIno == 0 {
bridge.automaticIno = 1 << 63
......@@ -465,7 +472,47 @@ func (b *rawBridge) Create(cancel <-chan struct{}, input *fuse.CreateIn, name st
func (b *rawBridge) Forget(nodeid, nlookup uint64) {
n, _ := b.inode(nodeid, 0)
n.removeRef(nlookup, false)
forgotten, _ := n.removeRef(nlookup, false)
if forgotten {
b.compactMemory()
}
}
// compactMemory tries to free memory that was previously used by forgotten
// nodes.
//
// Maps do not free all memory when elements get deleted
// ( https://github.com/golang/go/issues/20135 ).
// As a workaround, we recreate our two big maps (stableAttrs & kernelNodeIds)
// every time they have shrunk dramatically (100 x smaller).
// In this case, `nodeCountHigh` is reset to the new (smaller) size.
func (b *rawBridge) compactMemory() {
b.mu.Lock()
if b.nodeCountHigh <= len(b.kernelNodeIds)*100 {
b.mu.Unlock()
return
}
tmpStableAttrs := make(map[StableAttr]*Inode, len(b.stableAttrs))
for i, v := range b.stableAttrs {
tmpStableAttrs[i] = v
}
b.stableAttrs = tmpStableAttrs
tmpKernelNodeIds := make(map[uint64]*Inode, len(b.kernelNodeIds))
for i, v := range b.kernelNodeIds {
tmpKernelNodeIds[i] = v
}
b.kernelNodeIds = tmpKernelNodeIds
b.nodeCountHigh = len(b.kernelNodeIds)
b.mu.Unlock()
// Run outside b.mu
debug.FreeOSMemory()
}
func (b *rawBridge) SetDebug(debug bool) {}
......
......@@ -201,3 +201,40 @@ func (n *testDeletedIno) Getattr(ctx context.Context, f FileHandle, out *fuse.At
// Otherwise EILSEQ
return syscall.EILSEQ
}
// TestIno1 tests that inode number 1 is allowed.
//
// We used to panic like this because inode number 1 was special:
//
// panic: using reserved ID 1 for inode number
//
func TestIno1(t *testing.T) {
rootNode := testIno1{}
mnt, _, clean := testMount(t, &rootNode, nil)
defer clean()
var st syscall.Stat_t
err := syscall.Stat(mnt+"/ino1", &st)
if err != nil {
t.Fatal(err)
}
if st.Ino != 1 {
t.Errorf("wrong inode number: want=1 have=%d", st.Ino)
}
}
type testIno1 struct {
Inode
}
func (fn *testIno1) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*Inode, syscall.Errno) {
if name != "ino1" {
return nil, syscall.ENOENT
}
stable := StableAttr{
Mode: syscall.S_IFREG,
Ino: 1,
}
child := fn.NewInode(ctx, &testIno1{}, stable)
return child, 0
}
......@@ -27,8 +27,8 @@ type StableAttr struct {
// The inode number must be unique among the currently live
// objects in the file system. It is used to communicate to
// the kernel about this file object. The values uint64(-1),
// and 1 are reserved. When using Ino==0, a unique, sequential
// the kernel about this file object. The value uint64(-1)
// is reserved. When using Ino==0, a unique, sequential
// number is assigned (starting at 2^63 by default) on Inode creation.
Ino uint64
......@@ -41,7 +41,7 @@ type StableAttr struct {
// Reserved returns if the StableAttr is using reserved Inode numbers.
func (i *StableAttr) Reserved() bool {
return i.Ino == 1 || i.Ino == ^uint64(0)
return i.Ino == ^uint64(0) // fuse.pollHackInode = ^uint64(0)
}
// Inode is a node in VFS tree. Inodes are one-to-one mapped to
......@@ -104,7 +104,7 @@ type Inode struct {
}
func (n *Inode) IsDir() bool {
return n.stableAttr.Mode&syscall.S_IFDIR != 0
return n.stableAttr.Mode&syscall.S_IFMT == syscall.S_IFDIR
}
func (n *Inode) embed() *Inode {
......@@ -390,8 +390,8 @@ func (n *Inode) removeRef(nlookup uint64, dropPersistence bool) (forgotten bool,
n.bridge.mu.Lock()
if n.lookupCount == 0 {
forgotten = true
// Dropping the node from inoMap guarantees that no new references to this node are
// handed out to the kernel, hence we can also safely delete it from nodeidMap.
// Dropping the node from stableAttrs guarantees that no new references to this node are
// handed out to the kernel, hence we can also safely delete it from kernelNodeIds.
delete(n.bridge.stableAttrs, n.stableAttr)
delete(n.bridge.kernelNodeIds, n.nodeId)
}
......
package fs
import (
"syscall"
"testing"
)
func TestInodeIsDir(t *testing.T) {
cases := []struct {
mode uint32
dir bool
}{
{syscall.S_IFBLK, false},
{syscall.S_IFCHR, false},
{syscall.S_IFDIR, true},
{syscall.S_IFIFO, false},
{syscall.S_IFLNK, false},
{syscall.S_IFREG, false},
{syscall.S_IFSOCK, false},
}
var i Inode
for _, c := range cases {
i.stableAttr.Mode = c.mode
if i.IsDir() != c.dir {
t.Errorf("wrong result for case %#v", c)
}
}
}
......@@ -77,17 +77,49 @@
// Higher level interfaces
//
// As said above this packages provides way to implement filesystems in terms of
// raw FUSE protocol. Additionally packages nodefs and pathfs provide ways to
// implement filesystem at higher levels:
// raw FUSE protocol.
//
// Package github.com/hanwen/go-fuse/fuse/nodefs provides way to implement
// filesystems in terms of inodes. This resembles kernel's idea of what a
// filesystem looks like.
// Package github.com/hanwen/go-fuse/v2/fs provides way to implement
// filesystems in terms of paths and/or inodes.
//
// Package github.com/hanwen/go-fuse/fuse/pathfs provides way to implement
// filesystems in terms of path names. Working with path names is somewhat
// easier compared to inodes, however renames can be racy. Do not use pathfs if
// you care about correctness.
// Mount styles
//
// The NewServer() handles mounting the filesystem, which
// involves opening `/dev/fuse` and calling the
// `mount(2)` syscall. The latter needs root permissions.
// This is handled in one of three ways:
//
// 1) go-fuse opens `/dev/fuse` and executes the `fusermount`
// setuid-root helper to call `mount(2)` for us. This is the default.
// Does not need root permissions but needs `fusermount` installed.
//
// 2) If `MountOptions.DirectMount` is set, go-fuse calls `mount(2)` itself.
// Needs root permissions, but works without `fusermount`.
//
// 3) If `mountPoint` has the magic `/dev/fd/N` syntax, it means that that a
// privileged parent process:
//
// * Opened /dev/fuse
//
// * Called mount(2) on a real mountpoint directory that we don't know about
//
// * Inherited the fd to /dev/fuse to us
//
// * Informs us about the fd number via /dev/fd/N
//
// This magic syntax originates from libfuse [1] and allows the FUSE server to
// run without any privileges and without needing `fusermount`, as the parent
// process performs all privileged operations.
//
// The "privileged parent" is usually a container manager like Singularity [2],
// but for testing, it can also be the `mount.fuse3` helper with the
// `drop_privileges,setuid=$USER` flags. Example below for gocryptfs:
//
// $ sudo mount.fuse3 "/usr/local/bin/gocryptfs#/tmp/cipher" /tmp/mnt -o drop_privileges,setuid=$USER
//
// [1] https://github.com/libfuse/libfuse/commit/64e11073b9347fcf9c6d1eea143763ba9e946f70
//
// [2] https://sylabs.io/guides/3.7/user-guide/bind_paths_and_mounts.html#fuse-mounts
package fuse
// Types for users to implement.
......@@ -165,6 +197,23 @@ type MountOptions struct {
// The filesystem is fully responsible for invalidating data cache.
ExplicitDataCacheControl bool
// SyncRead is off by default, which means that go-fuse enable the
// FUSE_CAP_ASYNC_READ capability.
// The kernel then submits multiple concurrent reads to service
// userspace requests and kernel readahead.
//
// Setting SyncRead disables the FUSE_CAP_ASYNC_READ capability.
// The kernel then only sends one read request per file handle at a time,
// and orders the requests by offset.
//
// This is useful if reading out of order or concurrently is expensive for
// (example: Amazon Cloud Drive).
//
// See the comment to FUSE_CAP_ASYNC_READ in
// https://github.com/libfuse/libfuse/blob/master/include/fuse_common.h
// for more details.
SyncRead bool
// If set, fuse will first attempt to use syscall.Mount instead of
// fusermount to mount the filesystem. This will not update /etc/mtab
// but might be needed if fusermount is not available.
......
......@@ -68,18 +68,12 @@ func mountDirect(mountPoint string, opts *MountOptions, ready chan<- error) (fd
return
}
// Create a FUSE FS on the specified mount point. The returned
// mount point is always absolute.
func mount(mountPoint string, opts *MountOptions, ready chan<- error) (fd int, err error) {
if opts.DirectMount {
fd, err := mountDirect(mountPoint, opts, ready)
if err == nil {
return fd, nil
} else if opts.Debug {
log.Printf("mount: failed to do direct mount: %s", err)
}
}
// callFusermount calls the `fusermount` suid helper with the right options so
// that it:
// * opens `/dev/fuse`
// * mount()s this file descriptor to `mountPoint`
// * passes this file descriptor back to use via a unix domain socket
func callFusermount(mountPoint string, opts *MountOptions) (fd int, err error) {
local, remote, err := unixgramSocketpair()
if err != nil {
return
......@@ -121,11 +115,39 @@ func mount(mountPoint string, opts *MountOptions, ready chan<- error) (fd int, e
return -1, err
}
return
}
// Create a FUSE FS on the specified mount point. The returned
// mount point is always absolute.
func mount(mountPoint string, opts *MountOptions, ready chan<- error) (fd int, err error) {
if opts.DirectMount {
fd, err := mountDirect(mountPoint, opts, ready)
if err == nil {
return fd, nil
} else if opts.Debug {
log.Printf("mount: failed to do direct mount: %s", err)
}
}
// Magic `/dev/fd/N` mountpoint. See the docs for NewServer() for how this
// works.
fd = parseFuseFd(mountPoint)
if fd >= 0 {
if opts.Debug {
log.Printf("mount: magic mountpoint %q, using fd %d", mountPoint, fd)
}
} else {
// Usual case: mount via the `fusermount` suid helper
fd, err = callFusermount(mountPoint, opts)
if err != nil {
return
}
}
// golang sets CLOEXEC on file descriptors when they are
// acquired through normal operations (e.g. open).
// Buf for fd, we have to set CLOEXEC manually
syscall.CloseOnExec(fd)
close(ready)
return fd, err
}
......
package fuse
import (
"fmt"
"io/ioutil"
"syscall"
"testing"
)
// 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, err := ioutil.TempDir("", t.Name())
if err != nil {
t.Fatal(err)
}
defer syscall.Rmdir(realMountPoint)
// 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: true,
}
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)
}
}
......@@ -98,6 +98,10 @@ func doInit(server *Server, req *request) {
if server.opts.EnableAcl {
server.kernelSettings.Flags |= CAP_POSIX_ACL
}
if server.opts.SyncRead {
// Clear CAP_ASYNC_READ
server.kernelSettings.Flags &= ^uint32(CAP_ASYNC_READ)
}
dataCacheMode := input.Flags & CAP_AUTO_INVAL_DATA
if server.opts.ExplicitDataCacheControl {
......
......@@ -9,8 +9,10 @@ import (
"log"
"math"
"os"
"path"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"syscall"
......@@ -108,10 +110,20 @@ func (ms *Server) RecordLatencies(l LatencyMap) {
// Unmount calls fusermount -u on the mount. This has the effect of
// shutting down the filesystem. After the Server is unmounted, it
// should be discarded.
//
// Does not work when we were mounted with the magic /dev/fd/N mountpoint syntax,
// as we do not know the real mountpoint. Unmount using
//
// fusermount -u /path/to/real/mountpoint
//
/// in this case.
func (ms *Server) Unmount() (err error) {
if ms.mountPoint == "" {
return nil
}
if parseFuseFd(ms.mountPoint) >= 0 {
return fmt.Errorf("Cannot unmount magic mountpoint %q. Please use `fusermount -u REALMOUNTPOINT` instead.", ms.mountPoint)
}
delay := time.Duration(0)
for try := 0; try < 5; try++ {
err = unmount(ms.mountPoint, ms.opts)
......@@ -134,7 +146,11 @@ func (ms *Server) Unmount() (err error) {
return err
}
// NewServer creates a server and attaches it to the given directory.
// NewServer creates a FUSE server and attaches ("mounts") it to the
// `mountPoint` directory.
//
// See the "Mount styles" section in the package documentation if you want to
// know about the inner workings of the mount process. Usually you do not.
func NewServer(fs RawFileSystem, mountPoint string, opts *MountOptions) (*Server, error) {
if opts == nil {
opts = &MountOptions{
......@@ -870,5 +886,24 @@ func (ms *Server) WaitMount() error {
if err != nil {
return err
}
if parseFuseFd(ms.mountPoint) >= 0 {
// Magic `/dev/fd/N` mountpoint. We don't know the real mountpoint, so
// we cannot run the poll hack.
return nil
}
return pollHack(ms.mountPoint)
}
// parseFuseFd checks if `mountPoint` is the special form /dev/fd/N (with N >= 0),
// and returns N in this case. Returns -1 otherwise.
func parseFuseFd(mountPoint string) (fd int) {
dir, file := path.Split(mountPoint)
if dir != "/dev/fd/" {
return -1
}
fd, err := strconv.Atoi(file)
if err != nil || fd <= 0 {
return -1
}
return fd
}
......@@ -81,8 +81,7 @@ func TestDeleteNotify(t *testing.T) {
return
}
buf := bytes.Buffer{}
cmd := exec.Command("/usr/bin/tail", "-f", "testfile")
cmd.Dir = mnt + "/testdir"
cmd := exec.Command("/usr/bin/tail", "-f", mnt+"/testdir/testfile")
cmd.Stdin = &buf
cmd.Stdout = &bytes.Buffer{}
cmd.Stderr = os.Stderr
......
module github.com/hanwen/go-fuse/v2
require (
github.com/hanwen/go-fuse v1.0.0
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522
......
github.com/hanwen/go-fuse v1.0.0 h1:GxS9Zrn6c35/BnfiVsZVWmsG803xwE7eVRDvcf/BEVc=
github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs=
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment