OpenSMTPD Audit Report


(Sorry for the "CVE-2015-ABCD" place-holders in the report, but
OpenSMTPD's developers were ready with the patches before MITRE was
ready with the CVE-IDs.)

Qualys Security Advisory

OpenSMTPD Audit Report


Local Vulnerabilities
Remote Vulnerabilities
Inter-Process Vulnerabilities
Miscellaneous Bugs


For the past few months, one of our background projects has been to
audit OpenSMTPD, a free implementation of the server-side Simple Mail
Transfer Protocol (SMTP). OpenSMTPD replaces Sendmail as OpenBSD's
default Mail Transfer Agent (MTA) since OpenBSD 5.6, released on
November 1, 2014.

OpenSMTPD was designed to be secure, reliable, performant, and easy to
configure. Indeed, its codebase lives up to OpenBSD's reputation: it is
clean, modular, privilege-separated, and made our audit easy and really
enjoyable. However, the project is pretty much in its infancy (the first
stable version, 5.3, was released on March 17, 2013), which explains why
we discovered various vulnerabilities during our security assessment:

- an oversight in the portable version of fgetln() that allows attackers
to read and write out-of-bounds memory;

- multiple denial-of-service vulnerabilities that allow local users to
kill or hang OpenSMTPD;

- a stack-based buffer overflow that allows local users to crash
OpenSMTPD, or execute arbitrary code as the non-chrooted _smtpd user;

- a hardlink attack (or race-conditioned symlink attack) that allows
local users to unset the chflags() of arbitrary files;

- a hardlink attack that allows local users to read the first line of
arbitrary files (for example, root's hash from /etc/master.passwd);

- a denial-of-service vulnerability that allows remote attackers to fill
OpenSMTPD's queue or mailbox hard-disk partition;

- an out-of-bounds memory read that allows remote attackers to crash
OpenSMTPD, or leak information and defeat the ASLR protection;

- a use-after-free vulnerability that allows remote attackers to crash
OpenSMTPD, or execute arbitrary code as the non-chrooted _smtpd user;

- multiple inter-process vulnerabilities that allow attackers to
escalate from one (already-compromised) OpenSMTPD process to another.


The OpenSMTPD version that we audited is available at:

and is installed by default on OpenBSD's latest release (OpenBSD 5.7,
released on May 1, 2015). Unless otherwise noted, the vulnerabilities
that we discovered in OpenSMTPD 5.4.4p1 affect OpenSMTPD's latest
release as well (OpenSMTPD 5.7.1p1, released on June 30, 2015).

The "hybrid approach" that we adopted to review OpenSMTPD is described
in the bible of code auditing, "The Art of Software Security Assessment"
(by Mark Dowd, John McDonald, and Justin Schuh):

