Repository navigation
fix(processtree): re-read the parent from procfs for unknown host parents #1063
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
matthyx
wants to merge
5
commits into
main
Choose a base branch
from
fix/processtree-runc-parent-race
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
88365f0
fix(processtree): re-read the parent from procfs when an exec's paren…
matthyx ef59d15
fix(processtree): verify process identity before adopting a re-read p…
matthyx 6cdf49a
fix(processtree): keep re-read parent consistent across successive de…
matthyx 4eb7f03
fix(processtree): check ancestors against the child exec time and reu…
matthyx 8c6f618
test(processtree): give the child a valid exec time so identity-guard…
matthyx File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| # Process tree — re-reading an exec's parent after `runc exec` | ||
|
|
||
| A `kubectl exec` (or any `runc exec`) starts a process whose parent depends on | ||
| timing. When its exec event is captured, the parent is either: | ||
|
|
||
| - the **containerd-shim**, if `runc exec` has already exited and the kernel has | ||
| reparented the process; or | ||
| - the **runc** process, if runc is still alive. | ||
|
|
||
| runc is a host process. In Kubernetes mode the tree holds only a parentless | ||
| placeholder for it, so the branch from the exec'd process never reaches the | ||
| shim. `GetPidBranch` then fails and the alert falls back to a depth-1 tree whose | ||
| parent is `runc` and whose start time is zero. Before this fix only a later exec | ||
| on the same pid repaired it, through `UpdatePPID`. CI saw this on roughly 30% of | ||
| process-tree roots, which made `Test_24_ProcessTreeDepthTest` flaky. | ||
|
|
||
| ## What the creator does | ||
|
|
||
| `handleExecEvent` calls `rereadUnknownParent` after the normal `UpdatePPID`. It | ||
| re-reads the process's **current** parent from `/proc/<pid>/stat` field 4. By | ||
| the time the event is handled, runc has usually exited and the parent is the | ||
| shim. | ||
|
|
||
| | Step | Rule | | ||
| |---|---| | ||
| | Gate | Kubernetes mode only, a non-host container id, a non-zero parent, an acyclic chain that reaches neither pid 1 nor the container's shim. Every other exec skips the read. | | ||
| | Target | The topmost node below the unknown ancestor. This is usually the exec'd process. It is the process's parent when a child execs before the parent's chain was repaired. | | ||
| | Pid reuse (target) | Skip when the target has a pending exit. With a recorded `pidStartTimeNs`, skip unless the `/proc` start time (field 22) matches it. The target's `/proc` start time must also not be later than the event's exec timestamp mapped back to the boot clock. The target is the exec'd process or one of its ancestors, and both existed before that exec. This applies even with a recorded start time, because an earlier delayed exec may have recorded the replacement's time. A later start time means the exec was handled after a pid was recycled. If the timestamp is unknown, or the target is an ancestor with no recorded start time, skip. | | ||
| | Pid reuse (parent) | The new parent must already be a tree node with no pending exit. Its `/proc` start time is read too. It must match the node's recorded start time when there is one, and must not be later than the target's. Host shims have no recorded start time in Kubernetes mode, because host procfs events never reach the side map. | | ||
| | Adoption | Adoption goes through `UpdatePPID`, so the container-subtree rules still apply. The repaired node's `Pcomm` becomes the adopted parent's `Comm`. An exec event's `Pcomm` is applied only when the event's parent is the parent the node keeps, so a later exec that still reports runc does not relabel a repaired node. | | ||
| | No change | The pid is gone, the stat fails to parse, the parent is unchanged, or the new parent is unknown. The tree keeps today's behaviour. | | ||
|
|
||
| ## Cost and locking | ||
|
|
||
| Each read is one `/proc/<pid>/stat` open-read-parse, about 7.5 µs. An adoption | ||
| takes two reads: the target and the new parent. It runs under | ||
| the tree's write lock, the same trade-off `ensureStartTime` makes on the exec | ||
| path. It is acceptable because the gate keeps it off the common path: an exec | ||
| whose parent chain is already known never reads `/proc`. | ||
|
|
||
| The reader is the `readParent` field on the creator (`parent_reader.go`). Tests | ||
| inject a stub in its place. If `/proc` cannot be opened, the reader retries on | ||
| the next call instead of staying disabled. | ||
|
|
||
| The exec timestamp is inspektor-gadget's `bpf_ktime_get_boot_ns` value plus a | ||
| fixed wall-clock offset, added by `gadgets.WallTimeFromBootTime`. The creator | ||
| reads that same offset back from the function at construction | ||
| (`bootToWallNs`) instead of sampling its own, and subtracts it. | ||
|
|
||
| ## Limits | ||
|
|
||
| If runc is still alive when the event is handled, the read returns runc and | ||
| nothing changes. The next exec in that subtree, or the next `/proc` scan, can | ||
| still repair the chain. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| package processtreecreator | ||
|
|
||
| import ( | ||
| "sync" | ||
|
|
||
| "github.com/kubescape/go-logger" | ||
| "github.com/kubescape/go-logger/helpers" | ||
| "github.com/prometheus/procfs" | ||
| ) | ||
|
|
||
| // newProcfsParentReader returns a reader for a pid's CURRENT parent and its | ||
| // boot-relative start time, both from /proc/<pid>/stat (fields 4 and 22). It | ||
| // uses the same /proc mount as the on-demand start-time reader. | ||
| // | ||
| // Opening /proc is retried on every call until it succeeds, then kept. The | ||
| // reader only runs on the rare unknown-parent exec path, so a retry costs one | ||
| // stat of the mount point there and never touches the hot path. | ||
| // | ||
| // ok is false when the process is gone (ENOENT/ESRCH) or the stat cannot be | ||
| // parsed; callers must then keep the parent they already have. | ||
| func newProcfsParentReader() func(pid uint32) (ppid uint32, startTimeNs uint64, ok bool) { | ||
| var ( | ||
| mu sync.Mutex | ||
| fs procfs.FS | ||
| ready bool | ||
| warned bool | ||
| ) | ||
| return func(pid uint32) (uint32, uint64, bool) { | ||
| mu.Lock() | ||
| if !ready { | ||
| f, err := procfs.NewDefaultFS() | ||
| if err != nil { | ||
| if !warned { | ||
| logger.L().Warning("processtree: cannot open /proc, parent re-read unavailable until it can", helpers.Error(err)) | ||
| warned = true | ||
| } | ||
| mu.Unlock() | ||
| return 0, 0, false | ||
| } | ||
| fs, ready = f, true | ||
| } | ||
| mu.Unlock() | ||
| proc, err := fs.Proc(int(pid)) | ||
| if err != nil { | ||
| return 0, 0, false | ||
| } | ||
| stat, err := proc.Stat() | ||
| if err != nil || stat.PPID <= 0 { | ||
| return 0, 0, false | ||
| } | ||
| return uint32(stat.PPID), stat.Starttime * nsPerTick, true | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.