/
trespassing.go
296 lines (272 loc) · 11.2 KB
/
trespassing.go
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
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2017-2018 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"
"os"
"path/filepath"
"strings"
"syscall"
"github.com/snapcore/snapd/logger"
)
// Assumptions track the assumptions about the state of the filesystem.
//
// Assumptions constitute the global part of the write restriction management.
// Assumptions are global in the sense that they span multiple distinct write
// operations. In contrast, Restrictions track per-operation state.
type Assumptions struct {
unrestrictedPaths []string
pastChanges []*Change
// verifiedDevices represents the set of devices that are verified as a tmpfs
// that was mounted by snapd. Those are only discovered on-demand. The
// major:minor number is packed into one uint64 as in syscall.Stat_t.Dev
// field.
verifiedDevices map[uint64]bool
// modeHints overrides implicit 0755 mode of directories created while
// ensuring source and target paths exist.
modeHints []ModeHint
}
// ModeHint provides mode for directories created to satisfy mount changes.
type ModeHint struct {
PathGlob string
Mode os.FileMode
}
// AddUnrestrictedPaths adds a list of directories where writing is allowed
// even if it would hit the real host filesystem (or transit through the host
// filesystem). This is intended to be used with certain well-known locations
// such as /tmp, $SNAP_DATA and $SNAP.
func (as *Assumptions) AddUnrestrictedPaths(paths ...string) {
as.unrestrictedPaths = append(as.unrestrictedPaths, paths...)
}
// AddModeHint adds a path glob and mode used when creating path elements.
func (as *Assumptions) AddModeHint(pathGlob string, mode os.FileMode) {
as.modeHints = append(as.modeHints, ModeHint{PathGlob: pathGlob, Mode: mode})
}
// ModeForPath returns the mode for creating a directory at a given path.
//
// The default mode is 0755 but AddModeHint calls can influence the mode at a
// specific path. When matching path elements, "*" does not match the directory
// separator. In effect it can only be used as a wildcard for a specific
// directory name. This constraint makes hints easier to model in practice.
//
// When multiple hints match the given path, ModeForPath panics.
func (as *Assumptions) ModeForPath(path string) os.FileMode {
mode := os.FileMode(0755)
var foundHint *ModeHint
for _, hint := range as.modeHints {
if ok, _ := filepath.Match(hint.PathGlob, path); ok {
if foundHint == nil {
mode = hint.Mode
foundHint = &hint
} else {
panic(fmt.Errorf("cannot find unique mode for path %q: %q and %q both provide hints",
path, foundHint.PathGlob, foundHint.PathGlob))
}
}
}
return mode
}
// isRestricted checks whether a path falls under restricted writing scheme.
//
// Provided path is the full, absolute path of the entity that needs to be
// created (directory, file or symbolic link).
func (as *Assumptions) isRestricted(path string) bool {
// Anything rooted at one of the unrestricted paths is not restricted.
// Those are for things like /var/snap/, for example.
for _, p := range as.unrestrictedPaths {
if p == "/" || p == path || strings.HasPrefix(path, filepath.Clean(p)+"/") {
return false
}
}
// All other paths are restricted
return true
}
// MockUnrestrictedPaths replaces the set of path paths without any restrictions.
func (as *Assumptions) MockUnrestrictedPaths(paths ...string) (restore func()) {
old := as.unrestrictedPaths
as.unrestrictedPaths = paths
return func() {
as.unrestrictedPaths = old
}
}
// AddChange records the fact that a change was applied to the system.
func (as *Assumptions) AddChange(change *Change) {
as.pastChanges = append(as.pastChanges, change)
}
// canWriteToDirectory returns true if writing to a given directory is allowed.
//
// Writing is allowed in one of thee cases:
// 1. The directory is in one of the explicitly permitted locations.
// This is the strongest permission as it explicitly allows writing to
// places that may show up on the host, one of the examples being $SNAP_DATA.
// 2. The directory is on a read-only filesystem.
// 3. The directory is on a tmpfs created by snapd.
func (as *Assumptions) canWriteToDirectory(dirFd int, dirName string) (bool, error) {
if !as.isRestricted(dirName) {
return true, nil
}
var fsData syscall.Statfs_t
if err := sysFstatfs(dirFd, &fsData); err != nil {
return false, fmt.Errorf("cannot fstatfs %q: %s", dirName, err)
}
var fileData syscall.Stat_t
if err := sysFstat(dirFd, &fileData); err != nil {
return false, fmt.Errorf("cannot fstat %q: %s", dirName, err)
}
// Writing to read only directories is allowed because EROFS is handled
// by each of the writing helpers already.
if ok := isReadOnly(dirName, &fsData); ok {
return true, nil
}
// Writing to a trusted tmpfs is allowed because those are not leaking to
// the host. Also, each time we find a good tmpfs we explicitly remember the device major/minor,
if as.verifiedDevices[fileData.Dev] {
return true, nil
}
if ok := isPrivateTmpfsCreatedBySnapd(dirName, &fsData, &fileData, as.pastChanges); ok {
if as.verifiedDevices == nil {
as.verifiedDevices = make(map[uint64]bool)
}
// Don't record 0:0 as those are all to easy to add in tests and would
// skew tests using zero-initialized structures. Real device numbers
// are not zero either so this is not a test-only conditional.
if fileData.Dev != 0 {
as.verifiedDevices[fileData.Dev] = true
}
return true, nil
}
// If writing is not not allowed by one of the three rules above then it is
// disallowed.
return false, nil
}
// RestrictionsFor computes restrictions for the desired path.
func (as *Assumptions) RestrictionsFor(desiredPath string) *Restrictions {
// Writing to a restricted path results in step-by-step validation of each
// directory, starting from the root of the file system. Unless writing is
// allowed a mimic must be constructed to ensure that writes are not visible in
// undesired locations of the host filesystem.
if as.isRestricted(desiredPath) {
return &Restrictions{assumptions: as, desiredPath: desiredPath, restricted: true}
}
return nil
}
// Restrictions contains meta-data of a compound write operation.
//
// This structure helps functions that write to the filesystem to keep track of
// the ultimate destination across several calls (e.g. the function that
// creates a file needs to call helpers to create subsequent directories).
// Keeping track of the desired path aids in constructing useful error
// messages.
//
// In addition the structure keeps track of the restricted write mode flag which
// is based on the full path of the desired object being constructed. This allows
// various write helpers to avoid trespassing on host filesystem in places that
// are not expected to be written to by snapd (e.g. outside of $SNAP_DATA).
type Restrictions struct {
assumptions *Assumptions
desiredPath string
restricted bool
}
// Check verifies whether writing to a directory would trespass on the host.
//
// The check is only performed in restricted mode. If the check fails a
// TrespassingError is returned.
func (rs *Restrictions) Check(dirFd int, dirName string) error {
if rs == nil || !rs.restricted {
return nil
}
// In restricted mode check the directory before attempting to write to it.
ok, err := rs.assumptions.canWriteToDirectory(dirFd, dirName)
if ok || err != nil {
return err
}
if dirName == "/" {
// If writing to / is not allowed then we are in a tough spot because
// we cannot construct a writable mimic over /. This should never
// happen in normal circumstances because the root filesystem is some
// kind of base snap.
return fmt.Errorf("cannot recover from trespassing over /")
}
logger.Debugf("trespassing violated %q while striving to %q", dirName, rs.desiredPath)
logger.Debugf("restricted mode: %#v", rs.restricted)
logger.Debugf("unrestricted paths: %q", rs.assumptions.unrestrictedPaths)
logger.Debugf("verified devices: %v", rs.assumptions.verifiedDevices)
logger.Debugf("past changes: %v", rs.assumptions.pastChanges)
return &TrespassingError{ViolatedPath: filepath.Clean(dirName), DesiredPath: rs.desiredPath}
}
// Lift lifts write restrictions for the desired path.
//
// This function should be called when, as subsequent components of a path are
// either discovered or created, the conditions for using restricted mode are
// no longer true.
func (rs *Restrictions) Lift() {
if rs != nil {
rs.restricted = false
}
}
// TrespassingError is an error when filesystem operation would affect the host.
type TrespassingError struct {
ViolatedPath string
DesiredPath string
}
// Error returns a formatted error message.
func (e *TrespassingError) Error() string {
return fmt.Sprintf("cannot write to %q because it would affect the host in %q", e.DesiredPath, e.ViolatedPath)
}
// isReadOnly checks whether the underlying filesystem is read only or is mounted as such.
func isReadOnly(dirName string, fsData *syscall.Statfs_t) bool {
// If something is mounted with f_flags & ST_RDONLY then is read-only.
if fsData.Flags&StReadOnly == StReadOnly {
return true
}
// If something is a known read-only file-system then it is safe.
// Older copies of snapd were not mounting squashfs as read only.
if fsData.Type == SquashfsMagic {
return true
}
return false
}
// isPrivateTmpfsCreatedBySnapd checks whether a directory resides on a tmpfs mounted by snapd
//
// The function inspects the directory and a list of changes that were applied
// to the mount namespace. A directory is trusted if it is a tmpfs that was
// mounted by snap-confine or snapd-update-ns. Note that sub-directories of a
// trusted tmpfs are not considered trusted by this function.
func isPrivateTmpfsCreatedBySnapd(dirName string, fsData *syscall.Statfs_t, fileData *syscall.Stat_t, changes []*Change) bool {
// If something is not a tmpfs it cannot be the trusted tmpfs we are looking for.
if fsData.Type != TmpfsMagic {
return false
}
// Any of the past changes that mounted a tmpfs exactly at the directory we
// are inspecting is considered as trusted. This is conservative because it
// doesn't trust sub-directories of a trusted tmpfs. This approach is
// sufficient for the intended use.
//
// The algorithm goes over all the changes in reverse and picks up the
// first tmpfs mount or unmount action that matches the directory name.
// The set of constraints in snap-update-ns and snapd prevent from mounting
// over an existing mount point so we don't need to consider e.g. a bind
// mount shadowing an active tmpfs.
for i := len(changes) - 1; i >= 0; i-- {
change := changes[i]
if change.Entry.Type == "tmpfs" && change.Entry.Dir == dirName {
return change.Action == Mount || change.Action == Keep
}
}
return false
}