diff --git a/interpreter/beam/beam.go b/interpreter/beam/beam.go index ab72f280f..47d392b39 100644 --- a/interpreter/beam/beam.go +++ b/interpreter/beam/beam.go @@ -337,7 +337,7 @@ func (d *beamData) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, bias libp func (d *beamData) Unload(_ interpreter.EbpfHandler) { } -func (i *beamInstance) SynchronizeMappings(ebpf interpreter.EbpfHandler, _ reporter.ExecutableReporter, pr process.Process, mappings []process.Mapping) error { +func (i *beamInstance) SynchronizeMappings(ebpf interpreter.EbpfHandler, _ reporter.ExecutableReporter, pr process.Process, mappings []process.RawMapping) error { pid := pr.PID() i.mappingGeneration++ for idx := range mappings { diff --git a/interpreter/dotnet/instance.go b/interpreter/dotnet/instance.go index c59304247..0da42883f 100644 --- a/interpreter/dotnet/instance.go +++ b/interpreter/dotnet/instance.go @@ -141,7 +141,7 @@ type dotnetInstance struct { virtualCallStubManagerManagerPtr libpf.Address // mappings contains the PE mappings to process memory space. Multiple individual - // consecutive process.Mappings may be merged to one mapping per PE file. + // consecutive process.RawMappings may be merged to one mapping per PE file. mappings []dotnetMapping ranges map[libpf.Address]dotnetRangeSection @@ -552,7 +552,7 @@ func (i *dotnetInstance) getDacSlotPtr(slot uint) libpf.Address { func (i *dotnetInstance) SynchronizeMappings(ebpf interpreter.EbpfHandler, exeReporter reporter.ExecutableReporter, pr process.Process, - mappings []process.Mapping, + mappings []process.RawMapping, ) error { // get introspection data cdac, err := i.d.GetOrInit(func() (dotnetCdac, error) { return i.d.newVMData(i.rm, i.bias) }) @@ -599,7 +599,7 @@ func (i *dotnetInstance) SynchronizeMappings(ebpf interpreter.EbpfHandler, if m.IsAnonymous() { continue } - if !strings.HasSuffix(m.Path.String(), ".dll") { + if !strings.HasSuffix(m.Path, ".dll") { continue } diff --git a/interpreter/dotnet/pe.go b/interpreter/dotnet/pe.go index 952a92e76..fde83feb4 100644 --- a/interpreter/dotnet/pe.go +++ b/interpreter/dotnet/pe.go @@ -1209,7 +1209,7 @@ func (pc *peCache) init() { pc.peInfoCache = peInfoCache } -func (pc *peCache) Get(pr process.Process, mapping *process.Mapping) *peInfo { +func (pc *peCache) Get(pr process.Process, mapping *process.RawMapping) *peInfo { key := mapping.GetOnDiskFileIdentifier() lastModified := pr.GetMappingFileLastModified(mapping) if info, ok := pc.peInfoCache.Get(key); ok && info.lastModified == lastModified { @@ -1243,7 +1243,7 @@ func (pc *peCache) Get(pr process.Process, mapping *process.Mapping) *peInfo { if info.err == nil { mf := libpf.NewFrameMappingFile(libpf.FrameMappingFileData{ FileID: fileID, - FileName: libpf.Intern(path.Base(mapping.Path.String())), + FileName: libpf.Intern(path.Base(mapping.Path)), GnuBuildID: info.guid, }) info.mapping = libpf.NewFrameMapping(libpf.FrameMappingData{ diff --git a/interpreter/hotspot/instance.go b/interpreter/hotspot/instance.go index ce0802f68..e116b772e 100644 --- a/interpreter/hotspot/instance.go +++ b/interpreter/hotspot/instance.go @@ -854,7 +854,7 @@ func (d *hotspotInstance) updateStubMappings(vmd *hotspotVMData, } func (d *hotspotInstance) SynchronizeMappings(ebpf interpreter.EbpfHandler, - _ reporter.ExecutableReporter, pr process.Process, _ []process.Mapping, + _ reporter.ExecutableReporter, pr process.Process, _ []process.RawMapping, ) error { vmd, err := d.d.GetOrInit(func() (hotspotVMData, error) { return d.d.newVMData(d.rm, d.bias) }) if err != nil { diff --git a/interpreter/instancestubs.go b/interpreter/instancestubs.go index 662eb74d8..1eca53dea 100644 --- a/interpreter/instancestubs.go +++ b/interpreter/instancestubs.go @@ -17,7 +17,7 @@ type InstanceStubs struct { } func (is *InstanceStubs) SynchronizeMappings(EbpfHandler, reporter.ExecutableReporter, - process.Process, []process.Mapping) error { + process.Process, []process.RawMapping) error { return nil } diff --git a/interpreter/multi.go b/interpreter/multi.go index 08634384b..d976228d2 100644 --- a/interpreter/multi.go +++ b/interpreter/multi.go @@ -92,7 +92,7 @@ func (m *MultiInstance) Detach(ebpf EbpfHandler, pid libpf.PID) error { // SynchronizeMappings synchronizes mappings for all interpreter instances. func (m *MultiInstance) SynchronizeMappings(ebpf EbpfHandler, - exeReporter reporter.ExecutableReporter, pr process.Process, mappings []process.Mapping, + exeReporter reporter.ExecutableReporter, pr process.Process, mappings []process.RawMapping, ) error { var errs []error for _, instance := range m.instances { diff --git a/interpreter/nodev8/v8.go b/interpreter/nodev8/v8.go index 12cffa30a..62d9a8e6c 100644 --- a/interpreter/nodev8/v8.go +++ b/interpreter/nodev8/v8.go @@ -500,8 +500,8 @@ type v8Instance struct { addrToSource *freelru.LRU[libpf.Address, *v8Source] addrToType *freelru.LRU[libpf.Address, uint16] - // mappings is indexed by the Mapping to its generation - mappings map[process.Mapping]*uint32 + // mappings is indexed by the RawMapping to its generation + mappings map[process.RawMapping]*uint32 // prefixes is indexed by the prefix added to ebpf maps (to be cleaned up) to its generation prefixes map[lpm.Prefix]*uint32 // mappingGeneration is the current generation (so old entries can be pruned) @@ -555,7 +555,7 @@ func (i *v8Instance) Detach(ebpf interpreter.EbpfHandler, pid libpf.PID) error { } func (i *v8Instance) SynchronizeMappings(ebpf interpreter.EbpfHandler, - _ reporter.ExecutableReporter, pr process.Process, mappings []process.Mapping, + _ reporter.ExecutableReporter, pr process.Process, mappings []process.RawMapping, ) error { pid := pr.PID() i.mappingGeneration++ @@ -1841,7 +1841,7 @@ func (d *v8Data) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, _ libpf.Add return &v8Instance{ d: d, rm: rm, - mappings: make(map[process.Mapping]*uint32), + mappings: make(map[process.RawMapping]*uint32), prefixes: make(map[lpm.Prefix]*uint32), addrToString: addrToString, addrToCode: addrToCode, diff --git a/interpreter/php/opcache.go b/interpreter/php/opcache.go index 380e05120..2e8f5afdb 100644 --- a/interpreter/php/opcache.go +++ b/interpreter/php/opcache.go @@ -193,7 +193,7 @@ func (i *opcacheInstance) Detach(ebpf interpreter.EbpfHandler, pid libpf.PID) er } func (i *opcacheInstance) SynchronizeMappings(ebpf interpreter.EbpfHandler, - _ reporter.ExecutableReporter, pr process.Process, _ []process.Mapping, + _ reporter.ExecutableReporter, pr process.Process, _ []process.RawMapping, ) error { if i.prefixes != nil { // Already attached diff --git a/interpreter/types.go b/interpreter/types.go index 991ec65a8..4b2e219c2 100644 --- a/interpreter/types.go +++ b/interpreter/types.go @@ -137,11 +137,21 @@ type Instance interface { // simple interpreters can use the global Data also as the Instance implementation. Detach(ebpf EbpfHandler, pid libpf.PID) error - // SynchronizeMappings is called when the processmanager has reread process memory - // mappings. Interpreters not needing to process these events can simply ignore them - // by just returning a nil. + // SynchronizeMappings is called when the processmanager has reread process + // memory mappings. The mappings slice contains only the subset of mappings + // that are relevant to interpreters: executable anonymous mappings (for JIT + // engines like HotSpot, V8, BEAM) and DLL file-backed mappings (for .NET + // PE assemblies). The processmanager decides which mappings are included; + // this can be made more dynamic in the future if needed. + // + // The mappings are in /proc/PID/maps order (ascending by virtual address) + // but are NOT sorted by any other criteria. Interpreters that need a + // specific ordering must sort locally. + // + // Interpreters not needing to process these events can simply ignore them + // by returning nil. SynchronizeMappings(ebpf EbpfHandler, exeReporter reporter.ExecutableReporter, - pr process.Process, mappings []process.Mapping) error + pr process.Process, mappings []process.RawMapping) error // UpdateLibcInfo is called when the process C-library related // introspection data has been updated. diff --git a/process/coredump.go b/process/coredump.go index 385ce52d2..053fa7ccb 100644 --- a/process/coredump.go +++ b/process/coredump.go @@ -46,7 +46,7 @@ type CoredumpProcess struct { machineData MachineData // mappings contains the parsed mappings. - mappings []Mapping + mappings []RawMapping // threadInfo contains the parsed thread info. threadInfo []ThreadInfo @@ -145,7 +145,7 @@ func OpenCoredumpFile(f *pfelf.File) (*CoredumpProcess, error) { cd := &CoredumpProcess{ File: f, files: make(map[string]*CoredumpFile), - mappings: make([]Mapping, 0, len(f.Progs)), + mappings: make([]RawMapping, 0, len(f.Progs)), threadInfo: make([]ThreadInfo, 0, 8), } cd.machineData.Machine = cd.Machine @@ -157,7 +157,7 @@ func OpenCoredumpFile(f *pfelf.File) (*CoredumpProcess, error) { for i := range f.Progs { p := &f.Progs[i] if p.Type == elf.PT_LOAD && p.Flags != 0 { - m := Mapping{ + m := RawMapping{ Vaddr: p.Vaddr, Length: p.Memsz, Flags: p.Flags, @@ -269,9 +269,14 @@ func (cd *CoredumpProcess) GetExe() (libpf.String, error) { return cd.fname, nil } -// GetMappings implements the Process interface. -func (cd *CoredumpProcess) GetMappings() ([]Mapping, uint32, error) { - return cd.mappings, 0, nil +// IterateMappings implements the Process interface. +func (cd *CoredumpProcess) IterateMappings(callback func(m RawMapping) bool) (uint32, error) { + for _, m := range cd.mappings { + if !callback(m) { + return 0, ErrCallbackStopped + } + } + return 0, nil } // GetThreadInfo implements the Process interface. @@ -280,18 +285,18 @@ func (cd *CoredumpProcess) GetThreads() ([]ThreadInfo, error) { } // OpenMappingFile implements the Process interface. -func (cd *CoredumpProcess) OpenMappingFile(_ *Mapping) (ReadAtCloser, error) { +func (cd *CoredumpProcess) OpenMappingFile(_ *RawMapping) (ReadAtCloser, error) { // Coredumps do not contain the original backing files. return nil, errors.New("coredump does not support opening backing file") } // GetMappingFileLastModified implements the Process interface. -func (cd *CoredumpProcess) GetMappingFileLastModified(_ *Mapping) int64 { +func (cd *CoredumpProcess) GetMappingFileLastModified(_ *RawMapping) int64 { return 0 } // CalculateMappingFileID implements the Process interface. -func (cd *CoredumpProcess) CalculateMappingFileID(m *Mapping) (libpf.FileID, error) { +func (cd *CoredumpProcess) CalculateMappingFileID(m *RawMapping) (libpf.FileID, error) { // It is not possible to calculate the real FileID as the section headers // are likely missing. So just return a synthesized FileID. vaddr := make([]byte, 8) @@ -299,7 +304,7 @@ func (cd *CoredumpProcess) CalculateMappingFileID(m *Mapping) (libpf.FileID, err h := fnv.New128a() _, _ = h.Write(vaddr) - _, _ = h.Write([]byte(m.Path.String())) + _, _ = h.Write([]byte(m.Path)) return libpf.FileIDFromBytes(h.Sum(nil)) } @@ -399,7 +404,7 @@ func (cd *CoredumpProcess) parseMappings(desc []byte, cf.Mappings = append(cf.Mappings, cm) mapping := &cd.mappings[m.mappingIndex] - mapping.Path = cf.Name + mapping.Path = cf.Name.String() mapping.FileOffset = entry.FileOffset * hdr.PageSize // Synthesize non-zero device and inode indicating this is a filebacked mapping. mapping.Device = 1 @@ -408,14 +413,14 @@ func (cd *CoredumpProcess) parseMappings(desc []byte, // This file backed mapping is not in the coredump LOAD tables // Likely a executable mapping excluded by core_filter. Construct // the mappings assuming R+X. - cd.mappings = append(cd.mappings, Mapping{ + cd.mappings = append(cd.mappings, RawMapping{ Vaddr: entry.Start, Length: entry.End - entry.Start, Flags: elf.PF_R + elf.PF_X, FileOffset: entry.FileOffset * hdr.PageSize, Device: 1, Inode: cf.inode, - Path: cf.Name, + Path: cf.Name.String(), }) } strs = strs[fnlen+1:] @@ -438,7 +443,7 @@ func (cd *CoredumpProcess) parseAuxVector(desc []byte, vaddrToMappings map[uint6 vm.Inode = vdsoInode vm.Path = VdsoPathName - cf := cd.getFile(vm.Path.String()) + cf := cd.getFile(vm.Path) cm := CoredumpMapping{ Prog: m.prog, File: cf, diff --git a/process/process.go b/process/process.go index 6901a95e3..46e341945 100644 --- a/process/process.go +++ b/process/process.go @@ -27,9 +27,13 @@ import ( "go.opentelemetry.io/ebpf-profiler/stringutil" ) -// GetMappings returns this error when no mappings can be extracted. +// ErrNoMappings is returned when no mappings can be extracted. var ErrNoMappings = errors.New("no mappings") +// ErrCallbackStopped is returned when the IterateMappings callback returns +// false, signaling that iteration was intentionally interrupted. +var ErrCallbackStopped = errors.New("IterateMappings stopped by callback") + const ( containerSource = "[0-9a-f]{64}" taskSource = "[0-9a-f]{32}-\\d+" @@ -55,7 +59,7 @@ type systemProcess struct { mainThreadExit bool remoteMemory remotememory.RemoteMemory - fileToMapping map[string]*Mapping + fileToMapping map[string]*RawMapping } var _ Process = &systemProcess{} @@ -189,13 +193,12 @@ func trimMappingPath(path string) string { return path } -func parseMappings(mapsFile io.Reader) ([]Mapping, uint32, error) { +func iterateMappings(mapsFile io.Reader, callback func(m RawMapping) bool) (uint32, error) { numParseErrors := uint32(0) - mappings := make([]Mapping, 0, 32) scanner := bufio.NewScanner(mapsFile) scanBuf := bufPool.Get().(*[]byte) if scanBuf == nil { - return mappings, 0, errors.New("failed to get memory from sync pool") + return 0, errors.New("failed to get memory from sync pool") } defer func() { // Reset memory and return it for reuse. @@ -211,6 +214,10 @@ func parseMappings(mapsFile io.Reader) ([]Mapping, uint32, error) { var addrs [2]string var devs [2]string + // WARNING: line (and all substrings derived from it, including the + // Path field of the emitted RawMapping) points into scanBuf which is + // recycled after iteration. Callers must intern Path (libpf.Intern) + // before storing. line := pfunsafe.ToString(scanner.Bytes()) if stringutil.FieldsN(line, fields[:]) < 5 { numParseErrors++ @@ -266,7 +273,7 @@ func parseMappings(mapsFile io.Reader) ([]Mapping, uint32, error) { } device := major<<8 + minor - var path libpf.String + var path string if inode == 0 { if fields[5] == "[vdso]" { // Map to something filename looking with synthesized inode @@ -280,7 +287,7 @@ func parseMappings(mapsFile io.Reader) ([]Mapping, uint32, error) { continue } } else { - path = libpf.Intern(trimMappingPath(fields[5])) + path = trimMappingPath(fields[5]) } vaddr, err := strconv.ParseUint(addrs[0], 16, 64) @@ -304,7 +311,7 @@ func parseMappings(mapsFile io.Reader) ([]Mapping, uint32, error) { continue } - mappings = append(mappings, Mapping{ + if !callback(RawMapping{ Vaddr: vaddr, Length: length, Flags: flags, @@ -312,29 +319,39 @@ func parseMappings(mapsFile io.Reader) ([]Mapping, uint32, error) { Device: device, Inode: inode, Path: path, - }) + }) { + return numParseErrors, ErrCallbackStopped + } } - return mappings, numParseErrors, scanner.Err() + return numParseErrors, scanner.Err() } -// GetMappings will process the mappings file from proc. Additionally, -// a reverse map from mapping filename to a Mapping node is built to allow -// OpenELF opening ELF files using the corresponding proc map_files entry. -// WARNING: This implementation does not support calling GetMappings -// concurrently with itself, or with OpenELF. -func (sp *systemProcess) GetMappings() ([]Mapping, uint32, error) { +func (sp *systemProcess) IterateMappings(callback func(m RawMapping) bool) (uint32, error) { mapsFile, err := os.Open(fmt.Sprintf("/proc/%d/maps", sp.pid)) if err != nil { - return nil, 0, err + return 0, err } defer mapsFile.Close() - mappings, numParseErrors, err := parseMappings(mapsFile) + fileToMapping := make(map[string]*RawMapping) + gotMappings := false + + collectForOpenELF := func(m RawMapping) bool { + gotMappings = true + if m.IsExecutable() || m.IsVDSO() { + stored := m + stored.Path = libpf.Intern(m.Path).String() + fileToMapping[stored.Path] = &stored + } + return callback(m) + } + + numParseErrors, err := iterateMappings(mapsFile, collectForOpenELF) if err != nil { - return mappings, numParseErrors, err + return numParseErrors, err } - if len(mappings) == 0 { + if !gotMappings { // We could test for main thread exit here by checking for zombie state // in /proc/sp.pid/stat but it's simpler to assume that this is the case // and try extracting mappings for a different thread. Since we stopped @@ -344,7 +361,7 @@ func (sp *systemProcess) GetMappings() ([]Mapping, uint32, error) { sp.mainThreadExit = true if sp.pid == sp.tid { - return mappings, numParseErrors, ErrNoMappings + return numParseErrors, ErrNoMappings } log.Debugf("TID: %v extracting mappings", sp.tid) @@ -356,24 +373,17 @@ func (sp *systemProcess) GetMappings() ([]Mapping, uint32, error) { // the agent to unload process metadata when a thread exits but the process is still // alive). if err != nil { - return mappings, numParseErrors, ErrNoMappings + return numParseErrors, ErrNoMappings } defer mapsFileAlt.Close() - mappings, numParseErrors, err = parseMappings(mapsFileAlt) - if err != nil || len(mappings) == 0 { - return mappings, numParseErrors, ErrNoMappings + numParseErrors, err := iterateMappings(mapsFileAlt, collectForOpenELF) + if err != nil || !gotMappings { + return numParseErrors, ErrNoMappings } } - fileToMapping := make(map[string]*Mapping) - for idx := range mappings { - m := &mappings[idx] - if m.Path != libpf.NullString { - fileToMapping[m.Path.String()] = m - } - } sp.fileToMapping = fileToMapping - return mappings, numParseErrors, nil + return numParseErrors, nil } func (sp *systemProcess) GetThreads() ([]ThreadInfo, error) { @@ -388,7 +398,7 @@ func (sp *systemProcess) GetRemoteMemory() remotememory.RemoteMemory { return sp.remoteMemory } -func (sp *systemProcess) extractMapping(m *Mapping) (*bytes.Reader, error) { +func (sp *systemProcess) extractMapping(m *RawMapping) (*bytes.Reader, error) { data := make([]byte, m.Length) _, err := sp.remoteMemory.ReadAt(data, int64(m.Vaddr)) if err != nil { @@ -398,8 +408,8 @@ func (sp *systemProcess) extractMapping(m *Mapping) (*bytes.Reader, error) { return bytes.NewReader(data), nil } -func (sp *systemProcess) getMappingFile(m *Mapping) string { - if m.IsAnonymous() || m.IsVDSO() { +func (sp *systemProcess) getMappingFile(m *RawMapping) string { + if !m.IsFileBacked() { return "" } if sp.mainThreadExit { @@ -407,12 +417,12 @@ func (sp *systemProcess) getMappingFile(m *Mapping) string { // nor /proc/sp.pid/root exist if main thread has exited, so we use the // mapping path directly under the sp.tid root. rootPath := fmt.Sprintf("/proc/%v/task/%v/root", sp.pid, sp.tid) - return path.Join(rootPath, m.Path.String()) + return path.Join(rootPath, m.Path) } return fmt.Sprintf("/proc/%v/map_files/%x-%x", sp.pid, m.Vaddr, m.Vaddr+m.Length) } -func (sp *systemProcess) OpenMappingFile(m *Mapping) (ReadAtCloser, error) { +func (sp *systemProcess) OpenMappingFile(m *RawMapping) (ReadAtCloser, error) { filename := sp.getMappingFile(m) if filename == "" { return nil, errors.New("no backing file for anonymous memory") @@ -420,7 +430,7 @@ func (sp *systemProcess) OpenMappingFile(m *Mapping) (ReadAtCloser, error) { return os.Open(filename) } -func (sp *systemProcess) GetMappingFileLastModified(m *Mapping) int64 { +func (sp *systemProcess) GetMappingFileLastModified(m *RawMapping) int64 { filename := sp.getMappingFile(m) if filename != "" { var st unix.Stat_t @@ -435,7 +445,7 @@ func (sp *systemProcess) GetMappingFileLastModified(m *Mapping) int64 { // VDSO for the system. var vdsoFileID libpf.FileID -func (sp *systemProcess) CalculateMappingFileID(m *Mapping) (libpf.FileID, error) { +func (sp *systemProcess) CalculateMappingFileID(m *RawMapping) (libpf.FileID, error) { if m.IsVDSO() { if vdsoFileID != (libpf.FileID{}) { return vdsoFileID, nil diff --git a/process/process_test.go b/process/process_test.go index b7c943c9e..e67ac2593 100644 --- a/process/process_test.go +++ b/process/process_test.go @@ -5,6 +5,7 @@ package process import ( "debug/elf" + "io" "os" "runtime" "strings" @@ -30,87 +31,135 @@ var testMappings = `55fe82710000-55fe8273c000 r--p 00000000 fd:01 1068432 7f63c8eef000 r-xp 0001c000 1fd:01 1075944 7f8b929f0000-7f8b92a00000 r-xp 00000000 00:00 0 ` +var allExpectedMappings = []RawMapping{ + { + Vaddr: 0x55fe82710000, + Device: 0xfd01, + Flags: elf.PF_R, + Inode: 1068432, + Length: 0x2c000, + FileOffset: 0, + Path: "/tmp/usr_bin_seahorse", + }, + { + Vaddr: 0x55fe8273c000, + Device: 0xfd01, + Flags: elf.PF_R + elf.PF_X, + Inode: 1068432, + Length: 0x82000, + FileOffset: 0x2c000, + Path: "/tmp/usr_bin_seahorse", + }, + { + Vaddr: 0x55fe827be000, + Device: 0xfd01, + Flags: elf.PF_R, + Inode: 1068432, + Length: 0x78000, + FileOffset: 0xae000, + Path: "/tmp/usr_bin_seahorse", + }, + { + Vaddr: 0x55fe82836000, + Device: 0xfd01, + Flags: elf.PF_R, + Inode: 1068432, + Length: 0x7000, + FileOffset: 0x125000, + Path: "/tmp/usr_bin_seahorse", + }, + { + Vaddr: 0x55fe8283d000, + Device: 0xfd01, + Flags: elf.PF_R + elf.PF_W, + Inode: 1068432, + Length: 0x1000, + FileOffset: 0x12c000, + Path: "/tmp/usr_bin_seahorse", + }, + { + Vaddr: 0x7f63c8c3e000, + Device: 0x0801, + Flags: elf.PF_R + elf.PF_X, + Inode: 1048922, + Length: 0x1A2000, + FileOffset: 544768, + Path: "/tmp/usr_lib_x86_64-linux-gnu_libcrypto.so.1.1", + }, + { + Vaddr: 0x7f63c8ebf000, + Device: 0x1fd01, + Flags: elf.PF_R + elf.PF_X, + Inode: 1075944, + Length: 0x130000, + FileOffset: 114688, + Path: "/tmp/usr_lib_x86_64-linux-gnu_libopensc.so.6.0.0", + }, + { + Vaddr: 0x7f8b929f0000, + Device: 0x0, + Flags: elf.PF_R + elf.PF_X, + Inode: 0, + Length: 0x10000, + FileOffset: 0, + Path: "", + }, +} + +func getTestMappings(t *testing.T, mapsFile io.Reader) ([]RawMapping, uint32, error) { + t.Helper() + + mappings := make([]RawMapping, 0, 32) + numParseErrors, err := iterateMappings(mapsFile, func(m RawMapping) bool { + m.Path = libpf.Intern(m.Path).String() + mappings = append(mappings, m) + return true + }) + return mappings, numParseErrors, err +} + +func getTestMappingsFromProcess(t *testing.T, process Process) ([]RawMapping, uint32, error) { + t.Helper() + + mappings := make([]RawMapping, 0, 32) + numParseErrors, err := process.IterateMappings(func(m RawMapping) bool { + m.Path = libpf.Intern(m.Path).String() + mappings = append(mappings, m) + return true + }) + return mappings, numParseErrors, err +} + func TestParseMappings(t *testing.T) { - mappings, numParseErrors, err := parseMappings(strings.NewReader(testMappings)) + mappings, numParseErrors, err := getTestMappings(t, strings.NewReader(testMappings)) require.NoError(t, err) require.Equal(t, uint32(4), numParseErrors) - assert.NotNil(t, mappings) + assert.Equal(t, allExpectedMappings, mappings) +} - expected := []Mapping{ - { - Vaddr: 0x55fe82710000, - Device: 0xfd01, - Flags: elf.PF_R, - Inode: 1068432, - Length: 0x2c000, - FileOffset: 0, - Path: libpf.Intern("/tmp/usr_bin_seahorse"), - }, - { - Vaddr: 0x55fe8273c000, - Device: 0xfd01, - Flags: elf.PF_R + elf.PF_X, - Inode: 1068432, - Length: 0x82000, - FileOffset: 0x2c000, - Path: libpf.Intern("/tmp/usr_bin_seahorse"), - }, - { - Vaddr: 0x55fe827be000, - Device: 0xfd01, - Flags: elf.PF_R, - Inode: 1068432, - Length: 0x78000, - FileOffset: 0xae000, - Path: libpf.Intern("/tmp/usr_bin_seahorse"), - }, - { - Vaddr: 0x55fe82836000, - Device: 0xfd01, - Flags: elf.PF_R, - Inode: 1068432, - Length: 0x7000, - FileOffset: 0x125000, - Path: libpf.Intern("/tmp/usr_bin_seahorse"), - }, - { - Vaddr: 0x55fe8283d000, - Device: 0xfd01, - Flags: elf.PF_R + elf.PF_W, - Inode: 1068432, - Length: 0x1000, - FileOffset: 0x12c000, - Path: libpf.Intern("/tmp/usr_bin_seahorse"), - }, - { - Vaddr: 0x7f63c8c3e000, - Device: 0x0801, - Flags: elf.PF_R + elf.PF_X, - Inode: 1048922, - Length: 0x1A2000, - FileOffset: 544768, - Path: libpf.Intern("/tmp/usr_lib_x86_64-linux-gnu_libcrypto.so.1.1"), - }, - { - Vaddr: 0x7f63c8ebf000, - Device: 0x1fd01, - Flags: elf.PF_R + elf.PF_X, - Inode: 1075944, - Length: 0x130000, - FileOffset: 114688, - Path: libpf.Intern("/tmp/usr_lib_x86_64-linux-gnu_libopensc.so.6.0.0"), - }, - { - Vaddr: 0x7f8b929f0000, - Device: 0x0, - Flags: elf.PF_R + elf.PF_X, - Inode: 0, - Length: 0x10000, - FileOffset: 0, - Path: libpf.NullString, - }, +func TestMappingPredicates(t *testing.T) { + tests := []struct { + name string + m RawMapping + wantAnon bool + wantFile bool + wantMemFD bool + wantVDSO bool + }{ + {"anonymous", RawMapping{}, true, false, false, false}, + {"file-backed", RawMapping{Path: "/usr/lib/foo.so"}, false, true, false, false}, + {"memfd", RawMapping{Path: "/memfd:jit"}, true, false, true, false}, + {"vdso", RawMapping{Path: VdsoPathName}, false, false, false, true}, + {"/dev/zero normalized", RawMapping{Inode: 42, Device: 1}, true, false, false, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.wantAnon, tt.m.IsAnonymous(), "IsAnonymous") + assert.Equal(t, tt.wantFile, tt.m.IsFileBacked(), "IsFileBacked") + assert.Equal(t, tt.wantMemFD, tt.m.IsMemFD(), "IsMemFD") + assert.Equal(t, tt.wantVDSO, tt.m.IsVDSO(), "IsVDSO") + }) } - assert.Equal(t, expected, mappings) } func TestNewPIDOfSelf(t *testing.T) { @@ -121,7 +170,7 @@ func TestNewPIDOfSelf(t *testing.T) { pr := New(pid, pid) assert.NotNil(t, pr) - mappings, numParseErrors, err := pr.GetMappings() + mappings, numParseErrors, err := getTestMappingsFromProcess(t, pr) require.NoError(t, err) require.Equal(t, uint32(0), numParseErrors) assert.NotEmpty(t, mappings) diff --git a/process/types.go b/process/types.go index 54efc0dea..f838586e9 100644 --- a/process/types.go +++ b/process/types.go @@ -17,13 +17,19 @@ import ( ) // VdsoPathName is the path to use for VDSO mappings. -var VdsoPathName = libpf.Intern("linux-vdso.1.so") +const VdsoPathName = "linux-vdso.1.so" // vdsoInode is the synthesized inode number for VDSO mappings. const vdsoInode = 50 -// Mapping contains information about a memory mapping. -type Mapping struct { +// RawMapping represents a memory mapping parsed from /proc/pid/maps or a coredump. +// +// WARNING: When produced by the systemProcess IterateMappings implementation, +// Path may reference an internal scanner buffer that is recycled after the +// iteration completes. Callers that need to store the mapping beyond the +// callback scope must intern the Path via libpf.Intern to detach it from the +// buffer and deduplicate identical paths across mappings. +type RawMapping struct { // Vaddr is the virtual memory start for this mapping. Vaddr uint64 // Length is the length of the mapping. @@ -36,27 +42,34 @@ type Mapping struct { Device uint64 // Inode holds the mapped file's inode number. Inode uint64 - // Path contains the file name for file backed mappings. - Path libpf.String + // Path is the file path for file-backed and special mappings. + // When received from IterateMappings, this may point into an internal + // buffer. The caller must use libpf.Intern to detach it before storing + // the mapping long-term. + Path string } -func (m *Mapping) IsExecutable() bool { +func (m *RawMapping) IsExecutable() bool { return m.Flags&elf.PF_X == elf.PF_X } -func (m *Mapping) IsAnonymous() bool { - return m.Path == libpf.NullString || m.IsMemFD() +func (m *RawMapping) IsAnonymous() bool { + return !m.IsFileBacked() && !m.IsVDSO() } -func (m *Mapping) IsMemFD() bool { - return strings.HasPrefix(m.Path.String(), "/memfd:") +func (m *RawMapping) IsFileBacked() bool { + return m.Path != "" && !m.IsVDSO() && !m.IsMemFD() } -func (m *Mapping) IsVDSO() bool { +func (m *RawMapping) IsMemFD() bool { + return strings.HasPrefix(m.Path, "/memfd:") +} + +func (m *RawMapping) IsVDSO() bool { return m.Path == VdsoPathName } -func (m *Mapping) GetOnDiskFileIdentifier() util.OnDiskFileIdentifier { +func (m *RawMapping) GetOnDiskFileIdentifier() util.OnDiskFileIdentifier { return util.OnDiskFileIdentifier{ DeviceID: m.Device, InodeNum: m.Inode, @@ -124,8 +137,13 @@ type Process interface { // GetExe returns the executable path of the process. GetExe() (libpf.String, error) - // GetMappings reads and parses process memory mappings. - GetMappings() ([]Mapping, uint32, error) + // IterateMappings parses process memory mappings and calls the + // callback for each mapping. The RawMapping's Path field may reference + // an internal buffer that is recycled after the iteration completes; + // callers must use libpf.Intern to detach the Path before storing the + // mapping beyond the callback scope. Returning false from the callback + // stops iteration and causes ErrCallbackStopped to be returned. + IterateMappings(callback func(m RawMapping) bool) (uint32, error) // GetThreads reads the process thread states. GetThreads() ([]ThreadInfo, error) @@ -134,14 +152,14 @@ type Process interface { GetRemoteMemory() remotememory.RemoteMemory // OpenMappingFile returns ReadAtCloser accessing the backing file of the mapping. - OpenMappingFile(*Mapping) (ReadAtCloser, error) + OpenMappingFile(*RawMapping) (ReadAtCloser, error) // GetMappingFileLastModifed returns the timestamp when the backing file was last modified // or zero if an error occurs or mapping file is not accessible via filesystem. - GetMappingFileLastModified(*Mapping) int64 + GetMappingFileLastModified(*RawMapping) int64 // CalculateMappingFileID calculates FileID of the backing file. - CalculateMappingFileID(*Mapping) (libpf.FileID, error) + CalculateMappingFileID(*RawMapping) (libpf.FileID, error) io.Closer diff --git a/processmanager/processinfo.go b/processmanager/processinfo.go index f1efde54c..6df63f09d 100644 --- a/processmanager/processinfo.go +++ b/processmanager/processinfo.go @@ -18,6 +18,7 @@ import ( "path" "slices" "sort" + "strings" "syscall" "time" @@ -189,7 +190,7 @@ func (pm *ProcessManager) handleNewInterpreter(pr process.Process, bias libpf.Ad return nil } -func (pm *ProcessManager) getELFInfo(pr process.Process, mapping *process.Mapping, +func (pm *ProcessManager) getELFInfo(pr process.Process, mapping *process.RawMapping, elfRef *pfelf.Reference, ) elfInfo { key := mapping.GetOnDiskFileIdentifier() @@ -224,7 +225,7 @@ func (pm *ProcessManager) getELFInfo(pr process.Process, mapping *process.Mappin return info } - baseName := path.Base(mapping.Path.String()) + baseName := path.Base(mapping.Path) if baseName == "/" { // There are circumstances where there is no filename. // E.g. kernel module 'bpfilter_umh' before Linux 5.9-rc1 uses @@ -331,8 +332,8 @@ func (pm *ProcessManager) processRemovedInterpreters(pid libpf.PID, var errInvalidVirtualAddress = errors.New("invalid ELF virtual address") -func (pm *ProcessManager) newFrameMapping(pr process.Process, m *process.Mapping) (libpf.FrameMapping, error) { - elfRef := pfelf.NewReference(m.Path.String(), pr) +func (pm *ProcessManager) newFrameMapping(pr process.Process, m *process.RawMapping) (libpf.FrameMapping, error) { + elfRef := pfelf.NewReference(m.Path, pr) defer elfRef.Close() info := pm.getELFInfo(pr, m, elfRef) @@ -394,34 +395,93 @@ func compareMapping(a, b Mapping) int { return 0 } -// synchronizeMappings synchronizes executable mappings for the given PID. -// This method will be called when a PID is first encountered or when the eBPF -// code encounters an address in an executable mapping that HA has no information -// on. Therefore, executable mapping synchronization takes place lazily on-demand, -// and map/unmap operations are not precisely tracked (reduce processing load). -// This means that at any point, we may have cached stale (or miss) executable -// mappings. The expectation is that stale mappings will disappear and new -// mappings cached at the next synchronization triggered by process exit or -// unknown address encountered. +// processPIDExit informs the ProcessManager that a process exited and no longer will be scheduled. +// exitKTime is stored for later processing in ProcessedUntil, when traces up to this time have been +// processed. There can be a race condition if we can not clean up the references for this process +// fast enough and this particular pid is reused again by the system. +func (pm *ProcessManager) processPIDExit(pid libpf.PID) { + exitKTime := times.GetKTime() + log.Debugf("- PID: %v", pid) + + var err error + defer func() { + if err != nil { + log.Error(err) + } + }() + defer pm.ebpf.RemoveReportedPID(pid) + pm.mu.Lock() + defer pm.mu.Unlock() + + info, pidExists := pm.pidToProcessInfo[pid] + if !pidExists { + log.Debugf("Skip process exit handling for unknown PID %d", pid) + return + } + + // processPIDExit may be called multiple times in short succession + // for the same PID, don't update exitKTime if we've previously recorded it. + if _, pidExitProcessed := pm.exitEvents[pid]; !pidExitProcessed { + pm.exitEvents[pid] = exitKTime + } else { + log.Debugf("Skip duplicate process exit handling for PID %d", pid) + return + } + + // Delete all entries we have for this particular PID from pid_page_to_mapping_info. + deleted, err2 := pm.ebpf.DeletePidPageMappingInfo(pid, []lpm.Prefix{dummyPrefix}) + if err2 != nil { + err = errors.Join(err, fmt.Errorf("failed to delete dummy prefix for PID %d: %v", + pid, err2)) + } + pm.pidPageToMappingInfoSize -= uint64(deleted) + + for idx := range info.mappings { + pm.processRemovedMapping(pid, &info.mappings[idx]) + } + pm.processRemovedInterpreters(pid, libpf.Set[util.OnDiskFileIdentifier]{}) +} + +// SynchronizeProcess triggers ProcessManager to update its internal information +// about a process. It synchronizes executable mappings for the given PID by +// parsing /proc/PID/maps and building the internal mapping state directly in +// a single pass. This method will be called when a PID is first encountered or +// when the eBPF code encounters an address in an executable mapping that HA has +// no information on. Therefore, executable mapping synchronization takes place +// lazily on-demand, and map/unmap operations are not precisely tracked (reduce +// processing load). This means that at any point, we may have cached stale (or +// miss) executable mappings. The expectation is that stale mappings will +// disappear and new mappings cached at the next synchronization triggered by +// process exit or unknown address encountered. // // TODO: Periodic synchronization of mappings for every tracked PID. -func (pm *ProcessManager) synchronizeMappings(pr process.Process, - processMappings []process.Mapping) bool { +func (pm *ProcessManager) SynchronizeProcess(pr process.Process) { pid := pr.PID() + log.Debugf("= PID: %v", pid) + + // Abort early if process is waiting for cleanup in ProcessedUntil + pm.mu.Lock() + _, ok := pm.exitEvents[pid] + pm.mu.Unlock() + + if ok { + log.Debugf("PID %v waiting for cleanup, aborting SynchronizeProcess", pid) + pm.ebpf.RemoveReportedPID(pid) + return + } // Get current executable name - exe, err := pr.GetExe() - if err != nil && !os.IsNotExist(err) { + exe, exeErr := pr.GetExe() + if exeErr != nil && !os.IsNotExist(exeErr) { // The /proc/PID/exe returns "not exists" error also in // the case of main thread exit. Ignore it. - log.Warnf("Failed to get executable of process %d: %v", pid, err) } pm.mu.Lock() info := pm.getPidInformation(pid, pr) if info == nil { pm.mu.Unlock() - return false + return } // Check if process meta needs an update updateProcessMeta := exe != libpf.NullString && exe != info.meta.Executable @@ -442,50 +502,118 @@ func (pm *ProcessManager) synchronizeMappings(pr process.Process, mpRemove[uint64(m.Vaddr)] = m } - // Generate the list of new processmanager mappings and interpreters. - // Reuse existing mappings if possible. - mappings := make([]Mapping, 0, len(processMappings)) - mpAdd := make([]*Mapping, 0, len(processMappings)) + // interpreterMappings collects the subset of mappings relevant to interpreters: + // executable anonymous mappings (JIT) and DLL file-backed mappings (.NET PE). + // They are in /proc/PID/maps order (ascending Vaddr), not sorted otherwise. + interpreterMappings := make([]process.RawMapping, 0, 8) interpretersValid := make(libpf.Set[util.OnDiskFileIdentifier], numInterpreters) - for idx := range processMappings { - m := &processMappings[idx] - if !m.IsExecutable() || m.IsAnonymous() { - continue + capHint := max(32, min(len(oldMappings), 256)) + mappings := make([]Mapping, 0, capHint) + mpAdd := make([]*Mapping, 0, capHint) + + pm.mappingStats.numProcAttempts.Add(1) + start := time.Now() + + // This callback processes each memory mapping, keeping only executable + // file-backed mappings and anonymous executable/DLL mappings needed by interpreters. + // All other mappings are skipped. + numParseErrors, err := pr.IterateMappings(func(m process.RawMapping) bool { + // Executable mappings and VDSO, converted directly to libpf.FrameMapping + mappingNeeded := m.IsExecutable() && !m.IsAnonymous() + // Needed for JIT mappings (Hotspot, V8, BEAM, etc.) + interpreterNeeded := m.IsExecutable() && m.IsAnonymous() + // Needed by .NET to retrieve PE assembly mappings + interpreterNeeded = interpreterNeeded || strings.HasSuffix(m.Path, ".dll") + if !mappingNeeded && !interpreterNeeded { + return true } - var fm libpf.FrameMapping - if oldm, ok := mpRemove[m.Vaddr]; ok { - if oldm.Length == m.Length && oldm.Device == m.Device && oldm.Inode == m.Inode { - delete(mpRemove, m.Vaddr) - fm = oldm.FrameMapping + m.Path = libpf.Intern(m.Path).String() + + if mappingNeeded { + var fm libpf.FrameMapping + if oldm, ok := mpRemove[m.Vaddr]; ok { + if oldm.Length == m.Length && oldm.Device == m.Device && oldm.Inode == m.Inode { + delete(mpRemove, m.Vaddr) + fm = oldm.FrameMapping + } } - } - newMapping := false - if !fm.Valid() { - newMapping = true - var err error - fm, err = pm.newFrameMapping(pr, m) - if err != nil { - // newFrameMapping logged the message if needed - continue + newMapping := false + if !fm.Valid() { + newMapping = true + // Error is expected for non-ELF files (e.g. PE DLL); + // fm will be invalid and the mapping skipped below but will enter the interpreter mappings block. + fm, _ = pm.newFrameMapping(pr, &m) + } + if fm.Valid() { + key := m.GetOnDiskFileIdentifier() + interpretersValid[key] = libpf.Void{} + + mappings = append(mappings, Mapping{ + Vaddr: libpf.Address(m.Vaddr), + Length: m.Length, + Device: m.Device, + Inode: m.Inode, + FrameMapping: fm, + }) + if newMapping { + mpAdd = append(mpAdd, &mappings[len(mappings)-1]) + } } } - key := m.GetOnDiskFileIdentifier() - interpretersValid[key] = libpf.Void{} - - mappings = append(mappings, Mapping{ - Vaddr: libpf.Address(m.Vaddr), - Length: m.Length, - Device: m.Device, - Inode: m.Inode, - FrameMapping: fm, - }) - if newMapping { - mpAdd = append(mpAdd, &mappings[len(mappings)-1]) + if interpreterNeeded { + interpreterMappings = append(interpreterMappings, m) + } + return true + }) + + elapsed := time.Since(start) + pm.mappingStats.numProcParseErrors.Add(numParseErrors) + + if err != nil { + switch { + case errors.Is(err, process.ErrCallbackStopped): + // Defensive: the current callback does not stop early, but the + // IterateMappings contract allows it. Treat as non-fatal and + // continue with whatever mappings were collected so far. + err = nil + case os.IsPermission(err): + // Ignore the synchronization completely in case of permission + // error. This implies the process is still alive, but we cannot + // inspect it. Exiting here keeps the PID in the eBPF maps so + // we avoid a notification flood to resynchronize. + pm.mappingStats.errProcPerm.Add(1) + return + case errors.Is(err, process.ErrNoMappings): + // When no mappings can be extracted but the process is still alive, + // do not trigger a process exit to avoid unloading process metadata. + // As it's likely that a future iteration can extract mappings from a + // different thread in the process, notify eBPF to enable further notifications. + pm.ebpf.RemoveReportedPID(pid) + return + case os.IsNotExist(err): + // Since listing /proc and opening files in there later is inherently racy, + // we expect to lose the race sometimes and thus expect to hit os.IsNotExist. + pm.mappingStats.errProcNotExist.Add(1) + log.Debugf("removing pid due to mappings read error: %v", err) + pm.processPIDExit(pid) + return + default: + if e, ok := err.(*os.PathError); ok && e.Err == syscall.ESRCH { + // If the process exits while reading its /proc/$PID/maps, the kernel will + // return ESRCH. Handle it as if the process did not exist. + pm.mappingStats.errProcESRCH.Add(1) + } + log.Debugf("removing pid due to mappings read error: %v", err) + pm.processPIDExit(pid) + return } } + util.AtomicUpdateMaxUint32(&pm.mappingStats.maxProcParseUsec, uint32(elapsed.Microseconds())) + pm.mappingStats.totalProcParseUsec.Add(uint32(elapsed.Microseconds())) + // Detach removed interpreters and remove old mappings numChanges := uint64(0) for _, m := range mpRemove { @@ -524,7 +652,7 @@ func (pm *ProcessManager) synchronizeMappings(pr process.Process, // Synchronize all interpreters with updated mappings for _, instance := range interpreters { - err := instance.SynchronizeMappings(pm.ebpf, pm.exeReporter, pr, processMappings) + err := instance.SynchronizeMappings(pm.ebpf, pm.exeReporter, pr, interpreterMappings) if err != nil { if alive, _ := isPIDLive(pid); alive { log.Errorf("Failed to handle new anonymous mapping for PID %d: %v", pid, err) @@ -539,118 +667,8 @@ func (pm *ProcessManager) synchronizeMappings(pr process.Process, log.Debugf("Added %v mappings, removed %v mappings for PID %v with %d interpreters", len(mpAdd), len(mpRemove), pid, len(interpreters)) } - return newProcess -} - -// processPIDExit informs the ProcessManager that a process exited and no longer will be scheduled. -// exitKTime is stored for later processing in ProcessedUntil, when traces up to this time have been -// processed. There can be a race condition if we can not clean up the references for this process -// fast enough and this particular pid is reused again by the system. -func (pm *ProcessManager) processPIDExit(pid libpf.PID) { - exitKTime := times.GetKTime() - log.Debugf("- PID: %v", pid) - - var err error - defer func() { - if err != nil { - log.Error(err) - } - }() - defer pm.ebpf.RemoveReportedPID(pid) - pm.mu.Lock() - defer pm.mu.Unlock() - - info, pidExists := pm.pidToProcessInfo[pid] - if !pidExists { - log.Debugf("Skip process exit handling for unknown PID %d", pid) - return - } - - // processPIDExit may be called multiple times in short succession - // for the same PID, don't update exitKTime if we've previously recorded it. - if _, pidExitProcessed := pm.exitEvents[pid]; !pidExitProcessed { - pm.exitEvents[pid] = exitKTime - } else { - log.Debugf("Skip duplicate process exit handling for PID %d", pid) - return - } - - // Delete all entries we have for this particular PID from pid_page_to_mapping_info. - deleted, err2 := pm.ebpf.DeletePidPageMappingInfo(pid, []lpm.Prefix{dummyPrefix}) - if err2 != nil { - err = errors.Join(err, fmt.Errorf("failed to delete dummy prefix for PID %d: %v", - pid, err2)) - } - pm.pidPageToMappingInfoSize -= uint64(deleted) - - for idx := range info.mappings { - pm.processRemovedMapping(pid, &info.mappings[idx]) - } - pm.processRemovedInterpreters(pid, libpf.Set[util.OnDiskFileIdentifier]{}) -} - -// SynchronizeProcess triggers ProcessManager to update its internal information -// about a process. This includes process exit information as well as changed memory mappings. -func (pm *ProcessManager) SynchronizeProcess(pr process.Process) { - pid := pr.PID() - log.Debugf("= PID: %v", pid) - - // Abort early if process is waiting for cleanup in ProcessedUntil - pm.mu.Lock() - _, ok := pm.exitEvents[pid] - pm.mu.Unlock() - - if ok { - log.Debugf("PID %v waiting for cleanup, aborting SynchronizeProcess", pid) - pm.ebpf.RemoveReportedPID(pid) - return - } - - pm.mappingStats.numProcAttempts.Add(1) - start := time.Now() - mappings, numParseErrors, err := pr.GetMappings() - elapsed := time.Since(start) - pm.mappingStats.numProcParseErrors.Add(numParseErrors) - - if err != nil { - if os.IsPermission(err) { - // Ignore the synchronization completely in case of permission - // error. This implies the process is still alive, but we cannot - // inspect it. Exiting here keeps the PID in the eBPF maps so - // we avoid a notification flood to resynchronize. - pm.mappingStats.errProcPerm.Add(1) - return - } - - if errors.Is(err, process.ErrNoMappings) { - // When no mappings can be extracted but the process is still alive, - // do not trigger a process exit to avoid unloading process metadata. - // As it's likely that a future iteration can extract mappings from a - // different thread in the process, notify eBPF to enable further notifications. - pm.ebpf.RemoveReportedPID(pid) - return - } - - // All other errors imply that the process has exited. - if os.IsNotExist(err) { - // Since listing /proc and opening files in there later is inherently racy, - // we expect to lose the race sometimes and thus expect to hit os.IsNotExist. - pm.mappingStats.errProcNotExist.Add(1) - } else if e, ok := err.(*os.PathError); ok && e.Err == syscall.ESRCH { - // If the process exits while reading its /proc/$PID/maps, the kernel will - // return ESRCH. Handle it as if the process did not exist. - pm.mappingStats.errProcESRCH.Add(1) - } - // Clean up, and notify eBPF. - log.Debugf("removing pid due to mappings read error: %v", err) - pm.processPIDExit(pid) - return - } - - util.AtomicUpdateMaxUint32(&pm.mappingStats.maxProcParseUsec, uint32(elapsed.Microseconds())) - pm.mappingStats.totalProcParseUsec.Add(uint32(elapsed.Microseconds())) - if pm.synchronizeMappings(pr, mappings) { + if newProcess { log.Debugf("+ PID: %v", pid) // TODO: Fine-grained reported_pids handling (evaluate per-PID mapping // synchronization based on per-PID state such as time since last diff --git a/reporter/iface.go b/reporter/iface.go index d26b3f11e..f5291ef19 100644 --- a/reporter/iface.go +++ b/reporter/iface.go @@ -40,9 +40,9 @@ type ExecutableMetadata struct { // Process is the interface to the process holding the file. Process process.Process - // Mapping is the process.Mapping file. Process.OpenMappingFile can be used + // Mapping is the process.RawMapping file. Process.OpenMappingFile can be used // to open the file if needed. - Mapping *process.Mapping + Mapping *process.RawMapping // DebuglinkFileName is the path to the matching debug file // from the .gnu.debuglink, if any. The caller should diff --git a/tools/coredump/new.go b/tools/coredump/new.go index 73b382d82..3df9499f3 100644 --- a/tools/coredump/new.go +++ b/tools/coredump/new.go @@ -60,7 +60,7 @@ func newTrackedCoredump(corePath, filePrefix string) (*trackedCoredump, error) { }, nil } -func (tc *trackedCoredump) GetMappingFileLastModified(_ *process.Mapping) int64 { +func (tc *trackedCoredump) GetMappingFileLastModified(_ *process.RawMapping) int64 { return 0 } @@ -71,9 +71,9 @@ func (tc *trackedCoredump) warnMissing(fileName string) { } } -func (tc *trackedCoredump) CalculateMappingFileID(m *process.Mapping) (libpf.FileID, error) { +func (tc *trackedCoredump) CalculateMappingFileID(m *process.RawMapping) (libpf.FileID, error) { if !m.IsVDSO() && !m.IsAnonymous() { - file := m.Path.String() + file := m.Path fid, err := libpf.FileIDFromExecutableFile(path.Join(tc.prefix, file)) if err == nil { tc.seen[file] = libpf.Void{} @@ -84,9 +84,9 @@ func (tc *trackedCoredump) CalculateMappingFileID(m *process.Mapping) (libpf.Fil return tc.CoredumpProcess.CalculateMappingFileID(m) } -func (tc *trackedCoredump) OpenMappingFile(m *process.Mapping) (process.ReadAtCloser, error) { +func (tc *trackedCoredump) OpenMappingFile(m *process.RawMapping) (process.ReadAtCloser, error) { if !m.IsVDSO() && !m.IsAnonymous() { - file := m.Path.String() + file := m.Path rac, err := os.Open(path.Join(tc.prefix, file)) if err == nil { tc.seen[file] = libpf.Void{} @@ -98,7 +98,7 @@ func (tc *trackedCoredump) OpenMappingFile(m *process.Mapping) (process.ReadAtCl } func (tc *trackedCoredump) OpenELF(fileName string) (*pfelf.File, error) { - if fileName != process.VdsoPathName.String() { + if fileName != process.VdsoPathName { f, err := pfelf.Open(path.Join(tc.prefix, fileName)) if err == nil { tc.seen[fileName] = libpf.Void{} diff --git a/tools/coredump/storecoredump.go b/tools/coredump/storecoredump.go index 954c700a1..b1ecef55d 100644 --- a/tools/coredump/storecoredump.go +++ b/tools/coredump/storecoredump.go @@ -45,8 +45,8 @@ func (scd *StoreCoredump) openFile(path string) (process.ReadAtCloser, error) { return file, nil } -func (scd *StoreCoredump) OpenMappingFile(m *process.Mapping) (process.ReadAtCloser, error) { - return scd.openFile(m.Path.String()) +func (scd *StoreCoredump) OpenMappingFile(m *process.RawMapping) (process.ReadAtCloser, error) { + return scd.openFile(m.Path) } func (scd *StoreCoredump) OpenELF(path string) (*pfelf.File, error) {