Re: greenfly lwlock corruption in REL_14_STABLE and REL_15_STABLE

From: Alexander Lakhin <exclusion(at)gmail(dot)com>
To: Thomas Munro <thomas(dot)munro(at)gmail(dot)com>, Greg Burd <greg(at)burd(dot)me>, Andres Freund <andres(at)anarazel(dot)de>
Cc: pgsql-hackers <pgsql-hackers(at)postgresql(dot)org>
Subject: Re: greenfly lwlock corruption in REL_14_STABLE and REL_15_STABLE
Date: 2026-10-31 13:00:00
Message-ID: 47d1b5cf-2a6c-4720-bf8b-6ee9c45cd569@gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

Hello hackers,

12.12.2025 02:54, Thomas Munro wrote:
> Thanks! Could also be environmental. Maybe the snow arrived and you
> adjusted your thermostat? :-) But even then we'd still need to figure
> out what's different on those branches... One difference is autoconf
> vs meson. Both say -g -O2, but only Meson says -fPIC. Can you
> reproduce it on later branches with configure/make?

I'd like to revive this thread, while we have no snow in EU yet. We had
some off-list discussion of this topic at the end of the past year, but
didn't root-cause the issue then. But this week I have finally dedicated
some time to dive into this with my Orange Pi device, which is able to
reproduce those crashes more or less as often as greenfly.

I came to the following modification:
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -4250,3 +4250,10 @@ LockBuffer(Buffer buffer, int mode)
        else if (mode == BUFFER_LOCK_EXCLUSIVE)
+{
+for (int i = 0; i < 100; i++)
+{
+LWLockAcquire(BufferDescriptorGetContentLock(buf), LW_EXCLUSIVE);
+LWLockRelease(BufferDescriptorGetContentLock(buf));
+}
                LWLockAcquire(BufferDescriptorGetContentLock(buf), LW_EXCLUSIVE);
+}
        else

which makes a build produced on REL_15_STABLE with:
CC=clang-20 CFLAGS="-O1" ./configure -q --enable-debug --enable-cassert --enable-tap-tests && make -s -j8

fail for me reliably (0 of 10 runs succeeded) as below:
PROVE_TESTS="t/001*" make -s check -C src/bin/pgbench
...
Files=1, Tests=3,  9 wallclock secs ( 0.11 usr  0.03 sys +  4.83 cusr  1.02 csys =  5.99 CPU)
Result: FAIL

TRAP: FailedAssertion("!(oldstate & LW_VAL_EXCLUSIVE)", File: "lwlock.c", Line: 1843, PID: 64342)
postgres: main: orangepi postgres [local] CREATE TYPE(ExceptionalCondition+0x72)[0x2ab2066546]
postgres: main: orangepi postgres [local] CREATE TYPE(LWLockRelease+0x58e)[0x2ab2385160]
postgres: main: orangepi postgres [local] CREATE TYPE(_bt_relbuf+0x14)[0x2ab20d5060]
postgres: main: orangepi postgres [local] CREATE TYPE(+0x13bc2a)[0x2ab20d1c2a]
...
(other observed result is a hang, once per ~10 runs)

Please find attached the assembler code for the LWLockWakeup() function
that is known to produce the assertion failure and note two instructions
I commented there:
  3eee4a:       lw a3,4(s6)                                        # 1014: old_state = pg_atomic_read_u32(&lock->state)
  3eee4e:       lw a5,8(s6)                                        # 1026: proclist_is_empty(&lock->waiters)

