From 8eb8d60bb0f54cb144b3717e38d81601a8972206 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Thu, 2 Jun 2016 16:05:31 +1000 Subject: [PATCH] runtimetest: add validation of mount order In order to make sure that runtimes correctly implement the ordering of []spec.Mount, we need to check /proc/1/mountinfo (keeping in mind that Unix allows a user to mount over the top of an existing mountpoint -- thus masking it). We only run this test if there happen to be two mountpoints in the list where one is a parent of the other. An extension of this might be to check all of the nested mounts specified in the spec. Signed-off-by: Aleksa Sarai --- cmd/runtimetest/main.go | 127 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/cmd/runtimetest/main.go b/cmd/runtimetest/main.go index be5885775..22f06e559 100644 --- a/cmd/runtimetest/main.go +++ b/cmd/runtimetest/main.go @@ -1,6 +1,7 @@ package main import ( + "bufio" "bytes" "encoding/json" "fmt" @@ -244,6 +245,131 @@ func validateROPaths(spec *rspec.Spec) error { return nil } +func isParent(parent, child string) bool { + if parent == child { + return false + } + + cparts := filepath.SplitList(child) + for i, part := range filepath.SplitList(parent) { + if cparts[i] != part { + return false + } + } + + return true +} + +func isMountPoint(path string) (bool, error) { + // Checking lstat(path) and lstat(path/..) won't work because it doesn't + // treat bindmounts properly. We have to parse /proc/1/mountinfo. This + // doesn't contain symlinks so we don't have to worry about that whole + // mess. + mi, err := os.Open("/proc/1/mountinfo") + if err != nil { + return false, err + } + defer mi.Close() + + var mounts []string + pathindex := -1 + scan := bufio.NewScanner(mi) + for i := 0; scan.Scan(); i++ { + // The fifth field is the mountpoint. However, we can't assume that + // seeing a path in /proc/1/mountinfo means that it resolves to a + // mountpoint. Unix allows you to mount over a filesystem tree. + field := strings.Split(scan.Text(), " ")[4] + if field == path { + pathindex = i + } + + mounts = append(mounts, field) + } + + // It isn't in mountinfo. + if pathindex < 0 { + return false, nil + } + + // Check that the mount isn't followed by a mount on a parent directory. + hasParent := false + for _, other := range mounts[pathindex+1:] { + // If we see our mountpoint again, we reset the assumption. + if other == path { + hasParent = false + continue + } + + // If there's a case where something was mounted over then we + // invalidate the assumption. + if isParent(other, path) { + hasParent = true + } + } + + return !hasParent, nil +} + +// Finds and returns any two paths in the given slice where pathA is a parent of +// pathB. Otherwise it returns "", "", false. +func findNestedPaths(paths []string) (string, string, bool) { + for _, parent := range paths { + for _, child := range paths { + if isParent(parent, child) { + return parent, child, true + } + } + } + + return "", "", false +} + +func validateMountOrder(spec *rspec.Spec) error { + fmt.Println("validating mount order") + + var mounts []string + for _, m := range spec.Mounts { + mounts = append(mounts, filepath.Clean(m.Destination)) + } + + // If there are two mount options where A is a parent of B, then we can + // verify that the right order is maintained no matter which order they are + // in the mounts. + A, B, ok := findNestedPaths(mounts) + if !ok { + return nil + } + + // Figure out the order of A and B. + var first string + for _, m := range mounts { + if A == m || B == m { + first = m + break + } + } + + // A must *always* be a mountpoint. + if ok, err := isMountPoint(A); err != nil { + return fmt.Errorf("failed to get whether %q is a mountpoint: %q", A, err) + } else if !ok { + return fmt.Errorf("expected %q to be a mountpoint", A) + } + + // B must be a mountpoint iff A was first. + if ok, err := isMountPoint(B); err != nil { + return fmt.Errorf("failed to get whether %q is a mountpoint: %q", A, err) + } else { + if first == A && !ok { + return fmt.Errorf("expected %q to be a mountpoint", B) + } else if first == B && ok { + return fmt.Errorf("expected %q to not be a mountpoint", B) + } + } + + return nil +} + func main() { spec, err := loadSpecConfig() if err != nil { @@ -259,6 +385,7 @@ func main() { validateSysctls, validateMaskedPaths, validateROPaths, + validateMountOrder, } for _, v := range validations {