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 1vtKIk-00Gqkg-1t for pgsql-bugs@arkaria.postgresql.org; Fri, 20 Feb 2026 06:47:47 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1vtKId-006mew-0w for pgsql-bugs@arkaria.postgresql.org; Fri, 20 Feb 2026 06:47:39 +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 1vt4JU-003hEB-0m for pgsql-bugs@lists.postgresql.org; Thu, 19 Feb 2026 13:43:28 +0000 Received: from sender-pp-e102.zoho.in ([103.117.158.102]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.98.2) (envelope-from ) id 1vt4JO-000000008hl-36GQ for pgsql-bugs@lists.postgresql.org; Thu, 19 Feb 2026 13:43:27 +0000 ARC-Seal: i=1; a=rsa-sha256; t=1771508593; cv=none; d=zohomail.in; s=zohoarc; b=Z26CpdfPA9Rdw8GVvAD5aTsSoAY1tLXL/HG4f0yrBbZuG3Dvou4SGF+OOQBuXoMLQYMa2oBM9+kKBR2n3tIFvzZkujgYvHfTfvScqzTnRtIqFAM8foMh8cpXveMNSYmOdirXWgTww4DFdLPw4UQL3N9g1vbAjT4s4VXd5lFqcmM= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.in; s=zohoarc; t=1771508593; h=Content-Type:Date:Date:From:From:MIME-Version:Message-ID:Subject:Subject:To:To:Message-Id:Reply-To:Cc; bh=BR/zzqGjCtGTd0iX+yAxfZAe39sIMvcwjhT8CxZIk/A=; b=GEeVI/ENWmUii29cvh7/VjTOtGkOy2oDJBEkZ0Dcl7qbD0JPPH03Y6/vU4iyoDTy1zF6zVjD5R419XFil8gC7gNACYWLg+60KjGHj9i+2fsn5MZb2aiB6uc8HNBLTPMsKA92yXbDHtWMA3AtJEVtSn39HFjSiA2ky7IePNAO4js= ARC-Authentication-Results: i=1; mx.zohomail.in; dkim=pass header.i=zohocorp.com; spf=pass smtp.mailfrom=vishal.g@zohocorp.com; dmarc=pass header.from= DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; t=1771508593; s=admin1; d=zohocorp.com; i=vishal.g@zohocorp.com; h=Date:Date:From:From:To:To:Message-Id:Message-Id:Subject:Subject:MIME-Version:Content-Type:Reply-To:Cc; bh=BR/zzqGjCtGTd0iX+yAxfZAe39sIMvcwjhT8CxZIk/A=; b=Cc2FsU3cYQom/TeTGY5SEQL6dQ2iBmbpyZJu8agDMZsZ0/9msDZ6sD2RYT+vF2q2 6978TVCW2xr53D4EvZKUFwQqmuyLlN19PW7nPmzFV+T5P3WGy1cCKsT4934lLYPnmCF FLH0O5tuIvZYxxuXYH7msxGx4wKIDasWILJhoFMY= Received: from mail.zoho.in by mx.zoho.in with SMTP id 1771508590809789.276096957694; Thu, 19 Feb 2026 19:13:10 +0530 (IST) Date: Thu, 19 Feb 2026 19:13:10 +0530 From: Vishal Prasanna To: "pgsql-bugs" Message-Id: <19c7623e882.4080fd5426212.311756747309556767@zohocorp.com> Subject: [BUG] Assert failure in ReorderBufferReturnTXN during logical decoding due to leaked specinsert change MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="----=_Part_91527_1754798601.1771508590723" Importance: Medium User-Agent: Zoho Mail X-Mailer: Zoho Mail X-Zoho-Virus-Status: 1 X-Zoho-AV-Stamp: zmail-av-0.1.0.1.4.3/271.449.15 List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk ------=_Part_91527_1754798601.1771508590723 Content-Type: multipart/alternative; boundary="----=_Part_91528_1031153482.1771508590723" ------=_Part_91528_1031153482.1771508590723 Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable Hi, We recently encountered an issue with the Postgres server. After we dropped= a publication that was being used by a subscriber, the walsender process on the publisher= started crashing with an assertion failure in `ReorderBufferReturnTXN()` while decoding chan= ges from the replication slot. The error message: "TRAP: failed Assert("txn->size =3D=3D 0"), File: "reorderbuffer.c", Line: = 494" Issue observed in PostgreSQL 17.7 with assert-enabled builds. TRAP: failed=C2=A0Assert("txn->size =3D=3D 0"), File: "reorderbuffer.c", Li= ne: 494, PID: 96941 0=C2=A0=C2=A0=C2=A0postgres=C2=A0=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0ExceptionalCon= dition + 216 1=C2=A0=C2=A0=C2=A0postgres=C2=A0=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0ReorderBufferR= eturnTXN + 284 2=C2=A0=C2=A0=C2=A0postgres=C2=A0=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0ReorderBufferC= leanupTXN + 1292 3=C2=A0=C2=A0=C2=A0postgres=C2=A0=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0ReorderBufferP= rocessTXN + 4304 4=C2=A0=C2=A0=C2=A0postgres=C2=A0=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0ReorderBufferR= eplay + 284 5=C2=A0=C2=A0=C2=A0postgres=C2=A0=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0ReorderBufferC= ommit + 128 6=C2=A0=C2=A0=C2=A0postgres=C2=A0=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0DecodeCommit += 584 7=C2=A0=C2=A0=C2=A0postgres=C2=A0=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0xact_decode + = 408 8=C2=A0=C2=A0=C2=A0postgres=C2=A0=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0LogicalDecodin= gProcessRecord + 192 9=C2=A0=C2=A0=C2=A0postgres=C2=A0=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0XLogSendLogica= l + 204 10=C2=A0=C2=A0postgres=C2=A0=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0WalSndLoop + 256 11=C2=A0=C2=A0postgres=C2=A0=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0StartLogicalRepli= cation + 636 12=C2=A0=C2=A0postgres=C2=A0=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0exec_replication_= command + 1384 13=C2=A0=C2=A0postgres=C2=A0=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0PostgresMain + 24= 76 14=C2=A0=C2=A0postgres=C2=A0=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0BackendInitialize= + 0 15=C2=A0=C2=A0postgres=C2=A0=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0postmaster_child_= launch + 304 16=C2=A0=C2=A0postgres=C2=A0=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0BackendStartup + = 448 17=C2=A0=C2=A0postgres=C2=A0=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0ServerLoop + 372 18=C2=A0=C2=A0postgres=C2=A0=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0PostmasterMain + = 6396 19=C2=A0=C2=A0postgres=C2=A0=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0startup_hacks + 0 20=C2=A0=C2=A0dyld=C2=A0=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0start += 7184 Simplified steps to reproduce: Step 1: Create a table, set up a logical replication slot, and execute an `= INSERT ... ON CONFLICT ... DO UPDATE` statement. ``` CREATE TABLE test_updates (id=C2=A0INT=C2=A0PRIMARY KEY,=C2=A0value=C2=A0TE= XT); SELECT * FROM pg_create_logical_replication_slot('testing_slot', 'pgoutput'= ); INSERT INTO test_updates (id, value)=C2=A0VALUES (1, 'first_insert') ON CONFLICT(id) DO UPDATE=C2=A0SET value =3D excluded.value; ``` Step 2: Start logical decoding using `pg_logical_slot_peek_binary_changes` = on the slot, =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 referencing a publication (`pub_1= `) that does not exist. ``` SELECT * FROM pg_logical_slot_peek_binary_changes('testing_slot',=C2=A0NULL,=C2=A0NU= LL,=C2=A0'proto_version', '1',=C2=A0'publication_names', 'pub_1'); ``` After Step 2, the Postgres server crashes with the assertion failure shown = above. Reason for server crash: 1. In `ReorderBufferProcessTXN()`, when a `REORDER_BUFFER_CHANGE_INTERNAL_S= PEC_INSERT` is encountered, the change is unlinked from the list by `dlist_delete()` an= d stored in `specinsert`.=C2=A0=C2=A0 2. Next, when a `REORDER_BUFFER_CHANGE_INTERNAL_SPEC_CONFIRM` is encountere= d, the stored `specinsert` change is retrieved and applied as a regular insert= change. 3. Since publication `pub_1` does not exist, an error is raised when applyi= ng the change and caught by the `PG_CATCH` block in `ReorderBufferProcessTXN()`. The erro= r call stack is: ``` ReorderBufferProcessTXN() =C2=A0=C2=A0ReorderBufferApplyChange() =C2=A0 =C2=A0=C2=A0=E2=86=92 rb->apply_change()=C2=A0=C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0=C2=A0[pgoutput_change()] =C2=A0 =C2=A0 =C2=A0=C2=A0=E2=86=92 get_rel_sync_entry() =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0=E2=86=92 LoadPublications() =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0=E2=86=92 GetPublicationByName() =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0=E2=86=92 get_publication_oi= d()=C2=A0=C2=A0=C2=A0=E2=86=90 throws ERROR here ``` 4. In the `PG_CATCH` block, the `specinsert` change is cleaned up only for streaming/prepared transactions (via `ReorderBufferResetTXN()`) but not for= non-streaming transactions. In the `else` branch at line `2697`, `ReorderBufferCleanupTXN()` is called,= which iterates through the remaining changes and frees them. However, since the `specinsert` chang= e was already unlinked from the list, it is never found and never freed. This leaked change is sti= ll accounted for in `txn->size`, causing the assertion failure. Proposed Solution:=C2=A0Free the pending `specinsert` change in the `else` = branch before calling `ReorderBufferCleanupTXN()`. Since the `specinsert` change has already been unlinked, `ReorderBufferClea= nupTXN()` cannot find or free it. Explicitly freeing it here prevents the `Assert(txn->size =3D=3D 0)` failur= e in `ReorderBufferReturnTXN()`. ``` --- a/src/backend/replication/logical/reorderbuffer.c +++ b/src/backend/replication/logical/reorderbuffer.c @@ -2691,12 +2691,18 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderB= ufferTXN *txn, =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0=C2=A0ReorderBufferResetTXN(rb, txn, snapshot_now, =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0=C2=A0command_id, prev_lsn, =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0=C2=A0specinsert); =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0} =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0else =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0{ +=C2=A0=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0=C2=A0/* Return the spec insert change before cleaning up the tra= nsaction */ +=C2=A0=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0=C2=A0if (specinsert !=3D NULL) +=C2=A0=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0=C2=A0{ +=C2=A0=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0ReorderBufferReturnChange(rb, s= pecinsert, true); +=C2=A0=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0specinsert =3D NULL; +=C2=A0=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0=C2=A0} =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0=C2=A0ReorderBufferCleanupTXN(rb, txn); =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0=C2=A0MemoryContextSwitchTo(ecxt); =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0=C2=A0PG_RE_THROW(); =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0} =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0} =C2=A0 =C2=A0 =C2=A0 =C2=A0=C2=A0PG_END_TRY(); ``` Along with this fix, we have also added a TAP test to verify in patch. Additional Suggestion: Currently in `PG_CATCH` block, `specinsert` is only freed in the `ERRCODE_T= RANSACTION_ROLLBACK` branch for streaming or prepared transactions, via `ReorderBufferResetTXN()` at li= ne 2691. Would it make sense to move the freeing of `specinsert` before the if/else = branch, so that it is always freed regardless of the error path? This would avoid d= uplication and ensure that `specinsert` is always cleaned up. Regards, Vishal Prasanna Zoho Corporation ------=_Part_91528_1031153482.1771508590723 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable =

