From: Wietse Z Venema Date: Tue, 23 Jun 2026 05:00:00 +0000 (-0500) Subject: postfix-3.12-20260623 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=6473f9a6e3b90d5f010f0944e4e8a873f0d7adc5;p=thirdparty%2Fpostfix.git postfix-3.12-20260623 --- diff --git a/postfix/HISTORY b/postfix/HISTORY index bcb366b82..7e7684653 100644 --- a/postfix/HISTORY +++ b/postfix/HISTORY @@ -31385,6 +31385,67 @@ Apologies for any names omitted. DSN ORCPT parameter value. Found during code maintenance. File: smtp_proto.c. +20260618 + + Hardening: make sure that optimizers will not delete a + memset() call in myfree() that wipes memory. Files: mymalloc.c, + mymalloc_test.c. + + Bug (defect introduced: Postfix 2.8, date: 20100914): + read-after-free in the PSC_CALL_BACK_NOTIFY() macro. This + had no effect on program execution, because myfree() wiped + memory. Problem reported by Qualys, assisted by Claude + Mythos Preview. File: postscreen_dnsbl.c. + + Bug (defect introduced: Postfix 3.11, date: 20251025): a + missing return statement in the SHOWQ_CLEANUP_AND_RETURN() + macro. A local user could submit a crafted message that + triggered a read-after-free and panic() in the unprivileged + showq daemon (which scans the mail queue for the 'postqueue + -f' and 'mailq' commands). This could happen only before a + message had been picked up by the pickup(8) daemon. Problem + reported by Qualys, assisted by Claude Mythos Preview. File: + showq.c. + +20260619 + + Bug (defect introduced: Postfix 3.4, date: 20190121): missing + null termination in a postlogd process that was started + with an EMPTY maillog_file setting, while receiving a message + from a postlog command that was started with a NON-EMPTY + maillog_file setting. Under these contradicting conditions, + an unprivileged attacker could cause postlogd to write null + bytes to stack memory as it tokenized text outside the + receive buffer, and possibly gain 'postfix' privilege. + Problem reported by Qualys, assisted by Claude Mythos + Preview. File: postlogd.c. + +20260621 + + Bug (defect introduced: Postfix 2.3, date: 20051112): "read" + after realloc while parsing a malformed bounce template. + This cannot be triggered by an unprivileged user (templates + are owned by root). Problem reported by Qualys, assisted + by Claude Mythos Preview. File: bounce_templates.c. + + Bug (defect introduced: postfix-3.11.0-RC1, date: 20251222): + heap memory over-read in the cleanup daemon as it handled + a milter "shutdown" reply. The over-read memory was logged + after masking unprintable content. Problem reported by + Qualys, assisted by Claude Mythos Preview. File: cleanup_milter.c. + + Bug (defect introduced: Postfix 2.3, date: 20050526): limited + (<= 11 byte) heap over-read in the cleanup daemon. This + could be triggered by local user with a crafted queue file, + but the over-read content was not disclosed and there was + no other impact. Problem reported by Qualys, assisted by + Claude Mythos Preview. File: cleanup_extracted.c. + + Maintainer future proofing: allow zero-length memory + allocation requests. Many people have experience with systems + that allow this, therefore it should not trigger a panic + in Postfix. Files: mymalloc.c, mymalloc_test.c. + TODO Reorganize PTEST_LIB, PMOCK_LIB, TESTLIB, TESTLIBS, etc. diff --git a/postfix/src/bounce/bounce_templates.c b/postfix/src/bounce/bounce_templates.c index f81dfd4dd..358530b94 100644 --- a/postfix/src/bounce/bounce_templates.c +++ b/postfix/src/bounce/bounce_templates.c @@ -307,7 +307,7 @@ void bounce_templates_load(VSTREAM *fp, BOUNCE_TEMPLATES *ts) } if (vstream_feof(fp)) msg_warn("%s, line %d: missing \"%s\" end marker", - VSTREAM_PATH(fp), lineno, value); + VSTREAM_PATH(fp), lineno, STR(saved_end_marker)); member_name = STR(saved_member_name); value = STR(multi_line_buf); } diff --git a/postfix/src/cleanup/cleanup_extracted.c b/postfix/src/cleanup/cleanup_extracted.c index a0cbc5a64..43ea36fd9 100644 --- a/postfix/src/cleanup/cleanup_extracted.c +++ b/postfix/src/cleanup/cleanup_extracted.c @@ -107,6 +107,8 @@ void cleanup_extracted_process(CLEANUP_STATE *state, int type, char *attr_value; const char *error_text; int extra_opts; + int mapped_type = type; + const char *mapped_buf = buf; int junk; #ifdef DELAY_ACTION @@ -168,9 +170,10 @@ void cleanup_extracted_process(CLEANUP_STATE *state, int type, state->queue_id, attr_name); return; } + /* 202606 Qualys+Mythos: don't clobber 'type' and 'buf'. */ if ((junk = rec_attr_map(attr_name)) != 0) { - buf = attr_value; - type = junk; + mapped_buf = attr_value; + mapped_type = junk; } } @@ -240,24 +243,25 @@ void cleanup_extracted_process(CLEANUP_STATE *state, int type, state->dsn_notify = 0; return; } - if (type == REC_TYPE_DSN_ORCPT) { + if (mapped_type == REC_TYPE_DSN_ORCPT) { if (state->dsn_orcpt) { msg_warn("%s: ignoring out-of-order DSN original recipient record <%.200s>", state->queue_id, state->dsn_orcpt); myfree(state->dsn_orcpt); } - state->dsn_orcpt = mystrdup(buf); + state->dsn_orcpt = mystrdup(mapped_buf); return; } - if (type == REC_TYPE_DSN_NOTIFY) { + if (mapped_type == REC_TYPE_DSN_NOTIFY) { if (state->dsn_notify) { msg_warn("%s: ignoring out-of-order DSN notify record <%d>", state->queue_id, state->dsn_notify); state->dsn_notify = 0; } - if (!alldig(buf) || (junk = atoi(buf)) == 0 || DSN_NOTIFY_OK(junk) == 0) + if (!alldig(mapped_buf) || (junk = atoi(mapped_buf)) == 0 + || DSN_NOTIFY_OK(junk) == 0) msg_warn("%s: ignoring malformed dsn notify record <%.200s>", - state->queue_id, buf); + state->queue_id, mapped_buf); else state->qmgr_opts |= QMGR_READ_FLAG_FROM_DSN(state->dsn_notify = junk); diff --git a/postfix/src/cleanup/cleanup_milter.c b/postfix/src/cleanup/cleanup_milter.c index db7fa7d3b..832bb4451 100644 --- a/postfix/src/cleanup/cleanup_milter.c +++ b/postfix/src/cleanup/cleanup_milter.c @@ -2085,15 +2085,9 @@ static const char *cleanup_milter_apply(CLEANUP_STATE *state, const char *event, text = "milter triggers DISCARD action"; break; case 'S': - if (state->flags & CLEANUP_STAT_CONT) - return (0); - /* Shutdown' may be the default action for an I/O error. */ - CLEANUP_MILTER_SET_SMTP_REPLY(state, resp); - ret = state->reason; - state->errs |= CLEANUP_STAT_WRITE; - action = "milter-reject"; - text = resp + 4; - break; + /* 202606 Qualys+Mythos found heap over-read at "S" + 4. */ + resp = "421 4.7.0 Service unavailable"; /* See also smtpd.c. */ + /* FALLTHROUGH */ /* * Override permanent reject with temporary reject. This happens when diff --git a/postfix/src/global/mail_version.h b/postfix/src/global/mail_version.h index 54f31d114..cc6d52cfb 100644 --- a/postfix/src/global/mail_version.h +++ b/postfix/src/global/mail_version.h @@ -20,7 +20,7 @@ * Patches change both the patchlevel and the release date. Snapshots have no * patchlevel; they change the release date only. */ -#define MAIL_RELEASE_DATE "20260615" +#define MAIL_RELEASE_DATE "20260623" #define MAIL_VERSION_NUMBER "3.12" #ifdef SNAPSHOT diff --git a/postfix/src/postlogd/postlogd.c b/postfix/src/postlogd/postlogd.c index f844c3d57..6e7aee5d9 100644 --- a/postfix/src/postlogd/postlogd.c +++ b/postfix/src/postlogd/postlogd.c @@ -152,10 +152,10 @@ static void postlogd_fallback(const char *buf) static void postlogd_service(int sock, char *unused_service, char **unused_argv) { - char buf[DGRAM_BUF_SIZE]; + char buf[DGRAM_BUF_SIZE + 1]; ssize_t len; - if ((len = recv(sock, buf, sizeof(buf), 0)) < 0) { + if ((len = recv(sock, buf, sizeof(buf) - 1, 0)) < 0) { msg_warn("failed to receive message with recv: %m"); return; } @@ -173,6 +173,9 @@ static void postlogd_service(int sock, char *unused_service, char *bp = buf; char *progname_pid; + /* 202606 Qualys+Mythos: null-terminate the buffer. */ + buf[len] = 0; + /* * Avoid surprises: strip off the date, time, host, and program[pid]: * prefix that were prepended by msg_logger(3). Then, hope that the diff --git a/postfix/src/postscreen/postscreen_dnsbl.c b/postfix/src/postscreen/postscreen_dnsbl.c index d95e0b4ee..1f6c7052b 100644 --- a/postfix/src/postscreen/postscreen_dnsbl.c +++ b/postfix/src/postscreen/postscreen_dnsbl.c @@ -203,7 +203,8 @@ typedef struct { #define PSC_CALL_BACK_NOTIFY(sp, ev) do { \ PSC_CALL_BACK_ENTRY *_cb_; \ - for (_cb_ = (sp)->table; _cb_ < (sp)->table + (sp)->index; _cb_++) \ + PSC_CALL_BACK_ENTRY *_end_ = (sp)->table + (sp)->index; \ + for (_cb_ = (sp)->table; _cb_ < _end_; _cb_++) \ if (_cb_->callback != 0) \ _cb_->callback((ev), _cb_->context); \ } while (0) diff --git a/postfix/src/showq/showq.c b/postfix/src/showq/showq.c index 89ac9cf39..9021cea18 100644 --- a/postfix/src/showq/showq.c +++ b/postfix/src/showq/showq.c @@ -180,8 +180,10 @@ static void showq_report(VSTREAM *client, char *queue, char *id, /* * Let the optimizer worry about eliminating duplicate code. + * + * 202606 Qualys+Mythos: add missing return statement. */ -#define SHOWQ_CLEANUP_AND_RETURN { \ +#define SHOWQ_CLEANUP_AND_RETURN do { \ if (sender_seen > 0) \ attr_print(client, ATTR_FLAG_NONE, ATTR_TYPE_END); \ vstring_free(buf); \ @@ -193,7 +195,8 @@ static void showq_report(VSTREAM *client, char *queue, char *id, dsb_free(dsn_buf); \ if (dup_filter) \ htable_free(dup_filter, (void (*) (void *)) 0); \ - } + return; \ + } while (0) /* * XXX addresses in defer logfiles are in printable quoted form, while diff --git a/postfix/src/util/mymalloc.c b/postfix/src/util/mymalloc.c index 37a8afd2a..dbf0e496d 100644 --- a/postfix/src/util/mymalloc.c +++ b/postfix/src/util/mymalloc.c @@ -129,6 +129,12 @@ typedef struct MBLOCK { #define SPACE_FOR(len) (offsetof(MBLOCK, u.payload[0]) + len) + /* + * The memset-before-free safety net should not be optimized out by future + * compilers or linkers. + */ +static void *(*volatile safe_memset) (void *, int, size_t) = memset; + /* * Optimization for short strings. We share one copy with multiple callers. * This differs from normal heap memory in two ways, because the memory is @@ -140,6 +146,8 @@ typedef struct MBLOCK { * - myfree() cannot overwrite the memory with a filler pattern like it can do * with heap memory. Therefore, some dangling pointer bugs will be masked. */ +#undef NO_SHARED_EMPTY_STRINGS + #ifndef NO_SHARED_EMPTY_STRINGS static const char empty_string[] = ""; @@ -152,6 +160,15 @@ void *mymalloc(ssize_t len) void *ptr; MBLOCK *real_ptr; + /* + * Maintainer proofing: many people expect that a request for null memory + * will not result in a panic(). + */ +#ifndef NO_SHARED_EMPTY_STRINGS + if (len == 0) + return ((void *) empty_string); +#endif + /* * Note: for safety reasons the request length is a signed type. This * allows us to catch integer overflow problems that weren't already @@ -213,7 +230,7 @@ void myfree(void *ptr) if (ptr != empty_string) { #endif CHECK_IN_PTR(ptr, real_ptr, len, "myfree"); - memset((void *) real_ptr, FILLER, SPACE_FOR(len)); + safe_memset((void *) real_ptr, FILLER, SPACE_FOR(len)); free((void *) real_ptr); #ifndef NO_SHARED_EMPTY_STRINGS } diff --git a/postfix/src/util/mymalloc_test.c b/postfix/src/util/mymalloc_test.c index 4097b27ff..f4651a51e 100644 --- a/postfix/src/util/mymalloc_test.c +++ b/postfix/src/util/mymalloc_test.c @@ -20,6 +20,22 @@ */ #include + /* + * Scafffolding. + */ +static bool memset_was_called; + +void *memset(void *dst, int bval, size_t len) +{ + size_t n; + char *cp; + + memset_was_called = true; + for (cp = dst, n = 0; n < len; n++) + *cp++ = bval; + return (dst); +} + /* * See */ @@ -38,11 +54,32 @@ static void test_mymalloc_normal(PTEST_CTX *t, const PTEST_CASE *tp) myfree(ptr); } +static void test_mymalloc_static_empty(PTEST_CTX *t, const PTEST_CASE *tp) +{ + void *want; + void *got; + + /* + * Repeated mymalloc(0) produce the same result. + */ + want = mymalloc(0); + got = mymalloc(0); + if (got != want) + ptest_error(t, "mymalloc: zero length results differ: got %p, want %p", + got, want); + + /* + * myfree() is a NOOP. + */ + myfree(want); + myfree(got); +} + static void test_mymalloc_panic_too_small(PTEST_CTX *t, const PTEST_CASE *tp) { - expect_ptest_log_event(t, "panic: mymalloc: requested length 0"); - (void) mymalloc(0); - ptest_fatal(t, "mymalloc(0) returned"); + expect_ptest_log_event(t, "panic: mymalloc: requested length -1"); + (void) mymalloc(-1); + ptest_fatal(t, "mymalloc(-1) returned"); } static void test_mymalloc_fatal_out_of_mem(PTEST_CTX *t, const PTEST_CASE *tp) @@ -54,6 +91,17 @@ static void test_mymalloc_fatal_out_of_mem(PTEST_CTX *t, const PTEST_CASE *tp) ptest_fatal(t, "mymalloc(SSIZE_T_MAX-100) returned"); } +static void test_myfree_calls_memset(PTEST_CTX *t, const PTEST_CASE *tp) +{ + void *ptr; + + ptr = mymalloc(100); + memset_was_called = false; + myfree(ptr); + if (!memset_was_called) + ptest_fatal(t, "myfree did not call memset!"); +} + static void test_myfree_panic_double_free(PTEST_CTX *t, const PTEST_CASE *tp) { void *ptr; @@ -234,6 +282,27 @@ static void test_mymemdup_normal(PTEST_CTX *t, const PTEST_CASE *tp) myfree(ptr); } +static void test_mymemdup_static_empty(PTEST_CTX *t, const PTEST_CASE *tp) +{ + void *want; + void *got; + + /* + * Repeated mymemdup(_, 0) produce the same result. + */ + want = mymemdup("", 0); + got = mymemdup("", 0); + if (got != want) + ptest_error(t, "mymemdup: zero length results differ: got %p, want %p", + got, want); + + /* + * myfree() is a NOOP. + */ + myfree(want); + myfree(got); +} + static void test_mymemdup_panic_null(PTEST_CTX *t, const PTEST_CASE *tp) { expect_ptest_log_event(t, "panic: mymemdup: null pointer argument"); @@ -243,9 +312,9 @@ static void test_mymemdup_panic_null(PTEST_CTX *t, const PTEST_CASE *tp) static void test_mymemdup_panic_too_small(PTEST_CTX *t, const PTEST_CASE *tp) { - expect_ptest_log_event(t, "panic: mymalloc: requested length 0"); - (void) mymemdup("abcdef", 0); - ptest_fatal(t, "mymemdup(_, 0) returned"); + expect_ptest_log_event(t, "panic: mymalloc: requested length -1"); + (void) mymemdup("abcdef", -1); + ptest_fatal(t, "mymemdup(_, -1) returned"); } static void test_mymemdup_fatal_out_of_mem(PTEST_CTX *t, const PTEST_CASE *tp) @@ -260,10 +329,14 @@ static void test_mymemdup_fatal_out_of_mem(PTEST_CTX *t, const PTEST_CASE *tp) static const PTEST_CASE ptestcases[] = { {"mymalloc + myfree normal case", test_mymalloc_normal, }, + {"mymalloc static result for empty request", test_mymalloc_static_empty, + }, {"mymalloc panic for too small request", test_mymalloc_panic_too_small, }, {"mymalloc fatal for out of memory", test_mymalloc_fatal_out_of_mem, }, + {"myfree calls memset", test_myfree_calls_memset, + }, {"myfree panic for double free", test_myfree_panic_double_free, }, {"myfree panic for null input", test_myfree_panic_null, @@ -296,6 +369,8 @@ static const PTEST_CASE ptestcases[] = { }, {"mymemdup normal case", test_mymemdup_normal, }, + {"mymemdup static result for empty request", test_mymemdup_static_empty, + }, {"mymemdup panic for null input", test_mymemdup_panic_null, }, {"mymemdup panic for too small request", test_mymemdup_panic_too_small,