Skip to content

Commit

Permalink
feat: atomfs mount: use squashfuse for non-root users
Browse files Browse the repository at this point in the history
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 <serge@hallyn.com>
  • Loading branch information
hallyn committed Dec 21, 2022
1 parent 37021f0 commit 18b73d1
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 33 deletions.
2 changes: 1 addition & 1 deletion pkg/atomfs/molecule.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,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
}

Expand Down
84 changes: 53 additions & 31 deletions pkg/squashfs/squashfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"sync"
"syscall"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -297,15 +319,15 @@ 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}
}
} else {
search = []string{}
for _, p := range paths {
search = append(search, path.Join(p, name))
search = append(search, filepath.Join(p, name))
}
}

Expand Down
113 changes: 112 additions & 1 deletion pkg/squashfs/verity.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,15 @@ import (
"strconv"
"strings"
"syscall"
"time"
"unsafe"

"github.com/anuvu/squashfs"
"github.com/freddierice/go-losetup"
"github.com/martinjungblut/go-cryptsetup"
"github.com/pkg/errors"
"golang.org/x/sys/unix"
"stackerbuild.io/stacker/pkg/log"
"stackerbuild.io/stacker/pkg/mount"
)

Expand Down Expand Up @@ -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.Errorf("Failed stat'ing %q: %w", mountpoint, err)
}
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.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
}

// 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)
Expand Down
41 changes: 41 additions & 0 deletions pkg/squashfs/verity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down

0 comments on commit 18b73d1

Please sign in to comment.