From 879cef37a1ee70eccba6842a862e54a19e2d68e8 Mon Sep 17 00:00:00 2001 From: Harshavardhana Date: Tue, 15 Aug 2017 15:10:50 -0700 Subject: [PATCH] Fail to start server if detected cross-device mounts. (#4807) Fixes #4764 --- cmd/endpoint.go | 33 +++- cmd/fs-v1-helpers.go | 4 + cmd/posix-errors.go | 7 + cmd/posix.go | 1 + cmd/server-main.go | 1 + cmd/storage-errors.go | 3 + pkg/mountinfo/mountinfo.go | 34 ++++ pkg/mountinfo/mountinfo_linux.go | 131 ++++++++++++++ pkg/mountinfo/mountinfo_linux_test.go | 251 ++++++++++++++++++++++++++ pkg/mountinfo/mountinfo_others.go | 25 +++ 10 files changed, 487 insertions(+), 3 deletions(-) create mode 100644 pkg/mountinfo/mountinfo.go create mode 100644 pkg/mountinfo/mountinfo_linux.go create mode 100644 pkg/mountinfo/mountinfo_linux_test.go create mode 100644 pkg/mountinfo/mountinfo_others.go diff --git a/cmd/endpoint.go b/cmd/endpoint.go index 539ff4b1b..4c7b00c73 100644 --- a/cmd/endpoint.go +++ b/cmd/endpoint.go @@ -21,11 +21,13 @@ import ( "net" "net/url" "path" + "path/filepath" "sort" "strconv" "strings" "github.com/minio/minio-go/pkg/set" + "github.com/minio/minio/pkg/mountinfo" ) // EndpointType - enum for endpoint type. @@ -99,7 +101,8 @@ func NewEndpoint(arg string) (ep Endpoint, e error) { return ep, fmt.Errorf("invalid URL endpoint format") } - host, port, err := net.SplitHostPort(u.Host) + var host, port string + host, port, err = net.SplitHostPort(u.Host) if err != nil { if !strings.Contains(err.Error(), "missing port in address") { return ep, fmt.Errorf("invalid URL endpoint format: %s", err) @@ -226,6 +229,22 @@ func NewEndpointList(args ...string) (endpoints EndpointList, err error) { return endpoints, nil } +// Checks if there are any cross device mounts. +func checkCrossDeviceMounts(endpoints EndpointList) (err error) { + var absPaths []string + for _, endpoint := range endpoints { + if endpoint.IsLocal { + var absPath string + absPath, err = filepath.Abs(endpoint.Path) + if err != nil { + return err + } + absPaths = append(absPaths, absPath) + } + } + return mountinfo.CheckCrossDevice(absPaths) +} + // CreateEndpoints - validates and creates new endpoints for given args. func CreateEndpoints(serverAddr string, args ...string) (string, EndpointList, SetupType, error) { var endpoints EndpointList @@ -246,13 +265,16 @@ func CreateEndpoints(serverAddr string, args ...string) (string, EndpointList, S if err != nil { return serverAddr, endpoints, setupType, err } - if endpoint.Type() != PathEndpointType { return serverAddr, endpoints, setupType, fmt.Errorf("use path style endpoint for FS setup") } - endpoints = append(endpoints, endpoint) setupType = FSSetupType + + // Check for cross device mounts if any. + if err = checkCrossDeviceMounts(endpoints); err != nil { + return serverAddr, endpoints, setupType, err + } return serverAddr, endpoints, setupType, nil } @@ -261,6 +283,11 @@ func CreateEndpoints(serverAddr string, args ...string) (string, EndpointList, S return serverAddr, endpoints, setupType, err } + // Check for cross device mounts if any. + if err = checkCrossDeviceMounts(endpoints); err != nil { + return serverAddr, endpoints, setupType, err + } + // Return XL setup when all endpoints are path style. if endpoints[0].Type() == PathEndpointType { setupType = XLSetupType diff --git a/cmd/fs-v1-helpers.go b/cmd/fs-v1-helpers.go index ec5fda7be..673b0354e 100644 --- a/cmd/fs-v1-helpers.go +++ b/cmd/fs-v1-helpers.go @@ -17,6 +17,7 @@ package cmd import ( + "fmt" "io" "os" pathutil "path" @@ -360,6 +361,9 @@ func fsRenameFile(sourcePath, destPath string) error { return traceError(err) } if err := os.Rename((sourcePath), (destPath)); err != nil { + if isSysErrCrossDevice(err) { + return traceError(fmt.Errorf("%s (%s)->(%s)", errCrossDeviceLink, sourcePath, destPath)) + } return traceError(err) } return nil diff --git a/cmd/posix-errors.go b/cmd/posix-errors.go index 7bdde538d..e1e93c680 100644 --- a/cmd/posix-errors.go +++ b/cmd/posix-errors.go @@ -118,3 +118,10 @@ func isSysErrHandleInvalid(err error) bool { } return false } + +func isSysErrCrossDevice(err error) bool { + if e, ok := err.(*os.LinkError); ok { + return e.Err == syscall.EXDEV + } + return false +} diff --git a/cmd/posix.go b/cmd/posix.go index e2a179a8f..2e531655e 100644 --- a/cmd/posix.go +++ b/cmd/posix.go @@ -102,6 +102,7 @@ func newPosix(path string) (StorageAPI, error) { if err != nil { return nil, err } + st := &posix{ diskPath: diskPath, // 1MiB buffer pool for posix internal operations. diff --git a/cmd/server-main.go b/cmd/server-main.go index 930d7b352..6d056163e 100644 --- a/cmd/server-main.go +++ b/cmd/server-main.go @@ -91,6 +91,7 @@ func serverHandleCmdArgs(ctx *cli.Context) { var setupType SetupType var err error + globalMinioAddr, globalEndpoints, setupType, err = CreateEndpoints(serverAddr, ctx.Args()...) fatalIf(err, "Invalid command line arguments server=ā€˜%sā€™, args=%s", serverAddr, ctx.Args()) globalMinioHost, globalMinioPort = mustSplitHostPort(globalMinioAddr) diff --git a/cmd/storage-errors.go b/cmd/storage-errors.go index 12ba66b8a..01741fde3 100644 --- a/cmd/storage-errors.go +++ b/cmd/storage-errors.go @@ -83,6 +83,9 @@ var errFileAccessDenied = errors.New("file access denied") // verification is empty or invalid. var errBitrotHashAlgoInvalid = errors.New("bit-rot hash algorithm is invalid") +// errCrossDeviceLink - rename across devices not allowed. +var errCrossDeviceLink = errors.New("Rename across devices not allowed, please fix your backend configuration") + // hashMisMatchError - represents a bit-rot hash verification failure // error. type hashMismatchError struct { diff --git a/pkg/mountinfo/mountinfo.go b/pkg/mountinfo/mountinfo.go new file mode 100644 index 000000000..bca5224da --- /dev/null +++ b/pkg/mountinfo/mountinfo.go @@ -0,0 +1,34 @@ +/* + * Minio Cloud Storage, (C) 2017 Minio, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package mountinfo + +// mountInfo - This represents a single line in /proc/mounts. +type mountInfo struct { + Device string + Path string + FSType string + Options []string + Freq string + Pass string +} + +func (m mountInfo) String() string { + return m.Path +} + +// mountInfos - This represents the entire /proc/mounts. +type mountInfos []mountInfo diff --git a/pkg/mountinfo/mountinfo_linux.go b/pkg/mountinfo/mountinfo_linux.go new file mode 100644 index 000000000..04a5904de --- /dev/null +++ b/pkg/mountinfo/mountinfo_linux.go @@ -0,0 +1,131 @@ +// +build linux + +/* + * Minio Cloud Storage, (C) 2017 Minio, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package mountinfo + +import ( + "bufio" + "fmt" + "io" + "os" + "path/filepath" + "strconv" + "strings" +) + +const ( + // Number of fields per line in /proc/mounts as per the fstab man page. + expectedNumFieldsPerLine = 6 + // Location of the mount file to use + procMountsPath = "/proc/mounts" +) + +// CheckCrossDevice - check if any list of paths has any sub-mounts at /proc/mounts. +func CheckCrossDevice(absPaths []string) error { + return checkCrossDevice(absPaths, procMountsPath) +} + +// Check cross device is an internal function. +func checkCrossDevice(absPaths []string, mountsPath string) error { + mounts, err := readProcMounts(mountsPath) + if err != nil { + return err + } + for _, path := range absPaths { + if err := mounts.checkCrossMounts(path); err != nil { + return err + } + } + return nil +} + +// CheckCrossDevice - check if given path has any sub-mounts in the input mounts list. +func (mts mountInfos) checkCrossMounts(path string) error { + if !filepath.IsAbs(path) { + return fmt.Errorf("Invalid argument, path (%s) is expected to be absolute", path) + } + var crossMounts mountInfos + for _, mount := range mts { + // Add a separator to indicate that this is a proper mount-point. + // This is to avoid a situation where prefix is '/tmp/fsmount' + // and mount path is /tmp/fs. In such a scenario we need to check for + // `/tmp/fs/` to be a common prefix amount other mounts. + mpath := strings.TrimSuffix(mount.Path, "/") + "/" + ppath := strings.TrimSuffix(path, "/") + "/" + if strings.HasPrefix(mpath, ppath) { + // At this point if the mount point has a common prefix two conditions can happen. + // - mount.Path matches exact with `path` means we can proceed no error here. + // - mount.Path doesn't match (means cross-device mount), should error out. + if mount.Path != path { + crossMounts = append(crossMounts, mount) + } + } + } + msg := `Cross-device mounts detected on path (%s) at following locations %s. Export path should not have any sub-mounts, refusing to start.` + if len(crossMounts) > 0 { + // if paths didn't match then we do have cross-device mount. + return fmt.Errorf(msg, path, crossMounts) + } + return nil +} + +// readProcMounts reads the given mountFilePath (normally /proc/mounts) and produces a hash +// of the contents. If the out argument is not nil, this fills it with MountPoint structs. +func readProcMounts(mountFilePath string) (mountInfos, error) { + file, err := os.Open(mountFilePath) + if err != nil { + return nil, err + } + defer file.Close() + return parseMountFrom(file) +} + +func parseMountFrom(file io.Reader) (mountInfos, error) { + var mounts = mountInfos{} + scanner := bufio.NewReader(file) + for { + line, err := scanner.ReadString('\n') + if err == io.EOF { + break + } + fields := strings.Fields(line) + if len(fields) != expectedNumFieldsPerLine { + return nil, fmt.Errorf("wrong number of fields (expected %d, got %d): %s", expectedNumFieldsPerLine, len(fields), line) + } + + // Freq should be an integer. + if _, err := strconv.Atoi(fields[4]); err != nil { + return nil, err + } + + // Pass should be an integer. + if _, err := strconv.Atoi(fields[5]); err != nil { + return nil, err + } + + mounts = append(mounts, mountInfo{ + Device: fields[0], + Path: fields[1], + FSType: fields[2], + Options: strings.Split(fields[3], ","), + Freq: fields[4], + Pass: fields[5], + }) + } + return mounts, nil +} diff --git a/pkg/mountinfo/mountinfo_linux_test.go b/pkg/mountinfo/mountinfo_linux_test.go new file mode 100644 index 000000000..feb3a45fd --- /dev/null +++ b/pkg/mountinfo/mountinfo_linux_test.go @@ -0,0 +1,251 @@ +// +build linux + +/* + * Minio Cloud Storage, (C) 2017 Minio, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package mountinfo + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" +) + +// Tests cross device mount verification function, for both failure +// and success cases. +func TestCrossDeviceMountPaths(t *testing.T) { + successCase := + `/dev/0 /path/to/0/1 type0 flags 0 0 + /dev/1 /path/to/1 type1 flags 1 1 + /dev/2 /path/to/1/2 type2 flags,1,2=3 2 2 + /dev/3 /path/to/1.1 type3 falgs,1,2=3 3 3 + ` + dir, err := ioutil.TempDir("", "TestReadProcmountInfos") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + mountsPath := filepath.Join(dir, "mounts") + if err = ioutil.WriteFile(mountsPath, []byte(successCase), 0666); err != nil { + t.Fatal(err) + } + // Failure case where we detected successfully cross device mounts. + { + var absPaths = []string{"/path/to/1"} + if err = checkCrossDevice(absPaths, mountsPath); err == nil { + t.Fatal("Expected to fail, but found success") + } + + mp := []mountInfo{ + {"/dev/2", "/path/to/1/2", "type2", []string{"flags"}, "2", "2"}, + } + msg := fmt.Sprintf("Cross-device mounts detected on path (/path/to/1) at following locations %s. Export path should not have any sub-mounts, refusing to start.", mp) + if err.Error() != msg { + t.Fatalf("Expected msg %s, got %s", msg, err) + } + } + // Failure case when input path is not absolute. + { + var absPaths = []string{"."} + if err = checkCrossDevice(absPaths, mountsPath); err == nil { + t.Fatal("Expected to fail for non absolute paths") + } + expectedErrMsg := fmt.Sprintf("Invalid argument, path (%s) is expected to be absolute", ".") + if err.Error() != expectedErrMsg { + t.Fatalf("Expected %s, got %s", expectedErrMsg, err) + } + } + // Success case, where path doesn't have any mounts. + { + var absPaths = []string{"/path/to/x"} + if err = checkCrossDevice(absPaths, mountsPath); err != nil { + t.Fatalf("Expected success, failed instead (%s)", err) + } + } +} + +// Tests cross device mount verification function, for both failure +// and success cases. +func TestCrossDeviceMount(t *testing.T) { + successCase := + `/dev/0 /path/to/0/1 type0 flags 0 0 + /dev/1 /path/to/1 type1 flags 1 1 + /dev/2 /path/to/1/2 type2 flags,1,2=3 2 2 + /dev/3 /path/to/1.1 type3 falgs,1,2=3 3 3 + ` + dir, err := ioutil.TempDir("", "TestReadProcmountInfos") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + mountsPath := filepath.Join(dir, "mounts") + if err = ioutil.WriteFile(mountsPath, []byte(successCase), 0666); err != nil { + t.Fatal(err) + } + mounts, err := readProcMounts(mountsPath) + if err != nil { + t.Fatal(err) + } + // Failure case where we detected successfully cross device mounts. + { + if err = mounts.checkCrossMounts("/path/to/1"); err == nil { + t.Fatal("Expected to fail, but found success") + } + + mp := []mountInfo{ + {"/dev/2", "/path/to/1/2", "type2", []string{"flags"}, "2", "2"}, + } + msg := fmt.Sprintf("Cross-device mounts detected on path (/path/to/1) at following locations %s. Export path should not have any sub-mounts, refusing to start.", mp) + if err.Error() != msg { + t.Fatalf("Expected msg %s, got %s", msg, err) + } + } + // Failure case when input path is not absolute. + { + if err = mounts.checkCrossMounts("."); err == nil { + t.Fatal("Expected to fail for non absolute paths") + } + expectedErrMsg := fmt.Sprintf("Invalid argument, path (%s) is expected to be absolute", ".") + if err.Error() != expectedErrMsg { + t.Fatalf("Expected %s, got %s", expectedErrMsg, err) + } + } + // Success case, where path doesn't have any mounts. + { + if err = mounts.checkCrossMounts("/path/to/x"); err != nil { + t.Fatalf("Expected success, failed instead (%s)", err) + } + } +} + +// Tests read proc mounts file. +func TestReadProcmountInfos(t *testing.T) { + successCase := + `/dev/0 /path/to/0 type0 flags 0 0 + /dev/1 /path/to/1 type1 flags 1 1 + /dev/2 /path/to/2 type2 flags,1,2=3 2 2 + ` + dir, err := ioutil.TempDir("", "TestReadProcmountInfos") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + mountsPath := filepath.Join(dir, "mounts") + if err = ioutil.WriteFile(mountsPath, []byte(successCase), 0666); err != nil { + t.Fatal(err) + } + // Verifies if reading each line worked properly. + { + var mounts mountInfos + mounts, err = readProcMounts(mountsPath) + if err != nil { + t.Fatal(err) + } + if len(mounts) != 3 { + t.Fatalf("expected 3 mounts, got %d", len(mounts)) + } + mp := mountInfo{"/dev/0", "/path/to/0", "type0", []string{"flags"}, "0", "0"} + if !mountPointsEqual(mounts[0], mp) { + t.Errorf("got unexpected MountPoint[0]: %#v", mounts[0]) + } + mp = mountInfo{"/dev/1", "/path/to/1", "type1", []string{"flags"}, "1", "1"} + if !mountPointsEqual(mounts[1], mp) { + t.Errorf("got unexpected mountInfo[1]: %#v", mounts[1]) + } + mp = mountInfo{"/dev/2", "/path/to/2", "type2", []string{"flags", "1", "2=3"}, "2", "2"} + if !mountPointsEqual(mounts[2], mp) { + t.Errorf("got unexpected mountInfo[2]: %#v", mounts[2]) + } + } + // Failure case mounts path doesn't exist, if not fail. + { + if _, err = readProcMounts(filepath.Join(dir, "non-existent")); err != nil && !os.IsNotExist(err) { + t.Fatal(err) + } + } +} + +// Tests read proc mounts reader. +func TestReadProcMountFrom(t *testing.T) { + successCase := + `/dev/0 /path/to/0 type0 flags 0 0 + /dev/1 /path/to/1 type1 flags 1 1 + /dev/2 /path/to/2 type2 flags,1,2=3 2 2 + ` + // Success case, verifies if parsing works properly. + { + mounts, err := parseMountFrom(strings.NewReader(successCase)) + if err != nil { + t.Errorf("expected success") + } + if len(mounts) != 3 { + t.Fatalf("expected 3 mounts, got %d", len(mounts)) + } + mp := mountInfo{"/dev/0", "/path/to/0", "type0", []string{"flags"}, "0", "0"} + if !mountPointsEqual(mounts[0], mp) { + t.Errorf("got unexpected mountInfo[0]: %#v", mounts[0]) + } + mp = mountInfo{"/dev/1", "/path/to/1", "type1", []string{"flags"}, "1", "1"} + if !mountPointsEqual(mounts[1], mp) { + t.Errorf("got unexpected mountInfo[1]: %#v", mounts[1]) + } + mp = mountInfo{"/dev/2", "/path/to/2", "type2", []string{"flags", "1", "2=3"}, "2", "2"} + if !mountPointsEqual(mounts[2], mp) { + t.Errorf("got unexpected mountInfo[2]: %#v", mounts[2]) + } + } + // Error cases where parsing fails with invalid Freq and Pass params. + { + errorCases := []string{ + "/dev/0 /path/to/mount\n", + "/dev/1 /path/to/mount type flags a 0\n", + "/dev/2 /path/to/mount type flags 0 b\n", + } + for _, ec := range errorCases { + _, rerr := parseMountFrom(strings.NewReader(ec)) + if rerr == nil { + t.Errorf("expected error") + } + } + } +} + +// Helpers for tests. + +// Check if two `mountInfo` are equal. +func mountPointsEqual(a, b mountInfo) bool { + if a.Device != b.Device || a.Path != b.Path || a.FSType != b.FSType || !slicesEqual(a.Options, b.Options) || a.Pass != b.Pass || a.Freq != b.Freq { + return false + } + return true +} + +// Checks if two string slices are equal. +func slicesEqual(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} diff --git a/pkg/mountinfo/mountinfo_others.go b/pkg/mountinfo/mountinfo_others.go new file mode 100644 index 000000000..9b28d39c2 --- /dev/null +++ b/pkg/mountinfo/mountinfo_others.go @@ -0,0 +1,25 @@ +// +build !linux + +/* + * Minio Cloud Storage, (C) 2017 Minio, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package mountinfo + +// CheckCrossDevice - check if any input path has multiple sub-mounts. +// this is a dummy function and returns nil for now. +func CheckCrossDevice(paths []string) error { + return nil +}