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 }