Repository: python/cpython
Pull request: gh-107396: tarfiles: set self.exception before _init_read_gz()
In the stack call of: _init_read_gz()
```
_read, tarfile.py:548
read, tarfile.py:526
_init_read_gz, tarfile.py:491
```
a try;except exists that uses `self.exception`, so it needs to be set before calling _init_read_gz().

closes: #107396


<!-- gh-issue-number: gh-107396 -->
* Issue: gh-107396
<!-- /gh-issue-number -->


Problem statement:
tarfiles: `AttributeError: '_Stream' object has no attribute 'exception'` while trying to open tgz file
# Bug report

If a tar file appears to be a `tar.gz` file, it can fail in this block of `tarfile.py`

```
        while c < size:
            # Skip underlying buffer to avoid unaligned double buffering.
            if self.buf:
                buf = self.buf
                self.buf = b""
            else:
                buf = self.fileobj.read(self.bufsize)
                if not buf:
                    break
            try:
                buf = self.cmp.decompress(buf)
            except self.exception as e:
                raise ReadError("invalid compressed data") from e
            t.append(buf)
            c += len(buf)
```
at:
```
buf = self.cmp.decompress(buf)
```
before `self.exception` is set.
Because around:

```
            if comptype == "gz":
                try:
                    import zlib
                except ImportError:
                    raise CompressionError("zlib module is not available") from None
                self.zlib = zlib
                self.crc = zlib.crc32(b"")
                if mode == "r":
                    self._init_read_gz()
                    self.exception = zlib.error
                else:
                    self._init_write_gz()
```
This: `self._init_read_gz()`
is called before `self.exception = zlib.error`

Stack trace:
```
_read, tarfile.py:548
read, tarfile.py:526
_init_read_gz, tarfile.py:491
__init__, tarfile.py:375
open, tarfile.py:1827
<module>, test.py:2
```

In my case, `buf = self.cmp.decompress(buf)` failed to execute but that's a separate issue that may or may not be a bug

# Your environment

<!-- Include as many relevant details as possible about the environment you experienced the bug in -->

- CPython versions tested on: 3.11.4
- Operating system and architecture: Windows 10 21H2


<!-- gh-linked-prs -->
### Linked PRs
* gh-107485
* gh-108207
* gh-108208
<!-- /gh-linked-prs -->

Hints from issue discussion:
Looks reasonable. Will you open a PR?
Sure
Hello,
How does this issue manifest? `exception` appears like an write-only attribute of an internal class.
I'd like to add a test for the PR, but can't think of a reasonable one. Do you have a use case we currently don't have tests for?
@encukou Please see: https://github.com/python/cpython/issues/107398

Maybe that one will explain better the sequence where this failure can happen.

My interpretation is that gzip supports backwards skipping and it appears to be rarely used. If I open a tgz with `r|*` (which, by design, doesn't support backwards skipping) and the gzip stream requests a backwards skip, an exception is raised. At the `except` statement it has `except self.exception` which is were the attribute is read, which means the attribute is actually read-write.
Oh, I see!
Here's a reproducer:
```python
with open('hm.tar.gz', 'rb') as f:
    f = io.BytesIO(
        b'\x1f\x8b'  # header
        + b'\x08'  # compression method
        + b'\x04'  # flags
        + b'\0\0\0\0\0\0'  # padding?
        + b'\0\x01'  # size
        + bytes(5)  # corrupt data (zeros)
    )
    tarfile.open(fileobj=f, mode='r|gz')
```
If you can add it as a test using `with self.assertRaises(tarfile.ReadError):` to `Lib/test/test_tarfile.py`, it'd speed things up. If not I'll get to it later.

OK. I will.

Commit message: 📜🤖 Added by blurb_it.

Patch to review:
diff --git a/Lib/tarfile.py b/Lib/tarfile.py
index df4e41f7a0d23a..54293949e88fd1 100755
--- a/Lib/tarfile.py
+++ b/Lib/tarfile.py
@@ -372,8 +372,8 @@ def __init__(self, name, mode, comptype, fileobj, bufsize,
                 self.zlib = zlib
                 self.crc = zlib.crc32(b"")
                 if mode == "r":
-                    self._init_read_gz()
                     self.exception = zlib.error
+                    self._init_read_gz()
                 else:
                     self._init_write_gz(compresslevel)
 
diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py
index 2eda7fc4ceac71..d646611c331720 100644
--- a/Lib/test/test_tarfile.py
+++ b/Lib/test/test_tarfile.py
@@ -938,6 +938,23 @@ class LzmaDetectReadTest(LzmaTest, DetectReadTest):
     pass
 
 
+class GzipBrokenHeaderCorrectException(GzipTest, unittest.TestCase):
+    """
+    See: https://github.com/python/cpython/issues/107396
+    """
+    def runTest(self):
+        f = io.BytesIO(
+            b'\x1f\x8b'  # header
+            b'\x08'  # compression method
+            b'\x04'  # flags
+            b'\0\0\0\0\0\0'  # padding?
+            b'\0\x01'  # size
+            b'\0\0\0\0\0'  # corrupt data (zeros)
+        )
+        with self.assertRaises(tarfile.ReadError):
+            tarfile.open(fileobj=f, mode='r|gz')
+
+
 class MemberReadTest(ReadTest, unittest.TestCase):
 
     def _test_member(self, tarinfo, chksum=None, **kwargs):
diff --git a/Misc/NEWS.d/next/Library/2023-07-31-07-36-24.gh-issue-107396.3_Kh6D.rst b/Misc/NEWS.d/next/Library/2023-07-31-07-36-24.gh-issue-107396.3_Kh6D.rst
new file mode 100644
index 00000000000000..73bff4bdbe024d
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-07-31-07-36-24.gh-issue-107396.3_Kh6D.rst
@@ -0,0 +1 @@
+tarfiles; Fixed use before assignment of self.exception for gzip decompression


Code review comments:
diff_hunk: @@ -938,6 +938,23 @@ class LzmaDetectReadTest(LzmaTest, DetectReadTest):
     pass
 
 
+class GzipBrokenHeaderCorrectException(GzipTest, unittest.TestCase):
+    """
+    See: https://github.com/python/cpython/issues/107396
+    """
+    def runTest(self):
+        f = io.BytesIO(
+            b'\x1f\x8b'  # header
+            b'\x08'  # compression method
+            b'\x04'  # flags
+            b'\0\0\0\0\0\0'  # padding?
path: Lib/test/test_tarfile.py
text: @user1:
```suggestion
            b'\0\0\0\0\0\0'  # timestamp, compression data, OS ID
```