Skip to content

tarfile: zstopen uses except Exception: and leaks fileobj on BaseException, inconsistent with gzopen/bz2open/xzopen #150077

@lpyu001

Description

@lpyu001

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

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    stdlibStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or error

    Projects

    Status

    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions