You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Gzip header corruption not properly checked. #89672
Bump. This is a bug that allows corrupted gzip files to be processed without error. Therefore I bump this issue in the hopes someone will review the PR.
I think it is good idea, although we may get reports about regressions in 3.11 when Python will start to reject GZIP files which was successfully read before.
But before merging this I want to know:
How much overhead does it add for reading files with the FHCRC flag clear?
How much overhead does it add for reading files with the FHCRC flag set?
Are GZIP files created by other tools has the FHCRC flag by default?
rhpvorderman commented on Nov 22, 2021
rhpvordermanmannequin
MannequinAuthor
More actions
Quite a lot
I tested it for the two most common use case.
import timeit
import statistics
That is a dramatic increase in overhead. (Okay the decompressed data is empty, but still)
Haven't tested this yet. But the regression is quite unacceptable already.
Not that I know of. But if it is set, it is safe to assume they care. Nevertheless this is a bit of an edge-case.
rhpvorderman commented on Nov 23, 2021
rhpvordermanmannequin
MannequinAuthor
More actions
I increased the performance of the patch. I added the file used for benchmarking. I also test the FHCRC changes now.
The benchmark tests headers with different flags concatenated to a DEFLATE block with no data and a gzip trailer. The data is fed to gzip.decompress. Please note that this is the *worst-case* performance overhead. When there is actual data to decompress the overhead will get less. When GzipFile is used the overhead will get less as well.
An increase of .1 microsecond in the most common use cases. Roughly 3%. But now the FNAME field is correctly checked for truncation.
With the FHCRC the overhead is increased by 33%. But this is worth it, because the header is now actually checked. As it should.
rhpvorderman commented on Nov 24, 2021
rhpvordermanmannequin
MannequinAuthor
More actions
I have found that using the timeit module provides more precise measurements:
For a simple gzip header. (As returned by gzip.compress or zlib.compress with wbits=31)
./python -m timeit -s "import io; data = b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00'; from gzip import _read_gzip_header" '_read_gzip_header(io.BytesIO(data))'
For a gzip header with FNAME. (Returned by gzip itself and by Python's GzipFile)
./python -m timeit -s "import io; data = b'\x1f\x8b\x08\x08j\x1a\x9ea\x02\xffcompressable_file\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00'; from gzip import _read_gzip_header" '_read_gzip_header(io.BytesIO(data))'
For a gzip header with all flags set:
./python -m timeit -s 'import gzip, io; data = b"\x1f\x8b\x08\x1f\x00\x00\x00\x00\x00\xff\x05\x00extraname\x00comment\x00\xe9T"; from gzip import _read_gzip_header' '_read_gzip_header(io.BytesIO(data))'
Since performance is most critical for in-memory compression and decompression, I now optimized for no flags.
Before (current main): 500000 loops, best of 5: 469 nsec per loop
after (PR): 1000000 loops, best of 5: 390 nsec per loop
For the most common case of only FNAME set:
before: 200000 loops, best of 5: 1.48 usec per loop
after: 200000 loops, best of 5: 1.45 usec per loop
For the case where FCHRC is set:
before: 200000 loops, best of 5: 1.62 usec per loop
after: 100000 loops, best of 5: 2.43 usec per loop
So this PR is now a clear win for decompressing anything that has been compressed with gzip.compress. It is neutral for normal file decompression. There is a performance cost associated with correctly checking the header, but that is expected. It is better than the alternative of not checking it.
Thank you for your contribution @rhpvorderman. This feature will only be added in 3.15+, because there is a small chance of breaking user code that worked before. If this becomes a common problem, we can even add a parameter to make the verification optional.
Sure, make sense. Looks like nobody missed it before even. python-isal and python-zlib-ng check headers by default and I have had nobody complain yet. It looks like the FHCRC flag is hardly ever used.
Still SPEC compliance is important I feel. A specification feature should either be supported or be documented as explicitly not supported. Simply supporting it seems like the easiest option here.
Activity
rhpvorderman commentedon Oct 18, 2021
The following headers are currently allowed while being wrong:
rhpvorderman commentedon Nov 5, 2021
Bump. This is a bug that allows corrupted gzip files to be processed without error. Therefore I bump this issue in the hopes someone will review the PR.
rhpvorderman commentedon Nov 22, 2021
Ping
serhiy-storchaka commentedon Nov 22, 2021
I think it is good idea, although we may get reports about regressions in 3.11 when Python will start to reject GZIP files which was successfully read before.
But before merging this I want to know:
rhpvorderman commentedon Nov 22, 2021
I tested it for the two most common use case.
import timeit
import statistics
BEFORE:
with_fname
average: 3.27, range: 3.21-3.36 stdev: 0.05
with_noflags
average: 3.24, range: 3.14-3.37 stdev: 0.07
AFTER:
with_fname
average: 4.98, range: 4.85-5.14 stdev: 0.1
with_noflags
average: 4.87, range: 4.69-5.05 stdev: 0.1
That is a dramatic increase in overhead. (Okay the decompressed data is empty, but still)
Haven't tested this yet. But the regression is quite unacceptable already.
Not that I know of. But if it is set, it is safe to assume they care. Nevertheless this is a bit of an edge-case.
rhpvorderman commentedon Nov 23, 2021
I increased the performance of the patch. I added the file used for benchmarking. I also test the FHCRC changes now.
The benchmark tests headers with different flags concatenated to a DEFLATE block with no data and a gzip trailer. The data is fed to gzip.decompress. Please note that this is the *worst-case* performance overhead. When there is actual data to decompress the overhead will get less. When GzipFile is used the overhead will get less as well.
BEFORE (Current main branch):
$ ./python benchmark_gzip_read_header.py
with_fname
average: 3.01, range: 2.9-4.79 stdev: 0.19
with_noflags
average: 2.99, range: 2.93-3.04 stdev: 0.02
All flags (incl FHCRC)
average: 3.13, range: 3.05-3.16 stdev: 0.02
After (bpo-45509 PR):
with_fname
average: 3.09, range: 3.01-4.63 stdev: 0.16
with_noflags
average: 3.1, range: 3.03-3.38 stdev: 0.04
All flags (incl FHCRC)
average: 4.09, range: 4.05-4.49 stdev: 0.04
An increase of .1 microsecond in the most common use cases. Roughly 3%. But now the FNAME field is correctly checked for truncation.
With the FHCRC the overhead is increased by 33%. But this is worth it, because the header is now actually checked. As it should.
rhpvorderman commentedon Nov 24, 2021
I have found that using the timeit module provides more precise measurements:
For a simple gzip header. (As returned by gzip.compress or zlib.compress with wbits=31)
./python -m timeit -s "import io; data = b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00'; from gzip import _read_gzip_header" '_read_gzip_header(io.BytesIO(data))'
For a gzip header with FNAME. (Returned by gzip itself and by Python's GzipFile)
./python -m timeit -s "import io; data = b'\x1f\x8b\x08\x08j\x1a\x9ea\x02\xffcompressable_file\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00'; from gzip import _read_gzip_header" '_read_gzip_header(io.BytesIO(data))'
For a gzip header with all flags set:
./python -m timeit -s 'import gzip, io; data = b"\x1f\x8b\x08\x1f\x00\x00\x00\x00\x00\xff\x05\x00extraname\x00comment\x00\xe9T"; from gzip import _read_gzip_header' '_read_gzip_header(io.BytesIO(data))'
Since performance is most critical for in-memory compression and decompression, I now optimized for no flags.
Before (current main): 500000 loops, best of 5: 469 nsec per loop
after (PR): 1000000 loops, best of 5: 390 nsec per loop
For the most common case of only FNAME set:
before: 200000 loops, best of 5: 1.48 usec per loop
after: 200000 loops, best of 5: 1.45 usec per loop
For the case where FCHRC is set:
before: 200000 loops, best of 5: 1.62 usec per loop
after: 100000 loops, best of 5: 2.43 usec per loop
So this PR is now a clear win for decompressing anything that has been compressed with gzip.compress. It is neutral for normal file decompression. There is a performance cost associated with correctly checking the header, but that is expected. It is better than the alternative of not checking it.
serhiy-storchaka commentedon May 13, 2026
Thank you for your contribution @rhpvorderman. This feature will only be added in 3.15+, because there is a small chance of breaking user code that worked before. If this becomes a common problem, we can even add a parameter to make the verification optional.
rhpvorderman commentedon May 15, 2026
Sure, make sense. Looks like nobody missed it before even. python-isal and python-zlib-ng check headers by default and I have had nobody complain yet. It looks like the FHCRC flag is hardly ever used.
Still SPEC compliance is important I feel. A specification feature should either be supported or be documented as explicitly not supported. Simply supporting it seems like the easiest option here.