github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/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 "path/filepath" 25 "strings" 26 "syscall" 27 28 "github.com/snapcore/snapd/logger" 29 ) 30 31 // Assumptions track the assumptions about the state of the filesystem. 32 // 33 // Assumptions constitute the global part of the write restriction management. 34 // Assumptions are global in the sense that they span multiple distinct write 35 // operations. In contrast, Restrictions track per-operation state. 36 type Assumptions struct { 37 unrestrictedPaths []string 38 pastChanges []*Change 39 40 // verifiedDevices represents the set of devices that are verified as a tmpfs 41 // that was mounted by snapd. Those are only discovered on-demand. The 42 // major:minor number is packed into one uint64 as in syscall.Stat_t.Dev 43 // field. 44 verifiedDevices map[uint64]bool 45 } 46 47 // AddUnrestrictedPaths adds a list of directories where writing is allowed 48 // even if it would hit the real host filesystem (or transit through the host 49 // filesystem). This is intended to be used with certain well-known locations 50 // such as /tmp, $SNAP_DATA and $SNAP. 51 func (as *Assumptions) AddUnrestrictedPaths(paths ...string) { 52 as.unrestrictedPaths = append(as.unrestrictedPaths, paths...) 53 } 54 55 // isRestricted checks whether a path falls under restricted writing scheme. 56 // 57 // Provided path is the full, absolute path of the entity that needs to be 58 // created (directory, file or symbolic link). 59 func (as *Assumptions) isRestricted(path string) bool { 60 // Anything rooted at one of the unrestricted paths is not restricted. 61 // Those are for things like /var/snap/, for example. 62 for _, p := range as.unrestrictedPaths { 63 if p == "/" || p == path || strings.HasPrefix(path, filepath.Clean(p)+"/") { 64 return false 65 } 66 67 } 68 // All other paths are restricted 69 return true 70 } 71 72 // MockUnrestrictedPaths replaces the set of path paths without any restrictions. 73 func (as *Assumptions) MockUnrestrictedPaths(paths ...string) (restore func()) { 74 old := as.unrestrictedPaths 75 as.unrestrictedPaths = paths 76 return func() { 77 as.unrestrictedPaths = old 78 } 79 } 80 81 // AddChange records the fact that a change was applied to the system. 82 func (as *Assumptions) AddChange(change *Change) { 83 as.pastChanges = append(as.pastChanges, change) 84 } 85 86 // canWriteToDirectory returns true if writing to a given directory is allowed. 87 // 88 // Writing is allowed in one of thee cases: 89 // 1) The directory is in one of the explicitly permitted locations. 90 // This is the strongest permission as it explicitly allows writing to 91 // places that may show up on the host, one of the examples being $SNAP_DATA. 92 // 2) The directory is on a read-only filesystem. 93 // 3) The directory is on a tmpfs created by snapd. 94 func (as *Assumptions) canWriteToDirectory(dirFd int, dirName string) (bool, error) { 95 if !as.isRestricted(dirName) { 96 return true, nil 97 } 98 var fsData syscall.Statfs_t 99 if err := sysFstatfs(dirFd, &fsData); err != nil { 100 return false, fmt.Errorf("cannot fstatfs %q: %s", dirName, err) 101 } 102 var fileData syscall.Stat_t 103 if err := sysFstat(dirFd, &fileData); err != nil { 104 return false, fmt.Errorf("cannot fstat %q: %s", dirName, err) 105 } 106 // Writing to read only directories is allowed because EROFS is handled 107 // by each of the writing helpers already. 108 if ok := isReadOnly(dirName, &fsData); ok { 109 return true, nil 110 } 111 // Writing to a trusted tmpfs is allowed because those are not leaking to 112 // the host. Also, each time we find a good tmpfs we explicitly remember the device major/minor, 113 if as.verifiedDevices[fileData.Dev] { 114 return true, nil 115 } 116 if ok := isPrivateTmpfsCreatedBySnapd(dirName, &fsData, &fileData, as.pastChanges); ok { 117 if as.verifiedDevices == nil { 118 as.verifiedDevices = make(map[uint64]bool) 119 } 120 // Don't record 0:0 as those are all to easy to add in tests and would 121 // skew tests using zero-initialized structures. Real device numbers 122 // are not zero either so this is not a test-only conditional. 123 if fileData.Dev != 0 { 124 as.verifiedDevices[fileData.Dev] = true 125 } 126 return true, nil 127 } 128 // If writing is not not allowed by one of the three rules above then it is 129 // disallowed. 130 return false, nil 131 } 132 133 // RestrictionsFor computes restrictions for the desired path. 134 func (as *Assumptions) RestrictionsFor(desiredPath string) *Restrictions { 135 // Writing to a restricted path results in step-by-step validation of each 136 // directory, starting from the root of the file system. Unless writing is 137 // allowed a mimic must be constructed to ensure that writes are not visible in 138 // undesired locations of the host filesystem. 139 if as.isRestricted(desiredPath) { 140 return &Restrictions{assumptions: as, desiredPath: desiredPath, restricted: true} 141 } 142 return nil 143 } 144 145 // Restrictions contains meta-data of a compound write operation. 146 // 147 // This structure helps functions that write to the filesystem to keep track of 148 // the ultimate destination across several calls (e.g. the function that 149 // creates a file needs to call helpers to create subsequent directories). 150 // Keeping track of the desired path aids in constructing useful error 151 // messages. 152 // 153 // In addition the structure keeps track of the restricted write mode flag which 154 // is based on the full path of the desired object being constructed. This allows 155 // various write helpers to avoid trespassing on host filesystem in places that 156 // are not expected to be written to by snapd (e.g. outside of $SNAP_DATA). 157 type Restrictions struct { 158 assumptions *Assumptions 159 desiredPath string 160 restricted bool 161 } 162 163 // Check verifies whether writing to a directory would trespass on the host. 164 // 165 // The check is only performed in restricted mode. If the check fails a 166 // TrespassingError is returned. 167 func (rs *Restrictions) Check(dirFd int, dirName string) error { 168 if rs == nil || !rs.restricted { 169 return nil 170 } 171 // In restricted mode check the directory before attempting to write to it. 172 ok, err := rs.assumptions.canWriteToDirectory(dirFd, dirName) 173 if ok || err != nil { 174 return err 175 } 176 if dirName == "/" { 177 // If writing to / is not allowed then we are in a tough spot because 178 // we cannot construct a writable mimic over /. This should never 179 // happen in normal circumstances because the root filesystem is some 180 // kind of base snap. 181 return fmt.Errorf("cannot recover from trespassing over /") 182 } 183 logger.Debugf("trespassing violated %q while striving to %q", dirName, rs.desiredPath) 184 logger.Debugf("restricted mode: %#v", rs.restricted) 185 logger.Debugf("unrestricted paths: %q", rs.assumptions.unrestrictedPaths) 186 logger.Debugf("verified devices: %v", rs.assumptions.verifiedDevices) 187 logger.Debugf("past changes: %v", rs.assumptions.pastChanges) 188 return &TrespassingError{ViolatedPath: filepath.Clean(dirName), DesiredPath: rs.desiredPath} 189 } 190 191 // Lift lifts write restrictions for the desired path. 192 // 193 // This function should be called when, as subsequent components of a path are 194 // either discovered or created, the conditions for using restricted mode are 195 // no longer true. 196 func (rs *Restrictions) Lift() { 197 if rs != nil { 198 rs.restricted = false 199 } 200 } 201 202 // TrespassingError is an error when filesystem operation would affect the host. 203 type TrespassingError struct { 204 ViolatedPath string 205 DesiredPath string 206 } 207 208 // Error returns a formatted error message. 209 func (e *TrespassingError) Error() string { 210 return fmt.Sprintf("cannot write to %q because it would affect the host in %q", e.DesiredPath, e.ViolatedPath) 211 } 212 213 // isReadOnly checks whether the underlying filesystem is read only or is mounted as such. 214 func isReadOnly(dirName string, fsData *syscall.Statfs_t) bool { 215 // If something is mounted with f_flags & ST_RDONLY then is read-only. 216 if fsData.Flags&StReadOnly == StReadOnly { 217 return true 218 } 219 // If something is a known read-only file-system then it is safe. 220 // Older copies of snapd were not mounting squashfs as read only. 221 if fsData.Type == SquashfsMagic { 222 return true 223 } 224 return false 225 } 226 227 // isPrivateTmpfsCreatedBySnapd checks whether a directory resides on a tmpfs mounted by snapd 228 // 229 // The function inspects the directory and a list of changes that were applied 230 // to the mount namespace. A directory is trusted if it is a tmpfs that was 231 // mounted by snap-confine or snapd-update-ns. Note that sub-directories of a 232 // trusted tmpfs are not considered trusted by this function. 233 func isPrivateTmpfsCreatedBySnapd(dirName string, fsData *syscall.Statfs_t, fileData *syscall.Stat_t, changes []*Change) bool { 234 // If something is not a tmpfs it cannot be the trusted tmpfs we are looking for. 235 if fsData.Type != TmpfsMagic { 236 return false 237 } 238 // Any of the past changes that mounted a tmpfs exactly at the directory we 239 // are inspecting is considered as trusted. This is conservative because it 240 // doesn't trust sub-directories of a trusted tmpfs. This approach is 241 // sufficient for the intended use. 242 // 243 // The algorithm goes over all the changes in reverse and picks up the 244 // first tmpfs mount or unmount action that matches the directory name. 245 // The set of constraints in snap-update-ns and snapd prevent from mounting 246 // over an existing mount point so we don't need to consider e.g. a bind 247 // mount shadowing an active tmpfs. 248 for i := len(changes) - 1; i >= 0; i-- { 249 change := changes[i] 250 if change.Entry.Type == "tmpfs" && change.Entry.Dir == dirName { 251 return change.Action == Mount || change.Action == Keep 252 } 253 } 254 return false 255 }