Permalink
Switch branches/tags
Find file
Fetching contributors…
Cannot retrieve contributors at this time
391 lines (348 sloc) 12.5 KB
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2017 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package main
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"syscall"
"github.com/snapcore/snapd/interfaces/mount"
"github.com/snapcore/snapd/logger"
)
// not available through syscall
const (
umountNoFollow = 8
)
// For mocking everything during testing.
var (
osLstat = os.Lstat
osReadlink = os.Readlink
sysClose = syscall.Close
sysMkdirat = syscall.Mkdirat
sysMount = syscall.Mount
sysOpen = syscall.Open
sysOpenat = syscall.Openat
sysUnmount = syscall.Unmount
sysFchown = syscall.Fchown
ioutilReadDir = ioutil.ReadDir
)
// ReadOnlyFsError is an error encapsulating encountered EROFS.
type ReadOnlyFsError struct {
Path string
}
func (e *ReadOnlyFsError) Error() string {
return fmt.Sprintf("cannot operate on read-only filesystem at %s", e.Path)
}
// Create directories for all but the last segments and return the file
// descriptor to the leaf directory. This function is a base for secure
// variants of mkdir, touch and symlink.
func secureMkPrefix(segments []string, perm os.FileMode, uid, gid int) (int, error) {
logger.Debugf("secure-mk-prefix %q %v %d %d -> ...", segments, perm, uid, gid)
// Declare var and don't assign-declare below to ensure we don't swallow
// any errors by mistake.
var err error
var fd int
const openFlags = syscall.O_NOFOLLOW | syscall.O_CLOEXEC | syscall.O_DIRECTORY
// Open the root directory and start there.
fd, err = sysOpen("/", openFlags, 0)
if err != nil {
return -1, fmt.Errorf("cannot open root directory: %v", err)
}
if len(segments) > 1 {
defer sysClose(fd)
}
if len(segments) > 0 {
// Process all but the last segment.
for i := range segments[:len(segments)-1] {
fd, err = secureMkDir(fd, segments, i, perm, uid, gid)
if err != nil {
return -1, err
}
// Keep the final FD open (caller needs to close it).
if i < len(segments)-2 {
defer sysClose(fd)
}
}
}
logger.Debugf("secure-mk-prefix %q %v %d %d -> %d", segments, perm, uid, gid, fd)
return fd, nil
}
// secureMkdir creates a directory at i-th entry of absolute path represented
// by segments. This function can be used to construct subsequent elements of
// the constructed path. The return value contains the newly created file
// descriptor or -1 on error.
func secureMkDir(fd int, segments []string, i int, perm os.FileMode, uid, gid int) (int, error) {
logger.Debugf("secure-mk-dir %d %q %d %v %d %d -> ...", fd, segments, i, perm, uid, gid)
segment := segments[i]
made := true
var err error
var newFd int
const openFlags = syscall.O_NOFOLLOW | syscall.O_CLOEXEC | syscall.O_DIRECTORY
if err = sysMkdirat(fd, segment, uint32(perm)); err != nil {
switch err {
case syscall.EEXIST:
made = false
case syscall.EROFS:
// Treat EROFS specially: this is a hint that we have to poke a
// hole using tmpfs. The path below is the location where we
// need to poke the hole.
p := "/" + strings.Join(segments[:i], "/")
return -1, &ReadOnlyFsError{Path: p}
default:
return -1, fmt.Errorf("cannot mkdir path segment %q: %v", segment, err)
}
}
newFd, err = sysOpenat(fd, segment, openFlags, 0)
if err != nil {
return -1, fmt.Errorf("cannot open path segment %q (got up to %q): %v", segment,
"/"+strings.Join(segments[:i], "/"), err)
}
if made {
// Chown each segment that we made.
if err := sysFchown(newFd, uid, gid); err != nil {
// Close the FD we opened if we fail here since the caller will get
// an error and won't assume responsibility for the FD.
sysClose(newFd)
return -1, fmt.Errorf("cannot chown path segment %q to %d.%d (got up to %q): %v", segment, uid, gid,
"/"+strings.Join(segments[:i], "/"), err)
}
}
logger.Debugf("secure-mk-dir %d %q %d %v %d %d -> %d", fd, segments, i, perm, uid, gid, newFd)
return newFd, err
}
// secureMkdirAll is the secure variant of os.MkdirAll.
func secureMkFile(fd int, segments []string, i int, perm os.FileMode, uid, gid int) error {
logger.Debugf("secure-mk-file %d %q %d %v %d %d", fd, segments, i, perm, uid, gid)
segment := segments[i]
made := true
var newFd int
var err error
const openFlags = syscall.O_NOFOLLOW | syscall.O_CLOEXEC | syscall.O_RDWR
// Open the final path segment as a file. Try to create the file (so that
// we know if we need to chown it) but fall back to just opening an
// existing one.
newFd, err = sysOpenat(fd, segment, openFlags|syscall.O_CREAT|syscall.O_EXCL, 0644)
if err != nil {
switch err {
case syscall.EEXIST:
// If the file exists then just open it without O_EXCL
newFd, err = sysOpenat(fd, segment, openFlags, 0)
if err != nil {
return fmt.Errorf("cannot open file %q: %v", segment, err)
}
made = false
case syscall.EROFS:
// Treat EROFS specially: this is a hint that we have to poke a
// hole using tmpfs. The path below is the location where we
// need to poke the hole.
p := "/" + strings.Join(segments[:i], "/")
return &ReadOnlyFsError{Path: p}
default:
return fmt.Errorf("cannot open file %q: %v", segment, err)
}
}
defer sysClose(newFd)
if made {
// Chown the file if we made it.
if err := sysFchown(newFd, uid, gid); err != nil {
return fmt.Errorf("cannot chown file %q to %d.%d: %v", segment, uid, gid, err)
}
}
return nil
}
// SecureMkdirAll is the secure variant of os.MkdirAll.
//
// Unlike the regular version this implementation does not follow any symbolic
// links. At all times the new directory segment is created using mkdirat(2)
// while holding an open file descriptor to the parent directory.
//
// The only handled error is mkdirat(2) that fails with EEXIST. All other
// errors are fatal but there is no attempt to undo anything that was created.
//
// The uid and gid are used for the fchown(2) system call which is performed
// after each segment is created and opened. The special value -1 may be used
// to request that ownership is not changed.
func secureMkdirAll(name string, perm os.FileMode, uid, gid int) error {
logger.Debugf("secure-mkdir-all %q %v %d %d", name, perm, uid, gid)
// Only support absolute paths to avoid bugs in snap-confine when
// called from anywhere.
if !filepath.IsAbs(name) {
return fmt.Errorf("cannot create directory with relative path: %q", name)
}
segments := strings.FieldsFunc(filepath.Clean(name), func(c rune) bool { return c == '/' })
// Create the prefix.
fd, err := secureMkPrefix(segments, perm, uid, gid)
if err != nil {
return err
}
defer sysClose(fd)
if len(segments) > 0 {
// Create the final segment as a directory.
fd, err = secureMkDir(fd, segments, len(segments)-1, perm, uid, gid)
if err != nil {
return err
}
defer sysClose(fd)
}
return nil
}
// secureMkfileAll is a secure implementation of "mkdir -p $(dirname $1) && touch $1".
//
// This function is like secureMkdirAll but it creates an empty file instead of
// a directory for the final path component. Each created directory component
// is chowned to the desired user and group.
func secureMkfileAll(name string, perm os.FileMode, uid, gid int) error {
logger.Debugf("secure-mkfile-all %q %q %d %d", name, perm, uid, gid)
// Only support absolute paths to avoid bugs in snap-confine when
// called from anywhere.
if !filepath.IsAbs(name) {
return fmt.Errorf("cannot create file with relative path: %q", name)
}
segments := strings.FieldsFunc(filepath.Clean(name), func(c rune) bool { return c == '/' })
// Create the prefix.
fd, err := secureMkPrefix(segments, perm, uid, gid)
if err != nil {
return err
}
defer sysClose(fd)
// Create the final segment as a file.
err = secureMkFile(fd, segments, len(segments)-1, perm, uid, gid)
return err
}
func planWritableMimic(dir string) ([]*Change, error) {
// We need a place for "safe keeping" of what is present in the original
// directory as we are about to attach a tmpfs there, which will hide
// everything inside.
logger.Debugf("create-writable-mimic %q", dir)
safeKeepingDir := filepath.Join("/tmp/.snap/", dir)
var changes []*Change
// Bind mount the original directory elsewhere for safe-keeping.
changes = append(changes, &Change{
Action: Mount, Entry: mount.Entry{
// XXX: should we rbind here?
Name: dir, Dir: safeKeepingDir, Options: []string{"bind"}},
})
// Mount tmpfs over the original directory, hiding its contents.
changes = append(changes, &Change{
Action: Mount, Entry: mount.Entry{Name: "none", Dir: dir, Type: "tmpfs"},
})
// Iterate over the items in the original directory (nothing is mounted _yet_).
entries, err := ioutilReadDir(dir)
if err != nil {
return nil, err
}
for _, fi := range entries {
// Skip non-directory elements as we cannot handle those yet.
ch := &Change{Action: Mount, Entry: mount.Entry{
Name: filepath.Join(safeKeepingDir, fi.Name()),
Dir: filepath.Join(dir, fi.Name()),
// XXX: should we rbind here?
Options: []string{"bind", "ro"},
}}
// Bind mount each element from the safe-keeping directory into the
// tmpfs. Our Change.Perform() engine can create the missing
// directories automatically so we don't bother creating those.
m := fi.Mode()
switch {
case m.IsDir():
changes = append(changes, ch)
case m.IsRegular():
ch.Entry.Options = append(ch.Entry.Options, "x-snapd.kind=file")
changes = append(changes, ch)
case m&os.ModeSymlink != 0:
if target, err := osReadlink(filepath.Join(dir, fi.Name())); err == nil {
ch.Entry.Options = append(ch.Entry.Options, "x-snapd.kind=symlink", fmt.Sprintf("x-snapd.symlink=%s", target))
changes = append(changes, ch)
}
default:
logger.Noticef("skipping unsupported thing %s", fi)
}
}
// Finally unbind the safe-keeping directory as we don't need it anymore.
changes = append(changes, &Change{
Action: Unmount, Entry: mount.Entry{Name: "none", Dir: safeKeepingDir},
})
return changes, nil
}
func createWritableMimic(dir string) ([]*Change, error) {
changes, err := planWritableMimic(dir)
if err != nil {
return nil, err
}
// Now that we have a plan, we need to go and execute it.
for i, change := range changes {
if _, err := change.Perform(); err != nil {
logger.Debugf("DRAT, stuff failed: %v", err)
// Drat, undo everything we managed to do. This is better than
// trying to make the rest of the app handle this as we are messing
// around the tree in non-obvious and overlapping ways that are
// harder to reason about.
for j := i - 1; j >= 0; j-- {
if change.Action == Mount {
reverseChange := &Change{Action: Unmount, Entry: mount.Entry{Dir: change.Entry.Dir}}
// Best effort, no errors here.
reverseChange.Perform()
}
}
logger.Debugf("done undoing failed changes")
return nil, err
}
}
return changes, nil
}
func ensureMountPoint(path string, mode os.FileMode, uid int, gid int) error {
// If the mount point is not present then create a directory in its
// place. This is very naive, doesn't handle read-only file systems
// but it is a good starting point for people working with things like
// $SNAP_DATA/subdirectory.
//
// We use lstat to ensure that we don't follow the symlink in case one
// was set up by the snap. Note that at the time this is run, all the
// snap's processes are frozen.
fi, err := osLstat(path)
switch {
case err != nil && os.IsNotExist(err):
return secureMkdirAll(path, mode, uid, gid)
case err != nil:
return fmt.Errorf("cannot inspect %q: %v", path, err)
case err == nil:
// Ensure that mount point is a directory.
if !fi.IsDir() {
return fmt.Errorf("cannot use %q for mounting, not a directory", path)
}
}
return nil
}
func ensureMountPointUsingWritableMimic(dir string, mode os.FileMode, uid int, gid int) ([]*Change, error) {
var synth []*Change
err := ensureMountPoint(dir, mode, uid, gid)
if err != nil {
if err2, ok := err.(*ReadOnlyFsError); ok {
synth, err = createWritableMimic(err2.Path)
}
if err == nil {
err = ensureMountPoint(dir, mode, uid, gid)
}
}
return synth, err
}
var changePerform = func(chg *Change) ([]*Change, error) {
return chg.Perform()
}