In the 8bpp BMP decoder, aByte (a colour index, either pulled from the RLE stream or read directly as a per-pixel index) is used to index cmap without checking it against ncolors. The fuzzer trip site is the non-RLE branch at src/common/imagbmp.cpp:903:
else { ptr[poffset ] = cmap[aByte].r; // line 903, no aByte < ncolors check ptr[poffset + 1] = cmap[aByte].g; ptr[poffset + 2] = cmap[aByte].b; column++; }
The two RLE branches in the same function (imagbmp.cpp:876 absolute mode and :894 encoded mode) read from the same cmap with the same aByte source and have the same missing guard. cmap is the local BMPPalette[ncolors] populated earlier in the function; the column < width check the encoded loop already has bounds the destination but not the palette index.
Any 8bpp BMP whose pixel or RLE stream contains an index larger than the palette's colour count reads heap bytes past the palette buffer and writes them into the decoded image's pixel data.
Found by an ASAN libFuzzer harness against wxBMPHandler::LoadFile.
Loading an 8bpp BMP whose pixel stream references a colour index outside the palette should either be rejected or clamped. Currently it reads heap memory adjacent to the palette buffer.
==1==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7bb6a1a2a188
READ of size 1 at 0x7bb6a1a2a188 thread T0
#0 (anonymous namespace)::LoadBMPData(...) src/common/imagbmp.cpp:903:56
#1 wxBMPHandler::LoadDib(wxImage*, wxInputStream&, bool, bool) src/common/imagbmp.cpp:1269:11
#2 LLVMFuzzerTestOneInput tests/fuzz/bmp.cpp:14:7
0x7bb6a1a2a188 is located 16 bytes after 40-byte region [0x7bb6a1a2a150,0x7bb6a1a2a178)
allocated by:
#1 wxPalette::Create(int, ...) src/generic/paletteg.cpp:75:17
#3 (anonymous namespace)::LoadBMPData(...) src/common/imagbmp.cpp:597:27
SUMMARY: AddressSanitizer: heap-buffer-overflow src/common/imagbmp.cpp:903:56 in LoadBMPData
Drop the following as tests/fuzz/bmp.cpp next to the existing tests/fuzz/runner.cpp:
#include "wx/log.h" #include "wx/mstream.h" #include "wx/imagbmp.h" extern "C" int LLVMFuzzerTestOneInput(const wxUint8* data, size_t size) { wxLogNull noLog; wxMemoryInputStream mis(data, size); wxImage img; wxBMPHandler h; h.LoadFile(&img, mis, false); return 0; }
96-byte PoC, hex below. Reconstruct with xxd -r -p:
424d3a000000000000000000090000002800000028000000010000000100
0800000000000000424d3a000000000000000000030000000000000000
00000000002b000000000000000000000000000000000000000000000005
000000000028000000010000
Check out master at the current HEAD and apply the harness above to tests/fuzz/bmp.cpp.
Save the hex above to a file and decode: xxd -r -p hex.txt poc.bmp.
Build:
clang++ -g -fsanitize=address tests/fuzz/{bmp,runner}.cpp \
`wx-config --cxxflags --libs core,base` -o repro_bmp
Run ./repro_bmp poc.bmp and observe the ASAN trace.
Bound aByte against ncolors at each lookup, matching the surrounding if (!stream.IsOk()) return false pattern. I have a patch with a regression test (inline-bytes Catch2 case) ready, similar in shape to 05404fd379 ("Avoid negative shift count with valid BMP file"). Happy to send a PR.
f57b17ba5f63db53dd1b465646ade105fee22302, line 903 verified at tag v3.3.2; the same pattern is present (different line number) 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.![]()