Skip to content

Fix FiveTupleSplitter file collision on TCP session reuse - #2249

Open
rshayer95 wants to merge 2 commits into
seladb:devfrom
rshayer95:fix/five-tuple-splitter-file-collision
Open

rshayer95 wants to merge 2 commits into
seladb:devfrom
rshayer95:fix/five-tuple-splitter-file-collision

Conversation

@rshayer95

@rshayer95 rshayer95 commented Sep 2, 2026

Copy link
Copy Markdown

Summary

Fixes #2248.

FiveTupleSplitter::getFileNumber() deliberately allocates a new fileNumber when a fresh SYN arrives on a 5-tuple hash that's already been seen, to isolate the new logical session. But FiveTupleSplitter::getFileName() ignored the fileNumber parameter entirely, deriving the output path purely from the packet's own IP/port values — so the new session produced the exact same path string as the old one.

main.cpp's writer cache (outputFiles) is keyed by fileNumber, not by path. On collision, it treats the new fileNumber as never-seen and opens its writer fresh (no append flag) against a path an earlier, still-tracked writer object may still be using — two independent file handles on one inode, with whichever one flushes last silently discarding the other's data.

Fix

Fold fileNumber into the generated path for both the TCP and UDP branches, matching how every other splitter in this file avoids the problem by keeping fileNumber and filename 1:1. UDP's getFileNumber() never reallocates on hash reuse today, so the collision isn't reachable there, but fileNumber is included anyway for naming consistency and to stay correct if that ever changes.

Verification

Built both an unpatched and patched copy (Alpine 3.22, g++ 14.2.0) and ran both against:

  1. A synthetic capture with a deliberately reused 5-tuple (same 5-tuple, two SYNs 10 unrelated connections apart):

    • Unpatched: reports "written ... to 12 files", only 11 exist on disk. The file for the reused 5-tuple contains only session 1's 4 packets — session 2's data was silently discarded.
    • Patched: 12 files claimed, 12 exist. The reused 5-tuple now produces two separate files (...-0001.pcap, ...-0012.pcap), each complete and valid (48/48 packets accounted for across all 12 files, 0 read errors).
  2. A real 126,502-packet / 95-connection capture with no reused 5-tuples (regression check): 95/95 files clean, all packets accounted for, identical before and after the patch.

Notes

This is one of two independent resource-ceiling issues found while investigating the corruption reports in #2248 (the other being fd-exhaustion in the underlying pcapng writer on very high connection counts, unrelated to this code path). This PR addresses the file-collision mechanism specifically, which is the one with a deterministic, reproducible root cause and fix.

FiveTupleSplitter::getFileNumber() deliberately allocates a new
fileNumber when a fresh SYN arrives on a 5-tuple hash that's already
been seen, to isolate the new logical session. But
FiveTupleSplitter::getFileName() ignored the fileNumber parameter
entirely, deriving the output path purely from the packet's own
IP/port values -- so the new session produced the exact same path
string as the old one.

main.cpp's writer cache (outputFiles) is keyed by fileNumber, not by
path. On collision, it treats the new fileNumber as never-seen and
opens its writer fresh (no append flag) against a path an earlier,
still-tracked writer object may still be using -- two independent
file handles on one inode, with whichever one flushes last silently
discarding the other's data (in unordered_map iteration order at
program exit, since no LRU eviction of the earlier writer is
required to trigger this).

Fold fileNumber into the generated path for both the TCP and UDP
branches, matching how every other splitter avoids this by keeping
fileNumber and filename 1:1. UDP's getFileNumber() never reallocates
on hash reuse today, so it isn't reachable there, but fileNumber is
included for naming consistency and to stay correct if that changes.

Verified against a synthetic capture with a deliberately reused
5-tuple (same 5-tuple, two SYNs 10 unrelated connections apart):
unpatched, one session's data is silently discarded; patched, both
land in separate, complete, valid files. Also verified no regression
on a real 126,502-packet / 95-connection capture with no reused
5-tuples (95/95 files clean, all packets accounted for, before and
after).

Fixes seladb#2248

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@rshayer95
rshayer95 requested a review from seladb as a code owner September 2, 2026 20:20
@egecetin
egecetin changed the base branch from master to dev September 13, 2026 16:23
@egecetin egecetin closed this Sep 13, 2026
@egecetin egecetin reopened this Sep 13, 2026
@codecov

