github.com/anonymouse64/snapd@v0.0.0-20210824153203-04c4c42d842d/sandbox/cgroup/freezer.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2017 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 cgroup 21 22 import ( 23 "bytes" 24 "fmt" 25 "io/ioutil" 26 "os" 27 "path/filepath" 28 "strings" 29 "time" 30 31 "github.com/snapcore/snapd/dirs" 32 "github.com/snapcore/snapd/logger" 33 "github.com/snapcore/snapd/osutil" 34 ) 35 36 const defaultFreezerCgroupV1Dir = "/sys/fs/cgroup/freezer" 37 38 var freezerCgroupV1Dir = defaultFreezerCgroupV1Dir 39 40 func init() { 41 dirs.AddRootDirCallback(func(root string) { 42 freezerCgroupV1Dir = filepath.Join(root, defaultFreezerCgroupV1Dir) 43 }) 44 } 45 46 func pickFreezerV1Impl() { 47 FreezeSnapProcesses = freezeSnapProcessesImplV1 48 ThawSnapProcesses = thawSnapProcessesImplV1 49 } 50 51 func pickFreezerV2Impl() { 52 FreezeSnapProcesses = freezeSnapProcessesImplV2 53 ThawSnapProcesses = thawSnapProcessesImplV2 54 } 55 56 // FreezeSnapProcesses suspends execution of all the processes belonging to 57 // a given snap. Processes remain frozen until ThawSnapProcesses is called, 58 // care must be taken not to freezer processes indefinitely. 59 // 60 // The freeze operation is not instant. Once commenced it proceeds 61 // asynchronously. Internally the function waits for the freezing to complete 62 // in at most 3000ms. If this time is insufficient then the processes are 63 // thawed and an error is returned. 64 // 65 // A correct implementation is picked depending on cgroup v1 or v2 use in the 66 // system. When cgroup v1 is detected, the call will directly act on the freezer 67 // group created when a snap process was started, while with v2 the call will 68 // act on all tracking groups of a snap. 69 // 70 // This operation can be mocked with MockFreezing 71 var FreezeSnapProcesses = freezeSnapProcessesImplV1 72 73 // ThawSnapProcesses resumes execution of all processes belonging to a given snap. 74 // 75 // A correct implementation is picked depending on cgroup v1 or v2 use in the 76 // system. When cgroup v1 is detected, the call will directly act on the freezer 77 // group created when a snap process was started, while with v2 the call will 78 // act on all tracking groups of a snap. 79 // 80 // This operation can be mocked with MockFreezing 81 var ThawSnapProcesses = thawSnapProcessesImplV1 82 83 // freezeSnapProcessesImplV1 freezes all the processes originating from the given snap. 84 // Processes are frozen regardless of which particular snap application they 85 // originate from. 86 func freezeSnapProcessesImplV1(snapName string) error { 87 fname := filepath.Join(freezerCgroupV1Dir, fmt.Sprintf("snap.%s", snapName), "freezer.state") 88 if err := ioutil.WriteFile(fname, []byte("FROZEN"), 0644); err != nil && os.IsNotExist(err) { 89 // When there's no freezer cgroup we don't have to freeze anything. 90 // This can happen when no process belonging to a given snap has been 91 // started yet. 92 return nil 93 } else if err != nil { 94 return fmt.Errorf("cannot freeze processes of snap %q, %v", snapName, err) 95 } 96 for i := 0; i < 30; i++ { 97 data, err := ioutil.ReadFile(fname) 98 if err != nil { 99 return fmt.Errorf("cannot determine the freeze state of processes of snap %q, %v", snapName, err) 100 } 101 // If the cgroup is still freezing then wait a moment and try again. 102 if bytes.Equal(data, []byte("FREEZING")) { 103 time.Sleep(100 * time.Millisecond) 104 continue 105 } 106 return nil 107 } 108 // If we got here then we timed out after seeing FREEZING for too long. 109 ThawSnapProcesses(snapName) // ignore the error, this is best-effort. 110 return fmt.Errorf("cannot finish freezing processes of snap %q", snapName) 111 } 112 113 func thawSnapProcessesImplV1(snapName string) error { 114 fname := filepath.Join(freezerCgroupV1Dir, fmt.Sprintf("snap.%s", snapName), "freezer.state") 115 if err := ioutil.WriteFile(fname, []byte("THAWED"), 0644); err != nil && os.IsNotExist(err) { 116 // When there's no freezer cgroup we don't have to thaw anything. 117 // This can happen when no process belonging to a given snap has been 118 // started yet. 119 return nil 120 } else if err != nil { 121 return fmt.Errorf("cannot thaw processes of snap %q", snapName) 122 } 123 return nil 124 } 125 126 func applyToSnap(snapName string, action func(groupName string) error, skipError func(err error) bool) error { 127 if action == nil { 128 return fmt.Errorf("internal error: action is nil") 129 } 130 if skipError == nil { 131 return fmt.Errorf("internal error: skip error is nil") 132 } 133 canary := fmt.Sprintf("snap.%s.", snapName) 134 cgroupRoot := filepath.Join(rootPath, cgroupMountPoint) 135 if _, dir, _ := osutil.DirExists(cgroupRoot); !dir { 136 return nil 137 } 138 return filepath.Walk(filepath.Join(rootPath, cgroupMountPoint), func(name string, info os.FileInfo, err error) error { 139 if err != nil { 140 if skipError(err) { 141 // we don't know whether it's a file or 142 // directory, so just return nil instead 143 return nil 144 } 145 return err 146 } 147 if !info.IsDir() { 148 return nil 149 } 150 if !strings.HasPrefix(info.Name(), canary) { 151 return nil 152 } 153 // found a group 154 if err := action(name); err != nil && !skipError(err) { 155 return err 156 } 157 return filepath.SkipDir 158 }) 159 } 160 161 // writeExistingFile can be used as a drop-in replacement for ioutil.WriteFile, 162 // but does not create a file when it does not exist 163 func writeExistingFile(where string, data []byte, mode os.FileMode) error { 164 f, err := os.OpenFile(where, os.O_WRONLY|os.O_TRUNC, mode) 165 if err != nil { 166 return err 167 } 168 _, errW := f.Write(data) 169 errC := f.Close() 170 // pick the right error 171 if errW != nil { 172 return errW 173 } 174 return errC 175 } 176 177 // freezeSnapProcessesImplV2 freezes all the processes originating from the 178 // given snap. Processes are frozen regardless of which particular snap 179 // application they originate from. 180 func freezeSnapProcessesImplV2(snapName string) error { 181 // in case of v2, the process calling this code, (eg. snap-update-ns) 182 // may already be part of the trackign cgroup for particular snap, care 183 // must be taken to not freeze ourselves 184 ownGroup, err := cgroupProcessPathInTrackingCgroup(os.Getpid()) 185 if err != nil { 186 return err 187 } 188 ownGroupDir := filepath.Join(rootPath, cgroupMountPoint, ownGroup) 189 freezeOne := func(dir string) error { 190 if dir == ownGroupDir { 191 // let's not freeze ourselves 192 logger.Debugf("freeze, skipping own group %v", dir) 193 return nil 194 } 195 fname := filepath.Join(dir, "cgroup.freeze") 196 if err := writeExistingFile(fname, []byte("1"), 0644); err != nil { 197 if os.IsNotExist(err) { 198 // the group may be gone already 199 return nil 200 } 201 return fmt.Errorf("cannot freeze processes of snap %q, %v", snapName, err) 202 } 203 for i := 0; i < 30; i++ { 204 data, err := ioutil.ReadFile(fname) 205 if err != nil { 206 if os.IsNotExist(err) { 207 // group may be gone 208 return nil 209 } 210 return fmt.Errorf("cannot determine the freeze state of processes of snap %q, %v", snapName, err) 211 } 212 // If the cgroup is still freezing then wait a moment and try again. 213 if bytes.Equal(bytes.TrimSpace(data), []byte("1")) { 214 // we're done 215 return nil 216 } 217 // add a bit of delay 218 time.Sleep(100 * time.Millisecond) 219 } 220 return fmt.Errorf("cannot freeze processes of snap %q in group %v", snapName, filepath.Base(dir)) 221 } 222 // freeze, skipping ENOENT errors 223 err = applyToSnap(snapName, freezeOne, os.IsNotExist) 224 if err == nil { 225 return nil 226 } 227 // we either got here because we hit a timeout freezing snap processes 228 // or some other error 229 230 // ignore errors when thawing processes, this is best-effort. 231 alwaysSkipError := func(_ error) bool { return true } 232 thawSnapProcessesV2(snapName, alwaysSkipError) 233 return fmt.Errorf("cannot finish freezing processes of snap %q: %v", snapName, err) 234 } 235 236 func thawSnapProcessesV2(snapName string, skipError func(error) bool) error { 237 if skipError == nil { 238 return fmt.Errorf("internal error: skip error is nil") 239 } 240 thawOne := func(dir string) error { 241 fname := filepath.Join(dir, "cgroup.freeze") 242 if err := writeExistingFile(fname, []byte("0"), 0644); err != nil && os.IsNotExist(err) { 243 // the group may be gone already 244 return nil 245 } else if err != nil && !skipError(err) { 246 return fmt.Errorf("cannot thaw processes of snap %q, %v", snapName, err) 247 } 248 return nil 249 } 250 return applyToSnap(snapName, thawOne, skipError) 251 } 252 253 func thawSnapProcessesImplV2(snapName string) error { 254 // thaw skipping ENOENT errors 255 return thawSnapProcessesV2(snapName, os.IsNotExist) 256 } 257 258 // MockFreezing replaces the real implementation of freeze and thaw. 259 func MockFreezing(freeze, thaw func(snapName string) error) (restore func()) { 260 oldFreeze := FreezeSnapProcesses 261 oldThaw := ThawSnapProcesses 262 263 FreezeSnapProcesses = freeze 264 ThawSnapProcesses = thaw 265 266 return func() { 267 FreezeSnapProcesses = oldFreeze 268 ThawSnapProcesses = oldThaw 269 } 270 }