github.com/inspektor-gadget/inspektor-gadget@v0.28.1/pkg/operators/uidgidresolver/usergroupcache.go (about)

     1  // Copyright 2024 The Inspektor Gadget authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package uidgidresolver
    16  
    17  import (
    18  	"bufio"
    19  	"fmt"
    20  	"os"
    21  	"path/filepath"
    22  	"strconv"
    23  	"strings"
    24  	"sync"
    25  	"time"
    26  
    27  	"github.com/fsnotify/fsnotify"
    28  	log "github.com/sirupsen/logrus"
    29  
    30  	"github.com/inspektor-gadget/inspektor-gadget/pkg/cachedmap"
    31  	"github.com/inspektor-gadget/inspektor-gadget/pkg/utils/host"
    32  )
    33  
    34  // UserGroupCache is a cache of user names, uids, group names and gids
    35  type UserGroupCache interface {
    36  	Start() error
    37  	Stop()
    38  
    39  	GetUsername(uint32) string
    40  	GetGroupname(uint32) string
    41  }
    42  
    43  type userGroupCache struct {
    44  	userCache  cachedmap.CachedMap[uint32, string]
    45  	groupCache cachedmap.CachedMap[uint32, string]
    46  
    47  	loopFinished  chan struct{}
    48  	useCount      int
    49  	useCountMutex sync.Mutex
    50  	watcher       *fsnotify.Watcher
    51  }
    52  
    53  const (
    54  	passwdFileName = "passwd"
    55  	groupFileName  = "group"
    56  	baseDirPath    = "/etc"
    57  )
    58  
    59  var (
    60  	fullPasswdPath = filepath.Join(host.HostRoot, baseDirPath, passwdFileName)
    61  	fullGroupPath  = filepath.Join(host.HostRoot, baseDirPath, groupFileName)
    62  )
    63  
    64  func GetUserGroupCache() UserGroupCache {
    65  	return sync.OnceValue(func() *userGroupCache {
    66  		return &userGroupCache{}
    67  	})()
    68  }
    69  
    70  func (cache *userGroupCache) Start() error {
    71  	cache.useCountMutex.Lock()
    72  	defer cache.useCountMutex.Unlock()
    73  
    74  	// No uses before us, we are the first one
    75  	if cache.useCount == 0 {
    76  		watcher, err := fsnotify.NewWatcher()
    77  		if err != nil {
    78  			return fmt.Errorf("UserGroupCache: create watcher: %w", err)
    79  		}
    80  		defer func() {
    81  			// Only close the watcher if we are not going to use it
    82  			if watcher != nil {
    83  				watcher.Close()
    84  			}
    85  		}()
    86  
    87  		err = watcher.Add(filepath.Join(host.HostRoot, baseDirPath))
    88  		if err != nil {
    89  			return fmt.Errorf("UserGroupCache: add watch: %w", err)
    90  		}
    91  
    92  		cache.userCache = cachedmap.NewCachedMap[uint32, string](2 * time.Second)
    93  		cache.groupCache = cachedmap.NewCachedMap[uint32, string](2 * time.Second)
    94  
    95  		// Initial read
    96  		cache.userCache.Clear()
    97  		cache.groupCache.Clear()
    98  		passwdFile, err := os.OpenFile(fullPasswdPath, os.O_RDONLY, 0)
    99  		if err != nil {
   100  			return fmt.Errorf("UserGroupCache: open %q: %w", fullPasswdPath, err)
   101  		}
   102  		defer passwdFile.Close()
   103  		updateEntries(passwdFile, cache.userCache)
   104  
   105  		groupFile, err := os.OpenFile(fullGroupPath, os.O_RDONLY, 0)
   106  		if err != nil {
   107  			return fmt.Errorf("UserGroupCache: open %q: %w", fullGroupPath, err)
   108  		}
   109  		defer groupFile.Close()
   110  		updateEntries(groupFile, cache.groupCache)
   111  
   112  		cache.watcher = watcher
   113  		watcher = nil
   114  		cache.loopFinished = make(chan struct{})
   115  		go cache.watchUserGroupLoop()
   116  	}
   117  	cache.useCount++
   118  	return nil
   119  }
   120  
   121  func (cache *userGroupCache) Close() {
   122  	if cache.watcher != nil {
   123  		err := cache.watcher.Close()
   124  		if err != nil {
   125  			log.Warnf("UserGroupCache: close watcher: %v", err)
   126  		}
   127  		// Wait until the loop is finished, should be fast
   128  		<-cache.loopFinished
   129  		cache.watcher = nil
   130  
   131  		cache.userCache.Close()
   132  		cache.groupCache.Close()
   133  	}
   134  }
   135  
   136  func (cache *userGroupCache) Stop() {
   137  	cache.useCountMutex.Lock()
   138  	defer cache.useCountMutex.Unlock()
   139  
   140  	// We are the last user, stop everything
   141  	if cache.useCount == 1 {
   142  		cache.Close()
   143  	}
   144  	cache.useCount--
   145  }
   146  
   147  func (cache *userGroupCache) watchUserGroupLoop() {
   148  	defer close(cache.loopFinished)
   149  	for {
   150  		select {
   151  		case event, ok := <-cache.watcher.Events:
   152  			if !ok {
   153  				log.Warnf("UserGroupCache: watcher event not ok")
   154  				return
   155  			}
   156  			cache.handleEvent(event)
   157  		case err, ok := <-cache.watcher.Errors:
   158  			if !ok {
   159  				if err == nil {
   160  					// Watcher closed
   161  					return
   162  				}
   163  				log.Warnf("UserGroupCache: watcher error not ok: %v", err)
   164  				return
   165  			}
   166  			log.Warnf("UserGroupCache: watcher error: %v", err)
   167  		}
   168  	}
   169  }
   170  
   171  func (cache *userGroupCache) handleEvent(event fsnotify.Event) {
   172  	// Filter out chmod events first, to keep string comparisons to a minimum
   173  	if event.Has(fsnotify.Chmod) {
   174  		return
   175  	}
   176  
   177  	targetFilePath := ""
   178  	var resourceCache cachedmap.CachedMap[uint32, string]
   179  	if event.Name == fullPasswdPath {
   180  		targetFilePath = fullPasswdPath
   181  		resourceCache = cache.userCache
   182  	} else if event.Name == fullGroupPath {
   183  		targetFilePath = fullGroupPath
   184  		resourceCache = cache.groupCache
   185  	} else {
   186  		return
   187  	}
   188  
   189  	var targetFile *os.File
   190  	if event.Has(fsnotify.Remove) || event.Has(fsnotify.Rename) {
   191  		targetFile = nil
   192  	} else if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) {
   193  		var err error
   194  		targetFile, err = os.OpenFile(targetFilePath, os.O_RDONLY, 0)
   195  		if err != nil {
   196  			log.Warnf("UserGroupCache: open target file: %v", err)
   197  			return
   198  		}
   199  		defer targetFile.Close()
   200  	} else {
   201  		// Ignore all other events
   202  		return
   203  	}
   204  
   205  	updateEntries(targetFile, resourceCache)
   206  }
   207  
   208  func updateEntries(file *os.File, resourceCache cachedmap.CachedMap[uint32, string]) {
   209  	oldEntries := make(map[uint32]struct{})
   210  	for _, id := range resourceCache.Keys() {
   211  		oldEntries[id] = struct{}{}
   212  	}
   213  
   214  	if file != nil {
   215  		scanner := bufio.NewScanner(file)
   216  		for scanner.Scan() {
   217  			line := strings.TrimLeft(scanner.Text(), " \t")
   218  			if len(line) == 0 || line[0] == '#' {
   219  				continue
   220  			}
   221  			split := strings.Split(line, ":")
   222  			// We are interested only in the first and third field
   223  			if len(split) < 3 {
   224  				continue
   225  			}
   226  			name := split[0]
   227  			id_u64, err := strconv.ParseUint(split[2], 10, 32)
   228  			if err != nil {
   229  				log.Warnf("UserGroupCache: convert id: %v", err)
   230  				continue
   231  			}
   232  			id := uint32(id_u64)
   233  			delete(oldEntries, id)
   234  			resourceCache.Add(id, name)
   235  		}
   236  	}
   237  
   238  	for id := range oldEntries {
   239  		resourceCache.Remove(id)
   240  	}
   241  }
   242  
   243  func (cache *userGroupCache) GetUsername(uid uint32) string {
   244  	name, _ := cache.userCache.Get(uid)
   245  	return name
   246  }
   247  
   248  func (cache *userGroupCache) GetGroupname(gid uint32) string {
   249  	name, _ := cache.groupCache.Get(gid)
   250  	return name
   251  }