From 88b0636654db1f42441283410a06ea7af4d6fa80 Mon Sep 17 00:00:00 2001 From: Serge Hallyn Date: Mon, 19 Dec 2022 13:35:16 -0600 Subject: [PATCH] feat: atomfs mount: use squashfuse for non-root users While stacker knows how to use squashfuse for 'stacker grab', that function simply keeps the squashfuse process running for the duration of the grab, then lets it close. For atomfs molecule.Mount, we must release that process. So when doing atomfs.Mount(), first check whether we are definitely NOT root using amHostRoot(). There is a corner case which can slip past this - namely if you, as root, create a userns wherein you map the full host uid range. However, you'll never have real root being told it wasn't real root. Second check whether we were requested not to try as real root. Third, if neither of those are the case, then try the regular mount syscall, requiring root. If that succeeds, or fails with a non-permission error, then return. If we are detected as not-real-root, or were requested to not try as real root, or if mount failed as real root with a permission error, then use squashfuse, and release the exec'd process so that it can outlive us. The actual squashfuse mount function is shared with the extract path. Signed-off-by: Serge Hallyn --- pkg/squashfs/squashfs.go | 77 ++++++++++++++++++------------ pkg/squashfs/verity.go | 100 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 145 insertions(+), 32 deletions(-) diff --git a/pkg/squashfs/squashfs.go b/pkg/squashfs/squashfs.go index aa710d9fb..2e9c91ae6 100644 --- a/pkg/squashfs/squashfs.go +++ b/pkg/squashfs/squashfs.go @@ -9,6 +9,7 @@ import ( "os" "os/exec" "path" + "path/filepath" "strings" "sync" "syscall" @@ -71,7 +72,7 @@ func (eps *ExcludePaths) AddInclude(orig string, isDir bool) { } delete(eps.exclude, p) - p = path.Dir(p) + p = filepath.Dir(p) } // now add it to the list of includes, so we don't accidentally re-add @@ -214,6 +215,44 @@ func maybeKernelSquashMount(squashFile, extractDir string) (bool, error) { return false, kernelSquashMountFailed } +func findSquashfusePath() string { + if p := which("squashfuse_ll"); p != "" { + return p + } + return which("squashfuse") +} + +func squashFuse(cmdPath, squashFile, extractDir string) (*exec.Cmd, error) { + // given extractDir of path/to/some/dir[/], log to path/to/some/.dir-squashfs.log + extractDir = strings.TrimSuffix(extractDir, "/") + + var cmdOut io.Writer + var err error + var nilCmd *exec.Cmd + + logf := filepath.Join(path.Dir(extractDir), "."+filepath.Base(extractDir)+"-squashfuse.log") + if cmdOut, err = os.OpenFile(logf, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0644); err != nil { + log.Infof("Failed to open %s for write: %v", logf, err) + return nilCmd, err + } + + // It would be nice to only enable debug (or maybe to only log to file at all) + // if 'stacker --debug', but we do not have access to that info here. + // to debug squashfuse, use "allow_other,debug" + cmd := exec.Command("squashfuse", "-f", "-o", "allow_other,debug", squashFile, extractDir) + cmd.Stdin = nil + cmd.Stdout = cmdOut + cmd.Stderr = cmdOut + cmdOut.Write([]byte(fmt.Sprintf("# %s\n", strings.Join(cmd.Args, " ")))) + log.Debugf("Extracting %s -> %s with squashfuse [%s]", squashFile, extractDir, logf) + err = cmd.Start() + if err != nil { + return nilCmd, err + } + + return cmd, nil +} + func ExtractSingleSquash(squashFile string, extractDir string, storageType string) error { err := os.MkdirAll(extractDir, 0755) if err != nil { @@ -226,35 +265,11 @@ func ExtractSingleSquash(squashFile string, extractDir string, storageType strin return err } - findSqfusePath := func() string { - if p := which("squashfuse_ll"); p != "" { - return p - } - return which("squashfuse") + if sqfuse := findSquashfusePath(); sqfuse != "" { + _, err = squashFuse(sqfuse, squashFile, extractDir) + return err } - - if sqfuse := findSqfusePath(); sqfuse != "" { - // given extractDir of path/to/some/dir[/], log to path/to/some/.dir-squashfs.log - extractDir := strings.TrimSuffix(extractDir, "/") - - var cmdOut io.Writer - logf := path.Join(path.Dir(extractDir), "."+path.Base(extractDir)+"-squashfuse.log") - if cmdOut, err = os.OpenFile(logf, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0644); err != nil { - log.Infof("Failed to open %s for write: %v", logf, err) - return err - } - - // It would be nice to only enable debug (or maybe to only log to file at all) - // if 'stacker --debug', but we do not have access to that info here. - // to debug squashfuse, use "allow_other,debug" - cmd := exec.Command(sqfuse, "-f", "-o", "allow_other,debug", squashFile, extractDir) - cmd.Stdin = nil - cmd.Stdout = cmdOut - cmd.Stderr = cmdOut - cmdOut.Write([]byte(fmt.Sprintf("# %s\n", strings.Join(cmd.Args, " ")))) - log.Debugf("Extracting %s -> %s with squashfuse [%s]", squashFile, extractDir, logf) - return cmd.Start() - } else if p := which("unsquashfs"); p != "" { + if p := which("unsquashfs"); p != "" { log.Debugf("Extracting %s -> %s with unsquashfs -f -d %s %s", extractDir, squashFile, extractDir, squashFile) cmd := exec.Command("unsquashfs", "-f", "-d", extractDir, squashFile) cmd.Stdout = os.Stdout @@ -297,7 +312,7 @@ func whichSearch(name string, paths []string) string { var search []string if strings.ContainsRune(name, os.PathSeparator) { - if path.IsAbs(name) { + if filepath.IsAbs(name) { search = []string{name} } else { search = []string{"./" + name} @@ -305,7 +320,7 @@ func whichSearch(name string, paths []string) string { } else { search = []string{} for _, p := range paths { - search = append(search, path.Join(p, name)) + search = append(search, filepath.Join(p, name)) } } diff --git a/pkg/squashfs/verity.go b/pkg/squashfs/verity.go index 59411832f..a83daf1c9 100644 --- a/pkg/squashfs/verity.go +++ b/pkg/squashfs/verity.go @@ -71,6 +71,7 @@ import ( "strconv" "strings" "syscall" + "time" "unsafe" "github.com/anuvu/squashfs" @@ -78,6 +79,7 @@ import ( "github.com/martinjungblut/go-cryptsetup" "github.com/pkg/errors" "golang.org/x/sys/unix" + "stackerbuild.io/stacker/pkg/log" "stackerbuild.io/stacker/pkg/mount" ) @@ -200,7 +202,103 @@ func verityName(p string) string { return fmt.Sprintf("%s-%s", p, veritySuffix) } -func Mount(squashfs string, mountpoint string, rootHash string) error { +func fileChanged(a os.FileInfo, path string) bool { + b, err := os.Lstat(path) + if err != nil { + return true + } + return !os.SameFile(a, b) +} + +// Mount a filesystem as container root, without host root +// privileges. We do this using squashfuse. +func GuestMount(squashFile string, mountpoint string) error { + if isMountpoint(mountpoint) { + return fmt.Errorf("%s is already mounted") + } + + abs, err := filepath.Abs(squashFile) + if err != nil { + return errors.Errorf("Failed to get absolute path for %s: %v", squashFile, err) + } + squashFile = abs + + abs, err = filepath.Abs(mountpoint) + if err != nil { + return errors.Errorf("Failed to get absolute path for %s: %v", mountpoint, err) + } + mountpoint = abs + + cmdPath := findSquashfusePath() + if cmdPath == "" { + return fmt.Errorf("no squashfs binary found") + } + fiPre, err := os.Lstat(mountpoint) + if err != nil { + return fmt.Errorf("Failed stat'ing %q: %w", mountpoint, err) + } + if fiPre.Mode()&os.ModeSymlink != 0 { + return fmt.Errorf("Refusing to mount onto a symbolic linkd") + } + + cmd, err := squashFuse("", squashFile, mountpoint) + if err != nil { + return err + } + err = cmd.Process.Release() + if err != nil { + return fmt.Errorf("Failed releasing squashfuse process: %w", err) + } + + for count := 0; !fileChanged(fiPre, mountpoint); count++ { + if count % 10 == 0 { + log.Debugf("%s is not yet mounted...\n", mountpoint) + } + time.Sleep(time.Duration(100 * time.Millisecond)) + } + + return nil +} + +func isMountpoint(dest string) bool { + mounted, err := mount.IsMountpoint(dest) + return err == nil && mounted +} + +func amHostRoot() bool { + // if not uid 0, not host root + // if uid_map doesn't map 0 to 0, not host root + if os.Geteuid() != 0 { + return false + } + bytes, err := os.ReadFile("/proc/self/uid_map") + if err != nil { + return false + } + lines := string(bytes) + if len(lines) != 1 { + return false + } + words := strings.Fields(lines) + if len(words) != 3 || words[0] != "0" || words[1] != "0" { + return false + } + + return true +} + +func Mount(squashfs, mountpoint, rootHash string) error { + if !amHostRoot() || !tryKernelMountSquash { + return GuestMount(squashfs, mountpoint) + } + err := HostMount(squashfs, mountpoint, rootHash) + if err == nil || os.IsPermission(err) { + return err + } + return GuestMount(squashfs, mountpoint) +} + +func HostMount(squashfs string, mountpoint string, rootHash string) error { fi, err := os.Stat(squashfs) if err != nil { return errors.WithStack(err)