parent
3d21119ec8
commit
879cef37a1
@ -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 |
@ -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 |
||||||
|
} |
@ -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 |
||||||
|
} |
@ -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 |
||||||
|
} |
Loading…
Reference in new issue