Vulnerability Summary
mtheall/ftpd v3.2.1 is affected by a one-byte heap buffer overflow
(off-by-one, CWE-193) in the fillDirent() function at
source/ftpSession.cpp:1066-1070.
Affected Component
fillDirent() constructs MLSD/MLST directory listing facts. The
"Perm=" fact handler copies a fixed string into an IOBuffer without
accounting for the null terminator in its length check.
Root Cause
Affected code (source/ftpSession.cpp lines 1066-1070):
auto const header = "Perm="; // 5 characters
if (size - pos < std::strlen(header)) // strlen = 5
return EAGAIN;
std::strcpy(&buffer[pos], header); // writes 6 bytes
strlen("Perm=") returns 5. The check passes when size-pos >= 5.
But strcpy() requires 6 bytes (5 chars + null byte \0).
When size-pos == 5, the check passes (5 < 5 is false),
yet strcpy writes 6 bytes, overflowing by one byte.
Proof of Concept
Compiled with Clang 15 + AddressSanitizer on Ubuntu 24.04:
$ clang++-15 -std=c++20 -fsanitize=address -g -o poc poc.cpp
$ ./poc
ASAN output (key lines):
==ERROR: AddressSanitizer: stack-buffer-overflow
WRITE of size 6 at 0x72355cb00025 thread T0
#1 in bug(char*, unsigned long, unsigned long) poc.cpp:6:5
#2 in main poc.cpp:10:5
This frame has 1 object(s):
[32, 37) 'buf' <== Memory access at offset 37 overflows
Shadow bytes: f1 f1 f1 f1[05]f3 f3 f3
^^ ^^
32-36 buf 37=overflow
SUMMARY: AddressSanitizer: stack-buffer-overflow
A buffer of exactly 5 bytes at position 0 consistently triggers
the overflow. Exit code is 1 (ABORT).
Impact
The buffer in production is a heap-allocated IOBuffer of 65536 bytes
(XFER_BUFFERSIZE on Linux). The overflow writes \0 past the buffer
end into adjacent heap memory. Impact depends on heap layout:
- Best case: \0 lands in alignment padding — no observable effect
- Likely case: \0 corrupts heap chunk metadata — denial of service
- Worst case: \0 corrupts adjacent allocation data — further
memory corruption
Exploitation requires:
- Authentication to the FTP server
- m_mlstPerm flag enabled (default: on)
- IOBuffer free space exactly 5 bytes when fillDirent() runs
Fix
Line 1067, change:
if (size - pos < std::strlen(header))
To:
if (size - pos < std::strlen(header) + 1)
Affected Versions
All versions from initial release through v3.2.1.
(The fillDirent "Perm=" code has never been modified.)
Discovery
- Discovered by: Xu Haoyu (徐浩宇)
- Date: 2026-06-25
- Method: Source code review + AFL++ fuzzing project + ASAN verification
- Environment: Clang 15, Ubuntu 24.04 (WSL2), AFL++ 4.32a
- Vendor notified via GitHub Issue
Vulnerability Summary
mtheall/ftpd v3.2.1 is affected by a one-byte heap buffer overflow
(off-by-one, CWE-193) in the fillDirent() function at
source/ftpSession.cpp:1066-1070.
Affected Component
fillDirent() constructs MLSD/MLST directory listing facts. The
"Perm=" fact handler copies a fixed string into an IOBuffer without
accounting for the null terminator in its length check.
Root Cause
Affected code (source/ftpSession.cpp lines 1066-1070):
auto const header = "Perm="; // 5 characters
if (size - pos < std::strlen(header)) // strlen = 5
return EAGAIN;
std::strcpy(&buffer[pos], header); // writes 6 bytes
strlen("Perm=") returns 5. The check passes when size-pos >= 5.
But strcpy() requires 6 bytes (5 chars + null byte \0).
When size-pos == 5, the check passes (5 < 5 is false),
yet strcpy writes 6 bytes, overflowing by one byte.
Proof of Concept
Compiled with Clang 15 + AddressSanitizer on Ubuntu 24.04:
$ clang++-15 -std=c++20 -fsanitize=address -g -o poc poc.cpp
$ ./poc
ASAN output (key lines):
==ERROR: AddressSanitizer: stack-buffer-overflow
WRITE of size 6 at 0x72355cb00025 thread T0
#1 in bug(char*, unsigned long, unsigned long) poc.cpp:6:5
#2 in main poc.cpp:10:5
This frame has 1 object(s):
[32, 37) 'buf' <== Memory access at offset 37 overflows
Shadow bytes: f1 f1 f1 f1[05]f3 f3 f3
^^ ^^
32-36 buf 37=overflow
SUMMARY: AddressSanitizer: stack-buffer-overflow
A buffer of exactly 5 bytes at position 0 consistently triggers
the overflow. Exit code is 1 (ABORT).
Impact
The buffer in production is a heap-allocated IOBuffer of 65536 bytes
(XFER_BUFFERSIZE on Linux). The overflow writes \0 past the buffer
end into adjacent heap memory. Impact depends on heap layout:
memory corruption
Exploitation requires:
Fix
Line 1067, change:
if (size - pos < std::strlen(header))
To:
if (size - pos < std::strlen(header) + 1)
Affected Versions
All versions from initial release through v3.2.1.
(The fillDirent "Perm=" code has never been modified.)
Discovery