Hi,


We recently encountered an issue with the Pos= tgres server. After we dropped a publication

that was being used by a subscriber, the walsender process on the p= ublisher started crashing<= br>
with an = assertion failure in `ReorderBufferReturnTXN()` while decoding changes from= the replication slot.
=


The error message:

"TRAP: failed Assert("txn->size =3D=3D 0"), File: "= reorderbuffer.c", Line: 494"


Issue observed in PostgreSQL 17.7 with assert-enabled builds.


= TRAP: failed Assert("txn->size =3D=3D 0"), File: "r= eorderbuffer.c", Line: 494, PID: 96941

0  &= nbsp;postgres           &= nbsp;                 ExceptionalCondition + 216

1  &= nbsp;postgres           &= nbsp;                 ReorderBufferReturnTXN + 284=

2  &= nbsp;postgres           &= nbsp;                 ReorderBufferCleanupTXN + 1292

3   postgres          = ;                   = ;ReorderBufferProcessTXN + 4304

4   postgres         &nb= sp;                  &nb= sp;ReorderBufferReplay + 284

5   postgres          = ;                   = ;ReorderBufferCommit + 128=

6  &= nbsp;postgres           &= nbsp;                 DecodeCommit + 584

<= span class=3D"colour" style=3D"color:rgb(0, 0, 0)">7   postgres             &nbs= p;               xact_decode + 408

