Bug report
Bug description:
Bug report
Bug description
In Lib/tarfile.py, the four compressed-archive open classmethods all share
the same structure: open an underlying compressed file object, then wrap it
with cls.taropen(...). If taropen fails, they catch the exception, close
the just-opened fileobj, and re-raise.
Three of them — gzopen , bz2open , xzopen — use a bare except: for the final cleanup branch. zstopen
uses except Exception: (line 2099) instead:
# Lib/tarfile.py — gzopen / bz2open / xzopen (consistent across all three) except: fileobj.close() raise # Lib/tarfile.py — zstopen (the outlier) except Exception: fileobj.close() raise
A bare except: is equivalent to except BaseException: and catches
KeyboardInterrupt, SystemExit, and asyncio.CancelledError (which is a
BaseException subclass since 3.8). except Exception: does not. As a
result, if any of those exceptions is raised during the cls.taropen(...)
call inside zstopen — e.g. the user hits Ctrl+C, the surrounding async task
is cancelled, or the process is shutting down — fileobj.close() is never
called and the file descriptor held by ZstdFile is leaked.
The same scenario is handled correctly by gzopen/bz2open/xzopen.
This is the classic "catch-and-cleanup-and-rethrow" pattern: the bare
except: is intentional here, because the only purpose of the handler is to
release the resource before letting the exception propagate. The exception
is not swallowed.
Reproducer
import unittest.mock, tarfile fileobj = unittest.mock.Mock() with unittest.mock.patch("compression.zstd.ZstdFile", return_value=fileobj), \ unittest.mock.patch.object(tarfile.TarFile, "taropen", side_effect=KeyboardInterrupt): try: tarfile.TarFile.zstopen("foo.tar.zst") except KeyboardInterrupt: pass print("close called:", fileobj.close.called)
Actual output on current main:
close called: False
Expected (matching gzopen/bz2open/xzopen behavior):
close called: True
Replacing "compression.zstd.ZstdFile" with "gzip.GzipFile" (and
zstopen with gzopen) prints True, confirming the inconsistency.
Suggested fix
In Lib/tarfile.py:2099, change except Exception: to bare except: (or
the equivalent except BaseException:), matching the existing convention in
gzopen/bz2open/xzopen:
- except Exception: + except: fileobj.close() raise
A parameterized regression test across all four compressed-open methods
should be added to Lib/test/test_tarfile.py so that any future compressed
format added to tarfile is held to the same cleanup contract.
CPython versions tested on:
CPython main branch
Operating systems tested on:
Windows