fix(node-agent): fix ContainerProfile size accounting in ReportSyscall and ReportNetworkEvent - #882
Conversation
|
Warning Review limit reached
Next review available in: 16 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThe container profile estimator now uses serialized-size accounting for unique syscalls and network-neighbor expansion. Tests cover the updated estimates. The repository also ignores two local artifacts. ChangesContainer profile size accounting
Repository ignore rules
Estimated code review effort: 2 (Simple) | ~10 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
ReportSyscall added mapset.Append's return (element count, 0 or 1) straight into the byte-size accumulator, so syscalls contributed ~nothing toward MaxTsProfileSize. ReportNetworkEvent also sized only the raw NetworkEvent, missing the Identifier/DNS/selector fields createNetworkNeighbor adds at serialization - both let the pre-send estimate undercount, so profiles kept growing past storage's own cap instead of flushing early. Fixes kubescape#870. Signed-off-by: aryanghai12 <aryanghai1205@gmail.com>
b79a854 to
c576f98
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@pkg/containerprofilemanager/v1/event_reporting.go`:
- Around line 22-29: The fixed networkNeighborExpansionEstimate in event
reporting undercounts deferred NetworkNeighbor data. Replace it with an exact
pre-threshold size calculation or a documented maximum covering the identifier,
field encoding, DNS name, and selector payload; update the threshold accounting
around the relevant reporting function and add coverage for the maximum
supported DNS name and selector payload so requests remain within
MaxTsProfileSize.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ce72bc5c-6c86-4a53-a6ff-4425790dd696
📒 Files selected for processing (3)
.gitignorepkg/containerprofilemanager/v1/event_reporting.gopkg/containerprofilemanager/v1/event_reporting_test.go
…ocumented bounds The flat 256-byte surcharge was an unjustified guess: RFC 1035's max DNS name (253 bytes) alone, stored twice in DNS/DNSNames, already exceeds it before counting the Identifier, Ports entry, or selector maps createNetworkNeighbor adds at serialization. Replace it with a value computed from each field's documented worst case (sha256-hex identifier, RFC 1035 DNS name, exact NamespaceSelector shape, a generously budgeted PodSelector label count), and add tests that run the real createNetworkNeighbor path against a max-length DNS name and a populated selector payload to confirm the estimate covers it. Signed-off-by: aryanghai12 <aryanghai1205@gmail.com>
matthyx
left a comment
There was a problem hiding this comment.
Thanks for taking this on — the ReportSyscall half is exactly right, and the test coverage is genuinely good. One blocker on the network half before this can go in.
Blocker: the expansion estimate over-counts by ~6–20x, which trades the 413 undercount for constant premature splitting
networkNeighborExpansionEstimate evaluates to 4571 bytes. Component breakdown (measured on b9ad48c):
| component | bytes |
|---|---|
identifier |
80 |
Type |
24 |
Ports |
85 |
DNS + DNSNames |
562 |
NamespaceSelector |
164 |
PodSelector |
3656 |
| total | 4571 |
Measured against what createNetworkNeighbor actually produces:
| case | estimate (size.Of(event) + surcharge) |
actual size.Of(*neighbor) |
ratio |
|---|---|---|---|
| external IP, no DNS | 4726 | 242 | 19.5x |
| pod destination, 6 typical Helm labels | 4930 | 767 | 6.4x |
With the default maxTsProfileSize: 2Mi, that means a split fires after ~443 unique network events, whose real serialized weight is ~130–350 KB — roughly 6–15% of the intended threshold. Network neighbors are usually the dominant part of a profile, so in practice this makes ProfileRequiresSplit the normal path rather than the safety net #870 asks for ("Once this is fixed, the splitting mechanism added in #866 should become a rare safety net rather than something regularly exercised in the field"). Under-flushing became over-flushing; both miss the target.
The root cause is that the surcharge is unconditional, while createNetworkNeighbor takes exactly one branch per Destination.Kind — the doc comment says as much, but the code still sums all branches. And critically, most of the budget isn't actually unknown at report time:
PodSelectorforEndpointKindPodis already counted. It comes fromfilterLabels(networkEvent.GetDestinationPodLabels()), which parsesDestination.PodLabels— a plainstringfield on theNetworkEventthatsize.Of(networkEvent)already includes, andfilterLabelsonly ever removes entries. So the 3656 bytes are double-counted on the pod branch and pure fiction on the raw/DNS branch (wherePodSelectorisnil).NamespaceSelectoris exactly computable: the namespaces are both known at report time.Portsis exactly computable fromPort/Protocol.- Only two things are genuinely deferred: the DNS name (raw branch only) and the Service selector (
EndpointKindServiceonly).
Suggested shape — switch on event.Destination.Kind and add only that branch's cost:
// fixed for every neighbor
est := neighborFixedOverhead // identifier + Type + Ports
switch networkEvent.Destination.Kind {
case EndpointKindPod:
// labels already inside size.Of(networkEvent); only the selector wrappers are new
est += size.Of(namespaceSelectorFor(networkEvent.Destination.Namespace, namespace))
case EndpointKindService:
est += serviceSelectorBudget // genuinely deferred, but a Service selector is a handful of labels
default:
est += maxDNSNameBudget // 253*2, raw branch only
}That lands typical events in the few-hundred-bytes range — still conservative, but without turning every profile into a split. Keeping the existing tests as upper-bound assertions works fine with this shape.
Non-blocking
.gitignore:LFX_AGENT_SANDBOX_PREP.mdis unrelated to this fix (CodeRabbit's out-of-scope check flags it too) and looks like a personal working file — that belongs in your global gitignore or.git/info/exclude. Please drop it here. While you're in the file, it'd be nice to end it with a newline.- PR description is stale: it still describes a "conservative 256-byte
networkNeighborExpansionEstimate", which wasc576f98;b9ad48creplaced it with the computed 4571-byte value. Worth updating before merge so the commit history reads correctly. - Other accumulators still over-count (out of scope for this PR, but #870 asks for all accumulators to be consistent, so worth a follow-up):
ReportFileExecaddssize.Of(exec)unconditionally even thoughdata.execs.Setoverwrites the sameexecIdentifierkey, so a repeatedly-exec'd binary inflates the estimate without bound.ReportFileOpenre-addssize.Of(path)when only new flags are appended to an existing path. Both push the same direction as the blocker above. size.Ofmeasures in-memory Go size, not the JSON payload storage rejects with a 413. The new tests assertestimate >= size.Of(*neighbor), which doesn't directly bound the wire size. That's the pre-existing convention in this file so I'm not asking you to change it, but the estimator's relationship to the actual HTTP cap stays approximate.
Verified locally
go build ./... and go test ./pkg/containerprofilemanager/... both pass on b9ad48c; the numbers above come from instrumenting that build. Happy to re-review as soon as the branch-aware sizing is in.
…tual serialization branch The flat surcharge summed every createNetworkNeighbor branch onto every event, overcounting by 6-20x and making ProfileRequiresSplit the normal path instead of a rare backstop. Charge only the branch Destination.Kind actually takes: Ports/NamespaceSelector are computed exactly from data already on the event, PodSelector charges only the map-wrapping delta over what's already counted via Destination.PodLabels, and DNS/Service-selector budgets apply only on their respective branches. Drop LFX_AGENT_SANDBOX_PREP.md from .gitignore (moved to .git/info/exclude) and restore the file's trailing newline. Signed-off-by: aryanghai12 <aryanghai1205@gmail.com>
matthyx
left a comment
There was a problem hiding this comment.
Blocker resolved — this is the right shape now. Approving.
fe073b2 replaces the flat surcharge with networkNeighborIncrement(data, networkEvent), charging only the branch createNetworkNeighbor will actually take. Re-measured on this commit:
| constant | bytes |
|---|---|
neighborFixedOverhead |
104 |
maxDNSNameEstimate |
562 |
maxServiceSelectorEstimate |
1875 |
| case | estimate | actual size.Of(*neighbor) |
ratio (was) |
|---|---|---|---|
| external IP, DNS unresolved | 983 | 242 | 4.06x (19.5x) |
| external IP, 253-byte DNS name | 983 | 535 | 1.84x |
| pod dest, 6 Helm labels, cross-ns | 912 | 767 | 1.19x (6.4x) |
| pod dest, 6 Helm labels, same ns | 802 | 682 | 1.18x |
| pod dest, 30 long labels | 4152 | 4007 | 1.04x |
The upper-bound property still holds in every case I tried — no undercounts — and events-until-split at the default maxTsProfileSize: 2Mi goes from ~443 back to ~2133 for the external-IP case. The residual 4x on unresolved DNS is inherent (whether ResolveIPAddress will hit isn't knowable at report time) and the comment says so; charging the RFC 1035 bound there is the right call.
The PodSelector handling is better than what I suggested — subtracting size.Of(Destination.PodLabels) and charging only the map-wrapper delta (clamped at zero) is exact rather than budgeted, and it holds up: on the 30-label stress case the estimate lands 145 bytes over a 4007-byte neighbor.
Things I checked before approving:
- The two namespaces agree.
networkNeighborIncrementreadsdata.watchedContainerData.Namespace, which is populated fromcontainer.K8s.Namespace(pkg/containerwatcher/v2/containercallback.go:117) — the same valuemonitoring.go:205-206later passes tocreateNetworkNeighbor. So the estimator and the serializer make the samegetNamespaceMatchLabelscall, not two different ones. - No new race.
watchedContainerDatais read underentry.muinsidewithContainer, the same locklifecycle.go:154writes it under.go test -race ./pkg/containerprofilemanager/v1/is clean. - Nil
watchedContainerDataerrs conservative. Events reported before shared data is ready seesourceNamespace == "", which over-charges theNamespaceSelectorrather than under-charging it. - Cost is fine. The increment is ~2.5µs / 42 allocs more than the old constant on the pod branch, but it sits after the
data.networks.Containsearly return, so it's paid once per unique neighbor tuple, not per packet. Negligible against a profile's lifetime. .gitignorenow nets out to just adding the missing trailing newline. Thanks.go build ./...,go vet,gofmt -l, andgo test ./pkg/containerprofilemanager/...all clean onfe073b2.
Two things to tidy, neither blocking the approval:
- Please update the PR description before merging — it still describes "a conservative 256-byte
networkNeighborExpansionEstimate", which is two commits stale and describes a symbol that no longer exists. If this lands as a squash merge that text becomes the commit body. - Optional test gaps: nothing exercises the
EndpointKindServicebranch (maxServiceSelectorEstimateis the one budget left that's a genuine guess) or thewatchedContainerData == nilpath. Also a theoretical 164-byte undercount if aEndpointKindPod/Serviceevent ever arrives with an emptyDestination.Namespace— report time sees"" == ""and skips theNamespaceSelectorwhile serialization would add it. I don't think that's reachable in practice, so purely FYI.
The follow-ups I mentioned last time on ReportFileExec (unconditional size.Of(exec) despite execs.Set overwriting the same key) and ReportFileOpen (re-adding size.Of(path) for flag-only updates) are still open, but they're #870's remaining scope rather than this PR's — worth a separate issue.
Overview
MaxTsProfileSizepre-send threshold undercountedContainerProfilebyte sizes due to mixed accumulator units.ReportSyscalladdedmapset.Set.Append's return value (0 or 1) rather than the actual syscall byte size.ReportNetworkEventonly measured the rawNetworkEventstruct, ignoring post-serialization field expansions (DNS,PodSelector,NamespaceSelector, hashes) added later bycreateNetworkNeighbor. As a result, profiles grew past storage limits instead of flushing early, triggering HTTP 413 payload errors.ReportSyscalladdssize.Of(syscall)only when a syscall is newly appended.ReportNetworkEventapplies a conservative 256-bytenetworkNeighborExpansionEstimatesurcharge on top ofsize.Of(networkEvent)to account for deferred serialization expansions. Profiles now flush accurately before hitting storage caps.Additional Information
How to Test
Run the size accounting unit tests:
Examples/Screenshots
N/A (backend byte accounting fix)
Related issues/PRs:
Checklist before requesting a review
Please open the PR against the
devbranch (Unless the PR contains only documentation changes)Summary by CodeRabbit
Bug Fixes
Tests
Chores