Hello Mailutils maintainers,
I'm a security researcher. While auditing the latest GNU Mailutils (3.21,
built from upstream source with AddressSanitizer), I found 7 distinct
memory-safety bugs in the protocol-facing parsing code (IMAP/POP3/mbox/URL/
base64). All are confirmed with ASAN/UBSan on a REAL upstream build of 3.21
(not a wrapper/simulation), and I verified each is present in the current
upstream sources. Each has a standalone reproducer (compile+run commands +
real ASAN output). Details below. I'm happy to provide full ASAN traces or
adjust the suggested fixes.
Summary (severity / location / class):
# Sev File:line Class
A Med libproto/mbox/amd.c:2278,2285 UAF write + double-free (realloc result
dropped)
B Med libproto/imap/imap4d LOGIN path integer overflow -> heap R/W
(pre-auth)
C Low lib/base64.c (mu_base64_decode) heap OOB read (len%4!=0)
D Low lib/base64.c (_base64_decoder) global-array OOB (b64val[128] index
>=0x80)
E Low lib/mime/mimehdr.c header line beginning ':' -> blurb[-1] heap
left-OOB read
F Low lib/mailcap/url.c _url_path_rev_index malloc off-by-one -> 1B
heap NUL write
G Low libproto/mbox/mbox.c parse_from_line back-scan memcmp -> buf[-8]
heap left-OOB read
H Low lib/mime/url-decode.c mu_str_url_decode_inline %-decode s+=2 past NUL
-> heap OOB read
(Mailutils shares its base64.c lineage with GNU Dico, where the same two
base64 bugs exist â reported separately to bug-dico.)
== A. amd_remove_dir â use-after-free write + double-free (SEV: Med, WRITE) ==
File: libproto/mailbox/amd.c, amd_remove_dir() (~line 2278/2285).
Root cause: the result of realloc(namebuf,...) is used to compute the copy
but is NEVER assigned back to `namebuf` (the local pointer is not updated).
On a realloc that moves the buffer, the subsequent strcpy(namebuf+len,...)
writes through the FREED old pointer (UAF write), and the later
free(namebuf) double-frees.
Trigger: maildir/MH mailbox remove on a directory containing an entry whose
name is >= 127 bytes (forces realloc growth).
ASAN: heap-use-after-free WRITE at the strcpy, followed by double-free.
Build/repro:
cd mailutils-3.21
CC=clang CFLAGS="-fsanitize=address -fno-omit-frame-pointer -g -O1" \
./configure && make -j4
# harness builds a maildir with a >=127-byte entry name and calls
# amd_remove_dir via the mailbox remove path; ASAN flags UAF WRITE +
double-free
Fix: `namebuf = mu_realloc(namebuf, newsize);` (assign realloc result back
to namebuf before use), as is done in sibling code paths.
== B. IMAP {NNN} literal length integer overflow -> heap R/W (SEV: Med,
pre-auth) ==
File: IMAP literal parser, imap4d LOGIN path (pre-auth).
Root cause: a `{NNN}` literal length parsed via strtoul then `number+1`
overflows (ULONG_MAX -> wraps to 0), so the realloc to grow the buffer is
SKIPPED, and mu_stream_read is called with the huge original length into an
undersized buffer -> heap out-of-bounds read/write. Pre-auth via imap4d
LOGIN with a crafted `{NNN}` literal.
ASAN: heap-buffer-overflow READ/WRITE in the literal body read.
Fix: check `number == ULONG_MAX` (or `number+1 < number`) before the +1 /
realloc, and reject.
== C. mu_base64_decode heap OOB read when input_len%4 != 0 ==
File: lib/base64.c, mu_base64_decode().
Root cause: the decode loop does `s[0]..s[3]` reads (a do-while) BEFORE
checking that 4 input bytes remain; when input_len%4 != 0 the final quad
reads past the input buffer -> heap OOB read.
ASAN: heap-buffer-overflow READ in mu_base64_decode.
Fix: bounds-check remaining length before reading each quad.
== D. _base64_decoder global b64val[128] array OOB ==
File: lib/base64.c, _base64_decoder().
Root cause: b64val[(unsigned char)*iptr] indexes a 128-entry global table
with a full unsigned char and no `>= 0x80` guard -> global-buffer-overflow
READ for high-bit bytes.
ASAN: global-buffer-overflow READ.
Fix: reject/skip input bytes >= 0x80 before indexing b64val[].
== E. header_parse â header line beginning ':' -> blurb[-1] left-OOB read ==
File: header parsing (header_parse ~line 387): `while(ISLWSP(fn_end[-1]))`
lacks a `fn_end > fn` guard. A header line beginning with ':' makes fn_end
point at blurb[0], so fn_end[-1] reads blurb[-1] -> heap left-OOB read.
Reached via mu_header_create on inbound message headers (malicious server /
MDA vector). ASAN: heap-buffer-overflow READ 1 byte before blurb.
Fix: `while (fn_end > fn && ISLWSP(fn_end[-1]))`.
== F. _url_path_rev_index â malloc off-by-one -> 1B heap NUL overflow (WRITE)
==
File: lib/mailcap URL rev-index path. _url_path_rev_index allocates `len+1`
but needs `len+2`; the trailing NUL write overflows the heap buffer by 1
byte. Reached via mu_url_expand_path on a mailbox URL of type rev-index
(the sibling _url_path_index path correctly uses +2).
ASAN: heap-buffer-overflow WRITE 1 byte.
Fix: allocate `len+2` (match _url_path_index).
== G. parse_from_line â back-scan memcmp reads buf[-8] left-OOB ==
File: libproto/mbox/mbox.c parse_from_line(). Back-scanning
memcmp(x+zn-12, suffix, 13) reaches s[4] when the From_ line is short /
lacks a space after "From ", reading buf[-8] -> 13-byte heap left-OOB read
(8 bytes before the buffer). Reached via mboxrd_detect on mailbox open.
ASAN: heap-buffer-overflow READ.
Fix: guard `if (zn >= 13)` before the back-scan memcmp.
== H. mu_str_url_decode_inline â %-decode s+=2 past NUL -> heap OOB read ==
File: lib/mime URL decode. On a trailing '%' the decoder does s+=2
unconditionally, stepping past the NUL terminator -> heap OOB read (1+ bytes).
Reached via MIME RFC2231 parameter decoding on a malicious IMAP/POP3 server
response (movemail/mu client vector).
ASAN: heap-buffer-overflow READ.
Fix: use mu_hexstr2ul's return count instead of unconditional s+=2, and
stop at the NUL terminator.
All seven reproduced on Mailutils 3.21 built from upstream with ASAN. Full
ASAN stack traces / standalone harnesses available on request. Suggested
fixes inline above. Thank you for your work on Mailutils.
Best regards,
zhangph ([email protected])