From de0775551346558a36118387f9402968d4c3b04e Mon Sep 17 00:00:00 2001 From: Thierry Gosselin Date: Sun, 6 Sep 2026 13:50:54 +1000 Subject: [PATCH 1/2] Reject truncated and undersized BGZF input blocks Distinguish normal end-of-file from incomplete BGZF headers and bodies. Report input read errors explicitly instead of treating them as EOF. Validate block sizes before subtracting the header length, preventing unsigned underflow for undersized blocks. Preserve support for concatenated BGZF streams and complete streams without a terminal EOF marker. --- src/bgzf.h | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/bgzf.h b/src/bgzf.h index eb6eb241..f4ee161c 100644 --- a/src/bgzf.h +++ b/src/bgzf.h @@ -10,6 +10,7 @@ #include #include #include +#include "util.h" static const int BGZF_HEADER_SIZE = 18; static const int BGZF_MAX_BLOCK_SIZE = 65536; @@ -136,14 +137,25 @@ class BgzfMtReader { } size_t n = fread(header, 1, BGZF_HEADER_SIZE, mFp); - if (n < BGZF_HEADER_SIZE) { markDone(s); break; } + if (ferror(mFp)) + error_exit("BGZF input read error while reading block header"); + if (n == 0) { markDone(s); break; } + if (n < BGZF_HEADER_SIZE) + error_exit("Truncated BGZF block header"); uint32_t bsize = bgzfBlockSize(header); - if (bsize == 0 || bsize > BGZF_MAX_BLOCK_SIZE) { markDone(s); break; } + // Reject undersized blocks before subtracting the header length. + // A BGZF block includes the header, deflate stream and 8-byte trailer. + if (bsize < BGZF_HEADER_SIZE + 8 || bsize > BGZF_MAX_BLOCK_SIZE) + error_exit("Invalid BGZF block size or header"); memcpy(s.comp, header, BGZF_HEADER_SIZE); size_t rest = bsize - BGZF_HEADER_SIZE; - if (fread(s.comp + BGZF_HEADER_SIZE, 1, rest, mFp) < rest) { markDone(s); break; } + size_t bodyRead = fread(s.comp + BGZF_HEADER_SIZE, 1, rest, mFp); + if (ferror(mFp)) + error_exit("BGZF input read error while reading block body"); + if (bodyRead < rest) + error_exit("Truncated BGZF block body"); if (bsize == 28) { uint32_t isize = s.comp[24]|(s.comp[25]<<8)|(s.comp[26]<<16)|(s.comp[27]<<24); From a632eec776296327121514cdd92eebdcb3d8d700 Mon Sep 17 00:00:00 2001 From: Thierry Gosselin Date: Sun, 6 Sep 2026 13:52:09 +1000 Subject: [PATCH 2/2] Add regression tests for truncated BGZF input Test incomplete headers, payloads, trailers, and undersized blocks using synthetic BGZF fixtures with one and four processing threads. Verify that valid streams, concatenated streams, and complete streams without a terminal EOF marker preserve the input reads. All 14 cases pass with the fix. The unchanged BGZF reader incorrectly returns success for all eight malformed-input cases. --- scripts/test_bgzf_truncated_input.py | 71 ++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 scripts/test_bgzf_truncated_input.py diff --git a/scripts/test_bgzf_truncated_input.py b/scripts/test_bgzf_truncated_input.py new file mode 100644 index 00000000..9745f2e9 --- /dev/null +++ b/scripts/test_bgzf_truncated_input.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Run: python3 scripts/test_bgzf_truncated_input.py ./fastp + +Synthetic BGZF fixtures; no external data or compression tools required. +""" +import gzip +import pathlib +import struct +import subprocess +import sys +import tempfile +import zlib + + +def block(data): + compressor = zlib.compressobj(wbits=-15) + payload = compressor.compress(data) + compressor.flush() + size = 18 + len(payload) + 8 + assert size <= 65536 + return (b'\x1f\x8b\x08\x04' + b'\x00'*4 + b'\x00\xff' + + struct.pack(' 0 and diagnostic.encode() in run.stderr + else: + ok = (run.returncode == 0 and output.exists() and + output.read_bytes() == expected) + detail = 'exit=%d' % run.returncode + except subprocess.TimeoutExpired: + ok, detail = False, 'timeout' + print('%s %s threads=%d %s' % + ('PASS' if ok else 'FAIL', name, threads, detail)) + failed += not ok + return bool(failed) + + +if __name__ == '__main__': + sys.exit(main())