When wxXPMDecoder::ReadFile scans for the */ that closes a /* comment, the inner loop only stops at either */ or the buffer's null terminator. If the comment is unterminated, q ends pointing at the trailing \0, and the next line does strlen(q + 2) + 1, which reads past the allocated wxCharBuffer until it finds another null byte somewhere on the heap.
src/common/xpmdecod.cpp:166-174:
for (q = p + 2; *q != '\0'; q++) { if ( (*q == '*') && (*(q + 1) == '/') ) break; } // memmove allows overlaps (unlike strcpy): size_t cpylen = strlen(q + 2) + 1; // line 173 — reads past buffer end memmove(p, q + 2, cpylen);
The buffer is allocated at line 133 (wxCharBuffer xpm_buffer(srcLen + 1)); its accessible range is [xpm_buffer, xpm_buffer + srcLen] with the trailing null at srcLen. When q == xpm_buffer + srcLen, q + 2 is two bytes past the allocation's end and strlen walks until it finds an unrelated null byte.
Found by an ASAN libFuzzer harness against wxXPMHandler::LoadFile.
An XPM whose /* ... */ comment is missing the closing */ should be rejected or have the rest of the buffer skipped. Currently strlen reads past the allocation, and the subsequent memmove writes those out-of-bounds bytes back into the buffer.
==1==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7c280762827f
READ of size 1 at 0x7c280762827f thread T0
#0 strlen sanitizer_common_interceptors.inc:425:5
#1 wxXPMDecoder::ReadFile(wxInputStream&) src/common/xpmdecod.cpp:173:25
#2 wxXPMHandler::LoadFile(wxImage*, wxInputStream&, bool, int) src/common/imagxpm.cpp:95:27
#3 LLVMFuzzerTestOneInput tests/fuzz/xpm.cpp:13:7
0x7c280762827f is located 1 bytes after 62-byte region [0x7c2807628240,0x7c280762827e)
allocated by:
#1 wxCharTypeBuffer<char>::wxCharTypeBuffer(unsigned long) include/wx/buffer.h:340:43
#3 wxXPMDecoder::ReadFile(wxInputStream&) src/common/xpmdecod.cpp:133:18
SUMMARY: AddressSanitizer: heap-buffer-overflow src/common/xpmdecod.cpp:173:25 in wxXPMDecoder::ReadFile
Drop the following as tests/fuzz/xpm.cpp next to the existing tests/fuzz/runner.cpp:
#include "wx/log.h" #include "wx/mstream.h" #include "wx/imagxpm.h" extern "C" int LLVMFuzzerTestOneInput(const wxUint8* data, size_t size) { wxLogNull noLog; wxMemoryInputStream mis(data, size); wxImage img; wxXPMHandler h; h.LoadFile(&img, mis, false); return 0; }
61-byte PoC, hex below (an XPM-like payload that opens a /* comment without closing it). Reconstruct with xxd -r -p hex.txt poc.xpm:
2f2a20a8afb2dfd5d0dfce2f0a746174696320636861745b5d3d7b7b0a203120
312031222c0a226120632023666666666666222c0a22612261227d3b0a
Check out master at the current HEAD and apply the harness above to tests/fuzz/xpm.cpp.
Save the hex above and decode: xxd -r -p hex.txt poc.xpm.
Build:
clang++ -g -fsanitize=address tests/fuzz/{xpm,runner}.cpp \
`wx-config --cxxflags --libs core,base` -o repro_xpm
Run ./repro_xpm poc.xpm and observe the ASAN trace.
After the inner loop, detect the unterminated case before strlen:
if (*q == '\0') break; // unterminated /*-comment; stop processing
Patch ready with an inline-bytes Catch2 regression test. Happy to send a PR.
f57b17ba5f63db53dd1b465646ade105fee22302. Line 173 is unchanged at tag v3.3.2, and the same pattern is present at v3.2.0.gcr.io/oss-fuzz-base/base-builder).—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()
—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
You are receiving this because you are subscribed to this thread.![]()