Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wqaie-002Ik3-1B for pgsql-bugs@arkaria.postgresql.org; Sun, 02 Aug 2026 18:15:28 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wqaid-003Dgf-1H for pgsql-bugs@arkaria.postgresql.org; Sun, 02 Aug 2026 18:15:27 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wqaM2-0035S5-1H for pgsql-bugs@lists.postgresql.org; Sun, 02 Aug 2026 17:52:06 +0000 Received: from mahout.postgresql.org ([2001:4800:3e1:1::227]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.98.2) (envelope-from ) id 1wqaM0-00000001a7F-2vgz for pgsql-bugs@lists.postgresql.org; Sun, 02 Aug 2026 17:52:05 +0000 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=postgresql.org; s=20171124; h=Message-ID:Date:Reply-To:Cc:From:To:Subject: Content-Transfer-Encoding:MIME-Version:Content-Type:Sender:Content-ID: Content-Description:In-Reply-To:References; bh=KGyVvnP3mEJtonZ245oIGU0o8i/EZpL1j07tgdgturY=; b=ha+LIckCs1L01ilR5z7uHTmzSq lYniF8BAiUqRh8UnGF9xuSacqCeiwhOH/GlRp5NM1qaGXmjzJPaMakMgP2yxGQX1X7GVHQ0iFktdo MikyQ6h9NFFQajkpfV6UCwrJ2P53hDjS+FT0kugoT1nPJO5COQudtaZIGPsc/IRJlg9I3IBxqSv6J LR25ut/ZlxLsaZdEIgcl51PCrmnEXhn6lW75x19nf7igi5SRG7h68a3ABdBBfidcw3HqCBVLXXbut /1XfaVc6aCmrvY2p9+XQKdgHqCXxNWs4gr5Af3ZihxK+2uo7efFwj3fzr1fhrC9JgMNrQDxhrkQZW ndTQq4CA==; Received: from wrigleys.postgresql.org ([2a02:16a8:dc51::60]) by mahout.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wqaLz-000bEY-2h for pgsql-bugs@lists.postgresql.org; Sun, 02 Aug 2026 17:52:04 +0000 Received: from localhost ([127.0.0.1] helo=wrigleys.postgresql.org) by wrigleys.postgresql.org with esmtp (Exim 4.98.2) (envelope-from ) id 1wqaLz-0000000Bapl-213v for pgsql-bugs@lists.postgresql.org; Sun, 02 Aug 2026 17:52:03 +0000 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Subject: BUG #19599: RestoreBlockImage: the decode cross-checks never bound hole_offset + hole_length against BLCKSZ To: pgsql-bugs@lists.postgresql.org From: PG Bug reporting form Cc: malis@pgrust.com Reply-To: malis@pgrust.com, pgsql-bugs@lists.postgresql.org Date: Sun, 02 Aug 2026 17:51:51 +0000 Message-ID: <19599-8859c3822a831331@postgresql.org> X-Auto-Response-Suppress: All Auto-Submitted: auto-generated List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk The following bug has been logged on the website: Bug reference: 19599 Logged by: Michael Malis Email address: malis@pgrust.com PostgreSQL version: 18.3 Operating system: Debian Description: =20 I don't think this is a real issue because it requires a specific WAL structure, but I figured I would report it anyway. DecodeXLogRecord cross-checks a block image's hole descriptor for non-zero-ness only. It never checks that the hole fits inside the page. A record that sets both hole_offset and hole_length large =E2=80=94 each is a= uint16, so up to 65535 =E2=80=94 passes validation, and RestoreBlockImage then uses= those values directly as memcpy/MemSet offsets and lengths into an 8 kB page buffer. Evidence status =E2=80=94 please read ----------------------------- - What we verified: the missing bound, by reading 18.3. Line numbers below. - What we did NOT do: execute the overrun. Our differential harness detects the out-of-bounds geometry and skips the C oracle for those inputs precisely so it never drives undefined behaviour, so no ASan/valgrind report exists and we cannot attach one. We also have no live-server reproducer, because constructing the record requires authoring WAL with a valid CRC over the malformed body =E2=80=94 our builder does this inside = the harness, not against a server. - What is positively demonstrated: our reimplementation rejects this geometry, and that rejection is asserted under fuzzing (one-sided). That is evidence the input class is reachable through decode, not evidence about C's behaviour. We would rather file this as "here is a missing check, here is the input that reaches it" than overstate it. If you want the overrun demonstrated under a sanitizer before considering it, that is a reasonable ask and we can do it. Reproducer (harness-level) -------------------------- A WAL record whose block-image header carries, with a valid CRC over the body: bimg_len =3D 16 hole_offset =3D 8000 hole_length =3D 8000 (8000 + 8000 =3D 16000 > BLCKSZ 8192) bimg_info =3D BKPIMAGE_HAS_HOLE | BKPIMAGE_APPLY | BKPIMAGE_COMPRESS_PGLZ (0x07) hole_length is an on-wire field only for COMPRESSED + HAS_HOLE images, which is why the shape is specifically a compressed image. Expected vs. actual ------------------- - Expected: decode rejects the record with an invalid-state error, as it does for the zero-valued cases it already checks. - Actual (by inspection): decode accepts it and the reconstruction arithmetic runs with a hole larger than the page. Mechanism, with file:line into the 18.3 source ---------------------------------------------- Field widths, src/include/access/xlogreader.h: 139: uint16 hole_offset; 140: uint16 hole_length; 141: uint16 bimg_len; The cross-checks, xlogreader.c =E2=80=94 the comment states exactly what is= checked: 1826: /* 1827: * cross-check that hole_offset > 0, hole_length > 0 and 1828: * bimg_len < BLCKSZ if the HAS_HOLE flag is set. 1829: */ 1830: if ((blk->bimg_info & BKPIMAGE_HAS_HOLE) && 1831: (blk->hole_offset =3D=3D 0 || 1832: blk->hole_length =3D=3D 0 || 1833: blk->bimg_len =3D=3D BLCKSZ)) Three non-zero-ness conditions; no relation between the two fields and BLCKSZ. The consumer, RestoreBlockImage: 2170: memcpy(page, ptr, bkpb->hole_offset); 2172: MemSet(page + bkpb->hole_offset, 0, bkpb->hole_length); 2173: memcpy(page + (bkpb->hole_offset + bkpb->hole_length), 2174: ptr + bkpb->hole_offset, 2175: BLCKSZ - (bkpb->hole_offset + bkpb->hole_length)); page is BLCKSZ. With hole_offset =3D 8000 the first memcpy alone exceeds the page. The third length, BLCKSZ - (hole_offset + hole_length), is negative and converts to a very large size_t. A second path through the same missing bound: the decompressors are handed BLCKSZ - bkpb->hole_length as their output capacity into a BLCKSZ-sized tmp: 2109: if (pglz_decompress(ptr, bkpb->bimg_len, tmp.data, 2110: BLCKSZ - bkpb->hole_length, true) < 0) 2117: LZ4_decompress_safe(ptr, tmp.data, bkpb->bimg_len, BLCKSZ - bkpb->hole_length) 2131: ZSTD_decompress(tmp.data, BLCKSZ - bkpb->hole_length, ptr, bkpb->bimg_len) With hole_length > BLCKSZ that expression underflows, so a decompressor is told it has far more room than it does.