diff --git a/interpreter/perl/instance.go b/interpreter/perl/instance.go index 8f3364179..08508471f 100644 --- a/interpreter/perl/instance.go +++ b/interpreter/perl/instance.go @@ -74,7 +74,21 @@ func hashCOPKey(k copKey) uint32 { func (i *perlInstance) UpdateLibcInfo(ebpf interpreter.EbpfHandler, pid libpf.PID, libcInfo libc.LibcInfo) error { + // Perl requires TSDInfo to access thread state. If stateInTSD is true, + // we need valid TSDInfo to proceed. If it's false, we can proceed without it. + // Since UpdateLibcInfo may be called multiple times as LibcInfo is collected + // from multiple DSOs, we should only insert proc data when we have what we need. d := i.d + if d.stateInTSD && !libcInfo.HasTSDInfo() { + // We need TSDInfo but don't have it yet, wait for another call + return nil + } + + // If we've already inserted proc info, don't do it again + if i.procInfoInserted { + return nil + } + stateInTSD := uint8(0) if d.stateInTSD { stateInTSD = 1 diff --git a/interpreter/python/python.go b/interpreter/python/python.go index fb83ccc7d..ba52263bd 100644 --- a/interpreter/python/python.go +++ b/interpreter/python/python.go @@ -377,6 +377,20 @@ func (p *pythonInstance) GetAndResetMetrics() ([]metrics.Metric, error) { func (p *pythonInstance) UpdateLibcInfo(ebpf interpreter.EbpfHandler, pid libpf.PID, libcInfo libc.LibcInfo) error { d := p.d + + // If we don't have a static TLS offset (Python < 3.13 or extraction failed), + // we need TSDInfo to access thread state via pthread_getspecific. + // Since UpdateLibcInfo may be called multiple times as LibcInfo is collected + // from multiple DSOs, wait until we have TSDInfo before inserting proc data. + if d.staticTLSOffset == 0 && !libcInfo.HasTSDInfo() { + return nil + } + + // Prevent duplicate inserts + if p.procInfoInserted { + return nil + } + vm := &d.vmStructs cdata := support.PyProcInfo{ diff --git a/libc/libc.go b/libc/libc.go index 51d8498e4..25343cb6a 100644 --- a/libc/libc.go +++ b/libc/libc.go @@ -13,11 +13,73 @@ import ( ) type TSDInfo = support.TSDInfo +type DTVInfo = support.DTVInfo // LibcInfo contains introspection information extracted from the C-library type LibcInfo struct { // TSDInfo is the TSDInfo extracted for this C-library TSDInfo TSDInfo + // DTVInfo contains DTV (Dynamic Thread Vector) introspection data for accessing + // TLS variables when TLS descriptors are not available + DTVInfo DTVInfo +} + +// IsEqual checks if two LibcInfo instances are equal +func (l LibcInfo) IsEqual(other LibcInfo) bool { + return l.TSDInfo == other.TSDInfo && l.DTVInfo == other.DTVInfo +} + +// Merge fills in empty fields of the receiver with corresponding values from other. +// Fields already populated in the receiver are not overwritten. +func (l *LibcInfo) Merge(other LibcInfo) { + // If other has TSDInfo and this instance does not, take it + if l.TSDInfo == (TSDInfo{}) { + l.TSDInfo = other.TSDInfo + } + + // If other has DTVInfo and this instance does not, take it + if l.DTVInfo == (DTVInfo{}) { + l.DTVInfo = other.DTVInfo + } +} + +// HasTSDInfo returns true if the LibcInfo contains valid TSD information. +// TSDInfo is considered valid when the Multiplier field is non-zero. +func (l LibcInfo) HasTSDInfo() bool { + return l.TSDInfo.Multiplier != 0 +} + +// HasDTVInfo returns true if the LibcInfo contains valid DTV information. +// DTVInfo is considered valid when the Multiplier field is non-zero. +func (l LibcInfo) HasDTVInfo() bool { + return l.DTVInfo.Multiplier != 0 +} + +var ( + // regex for the libc + libcRegex = regexp.MustCompile(`.*/(ld-musl|ld-linux|libc|libpthread)([-.].*)?\.so`) +) + +// IsPotentialLibcDSO determines if the DSO filename potentially contains libc code +func IsPotentialLibcDSO(filename string) bool { + return libcRegex.MatchString(filename) +} + +func ExtractLibcInfo(ef *pfelf.File) (*LibcInfo, error) { + tsdinfo, err := extractTSDInfo(ef) + if err != nil { + return nil, err + } + + dtvinfo, err := extractDTVInfo(ef) + if err != nil { + return &LibcInfo{}, err + } + + return &LibcInfo{ + TSDInfo: tsdinfo, + DTVInfo: dtvinfo, + }, nil } // This code analyzes the C-library provided POSIX defined function which is used @@ -65,27 +127,6 @@ type LibcInfo struct { // // Reading the value is basically "return self->specific_1stblock[key].data;" -var ( - // regex for the libc - libcRegex = regexp.MustCompile(`.*/(ld-musl|libc|libpthread)([-.].*)?\.so`) -) - -// IsPotentialTSDDSO determines if the DSO filename potentially contains pthread code -func IsPotentialTSDDSO(filename string) bool { - return libcRegex.MatchString(filename) -} - -func ExtractLibcInfo(ef *pfelf.File) (*LibcInfo, error) { - tsdinfo, err := extractTSDInfo(ef) - if err != nil { - return nil, err - } - - return &LibcInfo{ - TSDInfo: tsdinfo, - }, nil -} - // extractTSDInfo extracts the introspection data for pthread thread specific data. func extractTSDInfo(ef *pfelf.File) (TSDInfo, error) { _, code, err := ef.SymbolData("__pthread_getspecific", 2048) @@ -113,3 +154,31 @@ func extractTSDInfo(ef *pfelf.File) (TSDInfo, error) { } return info, nil } + +// extractDTVInfo extracts the introspection data for the DTV to access TLS vars +func extractDTVInfo(ef *pfelf.File) (DTVInfo, error) { + var info DTVInfo + _, code, err := ef.SymbolData("__tls_get_addr", 2048) + if err != nil { + // If the symbol is not exported, this is not a critical error. + // Callers can check HasDTVInfo() to determine if DTV data is available. + return info, nil + } + + if len(code) < 8 { + return info, fmt.Errorf("__tls_get_addr function size is %d", len(code)) + } + + switch ef.Machine { + case elf.EM_AARCH64: + info, err = extractDTVInfoARM(code) + case elf.EM_X86_64: + info, err = extractDTVInfoX86(code) + default: + return info, fmt.Errorf("unsupported arch %s", ef.Machine.String()) + } + if err != nil { + return info, fmt.Errorf("failed to extract DTV data: %s", err) + } + return info, nil +} diff --git a/libc/libc_aarch64.go b/libc/libc_aarch64.go index adf9ee67c..d389694d8 100644 --- a/libc/libc_aarch64.go +++ b/libc/libc_aarch64.go @@ -268,3 +268,146 @@ func extractTSDInfoARM(code []byte) (TSDInfo, error) { Indirect: indirect, }, nil } + +func extractDTVInfoARM(code []byte) (DTVInfo, error) { + // Track register states similar to extractTSDInfoARM + var regs [32]regState + + dtvOffset := int16(0) + entryWidth := uint32(0) + resetReg := int(-1) + + // Scan entire function + for offs := 0; offs < len(code); offs += 4 { + if offs+4 > len(code) { + break + } + + if resetReg >= 0 { + // Reset register state if something unsupported happens on it + regs[resetReg] = regState{status: Unspec} + } + + inst, err := aa.Decode(code[offs:]) + if err != nil { + continue + } + if inst.Op == aa.RET { + break + } + + destReg, ok := arm.Xreg2num(inst.Args[0]) + if !ok { + continue + } + + resetReg = destReg + switch inst.Op { + case aa.MOV: + // Track register moves + srcReg, ok := arm.Xreg2num(inst.Args[1]) + if !ok { + continue + } + regs[destReg] = regs[srcReg] + + case aa.MRS: + // MRS X1, S3_3_C13_C0_2 (tpidr_el0) + if inst.Args[1].String() == "S3_3_C13_C0_2" { + regs[destReg] = regState{ + status: TSDBase, // Reuse TSDBase to mean thread pointer + multiplier: 1, + } + } + + case aa.LDUR: + // LDUR X1, [X1,#-8] + m, ok := inst.Args[1].(aa.MemImmediate) + if !ok { + continue + } + srcReg, ok := arm.Xreg2num(m.Base) + if !ok { + continue + } + if regs[srcReg].status == TSDBase { + imm, ok := arm.DecodeImmediate(m) + if !ok { + continue + } + // This is loading the DTV pointer from thread pointer + dtvOffset = int16(imm & 0xFFFF) + regs[destReg] = regState{ + status: TSDElementBase, // DTV pointer + offset: imm, + multiplier: 1, + } + } else { + continue + } + + case aa.LDR: + if len(inst.Args) < 2 { + continue + } + switch m := inst.Args[1].(type) { + case aa.MemImmediate: + // ldr x1, [x1, #0] or ldr x1, [x1] + srcReg, ok := arm.Xreg2num(m.Base) + if !ok { + continue + } + if regs[srcReg].status == TSDBase { + // Loading DTV pointer from thread pointer + imm, ok := arm.DecodeImmediate(m) + if !ok { + imm = 0 + } + dtvOffset = int16(imm & 0xFFFF) + regs[destReg] = regState{ + status: TSDElementBase, // DTV pointer + offset: imm, + multiplier: 1, + } + } else { + continue + } + + case aa.MemExtend: + // ldr x1, [x1, x2, lsl #3] + srcReg, ok := arm.Xreg2num(m.Base) + if !ok { + continue + } + if regs[srcReg].status == TSDElementBase { + // This is indexing into the DTV array + if m.Amount > 0 { + entryWidth = uint32(1 << m.Amount) + } + } + } + + case aa.LSL: + // lsl x3, x3, #4 + if len(inst.Args) >= 3 { + if imm, ok := inst.Args[2].(aa.Imm); ok { + entryWidth = uint32(1 << imm.Imm) + } + } + + case aa.CMP, aa.CBZ, aa.CMN: + // Opcode with no affect on first argument. + // Noop to exit switch without default continue. + + default: + continue + } + resetReg = -1 + } + + return DTVInfo{ + Offset: dtvOffset, + Multiplier: uint8(entryWidth), + Indirect: 1, + }, nil +} diff --git a/libc/libc_test.go b/libc/libc_test.go index bc7f108d5..24b5b81af 100644 --- a/libc/libc_test.go +++ b/libc/libc_test.go @@ -263,3 +263,515 @@ func TestExtractTSDInfo(t *testing.T) { }) } } + +func TestExtractDTVOffset(t *testing.T) { + testCases := map[string]struct { + machine elf.Machine + code []byte + info DTVInfo + }{ + "glibc 2.36 / debian 12 / x86_64": { + machine: elf.EM_X86_64, + code: []byte{ + // mov %fs:0x8,%rdx + // mov 0x1fc48(%rip),%rax # 0x7ffff7ffe0b8 <_rtld_global+4248> + // cmp %rax,(%rdx) + // jne 0x7ffff7fde48b <__tls_get_addr+43> + // mov (%rdi),%rax + // shl $0x4,%rax + // mov (%rdx,%rax,1),%rax + // cmp $0xffffffffffffffff,%rax + // je 0x7ffff7fde48b <__tls_get_addr+43> + // add 0x8(%rdi),%rax + // ret + // push %rbp + // mov %rsp,%rbp + // and $0xfffffffffffffff0,%rsp + // call 0x7ffff7fdbd40 <__tls_get_addr_slow> + // mov %rbp,%rsp + // pop %rbp + // ret + 0x64, 0x48, 0x8b, 0x14, 0x25, 0x08, 0x00, 0x00, 0x00, + 0x48, 0x8b, 0x05, 0x48, 0xfc, 0x01, 0x00, + 0x48, 0x39, 0x02, + 0x75, 0x16, + 0x48, 0x8b, 0x07, + 0x48, 0xc1, 0xe0, 0x04, + 0x48, 0x8b, 0x04, 0x02, + 0x48, 0x83, 0xf8, 0xff, + 0x74, 0x05, + 0x48, 0x03, 0x47, 0x08, + 0xc3, + 0x55, + 0x48, 0x89, 0xe5, + 0x48, 0x83, 0xe4, 0xf0, + 0xe8, 0xa8, 0xd8, 0xff, 0xff, + 0x48, 0x89, 0xec, + 0x5d, + 0xc3, + }, + info: DTVInfo{ + Offset: 8, + Multiplier: 16, + Indirect: 0, + }, + }, + "glibc 2.32 / Fedora 33 / x86_64": { + machine: elf.EM_X86_64, + code: []byte{ + // endbr64 + // mov %fs:0x8,%rdx + // mov 0x1394c(%rip),%rax # 0x7f48d90c2ff0 <_rtld_local+4080> + // cmp %rax,(%rdx) + // jne 0x7f48d90af6bf <__tls_get_addr+47> + // mov (%rdi),%rax + // shl $0x4,%rax + // mov (%rdx,%rax,1),%rax + // cmp $0xffffffffffffffff,%rax + // je 0x7f48d90af6bf <__tls_get_addr+47> + // add 0x8(%rdi),%rax + // ret + // push %rbp + // mov %rsp,%rbp + // and $0xfffffffffffffff0,%rsp + // call 0x7f48d90a9ed0 <__tls_get_addr_slow> + // mov %rbp,%rsp + // pop %rbp + // ret + 0xf3, 0x0f, 0x1e, 0xfa, + 0x64, 0x48, 0x8b, 0x14, 0x25, 0x08, 0x00, 0x00, 0x00, + 0x48, 0x8b, 0x05, 0x4c, 0x39, 0x01, 0x00, + 0x48, 0x39, 0x02, + 0x75, 0x16, + 0x48, 0x8b, 0x07, + 0x48, 0xc1, 0xe0, 0x04, + 0x48, 0x8b, 0x04, 0x02, + 0x48, 0x83, 0xf8, 0xff, + 0x74, 0x05, + 0x48, 0x03, 0x47, 0x08, + 0xc3, + 0x55, + 0x48, 0x89, 0xe5, + 0x48, 0x83, 0xe4, 0xf0, + 0xe8, 0x04, 0xa8, 0xff, 0xff, + 0x48, 0x89, 0xec, + 0x5d, + 0xc3, + }, + info: DTVInfo{ + Offset: 8, + Multiplier: 16, + Indirect: 0, + }, + }, + "musl 1.2.5 / alpine 3.22.2 / x86_64": { + machine: elf.EM_X86_64, + code: []byte{ + // mov %fs:0x0,%rax + // mov (%rdi),%rcx + // mov 0x8(%rax),%rdx + // mov 0x8(%rdi),%rax + // add (%rdx,%rcx,8),%rax + // ret + 0x64, 0x48, 0x8b, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00, + 0x48, 0x8b, 0x0f, + 0x48, 0x8b, 0x50, 0x08, + 0x48, 0x8b, 0x47, 0x08, + 0x48, 0x03, 0x04, 0xca, + 0xc3, + }, + info: DTVInfo{ + Offset: 8, + Multiplier: 8, + Indirect: 1, + }, + }, + "musl 1.1.5 / alpine 3.1 / x86_64": { + machine: elf.EM_X86_64, + code: []byte{ + // mov %fs:0x0,%rax + // mov 0x8(%rax),%rax + // mov (%rdi),%rdx + // cmp %rdx,(%rax) + // jae 0x7f824da49ef3 <__tls_get_addr+26> + // jmpq 0x7f824da191d5 + // mov (%rax,%rdx,8),%rax + // add 0x8(%rdi),%rax + // retq + 0x64, 0x48, 0x8b, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00, + 0x48, 0x8b, 0x40, 0x08, + 0x48, 0x8b, 0x17, + 0x48, 0x39, 0x10, + 0x73, 0x05, + 0xe9, 0xe2, 0xf2, 0xfc, 0xff, + 0x48, 0x8b, 0x04, 0xd0, + 0x48, 0x03, 0x47, 0x08, + 0xc3, + }, + info: DTVInfo{ + Offset: 8, + Multiplier: 8, + Indirect: 1, + }, + }, + "glibc 2.39 / ubuntu 24.04 / aarch64": { + machine: elf.EM_AARCH64, + code: []byte{ + 0x41, 0xd0, 0x3b, 0xd5, // mrs x1, tpidr_el0 + 0xfd, 0x7b, 0xbf, 0xa9, // stp x29, x30, [sp, #-16]! + 0x83, 0x01, 0x00, 0xb0, // adrp x3, 0xfffff7fff000 <_rtld_global+4096> + 0xfd, 0x03, 0x00, 0x91, // mov x29, sp + 0x63, 0x00, 0x05, 0x91, // add x3, x3, #0x140 + 0x21, 0x00, 0x40, 0xf9, // ldr x1, [x1] + 0x64, 0x00, 0x40, 0xf9, // ldr x4, [x3] + 0x25, 0x00, 0x40, 0xf9, // ldr x5, [x1] + 0xbf, 0x00, 0x04, 0xeb, // cmp x5, x4 + 0x41, 0x01, 0x00, 0x54, // b.ne 0xfffff7fce2bc <__GI___tls_get_addr+76> // b.any + 0x03, 0x00, 0x40, 0xf9, // ldr x3, [x0] + 0x63, 0xec, 0x7c, 0xd3, // lsl x3, x3, #4 + 0x23, 0x68, 0x63, 0xf8, // ldr x3, [x1, x3] + 0x7f, 0x04, 0x00, 0xb1, // cmn x3, #0x1 + 0x00, 0x01, 0x00, 0x54, // b.eq 0xfffff7fce2c8 <__GI___tls_get_addr+88> // b.none + 0x00, 0x04, 0x40, 0xf9, // ldr x0, [x0, #8] + 0xfd, 0x7b, 0xc1, 0xa8, // ldp x29, x30, [sp], #16 + 0x60, 0x00, 0x00, 0x8b, // add x0, x3, x0 + 0xc0, 0x03, 0x5f, 0xd6, // ret + 0x61, 0xfc, 0xdf, 0xc8, // ldar x1, [x3] + 0xfd, 0x7b, 0xc1, 0xa8, // ldp x29, x30, [sp], #16 + 0xd3, 0xff, 0xff, 0x17, // b 0xfffff7fce210 + 0xfd, 0x7b, 0xc1, 0xa8, // ldp x29, x30, [sp], #16 + 0x02, 0x00, 0x80, 0xd2, // mov x2, #0x0 // #0 + 0xa9, 0xfc, 0xff, 0x17, // b 0xfffff7fcd574 + + }, + info: DTVInfo{ + Offset: 0, + Multiplier: 16, + Indirect: 1, + }, + }, + "glibc / Fedora 39 / aarch64": { + machine: elf.EM_AARCH64, + code: []byte{ + 0x5f, 0x24, 0x03, 0xd5, // bti c + 0x41, 0xd0, 0x3b, 0xd5, // mrs x1, tpidr_el0 + 0x3f, 0x23, 0x03, 0xd5, // paciasp + 0xfd, 0x7b, 0xbf, 0xa9, // stp x29, x30, [sp, #-16]! + 0x83, 0x01, 0x00, 0xb0, // adrp x3, 0xeebc52855000 <_rtld_local+4096> + 0x63, 0x40, 0x05, 0x91, // add x3, x3, #0x150 + 0xfd, 0x03, 0x00, 0x91, // mov x29, sp + 0x21, 0x00, 0x40, 0xf9, // ldr x1, [x1] + 0x63, 0x00, 0x40, 0xf9, // ldr x3, [x3] + 0x24, 0x00, 0x40, 0xf9, // ldr x4, [x1] + 0x9f, 0x00, 0x03, 0xeb, // cmp x4, x3 + 0x61, 0x01, 0x00, 0x54, // b.ne 0xeebc52824278 <__GI___tls_get_addr+88> // b.any + 0x03, 0x00, 0x40, 0xf9, // ldr x3, [x0] + 0x63, 0xec, 0x7c, 0xd3, // lsl x3, x3, #4 + 0x23, 0x68, 0x63, 0xf8, // ldr x3, [x1, x3] + 0x7f, 0x04, 0x00, 0xb1, // cmn x3, #0x1 + 0x20, 0x01, 0x00, 0x54, // b.eq 0xeebc52824284 <__GI___tls_get_addr+100> // b.none + 0x00, 0x04, 0x40, 0xf9, // ldr x0, [x0, #8] + 0xfd, 0x7b, 0xc1, 0xa8, // ldp x29, x30, [sp], #16 + 0xbf, 0x23, 0x03, 0xd5, // autiasp + 0x60, 0x00, 0x00, 0x8b, // add x0, x3, x0 + 0xc0, 0x03, 0x5f, 0xd6, // ret + 0xfd, 0x7b, 0xc1, 0xa8, // ldp x29, x30, [sp], #16 + 0xbf, 0x23, 0x03, 0xd5, // autiasp + 0xcc, 0xff, 0xff, 0x17, // b 0xeebc528241b0 + 0xfd, 0x7b, 0xc1, 0xa8, // ldp x29, x30, [sp], #16 + 0xbf, 0x23, 0x03, 0xd5, // autiasp + 0x02, 0x00, 0x80, 0xd2, // mov x2, #0x0 // #0 + 0x70, 0xfc, 0xff, 0x17, // b 0xeebc52823450 + }, + info: DTVInfo{ + Offset: 0, + Multiplier: 16, + Indirect: 1, + }, + }, + "glibc / Fedora 33 / aarch64": { + machine: elf.EM_AARCH64, + code: []byte{ + 0x5f, 0x24, 0x03, 0xd5, //bti c + 0x41, 0xd0, 0x3b, 0xd5, //mrs x1, tpidr_el0 + 0x62, 0x01, 0x00, 0xf0, //adrp x2, 0xf58e5fcc1000 <_rtld_local+4072> + 0x43, 0x60, 0x40, 0xf9, //ldr x3, [x2, #192] + 0x21, 0x00, 0x40, 0xf9, //ldr x1, [x1] + 0x24, 0x00, 0x40, 0xf9, //ldr x4, [x1] + 0x9f, 0x00, 0x03, 0xeb, //cmp x4, x3 + 0x21, 0x01, 0x00, 0x54, //b.ne 0xf58e5fc921e0 <__GI___tls_get_addr+64> // b.any + 0x03, 0x00, 0x40, 0xf9, //ldr x3, [x0] + 0x63, 0xec, 0x7c, 0xd3, //lsl x3, x3, #4 + 0x23, 0x68, 0x63, 0xf8, //ldr x3, [x1, x3] + 0x7f, 0x04, 0x00, 0xb1, //cmn x3, #0x1 + 0xa0, 0x00, 0x00, 0x54, //b.eq 0xf58e5fc921e4 <__GI___tls_get_addr+68> // b.none + 0x00, 0x04, 0x40, 0xf9, //ldr x0, [x0, #8] + 0x60, 0x00, 0x00, 0x8b, //add x0, x3, x0 + 0xc0, 0x03, 0x5f, 0xd6, //ret + 0xd4, 0xff, 0xff, 0x17, //b 0xf58e5fc92130 + 0x02, 0x00, 0x80, 0xd2, //mov x2, #0x0 // #0 + 0x7e, 0xfc, 0xff, 0x17, //b 0xf58e5fc913e0 + }, + info: DTVInfo{ + Offset: 0, + Multiplier: 16, + Indirect: 1, + }, + }, + + "musl 1.2.5 / alpine 3.22 / aarch64": { // same as alpine 3.13, oldest on dockerhub + machine: elf.EM_AARCH64, + code: []byte{ + 0x02, 0x00, 0x40, 0xa9, // ldp x2, x0, [x0] + 0x41, 0xd0, 0x3b, 0xd5, // mrs x1, tpidr_el0 + 0x21, 0x80, 0x5f, 0xf8, // ldur x1, [x1, #-8] + 0x21, 0x78, 0x62, 0xf8, // ldr x1, [x1, x2, lsl #3] + 0x20, 0x00, 0x00, 0x8b, // add x0, x1, x0 + 0xc0, 0x03, 0x5f, 0xd6, // ret + }, + info: DTVInfo{ + Offset: -8, + Multiplier: 8, + Indirect: 1, + }, + }, + } + + for name, test := range testCases { + t.Run(name, func(t *testing.T) { + var info DTVInfo + var err error + switch test.machine { + case elf.EM_X86_64: + info, err = extractDTVInfoX86(test.code) + case elf.EM_AARCH64: + info, err = extractDTVInfoARM(test.code) + } + if assert.NoError(t, err) { + assert.Equal(t, test.info, info) + } + }) + } +} + +func TestLibcInfoIsEqual(t *testing.T) { + testCases := map[string]struct { + left LibcInfo + right LibcInfo + expectEqual bool + }{ + "empty libcinfos are equal": { + left: LibcInfo{}, + right: LibcInfo{}, + expectEqual: true, + }, + "nested values are equal": { + left: LibcInfo{ + TSDInfo{ + Offset: 8, + Multiplier: 8, + Indirect: 1, + }, + DTVInfo{ + Offset: -8, + Multiplier: 16, + Indirect: 0, + }, + }, + right: LibcInfo{ + TSDInfo{ + Offset: 8, + Multiplier: 8, + Indirect: 1, + }, + DTVInfo{ + Offset: -8, + Multiplier: 16, + Indirect: 0, + }, + }, + expectEqual: true, + }, + "nested values are not equal": { + left: LibcInfo{ + TSDInfo{}, + DTVInfo{ + Offset: -8, + Multiplier: 16, + Indirect: 0, + }, + }, + right: LibcInfo{ + TSDInfo{ + Offset: 8, + Multiplier: 8, + Indirect: 1, + }, + DTVInfo{}, + }, + expectEqual: false, + }, + } + + for name, test := range testCases { + t.Run(name, func(t *testing.T) { + assert.Equal(t, test.expectEqual, test.left.IsEqual(test.right)) + }) + } +} + +func TestLibcInfoMerge(t *testing.T) { + testCases := map[string]struct { + left LibcInfo + right LibcInfo + expected LibcInfo + }{ + "non-empty TSD values are accumulated from other": { + left: LibcInfo{}, + right: LibcInfo{ + TSDInfo{ + Offset: 8, + Multiplier: 8, + Indirect: 1, + }, + DTVInfo{}, + }, + expected: LibcInfo{ + TSDInfo{ + Offset: 8, + Multiplier: 8, + Indirect: 1, + }, + DTVInfo{}, + }, + }, + "non-empty DTV values are accumulated from other": { + left: LibcInfo{}, + right: LibcInfo{ + TSDInfo{}, + DTVInfo{ + Offset: -8, + Multiplier: 16, + Indirect: 0, + }, + }, + expected: LibcInfo{ + TSDInfo{}, + DTVInfo{ + Offset: -8, + Multiplier: 16, + Indirect: 0, + }, + }, + }, + "non-empty TSD values are accumulated from other, with DTV already set": { + left: LibcInfo{ + TSDInfo{}, + DTVInfo{ + Offset: -8, + Multiplier: 16, + Indirect: 0, + }, + }, + right: LibcInfo{ + TSDInfo{ + Offset: 8, + Multiplier: 8, + Indirect: 1, + }, + DTVInfo{}, + }, + expected: LibcInfo{ + TSDInfo{ + Offset: 8, + Multiplier: 8, + Indirect: 1, + }, + DTVInfo{ + Offset: -8, + Multiplier: 16, + Indirect: 0, + }, + }, + }, + "non-empty DTV values are accumulated from other, with TSD already set": { + left: LibcInfo{ + TSDInfo{ + Offset: 8, + Multiplier: 8, + Indirect: 1, + }, + DTVInfo{}, + }, + right: LibcInfo{ + TSDInfo{}, + DTVInfo{ + Offset: -8, + Multiplier: 16, + Indirect: 0, + }, + }, + expected: LibcInfo{ + TSDInfo{ + Offset: 8, + Multiplier: 8, + Indirect: 1, + }, + DTVInfo{ + Offset: -8, + Multiplier: 16, + Indirect: 0, + }, + }, + }, + + // This is not expected to actually happen, but we want to be clear + // that values already present, if not empty, are kept and not reset + "non-empty values are ignored if already set": { + left: LibcInfo{ + TSDInfo{ + Offset: 8, + Multiplier: 8, + Indirect: 1, + }, + DTVInfo{ + Offset: -8, + Multiplier: 16, + Indirect: 0, + }, + }, + right: LibcInfo{ + TSDInfo{ + Offset: 16, + Multiplier: 16, + Indirect: 0, + }, + DTVInfo{ + Offset: 8, + Multiplier: 16, + Indirect: 1, + }, + }, + expected: LibcInfo{ + TSDInfo{ + Offset: 8, + Multiplier: 8, + Indirect: 1, + }, + DTVInfo{ + Offset: -8, + Multiplier: 16, + Indirect: 0, + }, + }, + }, + } + + for name, test := range testCases { + t.Run(name, func(t *testing.T) { + merged := test.left + merged.Merge(test.right) + assert.True(t, test.expected.IsEqual(merged)) + }) + } +} diff --git a/libc/libc_x86.go b/libc/libc_x86.go index 161deb8a9..7df5cb792 100644 --- a/libc/libc_x86.go +++ b/libc/libc_x86.go @@ -81,3 +81,112 @@ func extractTSDInfoX86(code []byte) (TSDInfo, error) { } return TSDInfo{}, errors.New("could not extract tsdInfo amd") } + +// extractDTVInfoX86 analyzes __tls_get_addr to find the DTV offset from FS base +func extractDTVInfoX86(code []byte) (DTVInfo, error) { + it := amd.NewInterpreterWithCode(code) + + // __tls_get_addr takes a tlsIndex struct in RDI + tlsIndex := it.Regs.Get(amd.RDI) + moduleId := e.Mem8(tlsIndex) + tlsOffset := e.Mem8(e.Add(tlsIndex, e.Imm(8))) + + // Execute until RET + _, err := it.LoopWithBreak(func(op x86asm.Inst) bool { + return op.Op == x86asm.RET + }) + if err != nil { + return DTVInfo{}, err + } + + result := it.Regs.Get(amd.RAX) + + // Capture variables + var ( + dtvOffset = e.NewImmediateCapture("dtvOffset") + entryWidth = e.NewImmediateCapture("entryWidth") + ) + + // Pattern 1: glibc - Direct DTV access + expected := e.Add( + e.Mem8( + e.Add( + e.MemWithSegment8(x86asm.FS, dtvOffset), + e.Multiply(moduleId, entryWidth), + ), + ), + tlsOffset, + ) + + if result.Match(expected) { + return DTVInfo{ + Offset: int16(dtvOffset.CapturedValue() & 0xFFFF), + Multiplier: uint8(entryWidth.CapturedValue()), + Indirect: 0, + }, nil + } + + // Pattern 2: musl - The thread pointer itself might be represented differently + // Since FS:0 is the thread pointer, and DTV is at offset from it + threadPtr := e.MemWithSegment8(x86asm.FS, e.Imm(0)) + dtvPtr := e.Mem8(e.Add(threadPtr, dtvOffset)) + + expected = e.Add( + tlsOffset, + e.Mem8( + e.Add( + dtvPtr, + e.Multiply(moduleId, entryWidth), + ), + ), + ) + + if result.Match(expected) { + return DTVInfo{ + Offset: int16(dtvOffset.CapturedValue() & 0xFFFF), + Multiplier: uint8(entryWidth.CapturedValue()), + Indirect: 1, + }, nil + } + + // Pattern 3: Reverse addition order + expected = e.Add( + e.Mem8( + e.Add( + dtvPtr, + e.Multiply(moduleId, entryWidth), + ), + ), + tlsOffset, + ) + + if result.Match(expected) { + return DTVInfo{ + Offset: int16(dtvOffset.CapturedValue() & 0xFFFF), + Multiplier: uint8(entryWidth.CapturedValue()), + Indirect: 1, + }, nil + } + + // Pattern 4: Maybe the scale is encoded in the memory operand differently + // Try without explicit multiply + expected = e.Add( + tlsOffset, + e.Mem8( + e.Add( + e.Mem8(e.Add(threadPtr, dtvOffset)), + e.Multiply(e.ZeroExtend32(moduleId), entryWidth), + ), + ), + ) + + if result.Match(expected) { + return DTVInfo{ + Offset: int16(dtvOffset.CapturedValue() & 0xFFFF), + Multiplier: uint8(entryWidth.CapturedValue()), + Indirect: 1, + }, nil + } + + return DTVInfo{}, errors.New("could not extract DTV info: no matching pattern found") +} diff --git a/processmanager/execinfomanager/manager.go b/processmanager/execinfomanager/manager.go index 358d425cb..bccc49bd3 100644 --- a/processmanager/execinfomanager/manager.go +++ b/processmanager/execinfomanager/manager.go @@ -198,8 +198,8 @@ func (mgr *ExecutableInfoManager) AddOrIncRef(fileID host.FileID, intervalData = synthesizeIntervalData(ef) } - // Also gather TSD info if applicable. - if libc.IsPotentialTSDDSO(elfRef.FileName()) { + // Also gather Libc info if applicable. + if libc.IsPotentialLibcDSO(elfRef.FileName()) { if ef, errx := elfRef.GetELF(); errx == nil { libcInfo, _ = libc.ExtractLibcInfo(ef) } diff --git a/processmanager/processinfo.go b/processmanager/processinfo.go index f793c57fa..f1efde54c 100644 --- a/processmanager/processinfo.go +++ b/processmanager/processinfo.go @@ -77,20 +77,26 @@ func (pm *ProcessManager) assignLibcInfo(pid libpf.PID, libcInfo *libc.LibcInfo) return } + var newLibcInfo = *libcInfo info, ok := pm.pidToProcessInfo[pid] if !ok { // This is guaranteed not to happen since assignLibcInfo is always called after // pm.updatePidInformation - but to avoid a possible panic we just return here. return } else if info.libcInfo != nil { - return + if info.libcInfo.IsEqual(newLibcInfo) { + return + } else { + info.libcInfo.Merge(newLibcInfo) + newLibcInfo = *info.libcInfo + } } - info.libcInfo = libcInfo + info.libcInfo = &newLibcInfo // Update the tsdInfo to interpreters that are already attached for _, instance := range pm.interpreters[pid] { - if err := instance.UpdateLibcInfo(pm.ebpf, pid, *libcInfo); err != nil { + if err := instance.UpdateLibcInfo(pm.ebpf, pid, newLibcInfo); err != nil { log.Errorf("Failed to update PID %v LibcInfo: %v", pid, err) } diff --git a/processmanager/processinfo_test.go b/processmanager/processinfo_test.go new file mode 100644 index 000000000..8d6647508 --- /dev/null +++ b/processmanager/processinfo_test.go @@ -0,0 +1,82 @@ +package processmanager // import "go.opentelemetry.io/ebpf-profiler/processmanager" + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/ebpf-profiler/interpreter" + "go.opentelemetry.io/ebpf-profiler/libc" + "go.opentelemetry.io/ebpf-profiler/libpf" + "go.opentelemetry.io/ebpf-profiler/util" +) + +type TestInstance struct { + interpreter.InstanceStubs + info libc.LibcInfo +} + +func (ti *TestInstance) UpdateLibcInfo(handler interpreter.EbpfHandler, pid libpf.PID, info libc.LibcInfo) error { + ti.info = info + return nil +} + +func (ti *TestInstance) Detach(handler interpreter.EbpfHandler, pid libpf.PID) error { + return nil +} + +func TestAssignLibcInfoMergesLibcInfo(t *testing.T) { + assert := assert.New(t) + + pid := libpf.PID(1) + odid := util.OnDiskFileIdentifier{ + DeviceID: 1, + InodeNum: 1, + } + + interp := TestInstance{} + + pm := ProcessManager{ + interpreters: map[libpf.PID]map[util.OnDiskFileIdentifier]interpreter.Instance{ + pid: { + odid: &interp, + }, + }, + pidToProcessInfo: map[libpf.PID]*processInfo{ + pid: {}, + }, + } + + libcInfoWithTSD := libc.LibcInfo{ + libc.TSDInfo{ + Offset: 8, + Multiplier: 8, + Indirect: 0, + }, + libc.DTVInfo{}, + } + pm.assignLibcInfo(pid, &libcInfoWithTSD) + + assert.Equal(libcInfoWithTSD, interp.info) + + libcInfoWithDTV := libc.LibcInfo{ + libc.TSDInfo{}, + libc.DTVInfo{ + Offset: -8, + Multiplier: 16, + Indirect: 1, + }, + } + + merged := libcInfoWithTSD + merged.Merge(libcInfoWithDTV) + + pm.assignLibcInfo(pid, &libcInfoWithDTV) + assert.Equal(merged, interp.info) + assert.Equal(libcInfoWithTSD.TSDInfo, interp.info.TSDInfo) + assert.Equal(libcInfoWithDTV.DTVInfo, interp.info.DTVInfo) + + pm.assignLibcInfo(pid, &merged) + assert.Equal(merged, interp.info) + assert.Equal(libcInfoWithTSD.TSDInfo, interp.info.TSDInfo) + assert.Equal(libcInfoWithDTV.DTVInfo, interp.info.DTVInfo) +} diff --git a/support/ebpf/types.h b/support/ebpf/types.h index f6ed5b739..1168a761a 100644 --- a/support/ebpf/types.h +++ b/support/ebpf/types.h @@ -388,6 +388,18 @@ typedef struct TSDInfo { u8 indirect; } TSDInfo; +// DTVInfo contains data needed to read Thread Local Storage (TLS) values, which +// are located using the Dynamic Thread Vector (DTV) +typedef struct DTVInfo { + // Offset is the offset of DTV from FS base (or from thread pointer) + s16 offset; + // Multiplier is the size of each DTV entry in bytes + // Typically 8 bytes on 64bit musl and 16 bytes on 64bit glibc + u8 multiplier; + // Indirect is 0 if DTV is at FS+offset, 1 if at [FS+0]+offset + u8 indirect; +} DTVInfo; + // DotnetProcInfo is a container for the data needed to build stack trace for a dotnet process. typedef struct DotnetProcInfo { u32 version; diff --git a/support/types.go b/support/types.go index 36a5437ec..9c4e115fe 100644 --- a/support/types.go +++ b/support/types.go @@ -152,6 +152,11 @@ type TSDInfo struct { Multiplier uint8 Indirect uint8 } +type DTVInfo struct { + Offset int16 + Multiplier uint8 + Indirect uint8 +} type Trace struct { Pid uint32 Tid uint32 diff --git a/support/types_def.go b/support/types_def.go index d943cba91..12ac64a27 100644 --- a/support/types_def.go +++ b/support/types_def.go @@ -121,6 +121,7 @@ type StackDeltaPageInfo C.StackDeltaPageInfo type StackDeltaPageKey C.StackDeltaPageKey type SystemAnalysis C.SystemAnalysis type TSDInfo C.TSDInfo +type DTVInfo C.DTVInfo type Trace C.Trace type UnwindInfo C.UnwindInfo