github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/sandbox/cgroup/scanning.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2019 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  	"os"
    24  	"path/filepath"
    25  	"regexp"
    26  	"strings"
    27  
    28  	"github.com/snapcore/snapd/snap/naming"
    29  )
    30  
    31  var (
    32  	// string that looks like a hook security tag
    33  	roughHookTagPattern = regexp.MustCompile(`snap\.[^.]+\.hook\.[^.]+`)
    34  	// string that looks like an app security tag
    35  	roughAppTagPattern = regexp.MustCompile(`snap\.[^.]+\.[^.]+`)
    36  )
    37  
    38  // securityTagFromCgroupPath returns a security tag from cgroup path.
    39  func securityTagFromCgroupPath(path string) naming.SecurityTag {
    40  	leaf := filepath.Base(filepath.Clean(path))
    41  
    42  	// If the security cgroup name doesn't start with "snap." then there is no
    43  	// point in doing other checks.
    44  	if !strings.HasPrefix(leaf, "snap.") {
    45  		return nil
    46  	}
    47  
    48  	// We are only interested in cgroup directory names that correspond to
    49  	// services and scopes, as they contain processes that have been invoked
    50  	// from a snap.
    51  	// Expected format of leaf name:
    52  	//   snap.<pkg>.<app>.service - assigned by systemd for services
    53  	//   snap.<pkg>.<app>.<uuid>.scope - transient scope for apps
    54  	//   snap.<pkg>.hook.<app>.<uuid>.scope - transient scope for hooks
    55  	if ext := filepath.Ext(leaf); ext != ".service" && ext != ".scope" {
    56  		return nil
    57  	}
    58  
    59  	// There are two broad forms expressed by the pair of regular expressions defined above.
    60  	for _, re := range []*regexp.Regexp{roughHookTagPattern, roughAppTagPattern} {
    61  		if maybeTag := re.FindString(leaf); maybeTag != "" {
    62  			if tag, err := naming.ParseSecurityTag(maybeTag); err == nil {
    63  				return tag
    64  			}
    65  		}
    66  	}
    67  	return nil
    68  }
    69  
    70  // PidsOfSnap returns the association of security tags to PIDs.
    71  //
    72  // NOTE: This function returns a reliable result only if the refresh-app-awareness
    73  // feature was enabled since all processes related to the given snap were started.
    74  // If the feature wasn't always enabled then only service process are correctly
    75  // accounted for.
    76  //
    77  // The return value is a snapshot of the pids for a given snap, grouped by
    78  // security tag. The result may be immediately stale as processes fork and
    79  // exit.
    80  //
    81  // Importantly, if the per-snap lock is held while computing the set, then the
    82  // following guarantee is true: if a security tag is not among the results then
    83  // no such tag can come into existence while the lock is held.
    84  //
    85  // This can be used to classify the activity of a given snap into activity
    86  // classes, based on the nature of the security tags encountered.
    87  func PidsOfSnap(snapInstanceName string) (map[string][]int, error) {
    88  	// pidsByTag maps security tag to a list of pids.
    89  	pidsByTag := make(map[string][]int)
    90  
    91  	// Walk the cgroup tree and look for "cgroup.procs" files. Having found one
    92  	// we try to derive the snap security tag from it. If successful and the
    93  	// tag matches the snap we are interested in, we harvest the snapshot of
    94  	// PIDs that belong to the cgroup and put them into a bucket associated
    95  	// with the security tag.
    96  	walkFunc := func(path string, fileInfo os.FileInfo, err error) error {
    97  		if err != nil {
    98  			// See the documentation of path/filepath.Walk. The error we get is
    99  			// the error that was encountered while walking. We just surface
   100  			// that error quickly.
   101  			return err
   102  		}
   103  		if fileInfo.IsDir() {
   104  			// We don't care about directories.
   105  			return nil
   106  		}
   107  		if filepath.Base(path) != "cgroup.procs" {
   108  			// We are looking for "cgroup.procs" files. Those contain the set
   109  			// of processes that momentarily inhabit a cgroup.
   110  			return nil
   111  		}
   112  		// Now that we are confident that the file we're looking at is
   113  		// interesting, extract the security tag from the cgroup path and check
   114  		// if the security tag belongs to the snap we are interested in. Since
   115  		// not all cgroups are related to snaps it is not an error if the
   116  		// cgroup path does not denote a snap.
   117  		cgroupPath := filepath.Dir(path)
   118  		parsedTag := securityTagFromCgroupPath(cgroupPath)
   119  		if parsedTag == nil {
   120  			return nil
   121  		}
   122  		if parsedTag.InstanceName() != snapInstanceName {
   123  			return nil
   124  		}
   125  		pids, err := pidsInFile(path)
   126  		if err != nil {
   127  			return err
   128  		}
   129  		tag := parsedTag.String()
   130  		pidsByTag[tag] = append(pidsByTag[tag], pids...)
   131  		// Since we've found the file we are looking for (cgroup.procs) we no
   132  		// longer need to scan the remaining files of this directory.
   133  		return filepath.SkipDir
   134  	}
   135  
   136  	var cgroupPathToScan string
   137  	ver, err := Version()
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  	if ver == V2 {
   142  		// In v2 mode scan all of /sys/fs/cgroup as there is no specialization
   143  		// anymore (each directory represents a hierarchy with equal
   144  		// capabilities and old split into controllers is gone).
   145  		cgroupPathToScan = filepath.Join(rootPath, cgroupMountPoint)
   146  	} else {
   147  		// In v1 mode scan just /sys/fs/cgroup/systemd as that is sufficient
   148  		// for finding snap-specific cgroup names. Systemd uses this for
   149  		// tracking and scopes and services are represented there.
   150  		cgroupPathToScan = filepath.Join(rootPath, cgroupMountPoint, "systemd")
   151  	}
   152  	// NOTE: Walk is internally performed in lexical order so the output is
   153  	// deterministic and we don't need to sort the returned aggregated PIDs.
   154  	if err := filepath.Walk(cgroupPathToScan, walkFunc); err != nil {
   155  		if os.IsNotExist(err) {
   156  			return nil, nil
   157  		}
   158  		return nil, err
   159  	}
   160  
   161  	return pidsByTag, nil
   162  }