github.com/ttpreport/gvisor-ligolo@v0.0.0-20240123134145-a858404967ba/runsc/boot/procfs/dump.go (about) 1 // Copyright 2022 The gVisor 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 procfs holds utilities for getting procfs information for sandboxed 16 // processes. 17 package procfs 18 19 import ( 20 "bytes" 21 "fmt" 22 "strings" 23 24 "github.com/ttpreport/gvisor-ligolo/pkg/abi/linux" 25 "github.com/ttpreport/gvisor-ligolo/pkg/context" 26 "github.com/ttpreport/gvisor-ligolo/pkg/hostarch" 27 "github.com/ttpreport/gvisor-ligolo/pkg/log" 28 "github.com/ttpreport/gvisor-ligolo/pkg/sentry/fsimpl/proc" 29 "github.com/ttpreport/gvisor-ligolo/pkg/sentry/kernel" 30 "github.com/ttpreport/gvisor-ligolo/pkg/sentry/limits" 31 "github.com/ttpreport/gvisor-ligolo/pkg/sentry/mm" 32 "github.com/ttpreport/gvisor-ligolo/pkg/sentry/vfs" 33 ) 34 35 // FDInfo contains information about an application file descriptor. 36 type FDInfo struct { 37 // Number is the FD number. 38 Number int32 `json:"number"` 39 // Path is the path of the file that FD represents. 40 Path string `json:"path,omitempty"` 41 // Mode is the file mode. 42 Mode uint16 `json:"mode"` 43 } 44 45 // UIDGID contains information for /proc/[pid]/status/{uid,gid}. 46 type UIDGID struct { 47 Real uint32 `json:"real"` 48 Effective uint32 `json:"effective"` 49 Saved uint32 `json:"saved"` 50 } 51 52 // Status contains information for /proc/[pid]/status. 53 type Status struct { 54 Comm string `json:"comm,omitempty"` 55 PID int32 `json:"pid"` 56 PPID int32 `json:"ppid"` 57 UID UIDGID `json:"uid,omitempty"` 58 GID UIDGID `json:"gid,omitempty"` 59 VMSize uint64 `json:"vm_size,omitempty"` 60 VMRSS uint64 `json:"vm_rss,omitempty"` 61 } 62 63 // Stat contains information for /proc/[pid]/stat. 64 type Stat struct { 65 PGID int32 `json:"pgid"` 66 SID int32 `json:"sid"` 67 } 68 69 // Mapping contains information for /proc/[pid]/maps. 70 type Mapping struct { 71 Address hostarch.AddrRange `json:"address,omitempty"` 72 Permissions hostarch.AccessType `json:"permissions"` 73 Private string `json:"private,omitempty"` 74 Offset uint64 `json:"offset"` 75 DevMajor uint32 `json:"deviceMajor,omitempty"` 76 DevMinor uint32 `json:"deviceMinor,omitempty"` 77 Inode uint64 `json:"inode,omitempty"` 78 Pathname string `json:"pathname,omitempty"` 79 } 80 81 // ProcessProcfsDump contains the procfs dump for one process. For more details 82 // on fields that directly correspond to /proc fields, see proc(5). 83 type ProcessProcfsDump struct { 84 // Exe is the symlink target of /proc/[pid]/exe. 85 Exe string `json:"exe,omitempty"` 86 // Args is /proc/[pid]/cmdline split into an array. 87 Args []string `json:"args,omitempty"` 88 // Env is /proc/[pid]/environ split into an array. 89 Env []string `json:"env,omitempty"` 90 // CWD is the symlink target of /proc/[pid]/cwd. 91 CWD string `json:"cwd,omitempty"` 92 // FDs contains the directory entries of /proc/[pid]/fd and also contains the 93 // symlink target for each FD. 94 FDs []FDInfo `json:"fdlist,omitempty"` 95 // StartTime is the process start time in nanoseconds since Unix epoch. 96 StartTime int64 `json:"clone_ts,omitempty"` 97 // Root is /proc/[pid]/root. 98 Root string `json:"root,omitempty"` 99 // Limits constains resource limits for this process. Currently only 100 // RLIMIT_NOFILE is supported. 101 Limits map[string]limits.Limit `json:"limits,omitempty"` 102 // Cgroup is /proc/[pid]/cgroup split into an array. 103 Cgroup []kernel.TaskCgroupEntry `json:"cgroup,omitempty"` 104 // Status is /proc/[pid]/status. 105 Status Status `json:"status,omitempty"` 106 // Stat is /proc/[pid]/stat. 107 Stat Stat `json:"stat,omitempty"` 108 // Maps is /proc/[pid]/maps. 109 Maps []Mapping `json:"maps,omitempty"` 110 } 111 112 // getMM returns t's MemoryManager. On success, the MemoryManager's users count 113 // is incremented, and must be decremented by the caller when it is no longer 114 // in use. 115 func getMM(t *kernel.Task) *mm.MemoryManager { 116 var mm *mm.MemoryManager 117 t.WithMuLocked(func(*kernel.Task) { 118 mm = t.MemoryManager() 119 }) 120 if mm == nil || !mm.IncUsers() { 121 return nil 122 } 123 return mm 124 } 125 126 func getExecutablePath(ctx context.Context, pid kernel.ThreadID, mm *mm.MemoryManager) string { 127 exec := mm.Executable() 128 if exec == nil { 129 log.Warningf("No executable found for PID %s", pid) 130 return "" 131 } 132 defer exec.DecRef(ctx) 133 134 return exec.MappedName(ctx) 135 } 136 137 func getMetadataArray(ctx context.Context, pid kernel.ThreadID, mm *mm.MemoryManager, metaType proc.MetadataType) []string { 138 buf := bytes.Buffer{} 139 if err := proc.GetMetadata(ctx, mm, &buf, metaType); err != nil { 140 log.Warningf("failed to get %v metadata for PID %s: %v", metaType, pid, err) 141 return nil 142 } 143 // As per proc(5), /proc/[pid]/cmdline may have "a further null byte after 144 // the last string". Similarly, for /proc/[pid]/environ "there may be a null 145 // byte at the end". So trim off the last null byte if it exists. 146 return strings.Split(strings.TrimSuffix(buf.String(), "\000"), "\000") 147 } 148 149 func getCWD(ctx context.Context, t *kernel.Task, pid kernel.ThreadID) string { 150 cwdDentry := t.FSContext().WorkingDirectory() 151 if !cwdDentry.Ok() { 152 log.Warningf("No CWD dentry found for PID %s", pid) 153 return "" 154 } 155 156 root := vfs.RootFromContext(ctx) 157 if !root.Ok() { 158 log.Warningf("no root could be found from context for PID %s", pid) 159 return "" 160 } 161 defer root.DecRef(ctx) 162 163 vfsObj := cwdDentry.Mount().Filesystem().VirtualFilesystem() 164 name, err := vfsObj.PathnameWithDeleted(ctx, root, cwdDentry) 165 if err != nil { 166 log.Warningf("PathnameWithDeleted failed to find CWD: %v", err) 167 } 168 return name 169 } 170 171 func getFDs(ctx context.Context, t *kernel.Task, pid kernel.ThreadID) []FDInfo { 172 type fdInfo struct { 173 fd *vfs.FileDescription 174 no int32 175 } 176 var fds []fdInfo 177 defer func() { 178 for _, fd := range fds { 179 fd.fd.DecRef(ctx) 180 } 181 }() 182 183 t.WithMuLocked(func(t *kernel.Task) { 184 if fdTable := t.FDTable(); fdTable != nil { 185 fdNos := fdTable.GetFDs(ctx) 186 fds = make([]fdInfo, 0, len(fdNos)) 187 for _, fd := range fdNos { 188 file, _ := fdTable.Get(fd) 189 if file != nil { 190 fds = append(fds, fdInfo{fd: file, no: fd}) 191 } 192 } 193 } 194 }) 195 196 root := vfs.RootFromContext(ctx) 197 defer root.DecRef(ctx) 198 199 res := make([]FDInfo, 0, len(fds)) 200 for _, fd := range fds { 201 path, err := t.Kernel().VFS().PathnameWithDeleted(ctx, root, fd.fd.VirtualDentry()) 202 if err != nil { 203 log.Warningf("PathnameWithDeleted failed to find path for fd %d in PID %s: %v", fd.no, pid, err) 204 path = "" 205 } 206 mode := uint16(0) 207 if statx, err := fd.fd.Stat(ctx, vfs.StatOptions{Mask: linux.STATX_MODE}); err != nil { 208 log.Warningf("Stat(STATX_MODE) failed for fd %d in PID %s: %v", fd.no, pid, err) 209 } else { 210 mode = statx.Mode 211 } 212 res = append(res, FDInfo{Number: fd.no, Path: path, Mode: mode}) 213 } 214 return res 215 } 216 217 func getRoot(t *kernel.Task, pid kernel.ThreadID) string { 218 realRoot := t.MountNamespace().Root() 219 root := t.FSContext().RootDirectory() 220 defer root.DecRef(t) 221 path, err := t.Kernel().VFS().PathnameWithDeleted(t, realRoot, root) 222 if err != nil { 223 log.Warningf("PathnameWithDeleted failed to find root path for PID %s: %v", pid, err) 224 return "" 225 } 226 return path 227 } 228 229 func getFDLimit(ctx context.Context, pid kernel.ThreadID) (limits.Limit, error) { 230 if limitSet := limits.FromContext(ctx); limitSet != nil { 231 return limitSet.Get(limits.NumberOfFiles), nil 232 } 233 return limits.Limit{}, fmt.Errorf("could not find limit set for pid %s", pid) 234 } 235 236 func getStatus(t *kernel.Task, mm *mm.MemoryManager, pid kernel.ThreadID, pidns *kernel.PIDNamespace) Status { 237 creds := t.Credentials() 238 uns := creds.UserNamespace 239 ppid := kernel.ThreadID(0) 240 if parent := t.Parent(); parent != nil { 241 ppid = pidns.IDOfThreadGroup(parent.ThreadGroup()) 242 } 243 return Status{ 244 Comm: t.Name(), 245 PID: int32(pid), 246 PPID: int32(ppid), 247 UID: UIDGID{ 248 Real: uint32(creds.RealKUID.In(uns).OrOverflow()), 249 Effective: uint32(creds.EffectiveKUID.In(uns).OrOverflow()), 250 Saved: uint32(creds.SavedKUID.In(uns).OrOverflow()), 251 }, 252 GID: UIDGID{ 253 Real: uint32(creds.RealKGID.In(uns).OrOverflow()), 254 Effective: uint32(creds.EffectiveKGID.In(uns).OrOverflow()), 255 Saved: uint32(creds.SavedKGID.In(uns).OrOverflow()), 256 }, 257 VMSize: mm.VirtualMemorySize() >> 10, 258 VMRSS: mm.ResidentSetSize() >> 10, 259 } 260 } 261 262 func getStat(t *kernel.Task, pid kernel.ThreadID, pidns *kernel.PIDNamespace) Stat { 263 return Stat{ 264 PGID: int32(pidns.IDOfProcessGroup(t.ThreadGroup().ProcessGroup())), 265 SID: int32(pidns.IDOfSession(t.ThreadGroup().Session())), 266 } 267 } 268 269 func getMappings(ctx context.Context, mm *mm.MemoryManager) []Mapping { 270 var maps []Mapping 271 mm.ReadMapsDataInto(ctx, func(start, end hostarch.Addr, permissions hostarch.AccessType, private string, offset uint64, devMajor, devMinor uint32, inode uint64, path string) { 272 maps = append(maps, Mapping{ 273 Address: hostarch.AddrRange{ 274 Start: start, 275 End: end, 276 }, 277 Permissions: permissions, 278 Private: private, 279 Offset: offset, 280 DevMajor: devMajor, 281 DevMinor: devMinor, 282 Inode: inode, 283 Pathname: path, 284 }) 285 }) 286 287 return maps 288 } 289 290 // Dump returns a procfs dump for process pid. t must be a task in process pid. 291 func Dump(t *kernel.Task, pid kernel.ThreadID, pidns *kernel.PIDNamespace) (ProcessProcfsDump, error) { 292 ctx := t.AsyncContext() 293 294 mm := getMM(t) 295 if mm == nil { 296 return ProcessProcfsDump{}, fmt.Errorf("no MM found for PID %s", pid) 297 } 298 defer mm.DecUsers(ctx) 299 300 fdLimit, err := getFDLimit(ctx, pid) 301 if err != nil { 302 return ProcessProcfsDump{}, err 303 } 304 305 return ProcessProcfsDump{ 306 Exe: getExecutablePath(ctx, pid, mm), 307 Args: getMetadataArray(ctx, pid, mm, proc.Cmdline), 308 Env: getMetadataArray(ctx, pid, mm, proc.Environ), 309 CWD: getCWD(ctx, t, pid), 310 FDs: getFDs(ctx, t, pid), 311 StartTime: t.StartTime().Nanoseconds(), 312 Root: getRoot(t, pid), 313 Limits: map[string]limits.Limit{ 314 "RLIMIT_NOFILE": fdLimit, 315 }, 316 // We don't need to worry about fake cgroup controllers as that is not 317 // supported in runsc. 318 Cgroup: t.GetCgroupEntries(), 319 Status: getStatus(t, mm, pid, pidns), 320 Stat: getStat(t, pid, pidns), 321 Maps: getMappings(ctx, mm), 322 }, nil 323 }