On Thu, 29 Oct 2026 09:50:46 +0900, Michael Paquier <[email protected]> wrote:
> On Wed, Oct 28, 2026 at 03:38:55PM +0900, Michael Paquier wrote:
> > My blind suggestion of fix would be the attached, that I cannot really
> > test.  Thoughts or comments?
> 
> I unfortunately do not have a better idea after sleeping on it, so I
> have applied this palloc0/pfree solution.  Now I'll go light a candle
> to make the buildfarm gods kinder to me.
> --
> Michael

Hey Michael,

As requested (back channel) I spent some quality time with my
UltraSPARC 45 this morning and dug into this.

TL;DR: it's a GCC bug, and c97931c16d fixes it for us by keeping
perform_base_backup()'s frame under 4kB.

I reproduced it on an UltraSPARC-IIIi running illumos with gcc 13.4,
-O2 and cassert, at 4a92c8c29b.  Your suggested reproduction works: a
file with a 114-byte name in PGDATA, then BASE_BACKUP through psql
with "replication=1", gives the SIGSEGV every time.  So does
cancelling a BASE_BACKUP (MAX_RATE 32), which is thorntail's case.
The core (mdb, full output attached):

  status: process terminated by SIGSEGV, addr=fffffffe7f74733c
  %pc = perform_base_backup+0x1c0:   st  %o0, [%g1]
  %npc = perform_base_backup+0x1480
  %i0 = 0x00000001008b6000
  perform_base_backup+0x1c0(...)
  SendBaseBackup+0x5a0(...)
  exec_replication_command+0x5b4(...)
  PostgresMain+0x8f0(...)

That store is in the delay slot of the branch right after
sigsetjmp() in PG_TRY(), on its second return.  Here is the
disassembly you asked for, at that spot:

  227de22095 (before), frame 2224 bytes:
    call  sigsetjmp
    ...
    bne,pn  %icc, <catch>
    st    %o0, [ %fp + 0x57 ]

  4a92c8c29b, frame 10368 bytes:
    sethi %hi(0x1c00), %i0         ! in the prologue
    ...
    call  sigsetjmp
    xor   %i0, -837, %g1
    add   %g1, %fp, %g1
    bne,pn  %icc, <catch>
    st    %o0, [ %g1 ]

  c97931c16d (fix), frame 2096 bytes: same as before.

With the 8kB array, the frame slots are more than 4kB from %fp, too
far for SPARC's 13-bit immediate offset, so GCC needs a register to
hold the offset.  It keeps the constant 0x1c00 in %i0 across the
sigsetjmp() call and reuses it afterwards, and later in the PG_TRY
block it loads a string address into %i0.  On SPARC, siglongjmp()
doesn't restore %l and %i registers from the jmp_buf; it resumes the
register window from the frame's save area, so %i0 comes back with
the value it had at siglongjmp() time.  In the core, %i0 is the
string address instead of 0x1c00, and (0x1008b6000 ^ -837) + %fp is
exactly the faulting address.  On other platforms jmp_buf saves the
callee-saved registers, so the constant would come back intact.  The
garbage address explains SIGSEGV on billbug and the occasional
SIGBUS on thorntail, and why the WARNING from do_pg_abort_backup()
is never logged: the backend dies before the catch block runs.

The GCC RTL dumps show where it goes wrong.  LRA does reload the
constant after the call, (set (reg %i0) (const_int 7168)), but
postreload's reload_cse_move2add() turns that into a no-op move,
because it thinks %i0 survives the call.  cselib resets its state at
a REG_SETJMP call, but move2add has no such check, on GCC master as
well.

To rule out Postgres entirely, the attached sj.c/sj2.c is a small
program with no Postgres code: a function with an 8kB local array and
a sigsetjmp().  With gcc 13.4 -O2 it segfaults on this machine, and
it does the same when built with Debian's sparc64-linux-gnu-gcc-13
and run under qemu.  It runs fine at -O1 and -O0, and with gcc 4.4.
I'm not sure if this has been, or should be, reported to GCC as yet.

Validation of c97931c16d on the same machine: with it applied, three
runs each of both reproductions give the WARNING and ERROR and no
crash, and t/010_pg_basebackup.pl passes all 168 tests with no
backend crash in the server logs.  With it reverted, the same TAP run
logs the SIGSEGV.  The test still reports PASS in that case because
it only checks that pg_basebackup fails.

I don't see a better fix on our side.  A helper function with
the array would also work, but palloc0() is simpler, and both rely on
the same thing: no PG_TRY() frame larger than 4kB.  In a core build
on this machine, the only other function with a frame that large that
calls sigsetjmp() is be_lo_export() (char buf[BUFSIZE]).  Its code
after sigsetjmp() reloads the constant, so it is fine today, but only
by luck of register allocation.  I haven't checked contrib or other
GCC versions or as mentioned earlier against their bug system.

best.

-greg


Attachment: sj.c
Description: sj.c

Attachment: sj2.c
Description: sj2.c

Reply via email to