codecov Bot commented Sep 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.90%. Comparing base (bbe7ffe) to head (251f392).

Additional details and impacted files
@@           Coverage Diff            @@
##              dev    #2249    +/-   ##
========================================
  Coverage   82.89%   82.90%            
========================================
  Files         336      336            
  Lines       61062    61058     -4     
  Branches    12965    12669   -296     
========================================
- Hits        50620    50618     -2     
+ Misses       9566     9084   -482     
- Partials      876     1356   +480     
Flag Coverage Δ
23.11.6 7.19% <0.00%> (-0.02%) ⬇️
24.11.5 7.17% <0.00%> (-0.04%) ⬇️
25.11.1 7.17% <0.00%> (-0.01%) ⬇️
alpine320 77.08% <100.00%> (+<0.01%) ⬆️
fedora42 76.65% <100.00%> (+<0.01%) ⬆️
macos-15 82.46% <100.00%> (+<0.01%) ⬆️
macos-26 82.47% <100.00%> (+<0.01%) ⬆️
macos-26-intel 82.39% <100.00%> (+<0.01%) ⬆️
mingw32 71.70% <ø> (ø)
mingw64 71.69% <ø> (+0.07%) ⬆️
npcap ?
rhel94 76.44% <100.00%> (-0.02%) ⬇️
ubuntu2204 76.50% <100.00%> (+<0.01%) ⬆️
ubuntu2404 76.78% <100.00%> (-0.02%) ⬇️
ubuntu2604 76.71% <100.00%> (+<0.01%) ⬆️
ubuntu2604-arm64 76.55% <100.00%> (-0.02%) ⬇️
ubuntu2604-icpx 59.27% <ø> (ø)
unittest 82.90% <100.00%> (+<0.01%) ⬆️
windows-2022 85.89% <ø> (+0.10%) ⬆️
windows-2025 85.64% <ø> (+0.11%) ⬆️
winpcap 85.92% <ø> (+0.19%) ⬆️
xdp 53.77% <0.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@seladb

seladb commented Sep 14, 2026

Copy link
Copy Markdown
Owner

@rshayer95 I found the commit when I made this change: 6379100

To be honest, I don't remember why I made this change - in a 5-tuple splitter, it makes sense to include all packets of the same 5-tuple in one file...

In any case, I'm not sure your fix is the right one because it add a number to each file generated from now on. Maybe we should just revert my PR and let all packets to be written to the same file?

@rshayer95

Copy link
Copy Markdown
Author

@rshayer95 I found the commit when I made this change: 6379100

To be honest, I don't remember why I made this change - in a 5-tuple splitter, it makes sense to include all packets of the same 5-tuple in one file...

In any case, I'm not sure your fix is the right one because it add a number to each file generated from now on. Maybe we should just revert my PR and let all packets to be written to the same file?

@seladb Thanks for finding the original commit.

Looking at it, the intent was clearly separate files per session — the comment says as much. The filename side just never got updated to match, which is where the collision comes from.

Fair point on adding the number to every file — that changes output for everyone, including captures with no reuse.

On reverting: it fixes the collision but merges two distinct sessions into one file with no indication they're separate. Silent correctness issue rather than a visible one.

We can have a middle ground by implementing suffixes only when an actual collision occurs. The first session can keep its current filename while subsequent ones are assigned numbers. This way captures without reuse remain unchanged and important sessions stay separated where necessary.

Happy to rework it that way, or revert if you had prefer simpler.

@seladb

seladb commented Sep 14, 2026

Copy link
Copy Markdown
Owner

We can have a middle ground by implementing suffixes only when an actual collision occurs. The first session can keep its current filename while subsequent ones are assigned numbers. This way captures without reuse remain unchanged and important sessions stay separated where necessary.

Sure, I think that could work 👍
When making this change, you can add tests for it. We have tests written in Python for the examples: https://github.com/seladb/PcapPlusPlus/blob/master/Tests/ExamplesTest/tests/test_pcapsplitter.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: FiveTupleSplitter causes file truncation/corruption on TCP session reuse (PcapSplitter)

3 participants