| 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));
```
| 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 |