/src/libtiff/contrib/oss-fuzz/tiff_packbits_fuzzer.cc
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2026 Anthony Adame |
3 | | * |
4 | | * Permission to use, copy, modify, distribute, and sell this software and |
5 | | * its documentation for any purpose is hereby granted without fee, provided |
6 | | * that (i) the above copyright notice and this permission notice appear in |
7 | | * all copies of the software and related documentation, and (ii) the name of |
8 | | * the author may not be used in any advertising or publicity relating to the |
9 | | * software without the specific, prior written permission of the author. |
10 | | * |
11 | | * THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, |
12 | | * EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY |
13 | | * WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. |
14 | | * |
15 | | * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR |
16 | | * ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, |
17 | | * OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, |
18 | | * WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF |
19 | | * LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE |
20 | | * OF THIS SOFTWARE. |
21 | | */ |
22 | | |
23 | | /* |
24 | | * Targeted fuzzer for the PackBits decoder (tif_packbits.c). |
25 | | * |
26 | | * Feeding raw bytes to a whole-file TIFF fuzzer rarely reaches a specific |
27 | | * codec: almost every input dies in header/IFD parsing first. This harness |
28 | | * instead synthesises a minimal, always-valid single-strip TIFF container in |
29 | | * memory and uses the fuzz input as the *compressed strip payload*, so every |
30 | | * iteration reaches PackBitsDecode() with an attacker-controlled bitstream. |
31 | | * |
32 | | * A few leading bytes select the image geometry, which varies the size of the |
33 | | * output buffer the decoder must not overrun (the `occ` cursor) independently |
34 | | * of the payload it is decoding -- that pairing is what exercises the bounds |
35 | | * logic. Dimensions are clamped so the decoded strip stays small and the |
36 | | * fuzzer reports real memory errors rather than out-of-memory. |
37 | | */ |
38 | | |
39 | | #include <cstddef> |
40 | | #include <cstdint> |
41 | | #include <cstring> |
42 | | |
43 | | #include <vector> |
44 | | |
45 | | #include <fuzzer/FuzzedDataProvider.h> |
46 | | |
47 | | #include "tiffio.h" |
48 | | |
49 | | /* Keep the decoded strip small: OOM is not the bug class under test. */ |
50 | | static const uint32_t kMaxWidth = 1024; |
51 | | static const uint32_t kMaxHeight = 64; |
52 | | |
53 | | namespace |
54 | | { |
55 | | |
56 | | /* ---- in-memory TIFF client ------------------------------------------- */ |
57 | | |
58 | | struct MemFile |
59 | | { |
60 | | const uint8_t *data; |
61 | | toff_t size; |
62 | | toff_t pos; |
63 | | }; |
64 | | |
65 | | tmsize_t memRead(thandle_t handle, void *buf, tmsize_t want) |
66 | 1.49k | { |
67 | 1.49k | MemFile *m = static_cast<MemFile *>(handle); |
68 | 1.49k | if (want < 0 || m->pos >= m->size) |
69 | 0 | return 0; |
70 | 1.49k | toff_t avail = m->size - m->pos; |
71 | 1.49k | if (static_cast<toff_t>(want) > avail) |
72 | 0 | want = static_cast<tmsize_t>(avail); |
73 | 1.49k | memcpy(buf, m->data + m->pos, static_cast<size_t>(want)); |
74 | 1.49k | m->pos += static_cast<toff_t>(want); |
75 | 1.49k | return want; |
76 | 1.49k | } |
77 | | |
78 | 0 | tmsize_t memWrite(thandle_t, void *, tmsize_t) { return 0; } |
79 | | |
80 | | toff_t memSeek(thandle_t handle, toff_t off, int whence) |
81 | 596 | { |
82 | 596 | MemFile *m = static_cast<MemFile *>(handle); |
83 | 596 | toff_t base; |
84 | 596 | switch (whence) |
85 | 596 | { |
86 | 596 | case SEEK_SET: |
87 | 596 | base = 0; |
88 | 596 | break; |
89 | 0 | case SEEK_CUR: |
90 | 0 | base = m->pos; |
91 | 0 | break; |
92 | 0 | case SEEK_END: |
93 | 0 | base = m->size; |
94 | 0 | break; |
95 | 0 | default: |
96 | 0 | return static_cast<toff_t>(-1); |
97 | 596 | } |
98 | | /* Refuse wrap-around rather than silently aliasing a valid offset. */ |
99 | 596 | if (off > static_cast<toff_t>(-1) - base) |
100 | 0 | return static_cast<toff_t>(-1); |
101 | 596 | m->pos = base + off; |
102 | 596 | return m->pos; |
103 | 596 | } |
104 | | |
105 | 298 | int memClose(thandle_t) { return 0; } |
106 | | |
107 | | toff_t memSize(thandle_t handle) |
108 | 0 | { |
109 | 0 | return static_cast<MemFile *>(handle)->size; |
110 | 0 | } |
111 | | |
112 | | /* memMap/memUnmap are passed as null: libtiff substitutes no-op stubs. */ |
113 | | |
114 | | /* ---- quiet libtiff diagnostics --------------------------------------- */ |
115 | | |
116 | 174 | void silentError(const char *, const char *, va_list) {} |
117 | 252 | void silentWarning(const char *, const char *, va_list) {} |
118 | | |
119 | | /* ---- minimal little-endian TIFF writer -------------------------------- */ |
120 | | |
121 | | void put16(std::vector<uint8_t> &v, uint16_t x) |
122 | 5.96k | { |
123 | 5.96k | v.push_back(static_cast<uint8_t>(x & 0xff)); |
124 | 5.96k | v.push_back(static_cast<uint8_t>((x >> 8) & 0xff)); |
125 | 5.96k | } |
126 | | |
127 | | void put32(std::vector<uint8_t> &v, uint32_t x) |
128 | 5.96k | { |
129 | 5.96k | v.push_back(static_cast<uint8_t>(x & 0xff)); |
130 | 5.96k | v.push_back(static_cast<uint8_t>((x >> 8) & 0xff)); |
131 | 5.96k | v.push_back(static_cast<uint8_t>((x >> 16) & 0xff)); |
132 | 5.96k | v.push_back(static_cast<uint8_t>((x >> 24) & 0xff)); |
133 | 5.96k | } |
134 | | |
135 | | void putEntry(std::vector<uint8_t> &v, uint16_t tag, uint16_t type, |
136 | | uint32_t count, uint32_t value) |
137 | 2.68k | { |
138 | 2.68k | put16(v, tag); |
139 | 2.68k | put16(v, type); |
140 | 2.68k | put32(v, count); |
141 | | /* Single SHORT/LONG values fit inline; little-endian puts a SHORT in the |
142 | | * first two bytes of the field, so writing the full word is correct. */ |
143 | 2.68k | put32(v, value); |
144 | 2.68k | } |
145 | | |
146 | 1.19k | #define TIFF_TYPE_SHORT 3 |
147 | 1.49k | #define TIFF_TYPE_LONG 4 |
148 | | |
149 | | /* 9 directory entries, one strip, 8-bit grayscale, PackBits-compressed. */ |
150 | 894 | #define ENTRY_COUNT 9 |
151 | 894 | #define IFD_OFFSET 8 |
152 | 596 | #define IFD_BYTES (2 + (ENTRY_COUNT * 12) + 4) |
153 | 596 | #define STRIP_OFFSET (IFD_OFFSET + IFD_BYTES) |
154 | | |
155 | | std::vector<uint8_t> buildPackBitsTiff(uint32_t width, uint32_t height, |
156 | | const std::vector<uint8_t> &payload) |
157 | 298 | { |
158 | 298 | std::vector<uint8_t> t; |
159 | 298 | t.reserve(STRIP_OFFSET + payload.size()); |
160 | | |
161 | | /* Header: little-endian, classic TIFF, IFD immediately after. */ |
162 | 298 | t.push_back('I'); |
163 | 298 | t.push_back('I'); |
164 | 298 | put16(t, 42); |
165 | 298 | put32(t, IFD_OFFSET); |
166 | | |
167 | 298 | put16(t, ENTRY_COUNT); /* entries must be in ascending tag order */ |
168 | 298 | putEntry(t, 256, TIFF_TYPE_LONG, 1, width); /* ImageWidth */ |
169 | 298 | putEntry(t, 257, TIFF_TYPE_LONG, 1, height); /* ImageLength */ |
170 | 298 | putEntry(t, 258, TIFF_TYPE_SHORT, 1, 8); /* BitsPerSample */ |
171 | 298 | putEntry(t, 259, TIFF_TYPE_SHORT, 1, COMPRESSION_PACKBITS); |
172 | 298 | putEntry(t, 262, TIFF_TYPE_SHORT, 1, PHOTOMETRIC_MINISBLACK); |
173 | 298 | putEntry(t, 273, TIFF_TYPE_LONG, 1, STRIP_OFFSET); /* StripOffsets */ |
174 | 298 | putEntry(t, 277, TIFF_TYPE_SHORT, 1, 1); /* SamplesPerPixel */ |
175 | 298 | putEntry(t, 278, TIFF_TYPE_LONG, 1, height); /* RowsPerStrip */ |
176 | 298 | putEntry(t, 279, TIFF_TYPE_LONG, 1, |
177 | 298 | static_cast<uint32_t>(payload.size())); /* StripByteCounts */ |
178 | 298 | put32(t, 0); /* no next IFD */ |
179 | | |
180 | 298 | t.insert(t.end(), payload.begin(), payload.end()); |
181 | 298 | return t; |
182 | 298 | } |
183 | | |
184 | | } /* namespace */ |
185 | | |
186 | | extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) |
187 | 301 | { |
188 | 301 | FuzzedDataProvider fdp(data, size); |
189 | | |
190 | 301 | const uint32_t width = fdp.ConsumeIntegralInRange<uint32_t>(1, kMaxWidth); |
191 | 301 | const uint32_t height = fdp.ConsumeIntegralInRange<uint32_t>(1, kMaxHeight); |
192 | | |
193 | 301 | const std::vector<uint8_t> payload = fdp.ConsumeRemainingBytes<uint8_t>(); |
194 | 301 | if (payload.empty()) |
195 | 3 | return 0; |
196 | | |
197 | 298 | const std::vector<uint8_t> tiff = buildPackBitsTiff(width, height, payload); |
198 | | |
199 | 298 | TIFFSetErrorHandler(silentError); |
200 | 298 | TIFFSetWarningHandler(silentWarning); |
201 | | |
202 | 298 | MemFile mem; |
203 | 298 | mem.data = tiff.data(); |
204 | 298 | mem.size = static_cast<toff_t>(tiff.size()); |
205 | 298 | mem.pos = 0; |
206 | | |
207 | 298 | TIFF *tif = TIFFClientOpen("packbits_fuzz", "r", |
208 | 298 | static_cast<thandle_t>(&mem), memRead, memWrite, |
209 | 298 | memSeek, memClose, memSize, nullptr, nullptr); |
210 | 298 | if (!tif) |
211 | 0 | return 0; |
212 | | |
213 | 298 | const tmsize_t stripSize = TIFFStripSize(tif); |
214 | 298 | if (stripSize > 0) |
215 | 298 | { |
216 | 298 | void *buf = _TIFFmalloc(stripSize); |
217 | 298 | if (buf) |
218 | 298 | { |
219 | | /* The decode itself: PackBitsDecode() expands `payload` into a |
220 | | * buffer of exactly stripSize bytes and must not step outside it. |
221 | | */ |
222 | 298 | TIFFReadEncodedStrip(tif, 0, buf, stripSize); |
223 | 298 | _TIFFfree(buf); |
224 | 298 | } |
225 | 298 | } |
226 | | |
227 | 298 | TIFFClose(tif); |
228 | 298 | return 0; |
229 | 298 | } |