diff --git a/pkg/processtree/container/container_processtree.go b/pkg/processtree/container/container_processtree.go index b91c752e4f..c2089ef844 100644 --- a/pkg/processtree/container/container_processtree.go +++ b/pkg/processtree/container/container_processtree.go @@ -9,6 +9,7 @@ import ( containercollection "github.com/inspektor-gadget/inspektor-gadget/pkg/container-collection" "github.com/kubescape/go-logger" "github.com/kubescape/go-logger/helpers" + "github.com/kubescape/node-agent/pkg/utils" "github.com/prometheus/procfs" ) @@ -67,10 +68,17 @@ func (c *containerProcessTreeImpl) GetPidBranch(containerID string, targetPID ui return armotypes.Process{}, fmt.Errorf("container %s not found", containerID) } - // Find the process node for the shim PID - shimNode, ok := fullTree.Load(shimPID) + // The host pseudo-container has no shim: PID 1's parent is PID 0, + // which is not a process-tree node. Validate ancestry against PID 1, + // but keep PID 0 as the exclusive boundary so the branch includes init. + rootPID, rootKind := shimPID, "shim" + if utils.IsHost(containerID) { + rootPID, rootKind = 1, "host init" + shimPID = 0 + } + rootNode, ok := fullTree.Load(rootPID) if !ok { - return armotypes.Process{}, fmt.Errorf("shim process %d not found in process tree", shimPID) + return armotypes.Process{}, fmt.Errorf("%s process %d not found in process tree", rootKind, rootPID) } // Find the target process node @@ -79,8 +87,8 @@ func (c *containerProcessTreeImpl) GetPidBranch(containerID string, targetPID ui return armotypes.Process{}, fmt.Errorf("target process %d not found in process tree", targetPID) } - // Check if the target PID is within the shim's subtree - if !c.isProcessInSubtree(targetNode, shimNode, fullTree) { + // Check if the target PID is within the container or host root's subtree. + if !c.isProcessInSubtree(targetNode, rootNode, fullTree) { return armotypes.Process{}, fmt.Errorf("target process %d is not within container %s subtree", targetPID, containerID) } @@ -199,6 +207,7 @@ func (c *containerProcessTreeImpl) isProcessInSubtree(targetNode, rootNode *armo // buildBranchToShim builds a process tree branch from targetNode up to (but not including) shimPID // This creates a new process tree containing only the nodes along the path from target to shim +// A zero shimPID includes the host init node, whose PPID is zero. func (c *containerProcessTreeImpl) buildBranchToShim(targetNode *armotypes.Process, shimPID uint32, fullTree *maps.SafeMap[uint32, *armotypes.Process]) *armotypes.Process { if targetNode == nil { return nil @@ -206,8 +215,11 @@ func (c *containerProcessTreeImpl) buildBranchToShim(targetNode *armotypes.Proce pathNodes := make([]*armotypes.Process, 0, 8) current := targetNode - for current.PPID != 0 { + for { pathNodes = append(pathNodes, current) + if current.PPID == 0 { + break + } parent, ok := fullTree.Load(current.PPID) if !ok { diff --git a/pkg/processtree/container/container_processtree_test.go b/pkg/processtree/container/container_processtree_test.go index d0273ed1a7..a1696b5a84 100644 --- a/pkg/processtree/container/container_processtree_test.go +++ b/pkg/processtree/container/container_processtree_test.go @@ -1,6 +1,7 @@ package containerprocesstree import ( + "fmt" "testing" "time" @@ -9,6 +10,7 @@ import ( containercollection "github.com/inspektor-gadget/inspektor-gadget/pkg/container-collection" eventtypes "github.com/inspektor-gadget/inspektor-gadget/pkg/types" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // Helper function to create a SafeMap from regular map data @@ -28,6 +30,59 @@ func TestNewContainerProcessTree(t *testing.T) { var _ ContainerProcessTree = cpt } +func TestContainerProcessTreeImpl_GetPidBranch_Host(t *testing.T) { + for _, targetPID := range []uint32{1, 100, 101} { + t.Run(fmt.Sprint(targetPID), func(t *testing.T) { + cpt := NewContainerProcessTree().(*containerProcessTreeImpl) + cpt.containerIdToShimPid[armotypes.HostContainerID] = 0 + fullTree := createSafeMapFromData(map[uint32]*armotypes.Process{ + 1: {PID: 1, Comm: "systemd", Path: "/usr/lib/systemd/systemd"}, + 100: {PID: 100, PPID: 1, Comm: "sh", Path: "/usr/bin/sh"}, + 101: {PID: 101, PPID: 100, Comm: "ps", Path: "/usr/bin/ps", Cmdline: "ps aux"}, + 102: {PID: 102, PPID: 100, Comm: "sibling"}, + }) + + branch, err := cpt.GetPidBranch(armotypes.HostContainerID, targetPID, fullTree) + require.NoError(t, err) + require.Equal(t, uint32(1), branch.PID) + current := &branch + for current.PID != targetPID { + require.Len(t, current.ChildrenMap, 1, "only target ancestry belongs in the branch") + for _, child := range current.ChildrenMap { + current = child + } + } + require.Empty(t, current.ChildrenMap) + cached, ok := fullTree.Load(targetPID) + require.True(t, ok) + assert.Equal(t, cached.Path, current.Path) + assert.Equal(t, cached.Cmdline, current.Cmdline) + current.Path = "changed-copy" + assert.NotEqual(t, current.Path, cached.Path, "branch must not mutate cached nodes") + }) + } +} + +func TestContainerProcessTreeImpl_GetPidBranch_HostMissingAncestry(t *testing.T) { + for _, tc := range []struct { + name string + nodes map[uint32]*armotypes.Process + }{ + {"missing init", map[uint32]*armotypes.Process{100: {PID: 100, PPID: 1}}}, + {"missing target", map[uint32]*armotypes.Process{1: {PID: 1}}}, + {"missing parent", map[uint32]*armotypes.Process{1: {PID: 1}, 100: {PID: 100, PPID: 99}}}, + {"outside init subtree", map[uint32]*armotypes.Process{1: {PID: 1}, 2: {PID: 2}, 100: {PID: 100, PPID: 2}}}, + } { + t.Run(tc.name, func(t *testing.T) { + cpt := NewContainerProcessTree().(*containerProcessTreeImpl) + cpt.containerIdToShimPid[armotypes.HostContainerID] = 0 + branch, err := cpt.GetPidBranch(armotypes.HostContainerID, 100, createSafeMapFromData(tc.nodes)) + require.Error(t, err, "missing ancestry must remain eligible for caller fallback") + assert.Zero(t, branch.PID) + }) + } +} + func TestContainerProcessTreeImpl_ContainerCallback_AddContainer(t *testing.T) { cpt := NewContainerProcessTree().(*containerProcessTreeImpl) diff --git a/pkg/processtree/process_tree_manager_test.go b/pkg/processtree/process_tree_manager_test.go index ca4d039197..4384ee73ef 100644 --- a/pkg/processtree/process_tree_manager_test.go +++ b/pkg/processtree/process_tree_manager_test.go @@ -1,6 +1,7 @@ package processtree import ( + "fmt" "os" "testing" "time" @@ -9,6 +10,7 @@ import ( eventtypes "github.com/inspektor-gadget/inspektor-gadget/pkg/types" "github.com/kubescape/node-agent/pkg/config" "github.com/kubescape/node-agent/pkg/ebpf/events" + treeconfig "github.com/kubescape/node-agent/pkg/processtree/config" containerprocesstree "github.com/kubescape/node-agent/pkg/processtree/container" processtreecreator "github.com/kubescape/node-agent/pkg/processtree/creator" "github.com/kubescape/node-agent/pkg/utils" @@ -16,6 +18,51 @@ import ( "github.com/stretchr/testify/require" ) +func TestManager_GetContainerProcessTree_HostRetainsExitedProcess(t *testing.T) { + cfg := config.Config{ExitCleanup: treeconfig.ExitCleanupConfig{ + MaxPendingExits: 100, CleanupInterval: time.Hour, CleanupDelay: 5 * time.Minute, + }} + containerTree := containerprocesstree.NewContainerProcessTree() + creator := processtreecreator.NewProcessTreeCreator(containerTree, cfg) + mgr := NewProcessTreeManager(creator, containerTree, cfg) + containerTree.ContainerCallback(containercollection.PubSubEvent{ + Type: containercollection.EventTypeAddContainer, + Container: &containercollection.Container{ + Runtime: containercollection.RuntimeMetadata{ + BasicRuntimeMetadata: eventtypes.BasicRuntimeMetadata{ContainerID: "host", ContainerPID: 1}, + }, + }, + }) + require.NoError(t, mgr.ReportEvent(utils.ProcfsEventType, &events.ProcfsEvent{ + PID: 1, Comm: "systemd", Path: "/usr/lib/systemd/systemd", ContainerID: "host", + })) + + // A synthetic PID above Linux's PID limit ensures procfs cannot supply the + // executable or arguments. Feed exec then exit through the real manager. + const pid uint32 = 1 << 30 + require.NoError(t, mgr.ReportEvent(utils.ExecveEventType, &utils.StructEvent{ + ContainerID: "host", Pid: pid, Ppid: 1, Comm: "ps", + ExePath: "/usr/bin/ps", Args: []string{"ps", "aux"}, Timestamp: time.Now().UnixNano(), + })) + require.NoError(t, mgr.ReportEvent(utils.ExitEventType, &utils.StructEvent{ + ContainerID: "host", Pid: pid, Ppid: 1, Timestamp: time.Now().UnixNano(), + })) + _, err := os.Stat(fmt.Sprintf("/proc/%d", pid)) + require.True(t, os.IsNotExist(err)) + + for _, useCache := range []bool{false, true} { + branch, err := mgr.GetContainerProcessTree("host", pid, useCache) + require.NoError(t, err) + require.Equal(t, uint32(1), branch.PID) + require.Len(t, branch.ChildrenMap, 1) + for _, child := range branch.ChildrenMap { + assert.Equal(t, pid, child.PID) + assert.Equal(t, "/usr/bin/ps", child.Path) + assert.Equal(t, "ps aux", child.Cmdline) + } + } +} + // TestManager_GetProcessBootTimeNs exercises the accessor that the network-stream // attribution work calls to build a per-connection process reference: a procfs // event reported through the manager must surface its boot-relative start time.