]> git.ipfire.org Git - thirdparty/postfix.git/commitdiff
postfix-3.12-20260803
authorWietse Z Venema <wietse@porcupine.org>
Mon, 3 Aug 2026 05:00:00 +0000 (00:00 -0500)
committerViktor Dukhovni <ietf-dane@dukhovni.org>
Wed, 5 Aug 2026 12:03:08 +0000 (22:03 +1000)
postfix/HISTORY
postfix/src/global/mail_version.h
postfix/src/postscreen/postscreen_dnsbl.c
postfix/src/postscreen/postscreen_dnsbl_test.c
postfix/src/smtpd/smtpd.c

index f935fa78ae0e6fda671d715c6ef35f3c889f399d..f1de3aeeccb0511c5f575d5386e1be32907278ee 100644 (file)
@@ -31823,6 +31823,44 @@ Apologies for any names omitted.
        Code health: missing <arpa/inet> includes. Files:
        testing/mock_servent.c, util/find_inet_service.c.
 
+20260724
+
+       Bug (defect introduced: Postfix 3.4, date: 20180805): missing
+       RCPT TO state reset after a BDAT command error. The Postfix
+       SMTP server then accepted a DATA command and crashed with
+       a null pointer read error. Reported by OpenAI Security.
+       File: smtpd/smtpd.c.
+
+20260802
+
+       Bugfix (defect introduced: 20260618): the postscreen fix
+       for the PSC_CALL_BACK_NOTIFY() macro introduced a regression
+       (0xffffffff pointer dereference). Reported by OpenAI
+       Security. Files: postscreen_dnsbl.c, postscreen_dnsbl_test.c.
+
+20260803
+
+       Bug (introduced: Postfix 2.2, date: 20041102): missing
+       resets of MAIL FROM and RCPT TO command state, after
+       smtpd_end_of_data_restrictions processing rejected a message.
+
+       The Postfix SMTP server then accepted RCPT TO and DATA
+       without MAIL FROM, and skipped smtpd_end_of_data_restrictions
+       for the second message (smtpd_end_of_data_restrictions is
+       always skipped when a recipient counter is > 1). Reported
+       by OpenAI Security. File: smtpd/smtpd.c.
+
+       As reported by OpenAI Security, the failure to reset MAIL
+       FROM and RCPT TO state also affected Milter support (added
+       in Postfix 2.3). Here, after a Milter replied with "accept
+       this message" based on the message envelope, and
+       smtpd_end_of_data_restrictions rejected the message, the
+       Postfix SMTP server as before accepted RCPT TO and DATA
+       without MAIL FROM and skipped smtpd_end_of_data_restrictions
+       for the second message. But in this case, the Postfix Milter
+       client was still in the "accept this message" state, skipping
+       Milter policy enforcement for the second message.
+
 TODO
 
        Reorganize PTEST_LIB, PMOCK_LIB, TESTLIB, TESTLIBS, etc.
index 25f9cb49c9f6942099c9b175f772f8a9facde3ea..01d25434d4033cda172e61e2ceb2d10f9d254c3e 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      "20260721"
+#define MAIL_RELEASE_DATE      "20260803"
 #define MAIL_VERSION_NUMBER    "3.12"
 
 #ifdef SNAPSHOT
index c89b0a6dc652a26bea58c55fc540dd4b2f25a279..5d5cfee9bea0c8484fc8a38cf59a7b50bd488bbb 100644 (file)
@@ -201,12 +201,15 @@ typedef struct {
        _cb_->context = (ctx); \
     } while (0)
 
+/* 20260606 Qualys+Mythos: read after free, neutralized by memset(0xff). */
+/* 20260607 OpenAI Security: the 20260606 fix introduced a regression. */
 #define PSC_CALL_BACK_NOTIFY(sp, ev) do { \
        PSC_CALL_BACK_ENTRY *_cb_; \
        PSC_CALL_BACK_ENTRY *_end_ = (sp)->table + (sp)->index; \
-       for (_cb_ = (sp)->table; _cb_ < _end_; _cb_++) \
+       int _todo_ = (sp)->refcount; \
+       for (_cb_ = (sp)->table; _todo_ > 0 && _cb_ < _end_; _cb_++) \
            if (_cb_->callback != 0) \
