Skip to content

Feature/refactor container watcher - #591

Merged
amitschendel merged 97 commits into
mainfrom
feature/refactor_container_watcher
Jul 24, 2025
Merged

amitschendel merged 97 commits into
mainfrom
feature/refactor_container_watcher

Conversation

@afek854

@afek854 afek854 commented Jul 15, 2025

Copy link
Copy Markdown
Contributor

Overview

afek854 added 30 commits June 24, 2025 15:21
Signed-off-by: Afek <afekb@armosec.io>
Signed-off-by: Afek <afekb@armosec.io>
Signed-off-by: Afek <afekb@armosec.io>
Signed-off-by: Afek <afekb@armosec.io>
Signed-off-by: Afek <afekb@armosec.io>
Signed-off-by: Afek <afekb@armosec.io>
Signed-off-by: Afek <afekb@armosec.io>
Signed-off-by: Afek <afekb@armosec.io>
Signed-off-by: Afek <afekb@armosec.io>
Signed-off-by: Afek <afekb@armosec.io>
Signed-off-by: Afek <afekb@armosec.io>
Signed-off-by: Afek <afekb@armosec.io>
Signed-off-by: Afek <afekb@armosec.io>
Signed-off-by: Afek <afekb@armosec.io>
Signed-off-by: Afek <afekb@armosec.io>
Signed-off-by: Afek Berger <afekb@armosec.io>
Signed-off-by: Afek Berger <afekb@armosec.io>
Signed-off-by: Afek Berger <afekb@armosec.io>
Signed-off-by: Afek Berger <afekb@armosec.io>
Signed-off-by: Afek Berger <afekb@armosec.io>
Signed-off-by: Afek Berger <afekb@armosec.io>
Signed-off-by: Afek Berger <afekb@armosec.io>
Signed-off-by: Afek Berger <afekb@armosec.io>
Signed-off-by: Afek Berger <afekb@armosec.io>
Signed-off-by: Afek Berger <afekb@armosec.io>
Signed-off-by: Afek Berger <afekb@armosec.io>
Signed-off-by: Afek Berger <afekb@armosec.io>
Signed-off-by: Afek Berger <afekb@armosec.io>
Signed-off-by: Afek Berger <afekb@armosec.io>
Signed-off-by: Afek Berger <afekb@armosec.io>
afek854 added 3 commits July 22, 2025 14:03
Signed-off-by: Afek Berger <afekb@armosec.io>
Signed-off-by: Afek Berger <afekb@armosec.io>
Signed-off-by: Afek Berger <afekb@armosec.io>
@afek854
afek854 force-pushed the feature/refactor_container_watcher branch from b03389a to 69547d2 Compare July 22, 2025 12:08
afek854 added 5 commits July 22, 2025 15:21
Signed-off-by: Afek Berger <afekb@armosec.io>
Signed-off-by: Afek Berger <afekb@armosec.io>
Signed-off-by: Afek Berger <afekb@armosec.io>
Signed-off-by: Afek Berger <afekb@armosec.io>
Signed-off-by: Afek Berger <afekb@armosec.io>
@afek854
afek854 force-pushed the feature/refactor_container_watcher branch from 47ec528 to ddf37b5 Compare July 22, 2025 14:12
Signed-off-by: Afek Berger <afekb@armosec.io>
@afek854
afek854 force-pushed the feature/refactor_container_watcher branch from ddf37b5 to f7a6f7b Compare July 23, 2025 07:23
Comment thread coverage.out Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Remove this file

