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
24 changes: 18 additions & 6 deletions pkg/processtree/container/container_processtree.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
Comment on lines +75 to +77

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.

The KubernetesMode=true behavior described here is accurate, but it is pre-existing and outside this fix’s scope. This PR fixes standalone host-agent mode (KubernetesMode=false), which is the configuration used by the host-agent deployment example and host-agent integration suite; the new manager regression exercises that mode. In Kubernetes mode, handleProcfsEvent deliberately refuses to create host nodes (including PID 1), and handleExecEvent retains its existing reparenting policy. Those paths were not changed by this PR. Missing cached ancestry still returns an error so the caller can use its existing procfs fallback. Seeding PID 1 and retaining complete host ancestry in Kubernetes mode would change that caching/reparenting policy and warrants a separate change. I clarified the standalone-mode scope in the PR description; no code change is needed for this finding.

}
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
Expand All @@ -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)
}

Expand Down Expand Up @@ -199,15 +207,19 @@ 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
}

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 {
Expand Down
55 changes: 55 additions & 0 deletions pkg/processtree/container/container_processtree_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package containerprocesstree

import (
"fmt"
"testing"
"time"

Expand All @@ -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
Expand All @@ -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)

Expand Down
47 changes: 47 additions & 0 deletions pkg/processtree/process_tree_manager_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package processtree

import (
"fmt"
"os"
"testing"
"time"
Expand All @@ -9,13 +10,59 @@ 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"
"github.com/stretchr/testify/assert"
"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.
Expand Down
Loading