Skip to content

sniff: reap the tcpdump prefilter subprocess in offline mode#5046

Open
eugen-goebel wants to merge 2 commits into
secdev:masterfrom
eugen-goebel:fix-tcpdump-zombie-getproc
Open

sniff: reap the tcpdump prefilter subprocess in offline mode#5046
eugen-goebel wants to merge 2 commits into
secdev:masterfrom
eugen-goebel:fix-tcpdump-zombie-getproc

Conversation

@eugen-goebel

Copy link
Copy Markdown
Contributor

Problem

sniff(offline="file.pcap", filter="tcp") prefilters packets through a tcpdump subprocess. The offline path built PcapReader(tcpdump(..., getfd=True)), which returns only the stdout pipe, so the Popen object was discarded and never wait()ed. The process was left as a zombie, and Python 3 reports:

ResourceWarning: subprocess NNNNN is still running

Reproduction on current master:

from scapy.all import sniff, wrpcap, IP, TCP
wrpcap("/tmp/t.pcap", [IP()/TCP()] * 5)
sniff(offline="/tmp/t.pcap", filter="tcp")   # leaves a <defunct> tcpdump child

Fix

As discussed in the issue, the reader now takes the process via getproc=True and owns its lifetime. RawPcapReader.close() closes the read pipe, sends a still-running tcpdump a SIGTERM, and wait()s for it, so nothing is left behind. This also covers stopping early (count=, timeout=), where tcpdump has not exited on its own.

Thanks to @AbhishekPandey1998 for scoping the getproc approach in the thread.

Test

Added a regression test in test/regression.uts that reads a filtered offline capture (a full read and an early count= stop) and asserts no subprocess ... still running ResourceWarning is emitted.

Fixes #4512

sniff(offline=..., filter=...) prefilters packets through a tcpdump
subprocess, but the code kept only its stdout pipe and discarded the
Popen. The process was never waited on, so it lingered as a zombie and
Python emitted "ResourceWarning: subprocess N is still running".

Get the process with getproc=True, attach it to the PcapReader, and reap
it in close(): the read pipe is closed first, a still-running tcpdump then
gets a SIGTERM, and wait() collects it. This also handles early stops
(count=, timeout=), where tcpdump has not finished on its own.

Fixes secdev#4512

AI-Assisted: yes (Claude Opus 4.8)
@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.75000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 80.47%. Comparing base (de33992) to head (d2b66e6).
⚠️ Report is 11 commits behind head on master.

Files with missing lines Patch % Lines
scapy/sendrecv.py 90.90% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #5046      +/-   ##
==========================================
+ Coverage   80.13%   80.47%   +0.33%     
==========================================
  Files         388      388              
  Lines       96467    96657     +190     
==========================================
+ Hits        77308    77785     +477     
+ Misses      19159    18872     -287     
Files with missing lines Coverage Δ
scapy/utils.py 72.30% <100.00%> (+0.11%) ⬆️
scapy/sendrecv.py 87.26% <90.90%> (+1.22%) ⬆️

... and 25 files with indirect coverage changes

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

@guedou

guedou commented Jul 21, 2026

Copy link
Copy Markdown
Member

Looks good to me, but it only covers the filename paths.

Would you mind extending the same fix to the others ? One
is triggered by the existing sniff(offline=IP()/UDP(...), filter="tcp") test.

… paths

The previous commit only covered the offline sources given as filenames.
The packet iterable branch (sniff(offline=IP()/UDP(...), filter=...)) and
the file descriptor branch still built their PcapReader from a bare
getfd=True pipe, so the tcpdump process was left unreaped there.

Route both through the existing _offline_pcap_reader helper, generalized
to take any source tcpdump() accepts (filename, IterSocket, open file
descriptor), so every filtered offline path attaches the process to the
reader and reaps it in close().

AI-Assisted: yes (Claude Opus 5)
@eugen-goebel

Copy link
Copy Markdown
Contributor Author

Thanks @guedou, good catch. Extended the same treatment to the remaining two paths: the packet iterable branch (the one your sniff(offline=IP()/UDP(...), filter="tcp") example hits) and the file descriptor branch. Both still built their PcapReader from a bare getfd=True pipe, so the tcpdump process was left unreaped there.

They now share the same _offline_pcap_reader helper, generalized to take any source tcpdump() accepts (filename, IterSocket, open file descriptor), so every filtered offline path attaches the process to the reader and reaps it in close(). It ends up slightly less code than before.

Verified on the packet path: before the change one child process is left unreaped after sniff(offline=IP()/UDP(...), filter="tcp"), after it none. The existing offline sniff tests (by filename and by file object, with and without a filter) still pass.

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.

Zombie tcpdump Process After Using sniff() on pcap with Filter

2 participants