]> git.ipfire.org Git - thirdparty/postfix.git/commitdiff
postfix-3.12-20260623
authorWietse Z Venema <wietse@porcupine.org>
Tue, 23 Jun 2026 05:00:00 +0000 (00:00 -0500)
committerViktor Dukhovni <ietf-dane@dukhovni.org>
Thu, 25 Jun 2026 09:58:52 +0000 (19:58 +1000)
postfix/HISTORY
postfix/src/bounce/bounce_templates.c
postfix/src/cleanup/cleanup_extracted.c
postfix/src/cleanup/cleanup_milter.c
postfix/src/global/mail_version.h
postfix/src/postlogd/postlogd.c
postfix/src/postscreen/postscreen_dnsbl.c
postfix/src/showq/showq.c
postfix/src/util/mymalloc.c
postfix/src/util/mymalloc_test.c

index bcb366b82c7811763f7781dd15d17bb7c47e96c3..7e7684653c425226bccec779f4c86680d9646431 100644 (file)
@@ -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.
index f81dfd4dd045957a5775170de51beb9261c6b7dc..358530b947955ac62b342c4d8c04d399e30607cb 100644 (file)
@@ -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);
        }
index a0cbc5a64bdb213d0fd74cad024300138bd0c86a..43ea36fd9e2c36229804a0ddbffdf2234d21df47 100644 (file)
@@ -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);
index db7fa7d3b1ca8a85fc57d6b6d019a62d6bcfc3be..832bb4451844e13350403ab82a310781597228cf 100644 (file)
@@ -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
index 54f31d114ca5da64cd59d282c9134a809021f4b6..cc6d52cfb7c2fb77d1cae5114c0f5aac929783f0 100644 (file)
@@ -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
index f844c3d576966b446711d6e41c0e55ff44fec8e7..6e7aee5d9b6050342fae10b10c46e904c552cfa5 100644 (file)
@@ -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
index d95e0b4ee651c43f00d40a1c4d7730e53f72c23f..1f6c7052bf2d1e3a59c7d9439c3a97b339398e67 100644 (file)
@@ -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)
index 89ac9cf3953f98958be70067dc711c900c5aae59..9021cea18d009550d19f77847e612edc61ad98e0 100644 (file)
@@ -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
index 37a8afd2a8d0f676a001ef43a78dac9b9dbcac90..dbf0e496dd813d87b2a1c6e5b4edd4b9fc19ad67 100644 (file)
@@ -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
     }
index 4097b27ffced8b515b69cbe625305a0612764782..f4651a51e99ef894e4d815326e8d5d7c220de4cb 100644 (file)
   */
 #include <ptest.h>
 
+ /*
+  * 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 <ptest_main.h>
   */
@@ -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,