Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 72 additions & 1 deletion interpreter/ruby/ruby.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package ruby // import "go.opentelemetry.io/ebpf-profiler/interpreter/ruby"

import (
"debug/elf"
"encoding/binary"
"errors"
"fmt"
Expand All @@ -21,6 +22,7 @@ import (
"github.com/elastic/go-freelru"

"go.opentelemetry.io/ebpf-profiler/interpreter"
"go.opentelemetry.io/ebpf-profiler/libc"
"go.opentelemetry.io/ebpf-profiler/libpf"
"go.opentelemetry.io/ebpf-profiler/libpf/pfelf"
"go.opentelemetry.io/ebpf-profiler/libpf/pfunsafe"
Expand Down Expand Up @@ -123,6 +125,13 @@ type rubyData struct {
// Address to the ruby_current_ec variable in TLS, as an offset from tpbase
currentEcTpBaseTlsOffset libpf.Address

// For DTV-based TLS access: offset of ruby_current_ec within its TLS block
currentEcTlsOffset libpf.Address

// For DTV-based TLS access: ELF offset where the TLS module ID is stored
// (from DTPMOD64 relocation, the actual module ID is written by the linker at load time)
tlsModuleIdOffset libpf.Address

// Address to global symbols, for id to string mappings
globalSymbolsAddr libpf.Address
// version of the currently used Ruby interpreter.
Expand Down Expand Up @@ -301,11 +310,21 @@ func (r *rubyData) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, bias libp
tlsOffset = rm.Uint64(bias + r.currentEcTpBaseTlsOffset + 8)
}

// For DTV-based access: read the actual module ID from process memory.
// The linker writes the module ID at the relocation offset at load time.
var modID uint32
if r.tlsModuleIdOffset != 0 {
modID = uint32(rm.Uint64(bias + r.tlsModuleIdOffset))
log.Debugf("Ruby TLS module ID: %d", modID)
}

cdata := support.RubyProcInfo{
Version: r.version,

Current_ctx_ptr: uint64(r.currentCtxPtr + bias),
Current_ec_tpbase_tls_offset: tlsOffset,
Current_ec_tls_offset: uint64(r.currentEcTlsOffset),
Tls_module_id: modID,

Vm_stack: r.vmStructs.execution_context_struct.vm_stack,
Vm_stack_size: r.vmStructs.execution_context_struct.vm_stack_size,
Expand Down Expand Up @@ -345,6 +364,7 @@ func (r *rubyData) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, bias libp
return &rubyInstance{
r: r,
rm: rm,
procInfo: &cdata,
globalSymbolsAddr: r.globalSymbolsAddr + bias,
addrToString: addrToString,
memPool: sync.Pool{
Expand Down Expand Up @@ -381,6 +401,12 @@ type rubyIseq struct {
type rubyInstance struct {
interpreter.InstanceStubs

// procInfo stores the eBPF proc data for re-insertion when UpdateLibcInfo provides DTVInfo
procInfo *support.RubyProcInfo

// dtvInfoInserted tracks whether we have already updated procInfo with DTVInfo
dtvInfoInserted bool

// Ruby symbolization metrics
successCount atomic.Uint64
failCount atomic.Uint64
Expand Down Expand Up @@ -408,6 +434,33 @@ func (r *rubyInstance) Detach(ebpf interpreter.EbpfHandler, pid libpf.PID) error
return ebpf.DeleteProcData(libpf.Ruby, pid)
}

// UpdateLibcInfo is called when libc introspection data becomes available.
// Ruby uses this to receive DTVInfo for DTV-based TLS access to ruby_current_ec
// when TLSDESC relocations are unavailable.
func (r *rubyInstance) UpdateLibcInfo(ebpf interpreter.EbpfHandler, pid libpf.PID,
libcInfo libc.LibcInfo) error {
// Only need DTVInfo if we're using DTV-based access (have a module ID but no TLSDESC offset)
if r.procInfo.Tls_module_id == 0 {
return nil
}
if !libcInfo.HasDTVInfo() {
// DTV info not available yet (may arrive from a different DSO)
return nil
}
if r.dtvInfoInserted {
return nil
}

r.procInfo.Dtv_info = libcInfo.DTVInfo
if err := ebpf.UpdateProcData(libpf.Ruby, pid, unsafe.Pointer(r.procInfo)); err != nil {
return err
}
r.dtvInfoInserted = true
log.Debugf("Ruby: updated proc data with DTVInfo (offset=%d, multiplier=%d, indirect=%d)",
libcInfo.DTVInfo.Offset, libcInfo.DTVInfo.Multiplier, libcInfo.DTVInfo.Indirect)
return nil
}

// readRubyArrayDataPtr obtains the data pointer of a Ruby array (RArray).
//
// https://github.com/ruby/ruby/blob/95aff2146/include/ruby/internal/core/rarray.h#L87
Expand Down Expand Up @@ -1363,11 +1416,29 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr
log.Warnf("failed to locate TLS descriptor: %v", err)
}

log.Debugf("Discovered EC tls tpbase offset %x, fallback ctx %x, interp ranges: %v, global symbols: %x", currentEcTpBaseTlsOffset, currentCtxPtr, interpRanges, globalSymbols)
// Look for DTPMOD64 relocation to find the TLS module ID offset.
// This is used for DTV-based TLS access when TLSDESC is unavailable.
var tlsModuleIdOffset libpf.Address
if err = ef.VisitRelocations(func(r pfelf.ElfReloc, _ string) bool {
log.Debugf("Found DTPMOD64 relocation at offset %x", r.Off)
tlsModuleIdOffset = libpf.Address(r.Off)
return false
}, func(rela pfelf.ElfReloc) bool {
ty := rela.Info & 0xffff
return (ef.Machine == elf.EM_AARCH64 && elf.R_AARCH64(ty) == elf.R_AARCH64_TLS_DTPMOD64) ||
(ef.Machine == elf.EM_X86_64 && elf.R_X86_64(ty) == elf.R_X86_64_DTPMOD64)
}); err != nil {
log.Warnf("failed to find DTPMOD64 relocation: %v", err)
}

log.Debugf("Discovered EC tls tpbase offset %x, dtpmod offset %x, fallback ctx %x, interp ranges: %v, global symbols: %x",
currentEcTpBaseTlsOffset, tlsModuleIdOffset, currentCtxPtr, interpRanges, globalSymbols)

rid := &rubyData{
version: version,
currentEcTpBaseTlsOffset: libpf.Address(currentEcTpBaseTlsOffset),
currentEcTlsOffset: libpf.Address(currentEcSymbolAddress),
tlsModuleIdOffset: tlsModuleIdOffset,
currentCtxPtr: libpf.Address(currentCtxPtr),
hasGlobalSymbols: globalSymbols != 0,
globalSymbolsAddr: libpf.Address(globalSymbols),
Expand Down
22 changes: 17 additions & 5 deletions libpf/pfelf/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,19 @@ type ElfReloc *elf.Rela64
// for the TLS symbol, as well as a best-effort string for the symbol's name
// it continues until the visitor returns false
func (f *File) VisitTLSRelocations(visitor func(ElfReloc, string) bool) error {
checkFunc := func(rela ElfReloc) bool {
ty := rela.Info & 0xffff
return (f.Machine == elf.EM_AARCH64 && elf.R_AARCH64(ty) == elf.R_AARCH64_TLSDESC) ||
(f.Machine == elf.EM_X86_64 && elf.R_X86_64(ty) == elf.R_X86_64_TLSDESC)
}
return f.VisitRelocations(visitor, checkFunc)
}

// VisitRelocations visits all relocations matching checkFunc and provides the
// relocation and symbol name to the visitor. The visitor can return false to
// stop iteration. The checkFunc filters which relocation types to process.
func (f *File) VisitRelocations(visitor func(ElfReloc, string) bool,
checkFunc func(ElfReloc) bool) error {
var err error
if err = f.LoadSections(); err != nil {
return err
Expand All @@ -648,7 +661,7 @@ func (f *File) VisitTLSRelocations(visitor func(ElfReloc, string) bool) error {
section := &f.Sections[i]
// NOTE: SHT_REL is not relevant for the archs that we care about
if section.Type == elf.SHT_RELA {
cont, err := f.visitTLSDescriptorsForSection(visitor, section)
cont, err := f.visitRelocationsForSection(visitor, checkFunc, section)
if err != nil {
return err
}
Expand All @@ -661,7 +674,8 @@ func (f *File) VisitTLSRelocations(visitor func(ElfReloc, string) bool) error {
return nil
}

func (f *File) visitTLSDescriptorsForSection(visitor func(ElfReloc, string) bool,
func (f *File) visitRelocationsForSection(visitor func(ElfReloc, string) bool,
checkRelocation func(ElfReloc) bool,
relaSection *Section,
) (bool, error) {
if relaSection.Link > uint32(len(f.Sections)) {
Expand Down Expand Up @@ -704,9 +718,7 @@ func (f *File) visitTLSDescriptorsForSection(visitor func(ElfReloc, string) bool
for i := 0; i < len(relaData); i += relaSz {
rela := (*elf.Rela64)(unsafe.Pointer(&relaData[i]))

ty := rela.Info & 0xffff
if !(f.Machine == elf.EM_AARCH64 && elf.R_AARCH64(ty) == elf.R_AARCH64_TLSDESC) &&
!(f.Machine == elf.EM_X86_64 && elf.R_X86_64(ty) == elf.R_X86_64_TLSDESC) {
if !checkRelocation(rela) {
continue
}

Expand Down
5 changes: 4 additions & 1 deletion metrics/ids.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions metrics/metrics.json
Original file line number Diff line number Diff line change
Expand Up @@ -2063,5 +2063,12 @@
"name": "UnwindRubyErrCmeMaxEp",
"field": "bpf.ruby.errors.read_cme_max_ep",
"id": 285
},
{
"description": "Number of failures to read TLS variables via the DTV",
"type": "counter",
"name": "UnwindErrBadDTVRead",
"field": "bpf.errors.bad_dtv_read",
"id": 286
}
]
24 changes: 24 additions & 0 deletions support/ebpf/ruby_tracer.ebpf.c
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,30 @@ static EBPF_INLINE int unwind_ruby(struct pt_regs *ctx)
&current_ctx_addr, sizeof(current_ctx_addr), (void *)(tls_current_ec_addr))) {
goto exit;
}
DEBUG_PRINT("ruby: EC from TLS: 0x%llx", (u64)current_ctx_addr);
} else if (rubyinfo->tls_module_id != 0 && rubyinfo->dtv_info.multiplier != 0) {
// DTV-based TLS access: traverse the Dynamic Thread Vector to find ruby_current_ec.
// Used when TLSDESC relocations are unavailable (e.g. some musl setups,
// or when TLS is allocated dynamically).
u64 tsd_base;
if (tsd_get_base((void **)&tsd_base) != 0) {
DEBUG_PRINT("ruby: failed to get TSD base for DTV lookup");
error = ERR_RUBY_READ_TSD_BASE;
goto exit;
}

void *ec_ptr;
if (dtv_read(
&rubyinfo->dtv_info,
(void *)tsd_base,
rubyinfo->tls_module_id,
rubyinfo->current_ec_tls_offset,
&ec_ptr)) {
DEBUG_PRINT("ruby: failed to read EC from DTV");
goto exit;
}
current_ctx_addr = ec_ptr;
DEBUG_PRINT("ruby: EC from DTV: 0x%llx", (u64)current_ctx_addr);
} else if (rubyinfo->version >= 0x30000) {
void *single_main_ractor = NULL;
if (bpf_probe_read_user(
Expand Down
Binary file modified support/ebpf/tracer.ebpf.amd64
Binary file not shown.
Binary file modified support/ebpf/tracer.ebpf.arm64
Binary file not shown.
51 changes: 51 additions & 0 deletions support/ebpf/tsd.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,57 @@ tsd_read(const TSDInfo *tsi, const void *tsd_base, int key, void **out)
return -1;
}

// dtv_read reads a TLS variable by traversing the Dynamic Thread Vector (DTV).
// The DTV is an array of pointers to per-module TLS blocks, indexed by TLS module ID.
// On x86_64 the default TLS dialect uses General Dynamic (GD) relocations
// (R_X86_64_DTPMOD64) rather than TLSDESC, so the DTV path is the primary
// mechanism for resolving thread-local variables in shared libraries.
// This path is also needed on other platforms when TLSDESC is unavailable.
//
// Parameters:
// dtvi: DTVInfo extracted from __tls_get_addr disassembly (offset, multiplier, indirect)
// tsd_base: thread pointer base (from tsd_get_base)
// module_id: TLS module ID for the target DSO (from DTPMOD64 relocation)
// tls_offset: offset of the variable within its module's TLS block
// out: pointer to store the result
static inline EBPF_INLINE int
dtv_read(const DTVInfo *dtvi, const void *tsd_base, u32 module_id, u64 tls_offset, void **out)
{
const void *dtv_ptr = tsd_base + dtvi->offset;
if (dtvi->indirect) {
// DTV pointer is behind an indirection (e.g. musl: [TP+0] -> dtv_base)
if (bpf_probe_read_user(&dtv_ptr, sizeof(dtv_ptr), dtv_ptr)) {
goto err;
}
}

// Index into the DTV to find this module's TLS block base address.
// DTV layout: [generation, module1_block, module2_block, ...]
// Entry size varies: 8 bytes (musl) or 16 bytes (glibc).
void *tls_block;
u64 dtv_entry_offset = (u64)module_id * dtvi->multiplier;
if (bpf_probe_read_user(&tls_block, sizeof(tls_block), (void *)(dtv_ptr + dtv_entry_offset))) {
goto err;
}

// Read the actual TLS variable at tls_block + tls_offset
if (bpf_probe_read_user(out, sizeof(*out), tls_block + tls_offset)) {
goto err;
}

DEBUG_PRINT(
"readDTV module %d, entry_offset 0x%llx, tls_offset 0x%llx",
module_id,
(unsigned long long)dtv_entry_offset,
(unsigned long long)tls_offset);
return 0;

err:
DEBUG_PRINT("Failed to read TLS via DTV from 0x%lx", (unsigned long)dtv_ptr);
increment_metric(metricID_UnwindErrBadDTVRead);
return -1;
}

// tsd_get_base looks up the base address for TSD variables (TPBASE).
static inline EBPF_INLINE int tsd_get_base(void **tsd_base)
{
Expand Down
10 changes: 10 additions & 0 deletions support/ebpf/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,9 @@ enum {
// number of failed attempts to read a CME by exceeding max EP checks
metricID_UnwindRubyErrCmeMaxEp,

// number of failures to read TLS variables via the DTV
metricID_UnwindErrBadDTVRead,

//
// Metric IDs above are for counters (cumulative values)
//
Expand Down Expand Up @@ -471,6 +474,13 @@ typedef struct RubyProcInfo {
// tls_offset holds TLS base + ruby_current_ec tls symbol, as an offset from tpbase
u64 current_ec_tpbase_tls_offset;

// DTV-based TLS access for ruby_current_ec (fallback when TLSDESC unavailable)
DTVInfo dtv_info;
// Offset of ruby_current_ec within its module's TLS block
u64 current_ec_tls_offset;
// Runtime TLS module ID for libruby.so (from DTPMOD64 relocation, written by linker)
u32 tls_module_id;

// current_ctx_ptr holds the address of the symbol ruby_current_execution_context_ptr.
u64 current_ctx_ptr;

Expand Down
8 changes: 6 additions & 2 deletions support/types.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions support/types_def.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,4 +307,5 @@ var MetricsTranslation = []metrics.MetricID{
C.metricID_UnwindRubyErrReadSvar: metrics.IDUnwindRubyErrReadSvar,
C.metricID_UnwindRubyErrReadRbasicFlags: metrics.IDUnwindRubyErrReadRbasicFlags,
C.metricID_UnwindRubyErrCmeMaxEp: metrics.IDUnwindRubyErrCmeMaxEp,
C.metricID_UnwindErrBadDTVRead: metrics.IDUnwindErrBadDTVRead,
}
Loading
Loading