github.com/anonymouse64/snapd@v0.0.0-20210824153203-04c4c42d842d/cmd/snap-update-ns/trespassing.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2017-2018 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package main 21 22 import ( 23 "fmt" 24 "os" 25 "path/filepath" 26 "strings" 27 "syscall" 28 29 "github.com/snapcore/snapd/logger" 30 ) 31 32 // Assumptions track the assumptions about the state of the filesystem. 33 // 34 // Assumptions constitute the global part of the write restriction management. 35 // Assumptions are global in the sense that they span multiple distinct write 36 // operations. In contrast, Restrictions track per-operation state. 37 type Assumptions struct { 38 unrestrictedPaths []string 39 pastChanges []*Change 40 41 // verifiedDevices represents the set of devices that are verified as a tmpfs 42 // that was mounted by snapd. Those are only discovered on-demand. The 43 // major:minor number is packed into one uint64 as in syscall.Stat_t.Dev 44 // field. 45 verifiedDevices map[uint64]bool 46 47 // modeHints overrides implicit 0755 mode of directories created while 48 // ensuring source and target paths exist. 49 modeHints []ModeHint 50 } 51 52 // ModeHint provides mode for directories created to satisfy mount changes. 53 type ModeHint struct { 54 PathGlob string 55 Mode os.FileMode 56 } 57 58 // AddUnrestrictedPaths adds a list of directories where writing is allowed 59 // even if it would hit the real host filesystem (or transit through the host 60 // filesystem). This is intended to be used with certain well-known locations 61 // such as /tmp, $SNAP_DATA and $SNAP. 62 func (as *Assumptions) AddUnrestrictedPaths(paths ...string) { 63 as.unrestrictedPaths = append(as.unrestrictedPaths, paths...) 64 } 65 66 // AddModeHint adds a path glob and mode used when creating path elements. 67 func (as *Assumptions) AddModeHint(pathGlob string, mode os.FileMode) { 68 as.modeHints = append(as.modeHints, ModeHint{PathGlob: pathGlob, Mode: mode}) 69 } 70 71 // ModeForPath returns the mode for creating a directory at a given path. 72 // 73 // The default mode is 0755 but AddModeHint calls can influence the mode at a 74 // specific path. When matching path elements, "*" does not match the directory 75 // separator. In effect it can only be used as a wildcard for a specific 76 // directory name. This constraint makes hints easier to model in practice. 77 // 78 // When multiple hints match the given path, ModeForPath panics. 79 func (as *Assumptions) ModeForPath(path string) os.FileMode { 80 mode := os.FileMode(0755) 81 var foundHint *ModeHint 82 for _, hint := range as.modeHints { 83 if ok, _ := filepath.Match(hint.PathGlob, path); ok { 84 if foundHint == nil { 85 mode = hint.Mode 86 foundHint = &hint 87 } else { 88 panic(fmt.Errorf("cannot find unique mode for path %q: %q and %q both provide hints", 89 path, foundHint.PathGlob, foundHint.PathGlob)) 90 } 91 } 92 } 93 return mode 94 } 95 96 // isRestricted checks whether a path falls under restricted writing scheme. 97 // 98 // Provided path is the full, absolute path of the entity that needs to be 99 // created (directory, file or symbolic link). 100 func (as *Assumptions) isRestricted(path string) bool { 101 // Anything rooted at one of the unrestricted paths is not restricted. 102 // Those are for things like /var/snap/, for example. 103 for _, p := range as.unrestrictedPaths { 104 if p == "/" || p == path || strings.HasPrefix(path, filepath.Clean(p)+"/") { 105 return false 106 } 107 108 } 109 // All other paths are restricted 110 return true 111 } 112 113 // MockUnrestrictedPaths replaces the set of path paths without any restrictions. 114 func (as *Assumptions) MockUnrestrictedPaths(paths ...string) (restore func()) { 115 old := as.unrestrictedPaths 116 as.unrestrictedPaths = paths 117 return func() { 118 as.unrestrictedPaths = old 119 } 120 } 121 122 // AddChange records the fact that a change was applied to the system. 123 func (as *Assumptions) AddChange(change *Change) { 124 as.pastChanges = append(as.pastChanges, change) 125 } 126 127 // canWriteToDirectory returns true if writing to a given directory is allowed. 128 // 129 // Writing is allowed in one of thee cases: 130 // 1) The directory is in one of the explicitly permitted locations. 131 // This is the strongest permission as it explicitly allows writing to 132 // places that may show up on the host, one of the examples being $SNAP_DATA. 133 // 2) The directory is on a read-only filesystem. 134 // 3) The directory is on a tmpfs created by snapd. 135 func (as *Assumptions) canWriteToDirectory(dirFd int, dirName string) (bool, error) { 136 if !as.isRestricted(dirName) { 137 return true, nil 138 } 139 var fsData syscall.Statfs_t 140 if err := sysFstatfs(dirFd, &fsData); err != nil { 141 return false, fmt.Errorf("cannot fstatfs %q: %s", dirName, err) 142 } 143 var fileData syscall.Stat_t 144 if err := sysFstat(dirFd, &fileData); err != nil { 145 return false, fmt.Errorf("cannot fstat %q: %s", dirName, err) 146 } 147 // Writing to read only directories is allowed because EROFS is handled 148 // by each of the writing helpers already. 149 if ok := isReadOnly(dirName, &fsData); ok { 150 return true, nil 151 } 152 // Writing to a trusted tmpfs is allowed because those are not leaking to 153 // the host. Also, each time we find a good tmpfs we explicitly remember the device major/minor, 154 if as.verifiedDevices[fileData.Dev] { 155 return true, nil 156 } 157 if ok := isPrivateTmpfsCreatedBySnapd(dirName, &fsData, &fileData, as.pastChanges); ok { 158 if as.verifiedDevices == nil { 159 as.verifiedDevices = make(map[uint64]bool) 160 } 161 // Don't record 0:0 as those are all to easy to add in tests and would 162 // skew tests using zero-initialized structures. Real device numbers 163 // are not zero either so this is not a test-only conditional. 164 if fileData.Dev != 0 { 165 as.verifiedDevices[fileData.Dev] = true 166 } 167 return true, nil 168 } 169 // If writing is not not allowed by one of the three rules above then it is 170 // disallowed. 171 return false, nil 172 } 173 174 // RestrictionsFor computes restrictions for the desired path. 175 func (as *Assumptions) RestrictionsFor(desiredPath string) *Restrictions { 176 // Writing to a restricted path results in step-by-step validation of each 177 // directory, starting from the root of the file system. Unless writing is 178 // allowed a mimic must be constructed to ensure that writes are not visible in 179 // undesired locations of the host filesystem. 180 if as.isRestricted(desiredPath) { 181 return &Restrictions{assumptions: as, desiredPath: desiredPath, restricted: true} 182 } 183 return nil 184 } 185 186 // Restrictions contains meta-data of a compound write operation. 187 // 188 // This structure helps functions that write to the filesystem to keep track of 189 // the ultimate destination across several calls (e.g. the function that 190 // creates a file needs to call helpers to create subsequent directories). 191 // Keeping track of the desired path aids in constructing useful error 192 // messages. 193 // 194 // In addition the structure keeps track of the restricted write mode flag which 195 // is based on the full path of the desired object being constructed. This allows 196 // various write helpers to avoid trespassing on host filesystem in places that 197 // are not expected to be written to by snapd (e.g. outside of $SNAP_DATA). 198 type Restrictions struct { 199 assumptions *Assumptions 200 desiredPath string 201 restricted bool 202 } 203 204 // Check verifies whether writing to a directory would trespass on the host. 205 // 206 // The check is only performed in restricted mode. If the check fails a 207 // TrespassingError is returned. 208 func (rs *Restrictions) Check(dirFd int, dirName string) error { 209 if rs == nil || !rs.restricted { 210 return nil 211 } 212 // In restricted mode check the directory before attempting to write to it. 213 ok, err := rs.assumptions.canWriteToDirectory(dirFd, dirName) 214 if ok || err != nil { 215 return err 216 } 217 if dirName == "/" { 218 // If writing to / is not allowed then we are in a tough spot because 219 // we cannot construct a writable mimic over /. This should never 220 // happen in normal circumstances because the root filesystem is some 221 // kind of base snap. 222 return fmt.Errorf("cannot recover from trespassing over /") 223 } 224 logger.Debugf("trespassing violated %q while striving to %q", dirName, rs.desiredPath) 225 logger.Debugf("restricted mode: %#v", rs.restricted) 226 logger.Debugf("unrestricted paths: %q", rs.assumptions.unrestrictedPaths) 227 logger.Debugf("verified devices: %v", rs.assumptions.verifiedDevices) 228 logger.Debugf("past changes: %v", rs.assumptions.pastChanges) 229 return &TrespassingError{ViolatedPath: filepath.Clean(dirName), DesiredPath: rs.desiredPath} 230 } 231 232 // Lift lifts write restrictions for the desired path. 233 // 234 // This function should be called when, as subsequent components of a path are 235 // either discovered or created, the conditions for using restricted mode are 236 // no longer true. 237 func (rs *Restrictions) Lift() { 238 if rs != nil { 239 rs.restricted = false 240 } 241 } 242 243 // TrespassingError is an error when filesystem operation would affect the host. 244 type TrespassingError struct { 245 ViolatedPath string 246 DesiredPath string 247 } 248 249 // Error returns a formatted error message. 250 func (e *TrespassingError) Error() string { 251 return fmt.Sprintf("cannot write to %q because it would affect the host in %q", e.DesiredPath, e.ViolatedPath) 252 } 253 254 // isReadOnly checks whether the underlying filesystem is read only or is mounted as such. 255 func isReadOnly(dirName string, fsData *syscall.Statfs_t) bool { 256 // If something is mounted with f_flags & ST_RDONLY then is read-only. 257 if fsData.Flags&StReadOnly == StReadOnly { 258 return true 259 } 260 // If something is a known read-only file-system then it is safe. 261 // Older copies of snapd were not mounting squashfs as read only. 262 if fsData.Type == SquashfsMagic { 263 return true 264 } 265 return false 266 } 267 268 // isPrivateTmpfsCreatedBySnapd checks whether a directory resides on a tmpfs mounted by snapd 269 // 270 // The function inspects the directory and a list of changes that were applied 271 // to the mount namespace. A directory is trusted if it is a tmpfs that was 272 // mounted by snap-confine or snapd-update-ns. Note that sub-directories of a 273 // trusted tmpfs are not considered trusted by this function. 274 func isPrivateTmpfsCreatedBySnapd(dirName string, fsData *syscall.Statfs_t, fileData *syscall.Stat_t, changes []*Change) bool { 275 // If something is not a tmpfs it cannot be the trusted tmpfs we are looking for. 276 if fsData.Type != TmpfsMagic { 277 return false 278 } 279 // Any of the past changes that mounted a tmpfs exactly at the directory we 280 // are inspecting is considered as trusted. This is conservative because it 281 // doesn't trust sub-directories of a trusted tmpfs. This approach is 282 // sufficient for the intended use. 283 // 284 // The algorithm goes over all the changes in reverse and picks up the 285 // first tmpfs mount or unmount action that matches the directory name. 286 // The set of constraints in snap-update-ns and snapd prevent from mounting 287 // over an existing mount point so we don't need to consider e.g. a bind 288 // mount shadowing an active tmpfs. 289 for i := len(changes) - 1; i >= 0; i-- { 290 change := changes[i] 291 if change.Entry.Type == "tmpfs" && change.Entry.Dir == dirName { 292 return change.Action == Mount || change.Action == Keep 293 } 294 } 295 return false 296 }