The following minimal modification:
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -1016,2 +1016,3 @@ LWLockWakeup(LWLock *lock)
                {
+__asm__ __volatile__("nop" ::: "memory");
                        desired_state = old_state;

which turns that pair into:
  3eee4a:       lw      a3,4(s6)
  3eee4e:       nop
  3eee50:       lw      a5,8(s6)

makes the test pass reliably for me (10+ iterations).

So it's not surprising now that this crash depends on postgres version
(v14/v15 only), compiler (gcc doesn't produce the code needed), its'
options (not reproduced with -O0), etc.

I've generated a minimal standalone reproducer for this anomaly (please
find it attached too), which replicates an essential part of
LWLockWakeup():
      sb      pending_wakeup, (waiting)
      lw      old_state, 4(lock)
      lw      head, 8(lock)
      and     desired_state, old_state, mask
  1:  lr.w    cur, (state)
      bne     cur, old_state, 2f
      sc.w    tmp, desired_state, (state)
      bnez    tmp, 1b
  2:

and demonstrates that LR/SC mechanism doesn't work as expected at least on
the given RISC-V CPU (8 SpacemiT X60 cores in 2 clusters):
Machine model: ky x1 orangepi-rv2 board
...
processor       : 7
hart            : 7
model name      : Ky(R) X1
isa             :
rv64imafdcv_zicbom_zicboz_zicntr_zicond_zicsr_zifencei_zihintpause_zihpm_zfh_zfhmin_zca_zcd_zba_zbb_zbc_zbs_zkt_zve32f_zve32x_zve64d_zve64f_zve64x_zvfh_zvfhmin_zvkt_sscofpmf_sstc_svinval_svnapot_svpbmt
mmu             : sv39
uarch           : ky,x60
mvendorid       : 0x710
marchid         : 0x8000000058000001
mimpid          : 0x1000000049772200

Let me share results of a few experiments:
clang-20 -O1 ~/lrsc-anomaly.c -o ~/lrsc-anomaly
~/lrsc-anomaly
LOST UPDATE in round 28: value=0x70001d01 expected=0x00001d01
LOST UPDATE in round 29: value=0x70001e01 expected=0x00001e01
LOST UPDATE in round 30: value=0x70001f01 expected=0x00001f01
LOST UPDATE in round 31: value=0x70002001 expected=0x00002001
LOST UPDATE in round 32: value=0x70002101 expected=0x00002101
LOST UPDATE in round 33: value=0x70002201 expected=0x00002201
LOST UPDATE in round 34: value=0x70002301 expected=0x00002301
LOST UPDATE in round 35: value=0x70002401 expected=0x00002401
LOST UPDATE in round 37: value=0x70002601 expected=0x00002601
LOST UPDATE in round 38: value=0x70002701 expected=0x00002701
page_offset=0 rounds=1000000 retried=1824 lost=15523: FAILED

It reproduces the issue: process A's SC succeeds despite B's increment
happened in between.

for n in `seq 0 15`; do ~/lrsc-anomaly -n $n | tail -1; done
page_offset=0 rounds=1000000 retried=5374 lost=16160: FAILED
page_offset=1 rounds=1000000 retried=29563 lost=16438: FAILED
page_offset=2 rounds=1000000 retried=1251 lost=17209: FAILED
page_offset=3 rounds=1000000 retried=14960 lost=22: FAILED
page_offset=4 rounds=1000000 retried=12090 lost=19: FAILED
page_offset=5 rounds=1000000 retried=13561 lost=15: FAILED
page_offset=6 rounds=1000000 retried=15037 lost=32: FAILED
page_offset=7 rounds=1000000 retried=3124 lost=16774: FAILED
page_offset=8 rounds=1000000 retried=19813 lost=0: OK
page_offset=9 rounds=1000000 retried=20210 lost=0: OK
page_offset=10 rounds=1000000 retried=17992 lost=0: OK
page_offset=11 rounds=1000000 retried=18207 lost=0: OK
page_offset=12 rounds=1000000 retried=17903 lost=0: OK
page_offset=13 rounds=1000000 retried=18779 lost=0: OK
page_offset=14 rounds=1000000 retried=18384 lost=0: OK
page_offset=15 rounds=1000000 retried=18328 lost=0: OK

That is, the effect depends not only on that lw pair, but also on offset
of "waiting" in "sb pending_wakeup, (waiting)" preceding it.

The anomaly's probability also depends on whether these two cores/harts
are in the same cluster or not. For example, with cores 0 and 1 that belong
to cluster 0:
for n in `seq 0 4 15`; do taskset -c 0,1 ~/lrsc-anomaly -n $n | tail -1; done
page_offset=0 rounds=1000000 retried=1194 lost=16909: FAILED
page_offset=4 rounds=1000000 retried=1147 lost=16464: FAILED
page_offset=8 rounds=1000000 retried=18658 lost=0: OK
page_offset=12 rounds=1000000 retried=18154 lost=0: OK

And now with two cores that belong to different clusters:
for n in `seq 0 4 15`; do taskset -c 0,4 ~/lrsc-anomaly -n $n | tail -1; done
page_offset=0 rounds=1000000 retried=13350 lost=45: FAILED
page_offset=4 rounds=1000000 retried=15566 lost=37: FAILED
page_offset=8 rounds=1000000 retried=18223 lost=0: OK
page_offset=12 rounds=1000000 retried=17466 lost=0: OK

Also, nop inserted as above:
                /* old_state = pg_atomic_read_u32(&lock->state) */
                "       lw              %[old_state], 4(%[lock])\n"
+"nop\n"
                /* proclist_is_empty(&lock->waiters) */
                "       lw              %[head], 8(%[lock])\n"

changes the picture:
for n in `seq 0 15`; do taskset -c 0,1 ~/lrsc-anomaly -n $n | tail -1; done
page_offset=0 rounds=1000000 retried=15306 lost=17557: FAILED
page_offset=1 rounds=1000000 retried=2624 lost=16711: FAILED
page_offset=2 rounds=1000000 retried=1468 lost=16940: FAILED
page_offset=3 rounds=1000000 retried=815 lost=17098: FAILED
page_offset=4 rounds=1000000 retried=31171 lost=0: OK
page_offset=5 rounds=1000000 retried=17372 lost=0: OK
page_offset=6 rounds=1000000 retried=31803 lost=0: OK
page_offset=7 rounds=1000000 retried=19194 lost=0: OK
page_offset=8 rounds=1000000 retried=5777 lost=0: OK
page_offset=9 rounds=1000000 retried=5375 lost=0: OK
page_offset=10 rounds=1000000 retried=11920 lost=0: OK
page_offset=11 rounds=1000000 retried=5334 lost=0: OK
page_offset=12 rounds=1000000 retried=5087 lost=0: OK
page_offset=13 rounds=1000000 retried=12040 lost=0: OK
page_offset=14 rounds=1000000 retried=8479 lost=0: OK
page_offset=15 rounds=1000000 retried=4965 lost=0: OK

It kind of confirms the behavior observed with Postgres, when nop shifted
the code out of the problem area.

It also depends on number of distinct lines (buffers) in rotation; with
#define NBUFFERS        2

the probability drops almost to zero:
taskset -c 0,1 ~/lrsc-anomaly
LOST UPDATE in round 300830: value=0x7000ba01 expected=0x0000ba01
LOST UPDATE in round 300831: value=0x7000bb01 expected=0x0000bb01
page_offset=0 rounds=1000000 retried=2 lost=2: FAILED

Sorry for the long scroll, but it looks like this anomaly should be added
to our collection of the outer world's defects discovered during Postgres
testing.

Best regards,
Alexander

Attachment Content-Type Size
LWLockRelease-v15.asm text/plain 16.0 KB
lrsc-anomaly.c text/x-csrc 4.3 KB

In response to

Browse pgsql-hackers by date

  From Date Subject
Next Message Greg Burd 2026-10-31 13:34:16 Re: [PATCH] Corruption Issue: Fix missing tts_tid in ExecForceStoreHeapTuple
Previous Message Zhijie Hou 2026-10-31 12:35:15 Re: Add a hook for handling logical decoding messages on subscribers.