-               _cb_->callback((ev), _cb_->context); \
+               { _cb_->callback((ev), _cb_->context); _todo_ -= 1; } \
     } while (0)
 
 #define PSC_NULL_EVENT (0)
index ef7ccada37517895ab3b856dda619940fac33a77..379ac115ca18a005cd6c1f8383df461f459978ce 100644 (file)
@@ -555,6 +555,155 @@ static void test_multi_dnsbl(PTEST_CTX *t, const PTEST_CASE *tp)
     }
 }
 
+static void test_parallel_client_early_disc(PTEST_CTX *t, const PTEST_CASE *tp)
+{
+    MOCK_SERVER *mp;
+
+#define CONN_COUNT 3                           /* must be odd and > 1 */
+
+    struct session_state session_state[CONN_COUNT];
+    const char *dnsblog_path = "private/dnsblog";
+    VSTRING *serialized_req;
+    VSTRING *serialized_resp;
+    const int request_id = 0;
+    const struct single_dnsbl_data dnsbl_data = {
+       "not used",
+        /* dnsbl_sites */ "zen.spamhaus.org",
+        /* req_dnsbl */ "zen.spamhaus.org",
+        /* req_addr */ "127.0.0.2",
+        /* res_addr */ "127.0.0.2 127.0.0.4 127.0.0.10",
+        /* res_ttl */ 60,
+        /* want_score */ 1,
+    };
+    struct session_state init_session = {
+       .req_addr = dnsbl_data.req_addr,
+       .got_dnsbl = 0,
+       .got_ttl = INT_MAX,
+       .got_score = INT_MAX,
+    };
+    int     idx;
+    int     want_score;
+    int     got_score;
+    const char *got_dnsbl_name;
+    int     got_dnsbl_ttl;
+
+    /*
+     * The plan: simulate CONN_COUNT connections {0, 1, 2} from the same
+     * client IP address, simulate disconnecting connections {0, 2} before
+     * receiving the dnsblog(8) response, and collect the scores for
+     * connection {1}. The session at the highest index (2) needs to
+     * disconnect early to detect a crash that was introduced 20260618.
+     */
+
+    /*
+     * Reset global state and parameters used by postscreen_dnsbl.c.
+     */
+    init_psc_globals(dnsbl_data.dnsbl_sites);
+
+    /*
+     * Instantiate a mock dnsblog server.
+     */
+    mp = mock_unix_server_create(dnsblog_path);
+
+    /*
+     * Set up the expected dnsblog request, and the corresponding response.
+     * The mock dnsblog server immediately generates a read event request, so
+     * we should send something soon.
+     */
+    serialized_req =
+       make_attr(attr_vprint, ATTR_FLAG_NONE,
+                 SEND_ATTR_STR(MAIL_ATTR_RBL_DOMAIN, dnsbl_data.req_dnsbl),
+                 SEND_ATTR_STR(MAIL_ATTR_ACT_CLIENT_ADDR,
+                               dnsbl_data.req_addr),
+                 SEND_ATTR_INT(MAIL_ATTR_LABEL, request_id),
+                 ATTR_TYPE_END);
+    serialized_resp =
+       make_attr(attr_vprint, ATTR_FLAG_NONE,
+                 SEND_ATTR_STR(MAIL_ATTR_RBL_DOMAIN, dnsbl_data.req_dnsbl),
+                 SEND_ATTR_STR(MAIL_ATTR_ACT_CLIENT_ADDR,
+                               dnsbl_data.req_addr),
+                 SEND_ATTR_INT(MAIL_ATTR_LABEL, request_id),
+                 SEND_ATTR_STR(MAIL_ATTR_RBL_ADDR, dnsbl_data.res_addr),
+                 SEND_ATTR_INT(MAIL_ATTR_TTL, dnsbl_data.res_ttl),
+                 ATTR_TYPE_END);
+    mock_server_interact(mp, serialized_req, serialized_resp);
+
+    /*
+     * Simulate a connection by calling psc_dnsbl_request(), and run the
+     * event loop once to notify the mock dnsblog server that a request is
+     * pending. The mock dnsblog server will receive the request, and if it
+     * matches the expected request, the mock dnsblog server will immediately
+     * send the prepared response.
+     */
+
+    for (idx = 0; idx < CONN_COUNT; idx++) {
+       session_state[idx] = init_session;
+       msg_info("creating connection %d", idx);
+       session_state[idx].req_idx = psc_dnsbl_request(dnsbl_data.req_addr,
+                                                      psc_dnsbl_callback,
+                                                      &session_state[idx]);
+       if (session_state[idx].req_idx != idx)
+           ptest_fatal(t, "psc_dnsbl_request index: got %d, want %d",
+                       session_state[idx].req_idx, idx);
+    }
+    event_loop(2);
+
+    /*
+     * Simulate disconneting the connections with index {0, 2} by calling
+     * psc_dnsbl_retrieve() before receiving the DNSBL response. Their dnsbl
+     * scores should be zero.
+     */
+    want_score = 0;
+    for (idx = 0; idx < CONN_COUNT; idx++) {
+       if ((idx % 2) != 0)
+           continue;
+       msg_info("closing connection %d", idx);
+       got_score = psc_dnsbl_retrieve(dnsbl_data.req_addr, &got_dnsbl_name,
+                                      session_state[idx].req_idx,
+                                      &got_dnsbl_ttl);
+       if (got_score != want_score)
+           ptest_error(t, "psc_dnsbl_retrieve index %d score: got %d, want %d",
+                       idx, got_score, want_score);
+    }
+
+    /*
+     * Run the event loop another time to wake up psc_dnsbl_receive(). That
+     * function will deserialize the mock dnsblog server's response, and will
+     * immediately call our psc_dnsbl_callback() function to store the result
+     * into the session_state objects for the connection with index {1}.
+     */
+    event_loop(2);
+
+    /*
+     * Validate the response.
+     */
+    for (idx = 0; idx < CONN_COUNT; idx++) {
+       if ((idx % 2) == 0)
+           continue;
+       msg_info("retrieving connection %d", idx);
+       if (session_state[idx].got_ttl == INT_MAX) {
+           ptest_error(t, "psc_dnsbl_callback() was not called, "
+                       "or did not update the session_state[%d]", idx);
+       } else {
+           if (session_state[idx].got_ttl != dnsbl_data.res_ttl)
+               ptest_error(t, "index %d: unexpected ttl: got %d, want %d",
+                      idx, session_state[idx].got_ttl, dnsbl_data.res_ttl);
+           if (session_state[idx].got_score != dnsbl_data.want_score)
+               ptest_error(t, "index %d: unexpected score: got %d, want %d",
+                           idx, session_state[idx].got_score,
+                           dnsbl_data.want_score);
+       }
+    }
+
+    /*
+     * Clean up.
+     */
+    vstring_free(serialized_req);
+    vstring_free(serialized_resp);
+    mock_server_free(mp);
+    deinit_psc_globals();
+}
+
  /*
   * Test cases.
   */
