Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.98.2) (envelope-from ) id 1xBvPM-00000003SXf-26PI for pgsql-bugs@arkaria.postgresql.org; Wed, 30 Sep 2026 14:35:44 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.98.2) (envelope-from ) id 1xBvPL-00000002siA-2MJI for pgsql-bugs@arkaria.postgresql.org; Wed, 30 Sep 2026 14:35:43 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.98.2) (envelope-from ) id 1xBdJB-0000000GIXC-08OG for pgsql-bugs@lists.postgresql.org; Tue, 29 Sep 2026 19:16:09 +0000 Received: from mahout.postgresql.org ([2001:4800:3e1:1::227]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.98.2) (envelope-from ) id 1xBdJ8-00000001v2G-0y6N for pgsql-bugs@lists.postgresql.org; Tue, 29 Sep 2026 19:16:08 +0000 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=postgresql.org; s=20171124; h=Message-ID:Date:Reply-To:Cc:From:To:Subject: Content-Transfer-Encoding:MIME-Version:Content-Type:Sender:Content-ID: Content-Description:In-Reply-To:References; bh=KRpL+AaqDSw6BEnWZFDuL4xoEAoqjRrPKcbDUBF5dho=; b=1fXHePb2YoawQ5y0eqIHMJ6J2r N7TAqgjeMfEgKXOZL1ztI/o/bfuNSe+ksy6mVikdsmGRorafTZ01iTB+tjC3jZAu7pxQgd5uqdQl4 oapcCFzYmtM7BFgJ24xLhn+LnviM5NFFmVG26PclUz6GBbCgQ22WslOYWBoYNElD3viisi6BK92bj +LdixgwNBJ3OxI0YrvZG2GwedZ545abFX7OK3V3NQYjg1bTg/nHHqPpQU85D1GapJbrZNLG7CuMWu RYoQrfiYaSKvonhY3gRd69j3tq06j5WyN+ecg5+Pe3SmG9QU70kg82kGBkU5pC48JT/lkL6L17nwO RhM9N64A==; Received: from wrigleys.postgresql.org ([2a02:16a8:dc51::60]) by mahout.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1xBdJ7-000ZQW-2K for pgsql-bugs@lists.postgresql.org; Tue, 29 Sep 2026 19:16:05 +0000 Received: from localhost ([127.0.0.1] helo=wrigleys.postgresql.org) by wrigleys.postgresql.org with esmtp (Exim 4.98.2) (envelope-from ) id 1xBdJ5-0000000GVhQ-2ofE for pgsql-bugs@lists.postgresql.org; Tue, 29 Sep 2026 19:16:03 +0000 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Subject: BUG #19730: PostgreSQL `pg_backup_start` accepts newlines in the backup label, producing an unrestorable `backup To: pgsql-bugs@lists.postgresql.org From: PG Bug reporting form Cc: theshallow27@gmail.com Reply-To: theshallow27@gmail.com, pgsql-bugs@lists.postgresql.org Date: Tue, 29 Sep 2026 19:15:42 +0000 Message-ID: <19730-85a6044c9e72774a@postgresql.org> X-Auto-Response-Suppress: All Auto-Submitted: auto-generated List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk The following bug has been logged on the website: Bug reference: 19730 Logged by: Shallow Email address: theshallow27@gmail.com PostgreSQL version: 18.6 Operating system: Linux Description: =20 Summary `pg_backup_start()` accepts a label containing a newline and embeds it verbatim, unescaped, into the line-oriented `backup_label` contents returned by `pg_backup_stop()`. The extra line(s) shift the fields that follow `LABEL:`, so the file the documentation requires to be written "byte for byte without modification" is malformed, and the server's own `read_backup_label()` rejects it with `FATAL` at restore time. The same function already backslash-escapes `\n`/`\r` when building `tablespace_map`, but does nothing for the label. Reproducing the Bug ```python import db_harness db_harness.query(""" CREATE OR REPLACE FUNCTION repro(lbl text) RETURNS text LANGUAGE plpgsql AS $$ DECLARE r record; BEGIN PERFORM pg_backup_start(lbl, true); SELECT * INTO r FROM pg_backup_stop(false); RETURN r.labelfile; END $$; """) label =3D "mybackup\nINCREMENTAL FROM LSN: 0/0" r =3D db_harness.query("SELECT repro(%s)", [label]) assert r.ok, r.error print(r.rows[0][0]) ``` Output =E2=80=94 the `labelfile` that the documentation says must be written verbatim to `/backup_label`: ``` START WAL LOCATION: 5/BC000028 (file 0000000100000005000000BC) CHECKPOINT LOCATION: 5/BC000080 BACKUP METHOD: streamed BACKUP FROM: primary START TIME: 2026-09-26 08:46:09 UTC LABEL: mybackup INCREMENTAL FROM LSN: 0/0 START TIMELINE: 1 ``` Restoring from that backup makes the server refuse to start: ``` FATAL: this is an incremental backup, not a data directory HINT: Use pg_combinebackup to reconstruct a valid data directory. ``` With `label =3D "mybackup\nSTART TIMELINE: 0"` the same procedure yields: ``` FATAL: invalid data in file "backup_label" DETAIL: Timeline ID parsed is 0, but expected 1. ``` Fix Reject labels containing a newline or carriage return, alongside the existing length check. Escaping is not an option for the label without also changing `read_backup_label()`, whose `%1023[^\n]` conversion does no de-escaping; rejecting at `pg_backup_start()` time fails loudly and early, before a useless backup is taken. ```diff --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -8861,6 +8861,17 @@ do_pg_backup_start(const char *backupidstr, bool fast, List **tablespaces, if (strlen(backupidstr) > MAXPGPATH) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("backup label too long (max %d bytes)", MAXPGPATH))); =20 + /* + * The label is written as a single line of the backup_label file, and read + * back with a conversion that stops at a newline and does no de-escaping. + * An embedded newline would shift every field after "LABEL:" and make the + * resulting backup_label unreadable at recovery time, so refuse it here + * rather than producing an unrestorable backup. + */ + if (strpbrk(backupidstr, "\n\r") !=3D NULL) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("backup label must not contain newline or carriage return characters"))); + strlcpy(state->name, backupidstr, sizeof(state->name)); ```