BUG #19730: PostgreSQL `pg_backup_start` accepts newlines in the backup label, producing an unrestorable `backup

From: PG Bug reporting form <noreply(at)postgresql(dot)org>
To: pgsql-bugs(at)lists(dot)postgresql(dot)org
Cc: theshallow27(at)gmail(dot)com
Subject: BUG #19730: PostgreSQL `pg_backup_start` accepts newlines in the backup label, producing an unrestorable `backup
Date: 2026-09-29 19:15:42
Message-ID: 19730-85a6044c9e72774a@postgresql.org
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-bugs

The following bug has been logged on the website:

Bug reference: 19730
Logged by: Shallow
Email address: theshallow27(at)gmail(dot)com
PostgreSQL version: 18.6
Operating system: Linux
Description:

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 = "mybackup\nINCREMENTAL FROM LSN: 0/0"
r = db_harness.query("SELECT repro(%s)", [label])
assert r.ok, r.error
print(r.rows[0][0])
```

Output — the `labelfile` that the documentation says must be written
verbatim to
`<backup>/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 = "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)));

+ /*
+ * 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") != 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));
```

Responses

Browse pgsql-bugs by date

  From Date Subject
Next Message PG Bug reporting form 2026-09-29 19:16:36 BUG #19731: `first_value` returns an excluded peer for a nonempty `EXCLUDE TIES` frame
Previous Message Ross Burton 2026-09-29 16:45:07 Re: BUG #19727: pg-combinebackup fails to link