- We started with a "top-down approach" and reviewed the high-level
information that we gathered on OpenSMTPD: READMEs, manual pages, web
pages (

This approach allowed us to quickly understand OpenSMTPD's design
(seven privilege-separated, long-running, and event-driven processes
that communicate through UNIX sockets and the imsg API) and identify
its attack surface (local, remote, and inter-process entry points).

- We continued with a "bottom-up approach" and reviewed OpenSMTPD's
implementation: the lowest-level code first (openbsd-compat/ and
smtpd/mproc.c), followed by the higher-level code.

This approach allowed us to quickly identify complex vulnerabilities:
the remote out-of-bounds memory read and use-after-free are actually a
combination of several low-level and high-level bugs.

Privilege Separation

--[ PROC_PARENT ]-------------------------------------------------------

User: root

Chroot: no


PROC_PARENT, the "[priv]" process, spawns the six other long-running
processes at startup (by calling fork_peers() from main()), and the
transient Mail Delivery Agent (MDA) processes on demand (by calling
forkmda() from parent_imsg()).

If any of its long-running children dies, PROC_PARENT calls
parent_shutdown(), kill()s its remaining children, and exit()s, but does
not restart automatically: if we try to exploit a memory corruption, we
have to come up with a one-shot, not a brute-force.

--[ PROC_CONTROL ]------------------------------------------------------

User: _smtpd

Chroot: /var/empty


PROC_CONTROL, the "control" process, handles messages from the control
socket "/var/run/smtpd.sock" (by calling control_dispatch_ext()), and
gathers statistics from its peers (by calling control_imsg()).

--[ PROC_PONY ]---------------------------------------------------------

User: _smtpd

Chroot: /var/empty


PROC_PONY, the "pony express" process
(, handles the server-side
SMTP sessions (by calling smtp_imsg()), the client-side MTA sessions (by
calling mta_imsg()), and the local MDA deliveries (by calling

--[ PROC_LKA ]----------------------------------------------------------

User: _smtpd

Chroot: no (needs access to /etc/resolv.conf and /etc/ssl/cert.pem)


PROC_LKA, the "lookup" process, performs all lookups on behalf of the
other processes: asynchronous DNS resolution (by calling dns_imsg() and
libasr), user information and credentials lookup, SSL certificate
verification, alias expansion (by calling lka_imsg()).

--[ PROC_QUEUE ]--------------------------------------------------------

User: _smtpq (or _smtpd if _smtpq does not exist)

Chroot: /var/spool/smtpd


PROC_QUEUE, the "queue" process, manages the persistent storage of
messages and envelopes (by calling queue_imsg()). By default, the
smtpd/queue_fs.c backend is used.

--[ PROC_SCHEDULER ]----------------------------------------------------

User: _smtpd

Chroot: /var/empty


PROC_SCHEDULER, the "scheduler" process, knows about all existing
messages and envelopes (by calling scheduler_imsg()), and decides when
to relay or deliver them. By default, the smtpd/scheduler_ramqueue.c
backend is used.

--[ PROC_CA ]-----------------------------------------------------------

User: _smtpd

Chroot: /var/empty


PROC_CA, the "klondike" process
(, performs
privilege-separated RSA encryption and decryption on behalf of PROC_PONY
(by calling ca_imsg()).

Attack Surface

--[ Local Vectors ]-----------------------------------------------------

----[ .forward

Local users may put a .forward file in their home directory in order to
control how their incoming email is processed and delivered.

When PROC_PONY receives a CMD_RCPT_TO from one of its SMTP clients, it
sends an IMSG_SMTP_EXPAND_RCPT to PROC_LKA. If the recipient is a local
user, the (unprivileged) PROC_LKA sends an IMSG_LKA_OPEN_FORWARD to the
(privileged) PROC_PARENT. If PROC_PARENT manages to open() the user's
.forward file, it sends its file descriptor back to PROC_LKA, which
parses and expands its contents.

----[ Control Socket

PROC_CONTROL calls getpeereid(), or getsockopt(SO_PEERCRED), in order to
determine the credentials of the clients that connect to its UNIX socket
"/var/run/smtpd.sock". It processes all the messages received from
connections initiated by root, but otherwise processes only the

Clients normally connect to the control socket with the command-line
program "smtpctl", but we may also connect() to it directly, should we
ever want to exploit a vulnerability in the imsg API, for example.

----[ Offline Directory

The command-line program "smtpctl" can be used to send email (when
invoked as "sendmail"): it connects to the control socket, sends an
and enqueues the email through this local SMTP session (enqueue() in

However, if OpenSMTPD is not running, the connection to the control
socket will fail, and "smtpctl" will simply store the email into the
"/var/spool/smtpd/offline" directory, which is mode 01777
(enqueue_offline() in smtpd/enqueue.c).

Later, when OpenSMTPD restarts, it will execvp() "smtpctl" for each
email stored in the offline directory, exactly as if its owner had just
submitted it for the first time (offline_enqueue() in smtpd/smtpd.c).

--[ Remote Vectors ]----------------------------------------------------

----[ SMTP Client

By default, OpenSMTPD is configured to accept email from local users,
and connects to remote SMTP servers in order to relay and deliver it.
The code for these client-side MTA sessions (smtpd/mta_session.c) is
reachable remotely (and is also used for bounces) and represents an
important attack vector.

----[ SMTP Server

OpenSMTPD can be configured to accept email from remote SMTP clients,
and relay or deliver it to local users. The code for these server-side
SMTP sessions (smtpd/smtp_session.c) is reachable remotely and
represents another important attack vector.

----[ DNS Resolver

The libasr, an asynchronous DNS resolver, is used by OpenSMTPD and
represents yet another remote vector. However, its codebase is pretty
much independent and therefore beyond the scope of our OpenSMTPD audit.
The same can be said of OpenSSL and LibreSSL.

--[ Inter-Process Vectors ]---------------------------------------------

If we ever manage to compromise one of OpenSMTPD's processes, a
vulnerability in the inter-process communication code may allow us to
escalate from an unprivileged, chrooted process to a privileged,
non-chrooted process. For example, pivoting from PROC_PONY to
PROC_PARENT, or even PROC_LKA, would be a good move.

Local Vulnerabilities

CVE-2015-ABCD - Portable fgetln() can return a zero length

Constructs similar to the following appear several times throughout
OpenSMTPD's codebase:

while ((buf = fgetln(fp, &len))) {
if (buf[len - 1] == '\n')
buf[len - 1] = '\0';


line = fgetln(s->msgfp, &len);
if (line == NULL) break;
line[len - 1] = '\0';

In theory, if fgetln() succeeds (i.e., does not return NULL) but stores
a 0 length in len, an out-of-bounds memory read and (possibly) write is
triggered. In practice, this is impossible because OpenBSD's libc
implementation of fgetln() guarantees what the manpage says:

The length of the line, including the final newline, is stored
in the memory location to which len points and is guaranteed to
be greater than 0 upon successful completion.

Unfortunately, the portable implementation of fgetln() in
openbsd-compat/fgetln.c (which is used on Linux, at least) offers no
such guarantee:

38 char *
39 fgetln(stream, len)
40 FILE *stream;
41 size_t *len;
42 {
50 if (fgets(buffer, buflen+1, stream) == NULL)
51 return NULL;
52 *len = strlen(buffer);
60 return buffer;
61 }

For example, if fgets() reads the line "\0\n", fgetln() succeeds and
stores a 0 string-length in len (which should be impossible), and the
out-of-bounds memory is accessed upon return.

CVE-2015-ABCD - Local denial-of-service (invalid imsg)

The fatalx(NULL) in mproc_dispatch() can be triggered locally by
connecting directly to the control socket and sending an invalid imsg
(one that is smaller than IMSG_HEADER_SIZE or larger than MAX_IMSGSIZE).
imsg_get() will fail, fatalx() will be called, and PROC_CONTROL will
exit() (and, as mentioned earlier, if one OpenSMTPD process dies, all
OpenSMTPD processes die):

187 if ((n = imsg_get(&p->imsgbuf, &imsg)) == -1) {
188 log_warn("fatal: %s: error in imsg_get for %s",
189 proc_name(smtpd_process), p->name);
190 fatalx(NULL);
191 }

This local denial-of-service has been discovered independently by
OpenSMTPD's developers and fixed in version 5.4.6p1 (released on June
11, 2015):

188 if ((n = imsg_get(&p->imsgbuf, &imsg)) == -1) {
190 if (smtpd_process == PROC_CONTROL &&
191 p->proc == PROC_CLIENT) {
192 log_warnx("warn: client sent invalid imsg "
193 "over control socket");
194 p->handler(p, NULL);
195 return;
196 }
197 log_warn("fatal: %s: error in imsg_get for %s",
198 proc_name(smtpd_process), p->name);
199 fatalx(NULL);
200 }

CVE-2015-ABCD - Local denial-of-service (file-descriptor exhaustion)

By connecting locally to the control socket and passing many file
descriptors (~1024) to PROC_CONTROL (which does not really expect this),
it is possible to exhaust almost all of its available fds.

- In OpenSMTPD 5.4.4p1, PROC_CONTROL ends up calling fatal("exiting") in

153 if ((n = imsg_read(&p->imsgbuf)) == -1) {
154 log_warn("warn: %s -> %s: imsg_read",
155 proc_name(smtpd_process), p->name);
156 fatal("exiting");
157 }

- In OpenSMTPD 5.7.1p1, PROC_CONTROL does not call fatal("exiting")
(thanks to the EAGAIN check at the beginning of mproc_dispatch()), but
it will never again accept new client connections (because of how
control_accept() handles file-descriptor exhaustion):

155 if ((n = imsg_read(&p->imsgbuf)) == -1) {
156 log_warn("warn: %s -> %s: imsg_read",
157 proc_name(smtpd_process), p->name);
158 if (errno == EAGAIN)
159 return;
160 fatal("exiting");
161 }

There are actually three different ways to trigger this local

1/ Send one fd per imsg, with the IMSGF_HASFD flag turned on: imsg_get()
will move the fd from ibuf->fds to imsg->fd, but because PROC_CONTROL
does not expect a fd to be passed, this fd is leaked forever when
imsg_free() is called by mproc_dispatch().

2/ Send one fd per imsg, but with the IMSGF_HASFD flag turned off:
imsg_get() will leave the fd in ibuf->fds, which are supposed to be
closed when control_close() is called, but this never happens if all fds
are exhausted first.

3/ Send only one large (>1024) imsg, one byte at a time, with one fd
attached to every single byte sent: this will accumulate all passed fds
into ibuf->fds.

CVE-2015-ABCD - Local denial-of-service (connection-id wrap)

In control_accept(), it is possible to trigger the errx() of the
following tree_xset() call:

348 c = xcalloc(1, sizeof(*c), "control_accept");
349 if (getpeereid(connfd, &c->euid, &c->egid) == -1)
350 fatal("getpeereid");
351 c->id = ++connid;
352 c->mproc.proc = PROC_CLIENT;
353 c->mproc.handler = control_dispatch_ext;
354 c-> = c;
355 mproc_init(&c->mproc, connfd);
356 mproc_enable(&c->mproc);
357 tree_xset(&ctl_conns, c->id, c);

If we establish a first connection to the control socket (and keep it
alive), and then establish (and immediately close) new connections in a
loop, the "static uint32_t connid" will eventually wrap and collide with
our (kept-alive) first connection id, and the exclusive tree_xset() will
fail and terminate PROC_CONTROL with errx().

CVE-2015-ABCD - Local denial-of-service (WIFSTOPPED() child)

In parent_sig_handler(), it is possible to trigger the following
fatalx() call:

366 pid = waitpid(-1, &status, WNOHANG);
367 if (pid <= 0)
368 continue;
371 if (WIFSIGNALED(status)) {
375 } else if (WIFEXITED(status)) {
381 } else
382 fatalx("smtpd: unexpected cause of SIGCHLD");

If the child is ptraced, WIFSIGNALED() and WIFEXITED() can return false,
but WIFSTOPPED() can return true, even if WUNTRACED was not specified in
waitpid(). In order to trigger this in the context of OpenSMTPD, a local
user can add a "|exec /tmp/ptraceme" line to his ~/.forward file, where
ptraceme is a small program that simply calls ptrace(PT_TRACE_ME) and
execve() (it does not matter which binary is executed).

CVE-2015-ABCD - Local denial-of-service (blocking open() call)

The open() call in parent_forward_open() can block forever (if the
~/.forward was created by mkfifo, for example) and this will effectively
block OpenSMTPD as a whole (PROC_PARENT will not respond to
IMSG_LKA_OPEN_FORWARD and IMSG_MDA_FORK requests anymore):

1232 if (! bsnprintf(pathname, sizeof (pathname), "%s/.forward",
1233 directory))
1234 fatal("smtpd: parent_forward_open: snprintf");
1247 do {
1248 fd = open(pathname, O_RDONLY);
1249 } while (fd == -1 && errno == EINTR);

Multiple hardlink attacks in the offline directory

In the world-writable "/var/spool/smtpd/offline" directory, local users
can create hardlinks to files they do not own, and wait until the server
reboots (or, crash OpenSMTPD with a denial-of-service and wait until the
administrator restarts it) to carry out assorted attacks.

1/ The following code in offline_enqueue() allows an attacker to
chflags(0) arbitrary files, by hardlinking them to the offline directory

1117 if (lstat(path, &sb) == -1) {
1118 log_warn("warn: smtpd: lstat: %s", path);
1119 _exit(1);
1120 }
1122 #ifdef HAVE_CHFLAGS
1123 if (chflags(path, 0) == -1) {
1124 log_warn("warn: smtpd: chflags: %s", path);
1125 _exit(1);
1126 }
1127 #endif

2/ The following code in offline_enqueue() allows an attacker to
execvp() "/usr/sbin/smtpctl" as "sendmail", with a command-line argument
that is the hardlinked file's first line (CVE-2015-ABCD):

1149 if ((fp = fopen(path, "r")) == NULL)
1150 _exit(1);
1160 if ((p = fgetln(fp, &len)) == NULL)
1161 _exit(1);
1167 addargs(&args, "%s", "sendmail");
1169 while ((tmp = strsep(&p, "|")) != NULL)
1170 addargs(&args, "%s", tmp);
1179 execvp(PATH_SMTPCTL, args.list);
1180 _exit(1);

For example, an attacker can hardlink /etc/master.passwd to the offline
directory, and retrieve its first line (root's encrypted password) by
running ps (or a small program that simply calls sysctl() with

In the attacker's terminal:

$ ln /etc/master.passwd /var/spool/smtpd/offline
$ ./getargs &
[1] 23460

In the administrator's terminal:

# /etc/rc.d/smtpd restart

On the attacker's terminal:

root:$2b$09$pN5WRvGaiPHEXPsrIwSNWe1S0U5iTIvtWqPQgHmd0BAJK02GOYG.W:0:0:daemon:0:0:Charlie &:/root:/bin/ksh

3/ If an attacker controls at least part of another user's file, he can
hardlink this file to the offline directory, and try to exploit one of
the vulnerable fgetln() calls in the enqueue code, which runs with the
privileges of this other user. For example, in offline_enqueue():

1160 if ((p = fgetln(fp, &len)) == NULL)
1161 _exit(1);
1163 if (p[len - 1] != '\n')
1164 _exit(1);
1165 p[len - 1] = '\0';

And in savedeadletter():

898 while ((buf = fgetln(in, &len))) {
899 if (buf[len - 1] == '\n')
900 buf[len - 1] = '\0';

However, we did not investigate this vector any further, because on
OpenBSD (where an attacker is allowed to hardlink another user's file)
fgetln() is not vulnerable, and on Linux (where fgetln() is vulnerable)
an attacker is usually not allowed to hardlink another user's file.

4/ If an attacker is able to reach another user's file (i.e., +x on all
directories that lead to the file) but not read it, he can hardlink the
file to the offline directory, and wait for savedeadletter() to create a
world-readable copy of the file in this other user's home directory:

854 (void)snprintf(buffer, sizeof buffer, "%s/dead.letter", pw->pw_dir);
859 if ((fp = fopen(buffer, "w")) == NULL)
860 return 0;
898 while ((buf = fgetln(in, &len))) {
909 fprintf(fp, "%s\n", buf);
910 }

However, there are three reasons why this particular vector is useless
in practice:

a) In OpenSMTPD 5.4.4p1, the getlogin() call in enqueue() will always
return "root", which means that the world-readable "dead.letter" will
always be created in /root, unreachable by the attacker (drwx------).

b) In OpenSMTPD 5.4.5p2, smtpctl's -S command-line option was added to
work around the getlogin() problem, but the getopt() string was
incorrectly modified to "RS:" instead of "R:S".

c) In OpenSMTPD 5.7.1p1, the getopt() string was fixed to "R:S", but the
savedeadletter() code was removed altogether.

CVE-2015-ABCD - .forward stack-based buffer overflow

In lka_expand_format(), the exptoklen bytes returned by
lka_expand_token() are memcpy()ed to ptmp (a pointer into the
stack-based tmpbuf) without first checking that there is enough space
left in tmpbuf:

799 exptoklen = lka_expand_token(exptok, sizeof exptok, token, ep,
800 ui);
801 if (exptoklen == 0)
802 return 0;
804 memcpy(ptmp, exptok, exptoklen);

This stack-based buffer overflow can be triggered locally through
OpenSMTPD's .forward mechanism:

$ whoami

$ python -c 'print "/" * 1014 + "%{sender}"' > ~/.forward

$ python -c 'print "A" * 255 + "@" + "A" * 255'

$ telnet 25
RCPT TO:<john@localhost>
Connection closed by foreign host.

As a result, in the logs:

smtpd[9305]: warn: format string error while expanding for user john
smtpd: stack overflow in function lka_submit

It does not appear to be exploitable on OpenBSD x86 (beyond a local
denial-of-service), where even a one-byte overflow ended up smashing the
stack canary of every smtpd binary we tried. However, it may lead to
arbitrary code execution on other operating systems or platforms.

Remote Vulnerabilities

CVE-2015-ABCD - Remote denial-of-service (disk-space exhaustion)

The maximum size of an email, env->sc_maxsize (by default 35 megabytes),
is enforced in dataline_callback() for the email's body:

266 len = strlen(line) + 1;
268 if (s->datalen + len > env->sc_maxsize) {
269 s->msgflags |= MF_ERROR_SIZE;
270 return;
271 }
278 s->datalen += len;

but not in header_default_callback() for the email's headers:

243 len = strlen(hdr->name) + 1;
248 s->datalen += len;
250 TAILQ_FOREACH(l, &hdr->lines, next) {
251 len = strlen(l->buffer) + 1;
256 s->datalen += len;
257 }

It is therefore possible to send a headers-only email (i.e., no empty
line between the last header and the DATA-ending ".") that is much
larger than 35 megabytes, and fill OpenSMTPD's queue or mailbox
hard-disk partition.

Multiple vulnerabilities in IMSG_{SMTP,MTA}_SSL_VERIFY*

These IMSG_{SMTP,MTA}_SSL_VERIFY* messages are exchanged between

- after PROC_PONY successfully established an SSL connection with an
SMTP client (a client-certificate request is always made, but not
necessarily responded to), it calls smtp_verify_certificate() and
sends a few IMSG_SMTP_SSL_VERIFY* messages to PROC_LKA, which verifies
the client's SSL certificate (if any) on behalf of PROC_PONY.

- after PROC_PONY successfully established an SSL connection with an
SMTP server (opportunistic STARTTLS encryption is always attempted,
but not necessarily successful), it calls mta_verify_certificate() and
sends a few IMSG_MTA_SSL_VERIFY* messages to PROC_LKA, which verifies
the server's SSL certificate on behalf of PROC_PONY.

In lka_imsg(), PROC_LKA blindly trusts the contents of the
req_ca_vrfy_smtp, req_ca_vrfy_mta, and req_ca_vrfy_chain structures that
it receives from PROC_PONY (although this suggests vulnerabilities that
are inter-process only, they are also triggerable remotely through two
other low-level bugs in imsg and mproc, to be described shortly):

struct ca_vrfy_req_msg {
uint64_t reqid;
unsigned char *cert;
off_t cert_len;
size_t n_chain;
size_t chain_offset;
unsigned char **chain_cert;
off_t *chain_cert_len;

63 static void
64 lka_imsg(struct mproc *p, struct imsg *imsg)
65 {
70 static struct ca_vrfy_req_msg *req_ca_vrfy_smtp = NULL;
71 static struct ca_vrfy_req_msg *req_ca_vrfy_mta = NULL;
72 struct ca_vrfy_req_msg *req_ca_vrfy_chain;

attempts to read cert_len bytes from imsg->data, but does not check
first that PROC_PONY actually sent this amount of data (out-of-bounds
memory read, CVE-2015-ABCD):

149 req_ca_vrfy_smtp = xmemdup(imsg->data, sizeof *req_ca_vrfy_smtp, "lka:ca_vrfy");
150 req_ca_vrfy_smtp->cert = xmemdup((char *)imsg->data +
151 sizeof *req_ca_vrfy_smtp, req_ca_vrfy_smtp->cert_len, "lka:ca_vrfy");

159 if (req_ca_vrfy_smtp == NULL)
160 fatalx("lka:ca_vrfy: chain without a certificate");
161 req_ca_vrfy_chain = imsg->data;
162 req_ca_vrfy_smtp->chain_cert[req_ca_vrfy_smtp->chain_offset] = xmemdup((char *)imsg->data +
163 sizeof *req_ca_vrfy_chain, req_ca_vrfy_chain->cert_len, "lka:ca_vrfy");

- In the IMSG_{SMTP,MTA}_SSL_VERIFY_CERT case, PROC_LKA does not
sanity-check n_chain, the number of certificates in the chain that
will be sent by PROC_PONY:

149 req_ca_vrfy_smtp = xmemdup(imsg->data, sizeof *req_ca_vrfy_smtp, "lka:ca_vrfy");
150 req_ca_vrfy_smtp->cert = xmemdup((char *)imsg->data +
151 sizeof *req_ca_vrfy_smtp, req_ca_vrfy_smtp->cert_len, "lka:ca_vrfy");
152 req_ca_vrfy_smtp->chain_cert = xcalloc(req_ca_vrfy_smtp->n_chain,
153 sizeof (unsigned char *), "lka:ca_vrfy");
154 req_ca_vrfy_smtp->chain_cert_len = xcalloc(req_ca_vrfy_smtp->n_chain,
155 sizeof (off_t), "lka:ca_vrfy");
156 return;

not sanity-check chain_offset (out-of-bounds memory write,

. in the IMSG_{SMTP,MTA}_SSL_VERIFY_CERT case, chain_offset should be
initialized to 0, but PROC_LKA trusts PROC_PONY to do so (arbitrary
memory write);

. in the IMSG_{SMTP,MTA}_SSL_VERIFY_CHAIN case, chain_offset should be
checked against n_chain, but PROC_LKA trusts PROC_PONY to send no
more than n_chain certificates (heap-based buffer overflow):

159 if (req_ca_vrfy_smtp == NULL)
160 fatalx("lka:ca_vrfy: chain without a certificate");
161 req_ca_vrfy_chain = imsg->data;
162 req_ca_vrfy_smtp->chain_cert[req_ca_vrfy_smtp->chain_offset] = xmemdup((char *)imsg->data +
163 sizeof *req_ca_vrfy_chain, req_ca_vrfy_chain->cert_len, "lka:ca_vrfy");
164 req_ca_vrfy_smtp->chain_cert_len[req_ca_vrfy_smtp->chain_offset] = req_ca_vrfy_chain->cert_len;
165 req_ca_vrfy_smtp->chain_offset++;
166 return;

- In the IMSG_{SMTP,MTA}_SSL_VERIFY case, PROC_LKA does not reset the
static pointer req_ca_vrfy_{smtp,mta} to NULL after free(), but trusts
PROC_PONY to always send an IMSG_{SMTP,MTA}_SSL_VERIFY_CERT (which
re-initializes this static pointer) before sending an
IMSG_{SMTP,MTA}_SSL_VERIFY{_CHAIN,} (use-after-free, CVE-2015-ABCD):

169 if (req_ca_vrfy_smtp == NULL)
170 fatalx("lka:ca_vrfy: verify without a certificate");
185 for (i = 0; i < req_ca_vrfy_smtp->n_chain; ++i)
186 free(req_ca_vrfy_smtp->chain_cert[i]);
187 free(req_ca_vrfy_smtp->chain_cert);
188 free(req_ca_vrfy_smtp->chain_cert_len);
189 free(req_ca_vrfy_smtp->cert);
190 free(req_ca_vrfy_smtp);
191 return;

CVE-2015-ABCD - Integer truncation in the imsg API

There is a fundamental design flaw in the imsg_create(), imsg_add(), and
imsg_compose() functions (and imsg_composev(), which calls imsg_create()
and imsg_add()): their datalen argument is directly an u_int16_t, a fact
that is easily overlooked by their callers and makes them vulnerable to
integer truncation.

In OpenSMTPD, the nearly-identical functions smtp_verify_certificate()
and mta_verify_certificate() are vulnerable to this integer truncation,
and both are reachable remotely through SSL certificate verification:

2046 static int
2047 smtp_verify_certificate(struct smtp_session *s)
2048 {
2056 x = SSL_get_peer_certificate(s->io.ssl);
2059 xchain = SSL_get_peer_cert_chain(s->io.ssl);
2071 /* Send the client certificate */
2072 memset(&req_ca_vrfy, 0, sizeof req_ca_vrfy);
2083 req_ca_vrfy.cert_len = i2d_X509(x, &req_ca_vrfy.cert);
2084 if (xchain)
2085 req_ca_vrfy.n_chain = sk_X509_num(xchain);
2086 iov[0].iov_base = &req_ca_vrfy;
2087 iov[0].iov_len = sizeof(req_ca_vrfy);
2088 iov[1].iov_base = req_ca_vrfy.cert;
2089 iov[1].iov_len = req_ca_vrfy.cert_len;
2090 m_composev(p_lka, IMSG_SMTP_SSL_VERIFY_CERT, 0, 0, -1,
2091 iov, nitems(iov));
2095 if (xchain) {
2096 /* Send the chain, one cert at a time */
2097 for (i = 0; i < sk_X509_num(xchain); ++i) {
2098 memset(&req_ca_vrfy, 0, sizeof req_ca_vrfy);
2100 x = sk_X509_value(xchain, i);
2101 req_ca_vrfy.cert_len = i2d_X509(x, &req_ca_vrfy.cert);
2102 iov[0].iov_base = &req_ca_vrfy;
2103 iov[0].iov_len = sizeof(req_ca_vrfy);
2104 iov[1].iov_base = req_ca_vrfy.cert;
2105 iov[1].iov_len = req_ca_vrfy.cert_len;
2106 m_composev(p_lka, IMSG_SMTP_SSL_VERIFY_CHAIN, 0, 0, -1,
2107 iov, nitems(iov));
2109 }
2110 }
2112 /* Tell lookup process that it can start verifying, we're done */
2113 memset(&req_ca_vrfy, 0, sizeof req_ca_vrfy);
2115 m_compose(p_lka, IMSG_SMTP_SSL_VERIFY, 0, 0, -1,
2116 &req_ca_vrfy, sizeof req_ca_vrfy);
2119 }

If the cert_len returned by i2d_X509() exceeds 64k, integer truncation
occurs when m_composev() calls imsg_composev(). The following #define
from OpenSSL and LibreSSL confirms that this is indeed possible:

#define SSL_MAX_CERT_LIST_DEFAULT 1024*100 /* 100k max cert list :-) */

Surprisingly, this integer truncation in PROC_PONY triggers the
out-of-bounds memory read in PROC_LKA: xmemdup() tries to read cert_len
bytes (the non-truncated cert_len) from imsg->data, which contains only
the truncated number of cert_len bytes (i.e., xmemdup() tries to read an
extra 64k from imsg->data).

Our initial research suggests that this out-of-bounds memory read can be
transformed into a remote information leak that reveals heap addresses
and defeats the ASLR protection. Linux is almost certainly exploitable,
but OpenBSD's heavily randomized and hardened malloc significantly
raises the bar for successful exploitation.

CVE-2015-ABCD - Missing return-value checks in the mproc API

The m_forward(), m_compose(), and m_composev() functions do not check
the return value of imsg_compose() and imsg_composev(): if the message
to be sent is larger than MAX_IMSGSIZE (16k), these three functions will
fail to send the message, but they will not report this failure to their
callers, which have therefore no way of knowing whether the message was
actually sent or not.

Again, smtp_verify_certificate() and mta_verify_certificate() are
vulnerable: if the peer's certificate is larger than 16k (but smaller
than 64k, in order to avoid the integer truncation), PROC_PONY will fail
to send the IMSG_{SMTP,MTA}_SSL_VERIFY_CERT to PROC_LKA, and the
subsequent IMSG_{SMTP,MTA}_SSL_VERIFY_CHAIN will trigger the
use-after-free in PROC_LKA, which in turn will trigger the out-of-bounds
memory write in PROC_LKA.

Our initial research suggests that this use-after-free (and
out-of-bounds memory write) can be transformed into remote code
execution, when combined with the information leak described above.
Linux is almost certainly exploitable, but again, OpenBSD's heavily
randomized and hardened malloc significantly raises the bar for
successful exploitation.

Inter-Process Vulnerabilities

Although we focused our audit on local and remote vulnerabilities,
nevertheless we identified four classes of common inter-process
vulnerabilities in OpenSMTPD:

- Out-of-bounds memory read: one process X receives a specific type of
structure (or structured data) from another process Y, but does not
check that process Y actually sent enough data; or, checks are
performed, but incorrectly (e.g., because of an integer-wrap).

- Indirect information leak: if such an out-of-bounds memory read
survives (i.e., X does not segfault), and if the partial structure
received by X is sent back to Y as a complete structure, information
from the memory of process X is leaked to process Y.

- Direct information leak: a structure sent by one process to another
process contains uninitialized fields (e.g., union fields, or large
string buffers that are only partially strlcpy()ed to), thus leaking
information from the memory of the sending process to the receiving

- Out-of-bounds memory write: one process receives data from another
process and copies it into a buffer without checking that it actually
fits; or, a structure is received and its contents (e.g., size fields)
are trusted without checks.


In parent_imsg(), case IMSG_LKA_OPEN_FORWARD:

169 static void
170 parent_imsg(struct mproc *p, struct imsg *imsg)
171 {
172 struct forward_req *fwreq;
186 fwreq = imsg->data;
196 m_compose(p, IMSG_LKA_OPEN_FORWARD, 0, 0, fd,
197 fwreq, sizeof *fwreq);

- Indirect information leak: fwreq.


In queue_imsg(), case IMSG_SCHED_ENVELOPE_BOUNCE:

64 static void
65 queue_imsg(struct mproc *p, struct imsg *imsg)
66 {
68 struct bounce_req_msg *req_bounce;
239 req_bounce = imsg->data;
250 queue_bounce(&evp, &req_bounce->bounce);
512 static void
513 queue_bounce(struct envelope *e, struct delivery_bounce *d)
514 {
515 struct envelope b;
519 b.agent.bounce = *d;
543 m_create(p_scheduler, IMSG_QUEUE_ENVELOPE_SUBMIT, 0, 0, -1);
544 m_add_envelope(p_scheduler, &b);
545 m_close(p_scheduler);

- Indirect information leak: req_bounce->bounce.



82 static void
83 control_imsg(struct mproc *p, struct imsg *imsg)
84 {
86 struct stat_value val;
87 struct msg m;
88 const char *key;
89 const void *data;
90 size_t sz;
165 case IMSG_STAT_SET:
166 m_msg(&m, imsg);
167 m_get_string(&m, &key);
168 m_get_data(&m, &data, &sz);
169 m_end(&m);
170 memmove(&val, data, sz);

- Out-of-bounds memory write (stack-based buffer overflow): in the call
to memmove(), the sz returned by m_get_data() is blindly trusted to be
equal to the size of the stack-based val structure (CVE-2015-ABCD).


In mda_imsg(), case IMSG_MDA_LOOKUP_USERINFO:

114 void
115 mda_imsg(struct mproc *p, struct imsg *imsg)
116 {
124 const void *data;
127 size_t sz;
135 m_msg(&m, imsg);
136 m_get_id(&m, &reqid);
137 m_get_int(&m, (int *)&status);
138 if (status == LKA_OK)
139 m_get_data(&m, &data, &sz);
140 m_end(&m);
144 if (status == LKA_TEMPFAIL)
148 else if (status == LKA_PERMFAIL)
152 else {
153 memmove(&u->userinfo, data, sz);

- Out-of-bounds memory write (heap-based buffer overflow): in the call
to memmove(), the sz returned by m_get_data() is blindly trusted to be
equal to the size of the heap-based u->userinfo structure


In mta_start_tls():

1501 static void
1502 mta_start_tls(struct mta_session *s)
1503 {
1504 struct ca_cert_req_msg req_ca_cert;
1505 const char *certname;
1507 if (s->relay->pki_name)
1508 certname = s->relay->pki_name;
1509 else
1510 certname = s->helo;
1512 req_ca_cert.reqid = s->id;
1513 (void)strlcpy(, certname, sizeof;
1514 m_compose(p_lka, IMSG_MTA_SSL_INIT, 0, 0, -1,
1515 &req_ca_cert, sizeof(req_ca_cert));

- Direct information leak: is not fully initialized by
strlcpy() (unlike strncpy(), strlcpy() does not fill its destination
buffer with additional null-bytes), and therefore still contains
sensitive information from the stack.


In mta_session_imsg(), case IMSG_MTA_SSL_INIT:

253 struct ca_cert_resp_msg *resp_ca_cert;
315 resp_ca_cert = imsg->data;
336 resp_ca_cert = xmemdup(imsg->data, sizeof *resp_ca_cert, "mta:ca_cert");
337 resp_ca_cert->cert = xstrdup((char *)imsg->data +
338 sizeof *resp_ca_cert, "mta:ca_cert");
343 ssl = ssl_mta_init(pkiname,
344 resp_ca_cert->cert, resp_ca_cert->cert_len);
349 explicit_bzero(resp_ca_cert->cert, resp_ca_cert->cert_len);

- Out-of-bounds memory read: resp_ca_cert.

- Out-of-bounds memory read: the string passed to xstrdup() is not
guaranteed to be null-terminated.

- Out-of-bounds memory read: in the call to ssl_mta_init(),
resp_ca_cert->cert_len is blindly trusted to be equal to
resp_ca_cert->cert's xstrdup()ed length.

- Out-of-bounds memory write: in the call to explicit_bzero(),
resp_ca_cert->cert_len is blindly trusted to be equal to
resp_ca_cert->cert's xstrdup()ed length (CVE-2015-ABCD).


In mta_session_imsg(), case IMSG_MTA_SSL_VERIFY:

252 struct ca_vrfy_resp_msg *resp_ca_vrfy;
355 resp_ca_vrfy = imsg->data;
356 s = mta_tree_pop(&wait_ssl_verify, resp_ca_vrfy->reqid);
360 if (resp_ca_vrfy->status == CA_OK)

- Out-of-bounds memory read: resp_ca_vrfy.


In smtp_session_imsg(), case IMSG_SMTP_SSL_INIT:

551 struct ca_cert_resp_msg *resp_ca_cert;
832 resp_ca_cert = imsg->data;
842 resp_ca_cert = xmemdup(imsg->data, sizeof *resp_ca_cert, "smtp:ca_cert");
845 resp_ca_cert->cert = xstrdup((char *)imsg->data +
846 sizeof *resp_ca_cert, "smtp:ca_cert");
861 explicit_bzero(resp_ca_cert->cert, resp_ca_cert->cert_len);

- Out-of-bounds memory read: resp_ca_cert.

- Out-of-bounds memory read: the string passed to xstrdup() is not
guaranteed to be null-terminated.

- Out-of-bounds memory write: in the call to explicit_bzero(),
resp_ca_cert->cert_len is blindly trusted to be equal to
resp_ca_cert->cert's xstrdup()ed length (CVE-2015-ABCD).


In smtp_session_imsg(), case IMSG_SMTP_SSL_VERIFY:

552 struct ca_vrfy_resp_msg *resp_ca_vrfy;
867 resp_ca_vrfy = imsg->data;
868 s = tree_xpop(&wait_ssl_verify, resp_ca_vrfy->reqid);
870 if (resp_ca_vrfy->status == CA_OK)

- Out-of-bounds memory read: resp_ca_vrfy.


In smtp_mfa_response() and smtp_io(), cases IMSG_SMTP_REQ_CONNECT and
IO_LOWAT, respectively:

888 static void
889 smtp_mfa_response(struct smtp_session *s, int msg, int status, uint32_t code,
890 const char *line)
891 {
892 struct ca_cert_req_msg req_ca_cert;
915 if (s->listener->pki_name[0])
916 (void)strlcpy(, s->listener->pki_name,
917 sizeof;
918 else
919 (void)strlcpy(, s->smtpname,
920 sizeof;
921 m_compose(p_lka, IMSG_SMTP_SSL_INIT, 0, 0, -1,
922 &req_ca_cert, sizeof(req_ca_cert));

- Direct information leak: is not fully initialized by
strlcpy(), and therefore still contains sensitive information from the


In lka_imsg(), case IMSG_{SMTP,MTA}_SSL_INIT:

struct ca_cert_resp_msg {
uint64_t reqid;
enum ca_resp_status status;
char *cert;
off_t cert_len;

63 static void
64 lka_imsg(struct mproc *p, struct imsg *imsg)
65 {
75 struct ca_cert_resp_msg resp_ca_cert;
127 req_ca_cert = imsg->data;
128 resp_ca_cert.reqid = req_ca_cert->reqid;
133 if (pki == NULL) {
134 resp_ca_cert.status = CA_FAIL;
135 m_compose(p, IMSG_SMTP_SSL_INIT, 0, 0, -1, &resp_ca_cert,
136 sizeof(resp_ca_cert));
137 return;
138 }

- Out-of-bounds memory read: req_ca_cert.

- Direct information leak: resp_ca_cert's cert and cert_len fields.


In ca_imsg(), case IMSG_CA_PRIV{ENC,DEC}:

294 m_msg(&m, imsg);
295 m_get_id(&m, &id);
296 m_get_string(&m, &pkiname);
297 m_get_data(&m, &from, &flen);
298 m_get_size(&m, &tlen);
299 m_get_size(&m, &padding);
300 m_end(&m);
307 if ((to = calloc(1, tlen)) == NULL)
308 fatalx("ca_imsg: calloc");
310 switch (imsg->hdr.type) {
312 ret = RSA_private_encrypt(flen, from, to, rsa,
313 padding);
314 break;
316 ret = RSA_private_decrypt(flen, from, to, rsa,
317 padding);
318 break;
319 }

- Out-of-bounds memory write (heap-based buffer overflow): in the call
to RSA_private_{enc,dec}rypt(), the size (tlen) of the destination
buffer (to) is blindly trusted to be equal to RSA_size(rsa)


In m_get_typed_sized():

493 static inline void
494 m_get_typed_sized(struct msg *m, uint8_t type, const void **dst, size_t *sz)
495 {
496 if (m->pos + 1 + sizeof(*sz) > m->end)
497 m_error("msg too short");
498 if (*m->pos != type)
499 m_error("msg bad type");
500 memmove(sz, m->pos + 1, sizeof(*sz));
501 m->pos += sizeof(sz) + 1;
502 if (m->pos + *sz > m->end)
503 m_error("msg too short");
504 *dst = m->pos;
505 m->pos += *sz;
506 }

- Out-of-bounds memory read: *sz, the amount of data allegedly received,
is read directly from the wire and sanity-checked, but large *sz
values can integer-wrap the check.


In m_get_sockaddr():

705 void
706 m_get_sockaddr(struct msg *m, struct sockaddr *sa)
707 {
708 size_t s;
709 const void *d;
711 m_get_typed_sized(m, M_SOCKADDR, &d, &s);
712 memmove(sa, d, s);
713 }

- Out-of-bounds memory write (buffer overflow): in the call to
memmove(), the size s of the data d returned by m_get_typed_sized() is
blindly trusted to be equal to the size of the sockaddr structure sa

Miscellaneous Bugs


In mta_imsg(), case IMSG_CTL_RESUME_ROUTE, if u64 is 0 ("resuming all
routes"), mta_route_unref() may eventually free() route, which is then
used-after-free by SPLAY_NEXT() in SPLAY_FOREACH() (there is a SAFE
version of most FOREACH macros, but no SPLAY_FOREACH_SAFE()):

420 u64 = *((uint64_t *)imsg->data);
426 SPLAY_FOREACH(route, mta_route_tree, &routes) {
427 if (u64 && route->id != u64)
428 continue;
430 if (route->flags & ROUTE_DISABLED) {
441 mta_route_unref(route); /* from mta_route_disable */
442 }
444 if (u64)
445 break;
446 }
447 return;


In parent_sig_handler(), the cause pointer should always be initialized
to NULL before the calls to asprintf(), and the return value of these
calls should be checked (on OpenBSD, asprintf() will always reset the
cause pointer to NULL in case of a failure, but this behavior is

351 static void
352 parent_sig_handler(int sig, short event, void *p)
353 {
357 char *cause;
365 do {
366 pid = waitpid(-1, &status, WNOHANG);
371 if (WIFSIGNALED(status)) {
373 asprintf(&cause, "terminated; signal %d",
374 WTERMSIG(status));
375 } else if (WIFEXITED(status)) {
376 if (WEXITSTATUS(status) != 0) {
378 asprintf(&cause, "exited abnormally");
379 } else
380 asprintf(&cause, "exited okay");
381 } else
382 fatalx("smtpd: unexpected cause of SIGCHLD");
442 free(cause);
443 } while (pid > 0 || (pid == -1 && errno == EINTR));


Code similar to the following appears several times in OpenSMTPD:

979 (void)strlcpy(sfn, "/tmp/smtpd.out.XXXXXXXXXXX", sizeof(sfn));
980 omode = umask(7077);
981 allout = mkstemp(sfn);
982 umask(omode);
983 if (allout < 0) {
991 return;
992 }
993 unlink(sfn);

But 7077 is decimal, not octal; in octal, 7077 is 015645. Luckily, the
call to mkstemp() that always follows uses mode 0600, which results in
the final mode 0000 (0600 & ~015645). This is not a security issue,
because these permissions are even more restrictive than those
originally intended.


In do_show_queue(), chdir(".") should rather be chdir("/"), because the
current working directory may be outside the chroot tree:

652 if (chroot(PATH_SPOOL) == -1 || chdir(".") == -1)
653 err(1, "%s", PATH_SPOOL);

However, this is not a security issue either: do_show_queue() is an
smtpctl functionality, only root is allowed to run it, and all
subsequent filesystem accesses begin with '/' anyway.


We would like to thank OpenSMTPD's developers for their cooperation,
professional work, and minute attention to every detail in our audit

See this note in RAW Version

Bugtraq RSS

Copyright 2017,