postgres.git / summary / log / commit / refs

commit    a2eeb04f3a0fff86e0e94745003e705ec396d4ba
Author:   Heikki Linnakangas <heikki.linnakangas@iki.fi>
Date:     Thu Jan 15 18:57:12 2026 +0000
First released in: 14.21, 15.16, 16.12, 17.8, 18.2

    Fix 'unexpected data beyond EOF' on replica restart
    
    On restart, a replica can fail with an error like 'unexpected data
    beyond EOF in block 200 of relation T/D/R'. These are the steps to
    reproduce it:
    
    - A relation has a size of 400 blocks.
      - Blocks 201 to 400 are empty.
      - Block 200 has two rows.
      - Blocks 100 to 199 are empty.
    - A restartpoint is done
    - Vacuum truncates the relation to 200 blocks
    - A FPW deletes a row in block 200
    - A checkpoint is done
    - A FPW deletes the last row in block 200
    - Vacuum truncates the relation to 100 blocks
    - The replica restarts
    
    When the replica restarts:
    
    - The relation on disk starts at 100 blocks, because all the
      truncations were applied before restart.
    - The first truncate to 200 blocks is replayed. It silently fails, but
      it will still (incorrectly!) update the cache size to 200 blocks
    - The first FPW on block 200 is applied. XLogReadBufferForRead relies
      on the cached size and incorrectly assumes that the page already
      exists in the file, and thus won't extend the relation.
    - The online checkpoint record is replayed, calling smgrdestroyall
      which causes the cached size to be discarded
    - The second FPW on block 200 is applied. This time, the detected size
      is 100 blocks, an extend is attempted. However, the block 200 is
      already present in the buffer cache due to the first FPW. This
      triggers the 'unexpected data beyond EOF'.
    
    To fix, update the cached size in SmgrRelation with the current size
    rather than the requested new size, when the requested new size is
    greater.
    
    Author: Anthonin Bonnefoy <anthonin.bonnefoy@datadoghq.com>
    Discussion: https://www.postgresql.org/message-id/CAO6_Xqrv-snNJNhbj1KjQmWiWHX3nYGDgAc=vxaZP3qc4g1Siw@mail.gmail.com
    Backpatch-through: 14


src/backend/storage/smgr/md.c | 3 +++ src/backend/storage/smgr/smgr.c | 17 +++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c index 2a0dcc31945..47c01ed7271 100644 --- a/src/backend/storage/smgr/md.c +++ b/src/backend/storage/smgr/md.c @@ -997,6 +997,9 @@ mdnblocks(SMgrRelation reln, ForkNumber forknum) * functions for this relation or handled interrupts in between. This makes * sure we have opened all active segments, so that truncate loop will get * them all! + * + * If nblocks > curnblk, the request is ignored when we are InRecovery, + * otherwise, an error is raised. */ void mdtruncate(SMgrRelation reln, ForkNumber forknum, diff --git a/src/backend/storage/smgr/smgr.c b/src/backend/storage/smgr/smgr.c index f203c2e9efb..b9a95b00467 100644 --- a/src/backend/storage/smgr/smgr.c +++ b/src/backend/storage/smgr/smgr.c @@ -713,11 +713,20 @@ smgrtruncate2(SMgrRelation reln, ForkNumber *forknum, int nforks, /* * We might as well update the local smgr_cached_nblocks values. The * smgr cache inval message that this function sent will cause other - * backends to invalidate their copies of smgr_fsm_nblocks and - * smgr_vm_nblocks, and these ones too at the next command boundary. - * But these ensure they aren't outright wrong until then. + * backends to invalidate their copies of smgr_cached_nblocks, and + * these ones too at the next command boundary. But ensure they aren't + * outright wrong until then. + * + * We can have nblocks > old_nblocks when a relation was truncated + * multiple times, a replica applied all the truncations, and later + * restarts from a restartpoint located before the truncations. The + * relation on disk will be the size of the last truncate. When + * replaying the first truncate, we will have nblocks > current size. + * In such cases, smgr_truncate does nothing, so set the cached size + * to the old size rather than the requested size. */ - reln->smgr_cached_nblocks[forknum[i]] = nblocks[i]; + reln->smgr_cached_nblocks[forknum[i]] = + nblocks[i] > old_nblocks[i] ? old_nblocks[i] : nblocks[i]; } } [parent: c7946e6f32c9]