8   p= ostgres                 =             LogicalD= ecodingProcessRecord + 192

9   = postgres              =               = XLogSendLogical + 204
<= /span>

10  postg= res                 &nbs= p;           WalSndLoop += 256

=

<= span class=3D"size" style=3D"font-size: 14px; margin: 0px; font-style: norm= al; font-weight: normal; line-height: normal; font-size-adjust: none;">11  postgres &n= bsp;                     =       StartLogicalReplication + 636=

12  postgres &nb= sp;                     &= nbsp;     exec_replication_command + 138= 4

13  postgres &nb= sp;                     &= nbsp;     PostgresMain + 2476

<= span class=3D"highlight" style=3D"background-color:rgb(255, 255, 255)">14=   postgres     &nb= sp;                     &= nbsp; BackendInitialize + 0

15&n= bsp; postgres       &nbs= p;                     postmaster_child_launch + 304

16&n= bsp; postgres       &nbs= p;                     BackendStartup + 448

17 &nb= sp;postgres           &nb= sp;                 ServerLoop + 372

18  postgres               &nbs= p;             Postm= asterMain + 6396

19  postgres<= span>                   &= nbsp;         startup_hacks + = 0

20  dyld   =                      = ;         start + 7184<= /span>

=


Simplified steps to reproduce:


