Skip to content
Merged
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
58 changes: 58 additions & 0 deletions process/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
Expand Down Expand Up @@ -181,6 +183,62 @@ func extractContainerID(pid libpf.PID) (libpf.String, error) {
return parseContainerID(cgroupFile), nil
}

// CgroupRootInode returns the inode of /proc/<pid>/root/sys/fs/cgroup, which identifies
// the cgroup namespace root visible to the given process, unaffected by namespace masking.
func CgroupRootInode(pid libpf.PID) (uint64, error) {
var st unix.Stat_t
if err := unix.Stat(fmt.Sprintf("/proc/%d/root/sys/fs/cgroup", pid), &st); err != nil {
return 0, err
}
return st.Ino, nil
}

// DetectSelfContainerIDViaInode detects the current process's container ID by matching
// cgroup directory inodes. When the process runs in a private cgroup namespace (cgroup v2),
// /proc/self/cgroup returns a path relative to the namespace root (e.g. "0::/"), making it
// impossible to extract the container ID via the standard path. However, stat("/sys/fs/cgroup")
// returns the inode of the process's actual cgroup directory on the host, unaffected by
// namespace masking. This function walks the host's cgroup tree (via
// /proc/1/root/sys/fs/cgroup) to find the directory whose inode matches, then extracts
// the container ID from its path.
func DetectSelfContainerIDViaInode() (libpf.String, uint64, error) {
const hostCgroupRoot = "/proc/1/root/sys/fs/cgroup"

var selfStat unix.Stat_t
if err := unix.Stat("/sys/fs/cgroup", &selfStat); err != nil {
return libpf.NullString, 0, fmt.Errorf("failed to stat /sys/fs/cgroup: %w", err)
}
selfIno := selfStat.Ino

var matched libpf.String
err := filepath.WalkDir(hostCgroupRoot, func(path string, d fs.DirEntry, err error) error {
if err != nil {
if d == nil {
return err // root is inaccessible
}
return nil // skip inaccessible subdirectories
}
Comment on lines +215 to +220
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if /proc/1/root/sys/fs/cgroup can't be walked? Seems like we'll return nil and swallow an error that we should probably be logging?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, thanks for flagging this. Fixed in 4cd3c0c

if !d.IsDir() {
return nil
}
var st unix.Stat_t
if err := unix.Stat(path, &st); err != nil {
return nil
}
if st.Ino == selfIno {
if parts := expContainerID.FindStringSubmatch(path); len(parts) == 2 {
matched = libpf.Intern(parts[1])
}
return filepath.SkipAll
}
return nil
})
if err != nil {
return libpf.NullString, 0, fmt.Errorf("failed to walk host cgroup tree: %w", err)
}
Comment on lines +236 to +238
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this logic ever be triggered?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can now be triggered in the case added by 4cd3c0c.

return matched, selfIno, nil
}

