diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-13 10:21:56 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-13 10:21:56 +0300 |
| commit | 7c15d6058cf56e8c7801259f1f842a3a010c5f41 (patch) | |
| tree | 0b9f85dadade5958a281ab6abaa51dcb620603f2 /internal/event | |
| parent | 42d7821dc8d81781f2c3cfc269e99c0ee1dbd017 (diff) | |
fix: guard Pair.CalculateDurations against uint64 underflow on clock skew
BPF timestamps can be non-monotonic across CPUs (NTP step, TSC skew).
When exit < enter or enter < prevPairTime the uint64 subtraction wraps
to a huge value, corrupting latency histograms and flamegraph weights.
Clamp both Duration and DurationToPrev to 0 instead of underflowing.
Add TestPairCalculateDurationsNegativeDelta to cover this case.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/event')
| -rw-r--r-- | internal/event/pair.go | 23 | ||||
| -rw-r--r-- | internal/event/pair_test.go | 30 |
2 files changed, 50 insertions, 3 deletions
diff --git a/internal/event/pair.go b/internal/event/pair.go index 4d3f342..c31fd90 100644 --- a/internal/event/pair.go +++ b/internal/event/pair.go @@ -36,12 +36,29 @@ func NewPair(enterEv Event) *Pair { } func (e *Pair) CalculateDurations(prevPairTime uint64) { - // Duration is syscall runtime: exit(current) - enter(current). - e.Duration = e.ExitEv.GetTime() - e.EnterEv.GetTime() + exitTime := e.ExitEv.GetTime() + enterTime := e.EnterEv.GetTime() + + // Guard against uint64 underflow caused by non-monotonic BPF timestamps + // (e.g. cross-CPU clock skew or NTP adjustments). When exit < enter the + // syscall duration cannot be measured reliably; treat it as zero rather + // than wrapping around to an astronomically large value. + if exitTime >= enterTime { + e.Duration = exitTime - enterTime + } else { + e.Duration = 0 + } + if prevPairTime > 0 { // DurationToPrev is the inter-syscall gap on the same TID: // enter(current) - exit(previous). - e.DurationToPrev = e.EnterEv.GetTime() - prevPairTime + // Apply the same underflow guard: if the previous exit timestamp + // is ahead of this enter (clock skew), clamp the gap to zero. + if enterTime >= prevPairTime { + e.DurationToPrev = enterTime - prevPairTime + } else { + e.DurationToPrev = 0 + } } } diff --git a/internal/event/pair_test.go b/internal/event/pair_test.go index eb033dc..9aa3e11 100644 --- a/internal/event/pair_test.go +++ b/internal/event/pair_test.go @@ -56,6 +56,36 @@ func TestPairCalculateDurationsWithPreviousExit(t *testing.T) { } } +// TestPairCalculateDurationsNegativeDelta verifies that non-monotonic BPF +// timestamps (exit < enter due to cross-CPU clock skew) do not cause uint64 +// underflow. Both Duration and DurationToPrev must clamp to zero. +func TestPairCalculateDurationsNegativeDelta(t *testing.T) { + enter := &types.OpenEvent{ + Time: 2000, + Pid: 1, + Tid: 2, + } + // Simulate clock skew: exit timestamp is earlier than enter. + exit := &types.RetEvent{ + Time: 1900, + Pid: 1, + Tid: 2, + Ret: 0, + } + + pair := NewPair(enter) + pair.ExitEv = exit + // prevPairTime > enterTime also triggers underflow in DurationToPrev. + pair.CalculateDurations(3000) + + if pair.Duration != 0 { + t.Fatalf("Duration = %d, want 0 when exit < enter (underflow guard)", pair.Duration) + } + if pair.DurationToPrev != 0 { + t.Fatalf("DurationToPrev = %d, want 0 when enter < prevPairTime (underflow guard)", pair.DurationToPrev) + } +} + func TestPairRecycleHandlesMissingExitEvent(t *testing.T) { pair := NewPair(&types.OpenEvent{ Time: 1000, |
