File: mount.go

package info (click to toggle)
snapd 2.72-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 80,412 kB
  • sloc: sh: 16,506; ansic: 16,211; python: 11,213; makefile: 1,919; exp: 190; awk: 58; xml: 22
file content (275 lines) | stat: -rw-r--r-- 7,821 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
 * Copyright (C) 2022 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 ctlcmd

import (
	"fmt"
	"strings"

	"github.com/snapcore/snapd/i18n"
	"github.com/snapcore/snapd/interfaces"
	"github.com/snapcore/snapd/interfaces/utils"
	"github.com/snapcore/snapd/overlord/hookstate"
	"github.com/snapcore/snapd/overlord/ifacestate"
	"github.com/snapcore/snapd/overlord/snapstate"
	"github.com/snapcore/snapd/snap"
	"github.com/snapcore/snapd/strutil"
	"github.com/snapcore/snapd/systemd"
)

var (
	shortMountHelp = i18n.G("Create a temporary or permanent mount")
	longMountHelp  = i18n.G(`
The mount command mounts the given source onto the given destination path,
provided that the snap has a plug for the mount-control interface which allows
this operation.`)
)

func init() {
	addCommand("mount", shortMountHelp, longMountHelp, func() command { return &mountCommand{} })
}

type mountCommand struct {
	baseCommand
	Positional struct {
		What  string `positional-arg-name:"<what>" required:"yes" description:"path to the resource to be mounted"`
		Where string `positional-arg-name:"<where>" required:"yes" description:"path to the destination mount point"`
	} `positional-args:"yes" required:"yes"`
	Persistent  bool   `long:"persistent" description:"make the mount persist across reboots"`
	Type        string `long:"type" short:"t" description:"filesystem type"`
	Options     string `long:"options" short:"o" description:"comma-separated list of mount options"`
	snapInfo    *snap.Info
	optionsList []string
}

func matchMountPathAttribute(path string, attribute any, snapInfo *snap.Info) bool {
	pattern, ok := attribute.(string)
	if !ok {
		return false
	}

	expandedPattern := snapInfo.ExpandSnapVariables(pattern)

	const allowCommas = true
	pp, err := utils.NewPathPattern(expandedPattern, allowCommas)
	return err == nil && pp.Matches(path)
}

func matchMountSourceAttribute(path string, attribute any, fsType string, snapInfo *snap.Info) bool {
	switch fsType {
	case "nfs":
		// NFS mount source AppArmor profiles expects a match for "*:**", so
		// make sure that the attribute is unset, and the path matches the
		// format
		if _, ok := attribute.(string); ok {
			return false
		}

		host, share, found := strings.Cut(path, ":")
		if !found || host == "" || strings.Contains(host, "/") || share == "" {
			return false
		}

		return true
	case "cifs":
		// CIFS mount source AppArmor profiles expects a match for "//**",
		// make sure that the attribute is unset, and the path matches the
		// format
		if _, ok := attribute.(string); ok {
			return false
		}

		if !strings.HasPrefix(path, "//") {
			return false
		}

		hostSlashShare := strings.TrimPrefix(path, "//")
		if hostSlashShare == "" {
			return false
		}

		return true
	default:
		return matchMountPathAttribute(path, attribute, snapInfo)
	}
}

