mirror of
https://github.com/restic/restic.git
synced 2026-06-20 15:44:17 +00:00
Merge pull request #5348 from zmanda/fix-gh-5234-prevent-mount-command-over-repository
mount: prevent mounting over repository
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
Bugfix: Prevent mounting over the repository directory
|
||||
|
||||
Using a local repository directory as the `mount` target — or a path
|
||||
that contains it, or that it contains — caused the FUSE server to
|
||||
read its own backend files through the new mount, deadlocking the
|
||||
kernel and requiring a long reboot to recover.
|
||||
|
||||
Restic now resolves both paths and refuses any such overlap with a
|
||||
clear error before mounting.
|
||||
|
||||
https://github.com/restic/restic/issues/5234
|
||||
https://github.com/restic/restic/pull/5348
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -13,6 +14,8 @@ import (
|
||||
"github.com/spf13/pflag"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/restic/restic/internal/backend/local"
|
||||
"github.com/restic/restic/internal/backend/location"
|
||||
"github.com/restic/restic/internal/data"
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
@@ -149,6 +152,19 @@ func runMount(ctx context.Context, opts MountOptions, gopts global.Options, args
|
||||
return errors.Fatal("inaccessible mountpoint")
|
||||
}
|
||||
|
||||
// Refuse to mount onto (or under, or over) the local repository directory.
|
||||
// Doing so makes the FUSE server read its own backend files through the
|
||||
// mount it just created, deadlocking the kernel (GH #5234).
|
||||
loc, err := location.Parse(gopts.Backends, gopts.Repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if loc.Scheme == "local" {
|
||||
if err := checkMountpointOverlap(loc.Config.(*local.Config).Path, mountpoint); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
debug.Log("start mount")
|
||||
defer debug.Log("finish mount")
|
||||
|
||||
@@ -230,3 +246,59 @@ func runMount(ctx context.Context, opts MountOptions, gopts global.Options, args
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// checkMountpointOverlap returns an error.Fatal if the local repository at
|
||||
// repoPath and the mountpoint overlap: equal paths, mountpoint nested inside
|
||||
// the repo, or the repo nested inside the mountpoint. Any overlap deadlocks
|
||||
// the FUSE server (GH #5234).
|
||||
func checkMountpointOverlap(repoPath, mountpoint string) error {
|
||||
rp, err := resolvePath(repoPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mp, err := resolvePath(mountpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
const tail = "; refusing to mount to avoid deadlocking the FUSE server"
|
||||
switch {
|
||||
case rp == mp:
|
||||
return errors.Fatal(fmt.Sprintf("mountpoint %s is the local repository directory%s", mp, tail))
|
||||
case isInside(rp, mp):
|
||||
return errors.Fatal(fmt.Sprintf("mountpoint %s is inside the local repository directory %s%s", mp, rp, tail))
|
||||
case isInside(mp, rp):
|
||||
return errors.Fatal(fmt.Sprintf("local repository directory %s is inside the mountpoint %s%s", rp, mp, tail))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolvePath returns p as an absolute, symlink-resolved path. If EvalSymlinks
|
||||
// fails (e.g. the path does not fully exist), it falls back to the absolute
|
||||
// form: overlap detection is best-effort and we'd rather refuse a clear
|
||||
// overlap than abort on an unrelated stat error.
|
||||
func resolvePath(p string) (string, error) {
|
||||
abs, err := filepath.Abs(p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
resolved, err := filepath.EvalSymlinks(abs)
|
||||
if err != nil {
|
||||
return abs, nil
|
||||
}
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
// isInside reports whether child is strictly nested inside parent. Both paths
|
||||
// must already be cleaned and absolute. Equal paths return false; the caller
|
||||
// handles equality separately so it can produce a distinct error message.
|
||||
func isInside(parent, child string) bool {
|
||||
rel, err := filepath.Rel(parent, child)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if rel == "." || rel == ".." {
|
||||
return false
|
||||
}
|
||||
return !strings.HasPrefix(rel, ".."+string(filepath.Separator))
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -212,6 +213,62 @@ func TestMount(t *testing.T) {
|
||||
checkSnapshots(t, env.gopts, env.mountpoint, snapshotIDs, 4)
|
||||
}
|
||||
|
||||
func TestCheckMountpointOverlap(t *testing.T) {
|
||||
tempdir := t.TempDir()
|
||||
repo := filepath.Join(tempdir, "repo")
|
||||
repoSub := filepath.Join(repo, "sub")
|
||||
sibling := filepath.Join(tempdir, "mnt")
|
||||
for _, d := range []string{repo, repoSub, sibling} {
|
||||
rtest.OK(t, os.MkdirAll(d, 0700))
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
repo string
|
||||
mount string
|
||||
wantSub string // substring of expected error; empty means expect nil
|
||||
}{
|
||||
{"equal", repo, repo, "is the local repository directory"},
|
||||
{"mount inside repo", repo, repoSub, "is inside the local repository directory"},
|
||||
{"repo inside mount", repoSub, repo, "is inside the mountpoint"},
|
||||
{"disjoint", repo, sibling, ""},
|
||||
{"prefix-not-subpath", repo, repo + "-other", ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
rtest.OK(t, os.MkdirAll(tc.mount, 0700))
|
||||
err := checkMountpointOverlap(tc.repo, tc.mount)
|
||||
if tc.wantSub == "" {
|
||||
rtest.OK(t, err)
|
||||
return
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatalf("expected error containing %q, got nil", tc.wantSub)
|
||||
}
|
||||
if !strings.Contains(err.Error(), tc.wantSub) {
|
||||
t.Fatalf("error %q does not contain %q", err.Error(), tc.wantSub)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckMountpointOverlapSymlink(t *testing.T) {
|
||||
tempdir := t.TempDir()
|
||||
repo := filepath.Join(tempdir, "repo")
|
||||
rtest.OK(t, os.MkdirAll(repo, 0700))
|
||||
link := filepath.Join(tempdir, "link-to-repo")
|
||||
rtest.OK(t, os.Symlink(repo, link))
|
||||
|
||||
err := checkMountpointOverlap(repo, link)
|
||||
if err == nil {
|
||||
t.Fatal("expected overlap error when mountpoint is a symlink to repo, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "is the local repository directory") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMountSameTimestamps(t *testing.T) {
|
||||
if !rtest.RunFuseTest {
|
||||
t.Skip("Skipping fuse tests")
|
||||
|
||||
@@ -208,6 +208,12 @@ command needs to be in the ``PATH``. On macOS, you need `FUSE-T
|
||||
<https://www.fuse-t.org/>`__ or `FUSE for macOS <https://osxfuse.github.io/>`__.
|
||||
On FreeBSD, you may need to install FUSE and load the kernel module (``kldload fuse``).
|
||||
|
||||
.. note:: The mountpoint must not overlap the local repository directory.
|
||||
Using the repository directory itself, a subdirectory of it, or a parent
|
||||
of it as the mountpoint causes the FUSE server to read its own backend
|
||||
files through the new mount and deadlock the kernel. ``restic mount``
|
||||
detects this and refuses such mountpoints.
|
||||
|
||||
Restic supports storage and preservation of hard links. However, since
|
||||
hard links exist in the scope of a filesystem by definition, restoring
|
||||
hard links from a FUSE mount should be done by a program that preserves
|
||||
|
||||
Reference in New Issue
Block a user