postfix: long, non-repeating queue ids

The owner, 2026-10-16, of three ways to stop a reused queue id reaching an
older row (found by the technical review of inbound-upstream-result, L6):
"1" - enable_long_queue_ids=yes. A short id is the microsecond of arrival
and the queue file's inode in hex (Postfix 3.10.13, src/global/mail_queue.h,
MQID_SH_*) and repeats once the inode comes round; a later message with no
send-log row of its own (port-25 mail, a bounce Postfix sends) could then
rewrite an older Outbound log row of the same id and recipient, its page
could show the later message's stored lines (security.md's known exposure,
"enable_long_queue_ids ... ends it"), and the inbound journal's rows needed
detaching. A long id adds the time in seconds and does not repeat
(MQID_LG_*; proto/postconf.proto: existing queue files keep their names,
postqueue -p gets a wider Queue ID column). Every pattern of the panel
takes both shapes (queueLineRe's long branch, the [0-9A-Za-z]+ of the
others, queueEntryRe) - checked one by one.

Tests: e2e reads a long id on an outbound send-log row
(testExpiredMessageIsClosed) and on an inbound journal row
(testInboundUpstreamDown). go vet + go test (26 packages) and make e2e on
the test server for this tree.

Docs: security.md (the collision bullet closed for new mail, the upstream
bullet), architecture § Log tailer, delivery-log-storage's note, the
ForgetInboundQueueID comment; CHANGELOG [Unreleased] § Fixed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-16 18:59:18 +03:00
co-authored by Claude Opus 5.5
parent f4f6e0f23d
commit 22ce1bdfbb
8 changed files with 65 additions and 15 deletions
+3
View File
@@ -126,6 +126,9 @@ print("" if r is None else r[0]+" "+r[1])`, token)
}
if f := strings.Fields(out); len(f) == 2 && f[1] == "deferred" {
queueID = f[0]
if !longQueueID.MatchString(queueID) {
return false, fmt.Errorf("queue id %q is not a long one (enable_long_queue_ids)", queueID)
}
return true, nil
}
return false, fmt.Errorf("row %q", out)
+11
View File
@@ -9,6 +9,12 @@ import (
"time"
)
// longQueueID is the shape of a long queue id: six or more characters of the
// time in seconds and four of the microseconds, base 52 without vowels, "z",
// the inode in base 51, which has no "z" (Postfix 3.10.13,
// src/global/mail_queue.h, MQID_LG_*).
var longQueueID = regexp.MustCompile(`^[0-9B-DF-HJ-NP-TV-Zb-df-hj-np-tv-z]{10,}z[0-9B-DF-HJ-NP-TV-Zb-df-hj-np-tv-y]+$`)
// testInboundUpstreamDown: an inbound domain whose upstream refuses
// connections (the sink's address, a port nothing listens on). Port 25 takes
// the message; Postfix cannot hand it on and logs "relay=none ...
@@ -65,6 +71,11 @@ print("" if r is None else "|".join(r))`, token)
return false, fmt.Errorf("deferred with host %q, reply %q", f[2], f[3])
}
queueID = f[0]
// A long queue id (enable_long_queue_ids=yes,
// build/postfix-config.sh): the time, "z", the inode.
if !longQueueID.MatchString(queueID) {
return false, fmt.Errorf("queue id %q is not a long one", queueID)
}
return true, nil
}
return false, fmt.Errorf("row %q\n%s", out, mailLogTail(h, "postfix/"))