Step 1: Create a table, set= up a logical replication slot, and execute an `INSERT ... ON CONFLICT ... = DO UPDATE` statement.
<= /p>


```

CREATE TABLE test_updates (id INT&= nbsp;PRIMARY KEY, value TEXT);

SELECT * FROM pg_create_logical_replication_slot('testing_slot', 'pgou= tput');

INSERT INTO= test_updates (id, value) VALUES (1, 'first_insert')

= ON CONFLICT(id) DO UPDA= TE SET value =3D excluded.value;

= ```


=

Step 2: Start logical decoding using `pg_logical_slot_pee= k_binary_changes` on the slot,

 =           referencing a publication (`pub_1`) tha= t does not exist.


```

SELECT *

FROM pg_logical_slot_peek_binary_changes('testing_slot', NULL,&n= bsp;NULL, 'proto_version', '1', 'publication_names', 'pub_1');

```


= After Step 2, the Postg= res server crashes with the assertion failure shown above.


Reason for server crash:

1. In `ReorderBufferProcessTXN()`, when a `REORDER_BUFFER_CHANGE_INTER= NAL_SPEC_INSERT`

= is encountered, the = change is unlinked from the list by `dlist_delete()` and stored in `specins= ert`.  = ;


2. Next,= when a `REORDER_BUFFER_CHANGE_INTERNAL_SPEC_CONFIRM` is encountered,

the stored = `specinsert` change is retrieved and applied as a regular insert change.


<= span class=3D"colour" style=3D"color:rgb(0, 0, 0)">3. Since publication = `pub_1` does not exist, an error is raised when applying the change<= /span>

<= span>and caught by the `PG_CATCH` block in `Reord= erBufferProcessTXN()`. The error call stack is:=


<= span>```

= ReorderBufferProcessTXN()=

  ReorderBufferApplyChang= e()

    =E2=86=92 rb->apply_change()           = [pgoutput_change()]

=       =E2=86=92 get_rel_sync_entry()

       =  =E2=86=92 LoadP= ublications()

          = =E2=86=92 GetPublicationByName()

=             <= /span>=E2=86=92 get_publication_oid() =   = =E2=86=90 throws ERROR he= re

```=


4. In the `PG_CATC= H` block, the `specinsert` change is cleaned up only for

streaming/prepared transactions (via `ReorderBufferRes= etTXN()`) but not for non-streaming transactions.
In the `else` branch at line `2697`, `ReorderBufferCleanupTX= N()` is called, which iterates through
the remaining changes and frees them. However, since the `specinsert` c= hange was already unlinked=
from the= list, it is never found and never freed. This leaked change is still accou= nted for in `txn->size`,
causing= the assertion failure.



Proposed Solution: Free the pending `specinsert` change= in the `else` branch before calling `ReorderBufferCleanupTXN()`.

= Since the `specinsert` change has already been= unlinked, `ReorderBufferCleanupTXN()` cannot find or free it.
<= span class=3D"size" style=3D"font-size: 12px; margin: 0px; font-style: norm= al; font-weight: normal; line-height: normal; font-size-adjust: none;">Explicitly freeing it here prevents the `Assert= (txn->size =3D=3D 0)` failure in `ReorderBufferReturnTXN()`.

= ```

= --- a/src/backend/replication/logical/reorderbuffer.c<= /span>

+++ b/src/backend/replicati= on/logical/reorderbuffer.c=

@@ -2691,12 +2691,18 @@ ReorderBufferProcessTXN(Reorde= rBuffer *rb, ReorderBufferTXN *txn,

          &nbs= p;             ReorderBufferResetTXN(rb, txn, snapshot_= now,

                    =                      = ;                     &nb= sp;   command_id, prev_lsn,
=

              &n= bsp;                     =                      = ;         specinsert);<= /span>

            =     }

                else

<= span class=3D"highlight" style=3D"background-color:rgb(255, 255, 255)">    &nb= sp;           {

+                 &= nbsp;     /* Return the spec insert change before cleaning up the transacti= on */

= +   &n= bsp;                  &n= bsp;if (specinsert != =3D NULL)

+ &nbs= p;                     {

+ =       &nbs= p;                     &n= bsp; Reord= erBufferReturnChange(rb, specinsert, true);

+             &nb= sp;                 = specinsert =3D NULL;

+&= nbsp;    &nb= sp;                 = }

      &n= bsp;                 ReorderBufferCleanupTXN(= rb, txn);

                  &= nbsp;     MemoryContextSwitchTo(ecxt);

          &= nbsp;             PG_RE_THROW();<= /span>

      =           <= span class=3D"font" style=3D"font-family:verdana">}=

        }<= /span>

       <= span> PG_END_TRY= ();

= ```


