From 1e4402e8d91473190c72f39221b3441ec49e80fd 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/atomfs/molecule.go | 3 +- pkg/squashfs/squashfs.go | 84 +++++++++++++++++---------- pkg/squashfs/verity.go | 113 +++++++++++++++++++++++++++++++++++- pkg/squashfs/verity_test.go | 41 +++++++++++++ 4 files changed, 207 insertions(+), 34 deletions(-) diff --git a/pkg/atomfs/molecule.go b/pkg/atomfs/molecule.go index 6b3924a1a..8c7640618 100644 --- a/pkg/atomfs/molecule.go +++ b/pkg/atomfs/molecule.go @@ -1,7 +1,6 @@ package atomfs import ( - "fmt" "os" "path" "path/filepath" @@ -109,7 +108,7 @@ func makeLock(mountpoint string) (*os.File, error) { return lockfile, nil } - err = fmt.Errorf("Failed locking %s: %v\nFailed locking %s: %v", advisoryLockPath, err, lockPath, err2) + err = errors.Errorf("Failed locking %s: %v\nFailed locking %s: %v", advisoryLockPath, err, lockPath, err2) return lockfile, err } diff --git a/pkg/squashfs/squashfs.go b/pkg/squashfs/squashfs.go index aa710d9fb..a1b10006e 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,51 @@ func maybeKernelSquashMount(squashFile, extractDir string) (bool, error) { return false, kernelSquashMountFailed } +func findSquashfusePath() string { + if p := which("squashfuse_ll"); p != "" { + return p + } + return which("squashfuse") +} + +var squashNotFound = errors.Errorf("squashfuse program not found") + +func squashFuse(squashFile, extractDir string) (*exec.Cmd, error) { + sqfuse := findSquashfusePath() + if sqfuse == "" { + return nil, squashNotFound + } + + // 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(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) + 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 +272,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") + _, err = squashFuse(squashFile, extractDir) + if err == nil || err != squashNotFound { + 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 +319,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 +327,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..9201315fe 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,116 @@ 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 errors.Errorf("%s is already mounted", mountpoint) + } + + 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 errors.Errorf("no squashfs binary found") + } + fiPre, err := os.Lstat(mountpoint) + if err != nil { + return errors.Wrapf(err, "Failed stat'ing %q", mountpoint) + } + if fiPre.Mode()&os.ModeSymlink != 0 { + return errors.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 errors.Wrapf(err, "Failed releasing squashfuse process") + } + + 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 +} + +// Takes /proc/self/uid_map contents as one string +// Returns true if this is a uidmap representing the whole host +// uid range. +func uidmapIsHost(oneline string) bool { + if strings.HasSuffix(oneline, "\n") { + oneline = oneline[:len(oneline)-1] + } + if len(oneline) == 0 { + return false + } + lines := strings.Split(oneline, "\n") + if len(lines) != 1 { + return false + } + words := strings.Fields(lines[0]) + if len(words) != 3 || words[0] != "0" || words[1] != "0" || words[2] != "4294967295" { + return false + } + + return true +} + +func amHostRoot() bool { + // if not uid 0, not host root + if os.Geteuid() != 0 { + return false + } + // if uid_map doesn't map 0 to 0, not host root + bytes, err := os.ReadFile("/proc/self/uid_map") + if err != nil { + return false + } + return uidmapIsHost(string(bytes)) +} + +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) diff --git a/pkg/squashfs/verity_test.go b/pkg/squashfs/verity_test.go index 273f47c4c..7e9ac6603 100644 --- a/pkg/squashfs/verity_test.go +++ b/pkg/squashfs/verity_test.go @@ -11,6 +11,47 @@ import ( "github.com/stretchr/testify/assert" ) +type uidmapTestcase struct { + uidmap string + expected bool +} + +var uidmapTests = []uidmapTestcase{ + uidmapTestcase{ + uidmap: ` 0 0 4294967295`, + expected: true, + }, + uidmapTestcase{ + uidmap: ` 0 0 1000 +2000 2000 1`, + expected: false, + }, + uidmapTestcase{ + uidmap: ` 0 0 1000`, + expected: false, + }, + uidmapTestcase{ + uidmap: ` 10 0 4294967295`, + expected: false, + }, + uidmapTestcase{ + uidmap: ` 0 10 4294967295`, + expected: false, + }, + uidmapTestcase{ + uidmap: ` 0 0 1`, + expected: false, + }, +} + +func TestAmHostRoot(t *testing.T) { + assert := assert.New(t) + for _, testcase := range uidmapTests { + v := uidmapIsHost(testcase.uidmap) + assert.Equal(v, testcase.expected) + } +} + func TestVerityMetadata(t *testing.T) { assert := assert.New(t)