func trimMappingPath(path string) string {
// Trim the deleted indication from the path.
// See path_with_deleted in linux/fs/d_path.c
Expand Down
8 changes: 8 additions & 0 deletions processmanager/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"go.opentelemetry.io/ebpf-profiler/metrics"
"go.opentelemetry.io/ebpf-profiler/nativeunwind"
"go.opentelemetry.io/ebpf-profiler/periodiccaller"
"go.opentelemetry.io/ebpf-profiler/process"
pmebpf "go.opentelemetry.io/ebpf-profiler/processmanager/ebpfapi"
eim "go.opentelemetry.io/ebpf-profiler/processmanager/execinfomanager"
"go.opentelemetry.io/ebpf-profiler/reporter"
Expand Down Expand Up @@ -94,6 +95,11 @@ func New(ctx context.Context, includeTracers types.IncludedTracers, monitorInter

interpreters := make(map[libpf.PID]map[util.OnDiskFileIdentifier]interpreter.Instance)

selfContainerID, selfCgroupIno, err := process.DetectSelfContainerIDViaInode()
if err != nil {
log.Debugf("Failed to detect self container ID via inode: %v", err)
}

pm := &ProcessManager{
interpreterTracerEnabled: em.NumInterpreterLoaders() > 0,
eim: em,
Expand All @@ -108,6 +114,8 @@ func New(ctx context.Context, includeTracers types.IncludedTracers, monitorInter
metricsAddSlice: metrics.AddSlice,
filterErrorFrames: filterErrorFrames,
includeEnvVars: includeEnvVars,
selfCgroupIno: selfCgroupIno,
selfContainerID: selfContainerID,
}

collectInterpreterMetrics(ctx, pm, monitorInterval)
Expand Down
22 changes: 21 additions & 1 deletion processmanager/processinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,15 +129,34 @@ func (pm *ProcessManager) getPidInformation(pid libpf.PID, pr process.Process,
return nil
}

meta := pr.GetProcessMeta(process.MetaConfig{IncludeEnvVars: pm.includeEnvVars})
pm.fillSelfContainerID(pid, &meta)
info := &processInfo{
meta: pr.GetProcessMeta(process.MetaConfig{IncludeEnvVars: pm.includeEnvVars}),
meta: meta,
libcInfo: nil,
}
pm.pidToProcessInfo[pid] = info
pm.pidPageToMappingInfoSize++
return info
}

// fillSelfContainerID sets the container ID on meta if the process has the same cgroup
// directory root as the profiler and the standard cgroup-based detection returned no result.
func (pm *ProcessManager) fillSelfContainerID(pid libpf.PID, meta *process.ProcessMeta) {
if meta.ContainerID != libpf.NullString || pm.selfContainerID == libpf.NullString {
return
}
ino, err := process.CgroupRootInode(pid)
if err != nil {
return
}
if ino == pm.selfCgroupIno {
meta.ContainerID = pm.selfContainerID
} else {
log.Debugf("Process %d cgroup inode (%d) doesn't match profiler (%d)", pid, ino, pm.selfCgroupIno)
}
Comment thread
theomagellan marked this conversation as resolved.
}

// assignInterpreter will update the interpreters maps with given interpreter.Instance.
// Caller is responsible to hold pm.mu write lock to avoid race conditions.
func (pm *ProcessManager) assignInterpreter(pid libpf.PID, key util.OnDiskFileIdentifier,
Expand Down Expand Up @@ -648,6 +667,7 @@ func (pm *ProcessManager) SynchronizeProcess(pr process.Process) {
var meta process.ProcessMeta
if updateProcessMeta {
meta = pr.GetProcessMeta(process.MetaConfig{IncludeEnvVars: pm.includeEnvVars})
pm.fillSelfContainerID(pid, &meta)
}

// Sort and publish the new mappings and meta
Expand Down
10 changes: 10 additions & 0 deletions processmanager/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,16 @@ type ProcessManager struct {

// includeEnvVars holds a list of env vars that should be captured from processes
includeEnvVars libpf.Set[string]

// selfCgroupIno is the inode of the profiler's cgroup directory
// (stat("/sys/fs/cgroup")). Used to identify processes whose cgroup root
// matches the profiler's, which need the selfContainerID fallback.
selfCgroupIno uint64

// selfContainerID is the profiler's own container ID, detected once at startup.
// Used as a fallback when /proc/<pid>/cgroup yields no container ID for processes
// that share the profiler's cgroup directory (e.g., private cgroup namespace).
selfContainerID libpf.String
Comment on lines +120 to +125
Copy link
Copy Markdown
Contributor

@rogercoll rogercoll Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit (no need to be applied): These seem to be constant across the whole execution, one option would be replacing it with a closure that captures the values.

Suggested change
selfCgroupIno uint64
// selfContainerID is the profiler's own container ID, detected once at startup.
// Used as a fallback when /proc/<pid>/cgroup yields no container ID for processes
// that share the profiler's cgroup directory (e.g., private cgroup namespace).
selfContainerID libpf.String
fillContainerIDFallback func(pid libpf.PID, meta *process.ProcessMeta)

}

// Mapping represents an executable memory mapping of a process.
Expand Down
Loading