Along with this fix, we have also added a TAP tes= t to verify in patch.



<= /p>

Additional Suggestion:=

Currently in `PG_CATCH` block, `specinse= rt` is only freed in the `ERRCODE_TRANSACTION_ROLLBACK` branch

for streaming or prepared transactions, via `Reo= rderBufferResetTXN()` at line 2691.

= Would it make sense to move the freeing of = `specinsert` before the if/else branch,<= /span>

= so that it is always freed regardless of the error path? This would avoi= d duplication and ensure
that `sp= ecinsert` is always cleaned up.



<= span class=3D"font" style=3D"font-family:Menlo">Regards,

Vishal Prasanna
<= /p>

Zoho Corporation




<= /div>

------=_Part_91528_1031153482.1771508590723-- ------=_Part_91527_1754798601.1771508590723 Content-Type: application/octet-stream; name=0001-Fix-specinsert-leak-in-ReorderBufferProcessTXN-error.patch Content-Transfer-Encoding: 7bit X-ZM_AttachId: 139907157961110190 Content-Disposition: attachment; filename=0001-Fix-specinsert-leak-in-ReorderBufferProcessTXN-error.patch From a365dfb595f60750776538dcb530a0eb1fd60e98 Mon Sep 17 00:00:00 2001 From: Vishal Prasanna Date: Wed, 18 Feb 2026 15:55:35 +0530 Subject: [PATCH] Fix specinsert leak in ReorderBufferProcessTXN error path --- .../replication/logical/reorderbuffer.c | 6 +++ src/test/subscription/t/100_bugs.pl | 38 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c index 4bd1f7af061..eafec433d79 100644 --- a/src/backend/replication/logical/reorderbuffer.c +++ b/src/backend/replication/logical/reorderbuffer.c @@ -2694,6 +2694,12 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn, } else { + /* Return the specinsert change before cleaning up the transaction */ + if (specinsert != NULL) + { + ReorderBufferReturnChange(rb, specinsert, true); + specinsert = NULL; + } ReorderBufferCleanupTXN(rb, txn); MemoryContextSwitchTo(ecxt); PG_RE_THROW(); diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl index 17accd11d93..73dca25f0e5 100644 --- a/src/test/subscription/t/100_bugs.pl +++ b/src/test/subscription/t/100_bugs.pl @@ -597,4 +597,42 @@ $node_publisher->safe_psql('postgres', "DROP DATABASE regress_db"); $node_publisher->stop('fast'); +# Ensure logical decoder doesn't crash if error occurs +# while processing an INSERT ... ON CONFLICT statement +$node_publisher = PostgreSQL::Test::Cluster->new('logical_decoder'); +$node_publisher->init(allows_streaming => 'logical'); +$node_publisher->start; + +$node_publisher->safe_psql( + 'postgres', qq( + CREATE TABLE tab_upsert (a INT PRIMARY KEY, b INT); + SELECT * FROM pg_create_logical_replication_slot('upsert_slot', 'pgoutput'); + INSERT INTO tab_upsert (a, b) VALUES (1, 1) + ON CONFLICT(a) DO UPDATE SET b = excluded.b; +)); + +# Decode the changes without a publication and +# verify that the logical decoder doesn't crash. +($ret, $stdout, $stderr) = $node_publisher->psql( + 'postgres', qq( + SELECT * + FROM pg_logical_slot_peek_binary_changes( + 'upsert_slot', + NULL, + NULL, + 'proto_version', '1', + 'publication_names', 'pub_that_does_not_exist' + ); +)); + +ok( $stderr =~ qr/publication "pub_that_does_not_exist" does not exist/, + 'peek logical changes with non-existent publication throws error' +); + +# Clean up +$node_publisher->safe_psql('postgres', "SELECT pg_drop_replication_slot('upsert_slot')"); +$node_publisher->safe_psql('postgres', "DROP TABLE tab_upsert"); + +$node_publisher->stop('fast'); + done_testing(); -- 2.50.1 (Apple Git-155) ------=_Part_91527_1754798601.1771508590723--