// matchConnection checks whether the given mount connection attributes give
// the snap permission to execute the mount command
func (m *mountCommand) matchConnection(attributes map[string]any) bool {
	if m.Type != "" {
		if types, ok := attributes["type"].([]any); ok {
			found := false
			for _, iface := range types {
				if typeString, ok := iface.(string); ok && typeString == m.Type {
					found = true
					break
				}
			}
			if !found {
				return false
			}
		} else {
			return false
		}
	} else {
		// The filesystem type was not given; we let it through only if the
		// plug also did not specify a type.
		if _, typeIsSet := attributes["type"]; typeIsSet {
			return false
		}
	}

	if !matchMountSourceAttribute(m.Positional.What, attributes["what"], m.Type, m.snapInfo) {
		return false
	}

	if !matchMountPathAttribute(m.Positional.Where, attributes["where"], m.snapInfo) {
		return false
	}

	// TODO we do exact match on the mount options, which means that plugs
	// referencing filesystems, which may require authentication options passed
	// in -o <option-list>, would need to spell out all authentication options
	// directly in plug declaration. This may be unacceptable in certain
	// scenarios, e.g. CIFS with user=foo,password=foo options.
	if optionsIfaces, ok := attributes["options"].([]any); ok {
		var allowedOptions []string
		for _, iface := range optionsIfaces {
			if option, ok := iface.(string); ok {
				allowedOptions = append(allowedOptions, option)
			}
		}
		for _, option := range m.optionsList {
			if !strutil.ListContains(allowedOptions, option) {
				return false
			}
		}
	}

	if m.Persistent {
		if allowedPersistent, ok := attributes["persistent"].(bool); !ok || !allowedPersistent {
			return false
		}
	}

	return true
}

// checkConnections checks whether the established connections give the snap
// permission to execute the mount command
func (m *mountCommand) checkConnections(context *hookstate.Context) error {
	snapName := context.InstanceName()

	st := context.State()
	st.Lock()
	defer st.Unlock()

	conns, err := ifacestate.ConnectionStates(st)
	if err != nil {
		return fmt.Errorf("internal error: cannot get connections: %s", err)
	}

	m.snapInfo, err = snapstate.CurrentInfo(st, snapName)
	if err != nil {
		return fmt.Errorf("internal error: cannot get snap info: %s", err)
	}

	for connId, connState := range conns {
		if connState.Interface != "mount-control" {
			continue
		}

		if !connState.Active() {
			continue
		}

		connRef, err := interfaces.ParseConnRef(connId)
		if err != nil {
			return err
		}

		if connRef.PlugRef.Snap != snapName {
			continue
		}

		mounts, ok := connState.StaticPlugAttrs["mount"].([]any)
		if !ok {
			continue
		}

		for _, mountAttributes := range mounts {
			if m.matchConnection(mountAttributes.(map[string]any)) {
				return nil
			}
		}
	}
	return fmt.Errorf("no matching mount-control connection found")
}

func (m *mountCommand) ensureMount(sysd systemd.Systemd) (string, error) {
	snapName := m.snapInfo.InstanceName()
	revision := m.snapInfo.SnapRevision().String()
	lifetime := systemd.Transient
	if m.Persistent {
		lifetime = systemd.Persistent
	}
	unitName, err := sysd.EnsureMountUnitFileWithOptions(&systemd.MountUnitOptions{
		Lifetime:               lifetime,
		Description:            fmt.Sprintf("Mount unit for %s, revision %s via mount-control", snapName, revision),
		What:                   m.Positional.What,
		Where:                  m.Positional.Where,
		Fstype:                 m.Type,
		Options:                m.optionsList,
		Origin:                 "mount-control",
		EnsureStartIfUnchanged: true,
	})
	if err != nil {
		_ = sysd.RemoveMountUnitFile(m.Positional.Where)
	}
	return unitName, err
}

func (m *mountCommand) Execute([]string) error {
	context, err := m.ensureContext()
	if err != nil {
		return err
	}

	// Parse the mount options into an array
	for _, option := range strings.Split(m.Options, ",") {
		if option != "" {
			m.optionsList = append(m.optionsList, option)
		}
	}

	if err := m.checkConnections(context); err != nil {
		snapName := context.InstanceName()
		return fmt.Errorf("snap %q lacks permissions to create the requested mount: %v", snapName, err)
	}

	sysd := systemd.New(systemd.SystemMode, nil)
	_, err = m.ensureMount(sysd)
	if err != nil {
		return fmt.Errorf("cannot ensure mount unit: %v", err)
	}

	return nil
}