Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] many: export tools from core/snapd to mount namespaces #8843

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
73 changes: 27 additions & 46 deletions cmd/snap-confine/mount-support.c
Expand Up @@ -378,54 +378,35 @@ static void sc_bootstrap_mount_namespace(const struct sc_mount_config *config)
sc_do_mount("none", dst, NULL, MS_SLAVE, NULL);
}
}
// The "core" base snap is special as it contains snapd and friends.
// Other base snaps do not, so whenever a base snap other than core is
// in use we need extra provisions for setting up internal tooling to
// be available.
//
// However on a core18 (and similar) system the core snap is not
// a special base anymore and we should map our own tooling in.
if (config->distro == SC_DISTRO_CORE_OTHER
|| !sc_streq(config->base_snap_name, "core")) {
// when bases are used we need to bind-mount the libexecdir
// (that contains snap-exec) into /usr/lib/snapd of the
// base snap so that snap-exec is available for the snaps
// (base snaps do not ship snapd)

// dst is always /usr/lib/snapd as this is where snapd
// assumes to find snap-exec
sc_must_snprintf(dst, sizeof dst, "%s/usr/lib/snapd",
scratch_dir);

// bind mount the current $ROOT/usr/lib/snapd path,
// where $ROOT is either "/" or the "/snap/{core,snapd}/current"
// that we are re-execing from
char *src = NULL;
char self[PATH_MAX + 1] = { 0 };
ssize_t nread;
nread = readlink("/proc/self/exe", self, sizeof self - 1);
if (nread < 0) {
die("cannot read /proc/self/exe");
}
// Though we initialized self to NULs and passed one less to
// readlink, therefore guaranteeing that self is
// zero-terminated, perform an explicit assignment to make
// Coverity happy.
self[nread] = '\0';
// this cannot happen except when the kernel is buggy
if (strstr(self, "/snap-confine") == NULL) {
die("cannot use result from readlink: %s", self);
/* XXX: Put tools exported by snapd over /usr/lib/snapd. */
{
/* TODO: Use a fixed <snapd> export and have the export manager put the
* appropriate tools inside, this will automatically handle all
* possible transitions. */
static const char *providers[] = {"snapd", "core", NULL};
bool tools_mounted = false;
sc_must_snprintf(dst, sizeof dst, "%s/usr/lib/snapd", scratch_dir);
for (const char **provider = providers; *provider != NULL; ++provider) {
struct stat sb;
sc_must_snprintf(src, sizeof src, "/var/lib/snapd/export/%s/tools/snap-exec", *provider);
/* Check that snap-exec exists, the tools directory may be empty. */
if (lstat(src, &sb) != 0) {
continue;
}
/* Mount the entire tools directory as /usr/lib/snapd inside the mount namespace. */
sc_must_snprintf(src, sizeof src, "/var/lib/snapd/export/%s/tools", *provider);
sc_do_mount(src, dst, NULL, MS_BIND | MS_RDONLY, NULL);
sc_do_mount("none", dst, NULL, MS_SLAVE, NULL);
tools_mounted = true;
break;
}
src = dirname(self);
// dirname(path) might return '.' depending on path.
// /proc/self/exe should always point
// to an absolute path, but let's guarantee that.
if (src[0] != '/') {
die("cannot use the result of dirname(): %s", src);
if (!tools_mounted) {
/* XXX: We didn't find any exported tools. We could mount a
* snapshot of the host tools but since they would not be able to
* update, we should instead export the host tools with hostfs
* symlinks. */
die("cannot find snapd tools");
}

sc_do_mount(src, dst, NULL, MS_BIND | MS_RDONLY, NULL);
sc_do_mount("none", dst, NULL, MS_SLAVE, NULL);
}
// Bind mount the directory where all snaps are mounted. The location of
// the this directory on the host filesystem may not match the location in
Expand Down
5 changes: 2 additions & 3 deletions cmd/snap-confine/snap-confine.apparmor.in
Expand Up @@ -213,10 +213,9 @@
mount options=(rw bind) @LIBEXECDIR@/ -> /tmp/snap.rootfs_*/usr/lib/snapd/,
mount options=(rw slave) -> /tmp/snap.rootfs_*/usr/lib/snapd/,

# allow making re-execed host snap-exec available inside base snaps
mount options=(ro bind) @SNAP_MOUNT_DIR@/core/*/usr/lib/snapd/ -> /tmp/snap.rootfs_*/usr/lib/snapd/,
# allow making snapd snap tools available inside base snaps
mount options=(ro bind) @SNAP_MOUNT_DIR@/snapd/*/usr/lib/snapd/ -> /tmp/snap.rootfs_*/usr/lib/snapd/,
mount options=(rw bind) /var/lib/snapd/export/{core,snapd}/tools/ -> /tmp/snap.rootfs_*/usr/lib/snapd/,
mount options=(remount ro bind) -> /tmp/snap.rootfs_*/usr/lib/snapd/,

mount options=(rw bind) /usr/bin/snapctl -> /tmp/snap.rootfs_*/usr/bin/snapctl,
mount options=(rw slave) -> /tmp/snap.rootfs_*/usr/bin/snapctl,
Expand Down
4 changes: 4 additions & 0 deletions dirs/dirs.go
Expand Up @@ -33,6 +33,9 @@ var (
GlobalRootDir string

SnapMountDir string
// This is like SnapMountDir but always gives value true only
// from inside the snap mount namespace.
SnapMountDirInsideNs string

DistroLibExecDir string

Expand Down Expand Up @@ -270,6 +273,7 @@ func SetRootDir(rootdir string) {
} else {
SnapMountDir = filepath.Join(rootdir, defaultSnapMountDir)
}
SnapMountDirInsideNs = filepath.Join(rootdir, defaultSnapMountDir)

SnapDataDir = filepath.Join(rootdir, "/var/snap")
SnapDataHomeGlob = filepath.Join(rootdir, "/home/*/", UserHomeSnapDir)
Expand Down
9 changes: 5 additions & 4 deletions interfaces/apparmor/template.go
Expand Up @@ -165,14 +165,15 @@ var templateCommon = `
/etc/libnl-3/{classid,pktloc} r, # apps that use libnl

# For snappy reexec on 4.8+ kernels
/usr/lib/snapd/snap-exec m,
/{,snap/{core,snapd}/*/,var/lib/snapd/snap/{core,snapd}/*/}usr/lib/snapd/snap-exec m,
/{,snap/{core,snapd}/*/,var/lib/snapd/snap/{core,snapd}/*/}usr/lib/snapd/snap-exec ixr,

# For gdb support
/usr/lib/snapd/snap-gdb-shim ixr,
/{,snap/{core,snapd}/*/,var/lib/snapd/snap/{core,snapd}/*/}usr/lib/snapd/snap-gdb-shim ixr,

# For in-snap tab completion
/etc/bash_completion.d/{,*} r,
/usr/lib/snapd/etelpmoc.sh ixr, # marshaller (see complete.sh for out-of-snap unmarshal)
/{,snap/{core,snapd}/*/,var/lib/snapd/snap/{core,snapd}/*/}usr/lib/snapd/etelpmoc.sh ixr, # marshaller (see complete.sh for out-of-snap unmarshal)
/usr/share/bash-completion/bash_completion r, # user-provided completions (run in-snap) may use functions from here

# uptime
Expand All @@ -193,7 +194,7 @@ var templateCommon = `

# snapctl and its requirements
/usr/bin/snapctl ixr,
/usr/lib/snapd/snapctl ixr,
/{,snap/{core,snapd}/*/,var/lib/snapd/snap/{core,snapd}/*/}usr/lib/snapd/snapctl ixr,
@{PROC}/sys/net/core/somaxconn r,
/run/snapd-snap.socket rw,

Expand Down
1 change: 1 addition & 0 deletions overlord/devicestate/firstboot_preseed_test.go
Expand Up @@ -73,6 +73,7 @@ func checkPreseedTaskStates(c *C, st *state.State) {
"prerequisites": true,
"prepare-snap": true,
"link-snap": true,
"export-content": true,
"mount-snap": true,
"setup-profiles": true,
"copy-snap-data": true,
Expand Down
156 changes: 156 additions & 0 deletions overlord/exportstate/exportmgr.go
@@ -0,0 +1,156 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2020 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 exportstate

import (
"fmt"
"os"
"path/filepath"

"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/overlord/snapstate"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/snap"
)

const defaultExportDir = "/var/lib/snapd/export"

var exportDir = defaultExportDir

func init() {
dirs.AddRootDirCallback(func(root string) {
exportDir = filepath.Join(root, defaultExportDir)
})
}

type ExportManager struct {
state *state.State
runner *state.TaskRunner
}

// Manager returns a new ExportManager.
func Manager(s *state.State, runner *state.TaskRunner) *ExportManager {
manager := &ExportManager{state: s, runner: runner}
runner.AddHandler("export-content", manager.doExportContent, manager.undoExportContent)
runner.AddHandler("unexport-content", manager.doUnexportContent, manager.undoUnexportContent)
return manager
}

// Ensure implements StateManager.Ensure.
func (m *ExportManager) Ensure() error {
return nil
}

func (m *ExportManager) readInfo(task *state.Task) (*snap.Info, error) {
st := task.State()
st.Lock()
defer st.Unlock()

snapsup, err := snapstate.TaskSnapSetup(task)
if err != nil {
return nil, err
}
return snapstate.Info(st, snapsup.InstanceName(), snapsup.Revision())
}

func (m *ExportManager) exportContent(task *state.Task, info *snap.Info) error {
// TODO: store each exported file into the state.
for _, export := range info.NamespaceExports {
if err := m.exportOne(dirs.SnapMountDirInsideNs, export); err != nil {
return fmt.Errorf("cannot export %q from snap %s, to mount namespace, as %q: %v",
export.PrivatePath, info.SnapName(), export.PublicPath, err)
}
}
for _, export := range info.HostExports {
if err := m.exportOne(dirs.SnapMountDir, export); err != nil {
return fmt.Errorf("cannot export %q from snap %s, to the host, as %q: %v",
export.PrivatePath, info.SnapName(), export.PublicPath, err)
}
}
return nil
}
func (m *ExportManager) unexportContent(task *state.Task, info *snap.Info) error {
// TODO: use the state to know what to unexport from the given snap name.
for _, export := range info.NamespaceExports {
if err := m.unexportOne(export); err != nil {
return fmt.Errorf("cannot unexport %q from snap %s: %v",
export.PrivatePath, info.SnapName(), err)
}
}
for _, export := range info.HostExports {
if err := m.unexportOne(export); err != nil {
return fmt.Errorf("cannot unexport %q from snap %s: %v",
export.PrivatePath, info.SnapName(), err)
}
}
return nil
}

func (m *ExportManager) exportOne(baseDir string, export *snap.Export) error {
info := export.Snap
privateName := filepath.Join(baseDir, info.InstanceName(), info.Revision.String(), export.PrivatePath)
publicName := filepath.Join(exportDir, export.PublicPath)
if err := os.MkdirAll(filepath.Dir(publicName), 0755); err != nil {
return err
}
switch export.Method {
case snap.ExportMethodSymlink:
// Do we have an existing file?
fi, err := os.Lstat(publicName)
if err != nil && !os.IsNotExist(err) {
return err
}
// Verify existing symlink.
if fi != nil && fi.Mode()&os.ModeSymlink != 0 {
target, err := os.Readlink(publicName)
if err != nil {
return err
}
if target == privateName {
// Symlink is up-to-date.
return nil
}
}
// Remove existing file.
// XXX: This should never happen if we modeled the exported state.
if fi != nil {
if err := os.Remove(publicName); err != nil {
return err
}
}
// Export the current version.
return os.Symlink(privateName, publicName)
default:
return fmt.Errorf("unsupported export method %s", export.Method)
}
}

func (m *ExportManager) unexportOne(export *snap.Export) error {
publicPath := filepath.Join(exportDir, export.PublicPath)
switch export.Method {
case snap.ExportMethodSymlink:
if err := os.Remove(publicPath); err != nil {
return err
}
default:
return fmt.Errorf("unsupported export method %s", export.Method)
}
return nil
}
22 changes: 22 additions & 0 deletions overlord/exportstate/exportstate.go
@@ -0,0 +1,22 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2020 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 exportstate implements the manager and state aspects responsible
// for exporting content from snaps to the host system.
package exportstate
58 changes: 58 additions & 0 deletions overlord/exportstate/handlers.go
@@ -0,0 +1,58 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2020 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 exportstate

import (
"gopkg.in/tomb.v2"

"github.com/snapcore/snapd/overlord/state"
)

func (m *ExportManager) doExportContent(task *state.Task, tomb *tomb.Tomb) error {
info, err := m.readInfo(task)
if err != nil {
return err
}
return m.exportContent(task, info)
}

func (m *ExportManager) undoExportContent(task *state.Task, tomb *tomb.Tomb) error {
info, err := m.readInfo(task)
if err != nil {
return err
}
return m.unexportContent(task, info)
}

func (m *ExportManager) doUnexportContent(task *state.Task, tomb *tomb.Tomb) error {
info, err := m.readInfo(task)
if err != nil {
return err
}
return m.unexportContent(task, info)
}

func (m *ExportManager) undoUnexportContent(task *state.Task, tomb *tomb.Tomb) error {
info, err := m.readInfo(task)
if err != nil {
return err
}
return m.exportContent(task, info)
}