Hi,

On Tue, 26 May 2026 at 13:32, Michael Paquier <michael@paquier.xyz> wrote:
On Fri, May 22, 2026 at 10:21:32PM +0530, Ayush Tiwari wrote:
> I think the right fix is to remove that SimpleLruWriteAll() call while
> keeping the missing-page initialization logic.  The flush is only meant to
> make SimpleLruDoesPhysicalPageExist() see pages that exist in SLRU buffers
> but have not reached disk.  In this fallback path, I don't see a way for
> the tested next_pageno to be in that state: if RecordNewMultiXact() itself
> initializes the page, it writes it synchronously with SimpleLruWritePage()
> before setting last_initialized_offsets_page.

FWIW, I'm having a couple of customers complaining about that as well,
as cross-version physical replication is a thing for minor upgrade
flows.  This bug is making suddenly recovery disruptive for some folks
out there.  :(

We had faced a lot of replicas in bad state due to multixact replay with
16.12 release, and had to revert back the minor versions for them  until 
16.13 came out which was a blessing. Given the number of CVEs 
current one fixes, reverting too is scary.
 
> I attached a small patch for REL_16_STABLE.  The same self-deadlock pattern
> is also present on PG 14 and 15.  PG 17 and
> 18 have the same compatibility call, but SLRU locking is banked
> there, and RecordNewMultiXact() does not appear to hold the relevant bank
> lock before calling SimpleLruWriteAll(), so I would not describe those
> branches as having this exact self-deadlock, but needs more analysis.

So your root argument is that while the SimpleLruWriteAll() is
defensive, it is not actually necessary because it means that
last_initialized_offsets_page is -1 we have not yet replayed
ZERO_OFF_PAGE and that we have no dirty page that could make
SimpleLruDoesPhysicalPageExis() return an incorrect result, which
would be bad.  I am not sure to agree that this assumption is correct
all the time, see for example the WAL message mentioned in the thread
that has led to 77dff5d937b1:
https://www.postgresql.org/message-id/33319276-e4d0-4773-89e4-09084905fdb0%40iki.fi

Right, agreed.  Thanks for pointing to that case.  My v1 patch removes
the self-deadlock, but the "no relevant dirty pages" assumption is too
strong.

The dirty page does not have to be one initialized by the current
RecordNewMultiXact() call.  It can already contain offsets replayed from
later CREATE_ID records while last_initialized_offsets_page is still -1.
In that state, relying directly on SimpleLruDoesPhysicalPageExist() can
still produce a false negative because it only checks the physical file,
not dirty SLRU buffers.  So removing the flush can maybe reintroduce 
the kind of corruption that 77dff5d937b1 was trying to prevent.

A different approach would be to release and re-acquire the
MultiXactOffsetSLRULock while calling SimpleLruWriteAll(), and I think
that it should be actually safe.  Even if read-only backends evict
dirty pages between the moment the lock is released and the moment it
is re-acquired in SimpleLruWriteAll(), the pages would be would be
written to disk due to the eviction, which is what we want for
correctness.  And only the startup process dirties offset pages during
recovery, AFAIK.  Thoughts?

 That sounds like the right direction to me.

Releasing MultiXactOffsetSLRULock around SimpleLruWriteAll() preserves
the flush-before-physical-check rule while avoiding the self-deadlock.
I don't see a partial-state problem from the current record at that
point, since the compatibility check happens before RecordNewMultiXact()
has modified the current offsets page.  And as you said, during recovery
The startup process should be the only process dirtying offset pages; if
a hot standby reader causes eviction while the lock is released, that
should only help by writing the dirty page out.

> Added both Andrey and Heikki in to-mail, since I'm not sure if this
> is more extreme than the multixact offset issue we had with 16.12, or it
> is at par with that.

Indeed, let's wait for at least Heikki's input. 

Anyway, for any fixes, I don't think that it would be a good idea to
skip v17 and v18, relying on the SLRU bank locks to not conflict to
bypass the WriteAll() conflict.  Let's keep all the branches across
v14~v18 in sync.

Agreed.

Regards,
Ayush