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.
}
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);
}
char *attr_value;
const char *error_text;
int extra_opts;
+ int mapped_type = type;
+ const char *mapped_buf = buf;
int junk;
#ifdef DELAY_ACTION
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;
}
}
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);
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
* 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
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;
}
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
#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)
/*
* 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); \
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
#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
* - 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[] = "";
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
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
}
*/
#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>
*/
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)
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;
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");
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)
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,
},
{"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,