@@ -565,6 +714,9 @@ const PTEST_CASE ptestcases[] = {
     {
        "multi dnsbl", test_multi_dnsbl,
     },
+    {
+       "parallel client early disconnect", test_parallel_client_early_disc,
+    },
 };
 
 #include <ptest_main.h>
index 57e893fda2cdb40a8713d46c5849b878f3e45203..738575871b7528dd336d843af0192cfe695755be 100644 (file)
@@ -3831,13 +3831,10 @@ static int common_post_message_handling(SMTPD_STATE *state)
        && SMTPD_STAND_ALONE(state) == 0
        && (err = smtpd_check_eod(state)) != 0) {
        smtpd_chat_reply(state, "%s", err);
-       if (proxy) {
-           smtpd_proxy_close(state);
-       } else {
-           mail_stream_cleanup(state->dest);
-           state->dest = 0;
-           state->cleanup = 0;
-       }
+       /* 202607 OpenAI: reset state like normal end-of-data. */
+       chat_reset(state, var_smtpd_hist_thrsh);
+       mail_reset(state);
+       rcpt_reset(state);
        return (-1);
     }
 
@@ -4082,9 +4079,11 @@ static int skip_bdat(SMTPD_STATE *state, off_t chunk_size,
     /*
      * Reset state, or drop subsequent BDAT payloads until BDAT LAST or RSET.
      */
-    if (final_chunk)
+    if (final_chunk) {
        mail_reset(state);
-    else
+       /* 202607 OpenAI: also reset recipient state. */
+       rcpt_reset(state);
+    } else
        state->bdat_state = SMTPD_BDAT_STAT_ERROR;
     return (-1);
 }