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  }