// TracerFactoryInterface defines the interface for creating tracers
type TracerFactoryInterface interface {
CreateAllTracers(manager TracerRegistrer)
StartThirdPartyTracers(ctx context.Context) error

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why does a factory has start/stop methods?

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.

DONE

cw.sbomManager.ContainerCallback,
cw.dnsManager.ContainerCallback,
cw.networkStreamClient.ContainerCallback,
cw.containerProcessTree.ContainerCallback,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Don't you want to move the process tree callback to be before the other managers so it will reduce races?

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.

DONE


type OrderedEventQueue struct {
maxBufferSize int

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why do you have a new line between every field?

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.

DONE

func NewTracerManager(cfg config.Config, tracerFactory containerwatcherroot.TracerFactoryInterface) *TracerManager {
return &TracerManager{
cfg: cfg,
tracers: make(map[utils.EventType]containerwatcherroot.TracerInterface),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

containerwatcherroot -> containerwatcher

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.

DONE

tracerCollection: tracerCollection,
containerSelector: containerSelector,
eventCallback: eventCallback,
procfsFeeder: feeder.NewProcfsFeeder(cfg.ProcfsScanInterval), // Scan every 5 seconds

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Remove the comment

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.

DONE

path = event.Args[0]
}

if path == "" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do you use the path? it should be passed to the container profile manager if I recall.

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.

// Get command name
bpf_get_current_comm(&event->comm, sizeof(event->comm));

// Get executable path

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why?

}

// This gadget is used to trace the sched_process_exit tracepoint.
SEC("raw_tracepoint/sched_process_exit")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Did you test this exists in all archs (amd64/arm64)?
What happens if it doesn't load? crash the agent? do we have a fallback for this in case it can't load?

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.

crash

@afek854 afek854 Jul 23, 2025 •

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.

Also checked on kernel 5.14

}

// This gadget is used to trace the sched_process_fork tracepoint.
SEC("raw_tracepoint/sched_process_fork")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same comments as the exist tracer eBPF.

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.

Why? in fork we need the attributes

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.

DONE

afek854 added 2 commits July 23, 2025 14:02
Signed-off-by: Afek Berger <afekb@armosec.io>
Signed-off-by: Afek Berger <afekb@armosec.io>
Comment thread pkg/processtree/feeder/event_feeder.go Outdated
@@ -0,0 +1,158 @@
package feeder

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Change according to what we talked f2f.

@afek854
afek854 force-pushed the feature/refactor_container_watcher branch from 3c704bc to 4f30142 Compare July 24, 2025 07:01
Signed-off-by: Afek Berger <afekb@armosec.io>
@afek854
afek854 force-pushed the feature/refactor_container_watcher branch from 4f30142 to 74b9e97 Compare July 24, 2025 07:02
@amitschendel amitschendel added the release Create release label Jul 24, 2025
@amitschendel
amitschendel self-requested a review July 24, 2025 07:20
@amitschendel
amitschendel merged commit 531bac3 into main Jul 24, 2025
@amitschendel
amitschendel deleted the feature/refactor_container_watcher branch July 24, 2025 07:21
matthyx added a commit that referenced this pull request Aug 26, 2026
…store 5s alerting cadence

The gadget's own internal fetch schedule is now disabled entirely
(map-fetch-interval/count both "0"); every fetch, periodic or not, goes
through SyscallTracer.Peek. A single background loop in Start calls Peek
on syscallPollInterval (default 5s, was 30s) for live event delivery to
every consumer (profile-building and RuleManager's real-time syscall
alerting alike), and ContainerProfileManager calls Peek once more, out
of that schedule, right before a container's final forced save.

This restores the pre-PR-#591 architecture's actual property that
mattered: RuleManager's real-time alerting ran on its own dedicated
schedule (a 5s ticker per container) fully decoupled from
ContainerProfileManager's save-driven Peek() calls. The destructive
batch read the current OCI gadget uses was never actually why a single
shared 30s schedule was needed; there was simply one internal ticker
serving every consumer at the same cadence. Driving everything through
one explicit Peek call, and making that call on the original 5s cadence,
is now also cheaper than the pre-#591 design: one global fetch drains
and dispatches every currently traced container in a batch, rather than
one ticker per container.
matthyx added a commit that referenced this pull request Aug 26, 2026
…store 5s alerting cadence

The gadget's own internal fetch schedule is now disabled entirely
(map-fetch-interval/count both "0"); every fetch, periodic or not, goes
through SyscallTracer.Peek. A single background loop in Start calls Peek
on syscallPollInterval (default 5s, was 30s) for live event delivery to
every consumer (profile-building and RuleManager's real-time syscall
alerting alike), and ContainerProfileManager calls Peek once more, out
of that schedule, right before a container's final forced save.

This restores the pre-PR-#591 architecture's actual property that
mattered: RuleManager's real-time alerting ran on its own dedicated
schedule (a 5s ticker per container) fully decoupled from
ContainerProfileManager's save-driven Peek() calls. The destructive
batch read the current OCI gadget uses was never actually why a single
shared 30s schedule was needed; there was simply one internal ticker
serving every consumer at the same cadence. Driving everything through
one explicit Peek call, and making that call on the original 5s cadence,
is now also cheaper than the pre-#591 design: one global fetch drains
and dispatches every currently traced container in a batch, rather than
one ticker per container.

Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
matthyx added a commit that referenced this pull request Aug 26, 2026
…ation (#924)

* fix(containerprofile): reduce syscall loss window on container termination

The seccomp syscall tracer only fetches the advise_seccomp eBPF map on a
fixed 30s poll; a container terminating between polls loses whatever it
executed since the last fetch once its profile is saved and its data is
removed. Make the poll interval configurable (syscallPollInterval,
default 2s, down from a hardcoded 30s) and wait one bounded poll interval
before the final forced profile save on termination, so the tracer's next
poll cycle has a chance to land first.

Fixes #922

Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>

* fix(containerprofile): flush syscalls on demand instead of shrinking the poll interval

Bumps the inspektor-gadget fork to matthyx/inspektor-gadget#12, which adds
ebpfoperator.TriggerManualMapFetch: an on-demand trigger for the eBPF map
iterator that the periodic syscall poll relies on. ContainerProfileManager
now calls this (via SyscallTracer.Peek, wired through the new
SetSyscallFlusher hook) right before a container's final forced profile
save on termination, instead of guessing how long to wait for the next
scheduled poll.

This replaces the previous mitigation (shrinking syscallPollInterval to
2s and sleeping up to that long on every termination) with an actual
fix: the poll interval reverts to its original 30s default since it no
longer needs to be short for correctness, and the wait before saving is
now a fixed 500ms settle delay for the event pipeline, only paid when a
flush was actually requested.

Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>

* fix(containerprofile): drive all syscall map fetches through Peek, restore 5s alerting cadence

The gadget's own internal fetch schedule is now disabled entirely
(map-fetch-interval/count both "0"); every fetch, periodic or not, goes
through SyscallTracer.Peek. A single background loop in Start calls Peek
on syscallPollInterval (default 5s, was 30s) for live event delivery to
every consumer (profile-building and RuleManager's real-time syscall
alerting alike), and ContainerProfileManager calls Peek once more, out
of that schedule, right before a container's final forced save.

This restores the pre-PR-#591 architecture's actual property that
mattered: RuleManager's real-time alerting ran on its own dedicated
schedule (a 5s ticker per container) fully decoupled from
ContainerProfileManager's save-driven Peek() calls. The destructive
batch read the current OCI gadget uses was never actually why a single
shared 30s schedule was needed; there was simply one internal ticker
serving every consumer at the same cadence. Driving everything through
one explicit Peek call, and making that call on the original 5s cadence,
is now also cheaper than the pre-#591 design: one global fetch drains
and dispatches every currently traced container in a batch, rather than
one ticker per container.

Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>

* feat(containerprofile): batch syscall reporting, bypass generic pipeline

SyscallTracer.callback decodes one eBPF fetch into a batch of syscall
names per container, but was still emitting one SyscallEventType event
per syscall through the generic per-event pipeline (AddEventDirect,
dedup-key computation, handler dispatch) for a data source that isn't
actually a discrete event stream - it's a periodic snapshot of a
persistent per-mntns bitmap. That per-item overhead scales with how
many distinct syscalls a container has executed since its last fetch,
worst right after a container starts.

RuleManager's rule matching and the generic dedup cache both key off a
single event.syscall value, so they still need one event per syscall.
ContainerProfileManager has no such constraint - it only folds each
syscall into a per-container set - so it now gets the whole batch
directly via the new ReportSyscalls(containerID, []string) method,
bypassing the queue/dedup/dispatch pipeline entirely for this consumer.
RuleManager and metrics are unaffected, still receiving one event per
syscall exactly as before.

Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>

* fix(containerprofile): address review — flusher race, blocking dispatch, empty containerID

Fixes three issues from review on PR #924 (pullrequestreview-5027546778)
plus the CI failures they caused:

- ContainerProfileManager.syscallFlusher was a plain func() field written
  once by SetSyscallFlusher (from TracerFactory, during startup wiring)
  and read by flushAndSettle from concurrently-running per-container
  monitorContainer goroutines - an unsynchronized data race that could
  silently skip the flush at exactly the highest-container-churn moment
  there is (startup/restart). Now stored via atomic.Pointer[func()].

- SyscallTracer.callback called ContainerProfileManager.ReportSyscalls
  synchronously and inline, on the single goroutine the gadget uses to
  process every currently-traced container's fetch results.
  ReportSyscalls can itself block (withContainer sends on a container's
  bounded SyncChannel while holding its lock, on a profile-size-split).
  Previously this same call went through the generic queue and one of
  many pooled workers, so one slow container never affected any other.
  Now dispatched on its own goroutine so it can't stall the shared
  fetch-processing path.

- The seccomp map covers every mount namespace on the node, including
  ones never resolved to a container (host processes, etc). The generic
  pipeline used to drop those via EventHandlerFactory.ProcessEvent's
  empty-ContainerID check before they reached ReportSyscall; the direct
  call bypasses that check, so every such row was logging "invalid
  empty containerID" and wasting a container-map lookup, every tick -
  visible as hundreds of these errors in the failing CI runs
  (Test_02_AllAlertsFromMaliciousApp, Test_23_RuleCooldownTest,
  Test_28_UserDefinedNetworkNeighborhood). callback now filters these
  out itself before decoding or reporting.

Also: shared config.DefaultSyscallPollInterval constant instead of
duplicating the 5s literal in both packages, and decodeSyscalls
pre-sizes its slice since the upper bound is already known.

Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>

* docs: correct flushAndSettle description to match the direct-call path

Leftover from before the review-fix commit: ContainerProfileManager no
longer goes through the generic event queue/worker pool for syscalls,
so the 500ms wait description was stale (CodeRabbit review comment on
PR #924).

Docs-exempt: documentation-only correction, no behavioral change
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>

---------

Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
ConstanzeTU pushed a commit to k8sstormcenter/node-agent that referenced this pull request Aug 26, 2026
…ation (kubescape#924)

* fix(containerprofile): reduce syscall loss window on container termination

The seccomp syscall tracer only fetches the advise_seccomp eBPF map on a
fixed 30s poll; a container terminating between polls loses whatever it
executed since the last fetch once its profile is saved and its data is
removed. Make the poll interval configurable (syscallPollInterval,
default 2s, down from a hardcoded 30s) and wait one bounded poll interval
before the final forced profile save on termination, so the tracer's next
poll cycle has a chance to land first.

Fixes kubescape#922

Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>

* fix(containerprofile): flush syscalls on demand instead of shrinking the poll interval

Bumps the inspektor-gadget fork to matthyx/inspektor-gadget#12, which adds
ebpfoperator.TriggerManualMapFetch: an on-demand trigger for the eBPF map
iterator that the periodic syscall poll relies on. ContainerProfileManager
now calls this (via SyscallTracer.Peek, wired through the new
SetSyscallFlusher hook) right before a container's final forced profile
save on termination, instead of guessing how long to wait for the next
scheduled poll.

This replaces the previous mitigation (shrinking syscallPollInterval to
2s and sleeping up to that long on every termination) with an actual
fix: the poll interval reverts to its original 30s default since it no
longer needs to be short for correctness, and the wait before saving is
now a fixed 500ms settle delay for the event pipeline, only paid when a
flush was actually requested.

Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>

* fix(containerprofile): drive all syscall map fetches through Peek, restore 5s alerting cadence

The gadget's own internal fetch schedule is now disabled entirely
(map-fetch-interval/count both "0"); every fetch, periodic or not, goes
through SyscallTracer.Peek. A single background loop in Start calls Peek
on syscallPollInterval (default 5s, was 30s) for live event delivery to
every consumer (profile-building and RuleManager's real-time syscall
alerting alike), and ContainerProfileManager calls Peek once more, out
of that schedule, right before a container's final forced save.

This restores the pre-PR-kubescape#591 architecture's actual property that
mattered: RuleManager's real-time alerting ran on its own dedicated
schedule (a 5s ticker per container) fully decoupled from
ContainerProfileManager's save-driven Peek() calls. The destructive
batch read the current OCI gadget uses was never actually why a single
shared 30s schedule was needed; there was simply one internal ticker
serving every consumer at the same cadence. Driving everything through
one explicit Peek call, and making that call on the original 5s cadence,
is now also cheaper than the pre-kubescape#591 design: one global fetch drains
and dispatches every currently traced container in a batch, rather than
one ticker per container.

Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>

* feat(containerprofile): batch syscall reporting, bypass generic pipeline

SyscallTracer.callback decodes one eBPF fetch into a batch of syscall
names per container, but was still emitting one SyscallEventType event
per syscall through the generic per-event pipeline (AddEventDirect,
dedup-key computation, handler dispatch) for a data source that isn't
actually a discrete event stream - it's a periodic snapshot of a
persistent per-mntns bitmap. That per-item overhead scales with how
many distinct syscalls a container has executed since its last fetch,
worst right after a container starts.

RuleManager's rule matching and the generic dedup cache both key off a
single event.syscall value, so they still need one event per syscall.
ContainerProfileManager has no such constraint - it only folds each
syscall into a per-container set - so it now gets the whole batch
directly via the new ReportSyscalls(containerID, []string) method,
bypassing the queue/dedup/dispatch pipeline entirely for this consumer.
RuleManager and metrics are unaffected, still receiving one event per
syscall exactly as before.

Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>

* fix(containerprofile): address review — flusher race, blocking dispatch, empty containerID

Fixes three issues from review on PR kubescape#924 (pullrequestreview-5027546778)
plus the CI failures they caused:

- ContainerProfileManager.syscallFlusher was a plain func() field written
  once by SetSyscallFlusher (from TracerFactory, during startup wiring)
  and read by flushAndSettle from concurrently-running per-container
  monitorContainer goroutines - an unsynchronized data race that could
  silently skip the flush at exactly the highest-container-churn moment
  there is (startup/restart). Now stored via atomic.Pointer[func()].

- SyscallTracer.callback called ContainerProfileManager.ReportSyscalls
  synchronously and inline, on the single goroutine the gadget uses to
  process every currently-traced container's fetch results.
  ReportSyscalls can itself block (withContainer sends on a container's
  bounded SyncChannel while holding its lock, on a profile-size-split).
  Previously this same call went through the generic queue and one of
  many pooled workers, so one slow container never affected any other.
  Now dispatched on its own goroutine so it can't stall the shared
  fetch-processing path.

- The seccomp map covers every mount namespace on the node, including
  ones never resolved to a container (host processes, etc). The generic
  pipeline used to drop those via EventHandlerFactory.ProcessEvent's
  empty-ContainerID check before they reached ReportSyscall; the direct
  call bypasses that check, so every such row was logging "invalid
  empty containerID" and wasting a container-map lookup, every tick -
  visible as hundreds of these errors in the failing CI runs
  (Test_02_AllAlertsFromMaliciousApp, Test_23_RuleCooldownTest,
  Test_28_UserDefinedNetworkNeighborhood). callback now filters these
  out itself before decoding or reporting.

Also: shared config.DefaultSyscallPollInterval constant instead of
duplicating the 5s literal in both packages, and decodeSyscalls
pre-sizes its slice since the upper bound is already known.

Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>

* docs: correct flushAndSettle description to match the direct-call path

Leftover from before the review-fix commit: ContainerProfileManager no
longer goes through the generic event queue/worker pool for syscalls,
so the 500ms wait description was stale (CodeRabbit review comment on
PR kubescape#924).

Docs-exempt: documentation-only correction, no behavioral change
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>

---------

Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release Create release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants