Extractor._cleanup() closes the input file only when the caller supplied the stream, and never when Extractor opened it itself. Ownership is inverted: it leaks what it owns and closes what it does not.
Mechanism
pcapkit/foundation/extraction.py records how the input arrived:
self._flag_s = isinstance(fin, str) # input filename flag
...
if self._flag_s:
self._ifile = open(ifnm, 'rb') # pcapkit opens it
else:
... # caller supplied a stream
and _cleanup() at extraction.py:1095-1099 closes under the negation of that flag:
if isinstance(self._ifile, SeekableReader):
self._ifile.close()
elif not self._flag_s:
self._ifile.close()
So _flag_s is True — pcapkit opened the file — takes neither branch, because a plain open() returns a BufferedReader rather than a SeekableReader. The handle is never closed. _flag_s is False — the caller owns the stream — is closed, which is also the wrong choice on its own terms: closing a caller's file object is not Extractor's to do.
Measured
On origin/main (4529fdb1f), CPython 3.14.7, with ResourceWarning always enabled and an explicit gc.collect():
path given (pcapkit opens it): 1 unclosed
stream given (caller owns it) : 0 unclosed
The leaked warning is the familiar one:
ResourceWarning: unclosed file <_io.BufferedReader name='.../examples/captures/in.pcap'>
Why this is worth fixing beyond tidiness
This is the production root cause of #606, the flake that reddened three unrelated pull requests — #577, #596 and #600 — and was observed migrating between two of them in eleven minutes with neither branch touched. #606's fix made the assertion in tests/utilities/test_stacklevel.py immune to foreign warnings, which is what made the CI signal safe. It did not stop the leak, and could not: the leak is here, in library code.
Confirmation that the leak survives #606: pytest tests/integration/test_runtime_extract.py tests/protocols/misc/pcap/test_frame_runtime.py -W always::ResourceWarning still emits two unclosed-in.pcap warnings. Any test or caller that passes a path leaks a descriptor, and on a long-running process that accumulates.
Extractor(fin=<path>) is the ordinary documented usage, so this affects normal callers rather than a corner.
Suggested direction
Close when _flag_s is true and leave the caller's stream alone when it is false — the inverse of the present condition. A context-manager protocol on Extractor would be the fuller answer, but the one-line inversion is the defect.
A regression test should assert on ResourceWarning for the path-given case specifically, since the stream-given case has always been quiet and would not catch a reintroduction.
Notes
Extractor._cleanup()closes the input file only when the caller supplied the stream, and never whenExtractoropened it itself. Ownership is inverted: it leaks what it owns and closes what it does not.Mechanism
pcapkit/foundation/extraction.pyrecords how the input arrived:and
_cleanup()atextraction.py:1095-1099closes under the negation of that flag:So
_flag_s is True— pcapkit opened the file — takes neither branch, because a plainopen()returns aBufferedReaderrather than aSeekableReader. The handle is never closed._flag_s is False— the caller owns the stream — is closed, which is also the wrong choice on its own terms: closing a caller's file object is notExtractor's to do.Measured
On
origin/main(4529fdb1f), CPython 3.14.7, withResourceWarningalways enabled and an explicitgc.collect():The leaked warning is the familiar one:
Why this is worth fixing beyond tidiness
This is the production root cause of #606, the flake that reddened three unrelated pull requests — #577, #596 and #600 — and was observed migrating between two of them in eleven minutes with neither branch touched. #606's fix made the assertion in
tests/utilities/test_stacklevel.pyimmune to foreign warnings, which is what made the CI signal safe. It did not stop the leak, and could not: the leak is here, in library code.Confirmation that the leak survives #606:
pytest tests/integration/test_runtime_extract.py tests/protocols/misc/pcap/test_frame_runtime.py -W always::ResourceWarningstill emits two unclosed-in.pcapwarnings. Any test or caller that passes a path leaks a descriptor, and on a long-running process that accumulates.Extractor(fin=<path>)is the ordinary documented usage, so this affects normal callers rather than a corner.Suggested direction
Close when
_flag_sis true and leave the caller's stream alone when it is false — the inverse of the present condition. A context-manager protocol onExtractorwould be the fuller answer, but the one-line inversion is the defect.A regression test should assert on
ResourceWarningfor the path-given case specifically, since the stream-given case has always been quiet and would not catch a reintroduction.Notes
main, and this is library code that needs a PR.tests/utilities/test_logging.py, which is the file that actually leaked intotest_stacklevel's directory, because my crudeclose-ishtally was satisfied by an unrelatedwith openelsewhere in that file; and it wrongly includedtests/_tiers.pyandtests/test_tier_guard.py, which mentionin.pcaponly in prose. The tally was a proxy and should have been labelled as a weaker one than it was.