/src/logging-log4cxx/src/fuzzers/cpp/TranscoderFuzzer.cpp
Line | Count | Source |
1 | | /* |
2 | | * Licensed to the Apache Software Foundation (ASF) under one or more |
3 | | * contributor license agreements. See the NOTICE file distributed with |
4 | | * this work for additional information regarding copyright ownership. |
5 | | * The ASF licenses this file to You under the Apache License, Version 2.0 |
6 | | * (the "License"); you may not use this file except in compliance with |
7 | | * the License. You may obtain a copy of the License at |
8 | | * |
9 | | * http://www.apache.org/licenses/LICENSE-2.0 |
10 | | * |
11 | | * Unless required by applicable law or agreed to in writing, software |
12 | | * distributed under the License is distributed on an "AS IS" BASIS, |
13 | | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
14 | | * See the License for the specific language governing permissions and |
15 | | * limitations under the License. |
16 | | */ |
17 | | |
18 | | // |
19 | | // Fuzzer for the charset transcoding layer |
20 | | // (Transcoder, CharsetDecoder, CharsetEncoder). |
21 | | // |
22 | | // This is the code that turns untrusted external bytes into the internal |
23 | | // LogString and back out again for every appender. It has historically been |
24 | | // the source of several memory-safety and correctness defects in the decode |
25 | | // boundary, for example: |
26 | | // |
27 | | // * reject invalid UTF-8 lead bytes F8..FF in Transcoder::decode (#699) |
28 | | // * UTF-8 recovery loop end-of-input handling (#695) |
29 | | // * reject UTF-16 surrogate-half encodings in UTF-8 (#669) |
30 | | // * nullptr pointer arithmetic in charset decoder (#670) |
31 | | // * UTF-8 decoder rejecting valid U+0800 three-byte sequence (#664) |
32 | | // * ISO Latin-1 decoder sign extension (#660) |
33 | | // * UTF-16 supplementary character encoding (#659) |
34 | | // * infinite loop in MbstowcsCharsetDecoder (#589) |
35 | | // |
36 | | // The harness drives that layer with arbitrary bytes under ASan and the |
37 | | // integer-overflow sanitizer, and additionally checks two round-trip |
38 | | // invariants that a substitution-collision / aliasing defect (the class of |
39 | | // bug behind #699 and #669) would break. |
40 | | // |
41 | | |
42 | | #include <fuzzer/FuzzedDataProvider.h> |
43 | | #include <log4cxx/logstring.h> |
44 | | #include <log4cxx/helpers/transcoder.h> |
45 | | #include <log4cxx/helpers/charsetdecoder.h> |
46 | | #include <log4cxx/helpers/charsetencoder.h> |
47 | | #include <log4cxx/helpers/bytebuffer.h> |
48 | | #include <cstdio> |
49 | | #include <cstdlib> |
50 | | #include <string> |
51 | | #include <exception> |
52 | | |
53 | | using namespace LOG4CXX_NS; |
54 | | using namespace LOG4CXX_NS::helpers; |
55 | | |
56 | | namespace |
57 | | { |
58 | | const size_t MaximumByteCount = 1 << 16; |
59 | | |
60 | | // Abort so libFuzzer/ASan reports the violation with a saved reproducer. |
61 | | void requireInvariant(bool ok, const char* what) |
62 | 5.81M | { |
63 | 5.81M | if (!ok) |
64 | 0 | { |
65 | 0 | fprintf(stderr, "transcoder invariant violated: %s\n", what); |
66 | 0 | abort(); |
67 | 0 | } |
68 | 5.81M | } |
69 | | |
70 | | // Independent UTF-16 reference decoder used only by the oracle below. |
71 | | // Kept deliberately tiny and self-contained so that it cannot share a |
72 | | // defect with the Transcoder::encodeUTF16BE/LE functions it validates: it |
73 | | // reassembles the two big/little-endian bytes of each code unit and applies |
74 | | // the standard surrogate-pair formula, nothing more. |
75 | | unsigned int referenceDecodeUTF16(const char* raw, size_t n, bool bigEndian) |
76 | 5.81M | { |
77 | 5.81M | auto unit = [&](size_t i) -> unsigned int |
78 | 5.82M | { |
79 | 5.82M | unsigned char hi = (unsigned char) raw[bigEndian ? i : i + 1]; |
80 | 5.82M | unsigned char lo = (unsigned char) raw[bigEndian ? i + 1 : i]; |
81 | 5.82M | return (unsigned int) ((hi << 8) | lo); |
82 | 5.82M | }; |
83 | | |
84 | 5.81M | if (n == 2) |
85 | 5.79M | return unit(0); |
86 | 15.7k | if (n == 4) |
87 | 15.7k | { |
88 | 15.7k | unsigned int hs = unit(0); |
89 | 15.7k | unsigned int ls = unit(2); |
90 | 15.7k | return (hs - 0xD800) * 0x400 + (ls - 0xDC00) + 0x10000; |
91 | 15.7k | } |
92 | 0 | return 0xFFFFFFFFu; // unexpected length -- not this oracle's concern |
93 | 15.7k | } |
94 | | |
95 | | // Oracle: a Unicode scalar encoded to UTF-16BE/LE and decoded back through |
96 | | // the independent reference above must reproduce the scalar. This is the |
97 | | // round trip that exercises *and checks* Transcoder::encodeUTF16BE/LE -- the |
98 | | // surrogate-pair byte encoders fixed in #659. Those functions are reached |
99 | | // from UTF16BECharsetEncoder::encode, but the named-codec path discards the |
100 | | // bytes, leaving the encoders without a correctness oracle; this supplies |
101 | | // one. A defect that mis-derives a surrogate byte (in bounds, no crash) |
102 | | // silently decodes to the wrong code point and trips this check. |
103 | | void checkUTF16RoundTrip(unsigned int sv) |
104 | 2.90M | { |
105 | 2.90M | char be[4] = { 0, 0, 0, 0 }; |
106 | 2.90M | ByteBuffer beBuf(be, sizeof be); |
107 | 2.90M | Transcoder::encodeUTF16BE(sv, beBuf); |
108 | 2.90M | requireInvariant(referenceDecodeUTF16(be, beBuf.position(), true) == sv, |
109 | 2.90M | "UTF-16BE encode/decode round trip corrupted the code point"); |
110 | | |
111 | 2.90M | char le[4] = { 0, 0, 0, 0 }; |
112 | 2.90M | ByteBuffer leBuf(le, sizeof le); |
113 | 2.90M | Transcoder::encodeUTF16LE(sv, leBuf); |
114 | 2.90M | requireInvariant(referenceDecodeUTF16(le, leBuf.position(), false) == sv, |
115 | 2.90M | "UTF-16LE encode/decode round trip corrupted the code point"); |
116 | 2.90M | } |
117 | | |
118 | | // Drive a decoder over every byte of `bytes`, mirroring the error-recovery |
119 | | // loop in Transcoder::decode so that a single invalid byte advances the |
120 | | // cursor instead of stalling it. The defensive no-progress break guards |
121 | | // the harness against a hang if a decoder ever returns success without |
122 | | // consuming input (that condition is itself worth surfacing, but as a |
123 | | // finding rather than a fuzzer timeout). |
124 | | void exerciseDecoder(const CharsetDecoderPtr& decoder, const std::string& bytes) |
125 | 1.16k | { |
126 | 1.16k | if (!decoder || bytes.empty()) |
127 | 20 | return; |
128 | | |
129 | 1.14k | LogString out; |
130 | 1.14k | ByteBuffer buf(const_cast<char*>(bytes.data()), bytes.size()); |
131 | | |
132 | 1.44M | while (buf.remaining() > 0) |
133 | 1.44M | { |
134 | 1.44M | size_t before = buf.position(); |
135 | 1.44M | log4cxx_status_t stat = decoder->decode(buf, out); |
136 | | |
137 | 1.44M | if (CharsetDecoder::isError(stat)) |
138 | 1.44M | { |
139 | 1.44M | out.append(1, (logchar) Transcoder::LOSSCHAR); |
140 | 1.44M | buf.increment_position(1); |
141 | 1.44M | } |
142 | 688 | else if (buf.position() == before) |
143 | 0 | { |
144 | 0 | break; |
145 | 0 | } |
146 | 1.44M | } |
147 | | |
148 | 1.14k | decoder->decode(buf, out); // flush any pending state |
149 | 1.14k | } |
150 | | |
151 | | // Drain an entire LogString through `encoder`, mirroring the loop in |
152 | | // Transcoder::encode (flip / consume / clear) and advancing past any |
153 | | // character the target charset cannot represent. |
154 | | void exerciseEncoder(const CharsetEncoderPtr& encoder, const LogString& in) |
155 | 1.16k | { |
156 | 1.16k | if (!encoder) |
157 | 0 | return; |
158 | | |
159 | 1.16k | char scratch[128]; |
160 | 1.16k | std::string sink; |
161 | 1.16k | ByteBuffer out(scratch, sizeof scratch); |
162 | 1.16k | LogString::const_iterator iter = in.begin(); |
163 | | |
164 | 62.3k | while (iter != in.end()) |
165 | 61.1k | { |
166 | 61.1k | LogString::const_iterator before = iter; |
167 | 61.1k | log4cxx_status_t stat = encoder->encode(in, iter, out); |
168 | 61.1k | out.flip(); |
169 | 61.1k | sink.append(out.data(), out.limit()); |
170 | 61.1k | out.clear(); |
171 | | |
172 | 61.1k | if (CharsetEncoder::isError(stat)) |
173 | 9.25k | { |
174 | 9.25k | if (iter != in.end()) |
175 | 9.25k | ++iter; // skip the unrepresentable character |
176 | 9.25k | } |
177 | 51.9k | else if (iter == before) |
178 | 0 | { |
179 | 0 | break; // defensive: success without progress |
180 | 0 | } |
181 | 61.1k | } |
182 | | |
183 | 1.16k | encoder->flush(out); |
184 | 1.16k | } |
185 | | } |
186 | | |
187 | | extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) |
188 | 1.16k | { |
189 | 1.16k | FuzzedDataProvider fdp(data, size); |
190 | | |
191 | | // Reserve a one-byte selector for the named charset exercised below, then |
192 | | // treat the remainder as the untrusted byte stream. |
193 | 1.16k | const int charsetSel = fdp.ConsumeIntegralInRange<int>(0, 4); |
194 | 1.16k | std::string bytes = fdp.ConsumeRandomLengthString(MaximumByteCount); |
195 | | |
196 | | // Path 1: decode in the configured default code page |
197 | | // (this is what every std::string -> LogString conversion uses). |
198 | 1.16k | LogString viaDefault; |
199 | 1.16k | Transcoder::decode(bytes, viaDefault); |
200 | | |
201 | | // Path 2: explicit UTF-8 decode through the hardened scalar decoder. |
202 | | // decodeUTF8 replaces every malformed sequence with LOSSCHAR, so the |
203 | | // result contains only well-formed Unicode scalar values. |
204 | 1.16k | LogString sanitized; |
205 | 1.16k | Transcoder::decodeUTF8(bytes, sanitized); |
206 | | |
207 | | // Invariant A: because `sanitized` is already well-formed, encoding it to |
208 | | // UTF-8 and decoding it again must reproduce it exactly. A decode/encode |
209 | | // asymmetry -- e.g. a malformed input aliasing onto an in-range code point |
210 | | // inconsistently -- breaks this fixed point. |
211 | 1.16k | std::string utf8; |
212 | 1.16k | Transcoder::encodeUTF8(sanitized, utf8); |
213 | 1.16k | LogString reSanitized; |
214 | 1.16k | Transcoder::decodeUTF8(utf8, reSanitized); |
215 | 1.16k | requireInvariant(sanitized == reSanitized, |
216 | 1.16k | "decodeUTF8/encodeUTF8 round trip is not idempotent"); |
217 | | |
218 | | // Path 3: default-charset encode of the sanitized string. |
219 | 1.16k | std::string reencoded; |
220 | 1.16k | Transcoder::encode(sanitized, reencoded); |
221 | | |
222 | | // Path 4: every named codec, driven over the raw fuzz bytes (decode) and |
223 | | // over the full Unicode range (encode -- exercises the unrepresentable |
224 | | // character / error-recovery branches of US-ASCII and ISO-8859-1). |
225 | 1.16k | static const LogString charsetNames[] = |
226 | 1.16k | { |
227 | 1.16k | LOG4CXX_STR("US-ASCII"), |
228 | 1.16k | LOG4CXX_STR("ISO-8859-1"), |
229 | 1.16k | LOG4CXX_STR("UTF-8"), |
230 | 1.16k | LOG4CXX_STR("UTF-16BE"), |
231 | 1.16k | LOG4CXX_STR("UTF-16LE"), |
232 | 1.16k | }; |
233 | 1.16k | const LogString& charset = charsetNames[charsetSel % 5]; |
234 | | |
235 | 1.16k | try |
236 | 1.16k | { |
237 | 1.16k | exerciseDecoder(CharsetDecoder::getDecoder(charset), bytes); |
238 | 1.16k | exerciseEncoder(CharsetEncoder::getEncoder(charset), sanitized); |
239 | 1.16k | } |
240 | 1.16k | catch (const std::exception&) |
241 | 1.16k | { |
242 | | // getDecoder/getEncoder throw IllegalArgumentException for an |
243 | | // unrecognised name; all names above are valid, but stay defensive. |
244 | 0 | } |
245 | | |
246 | | // Path 6: UTF-16BE/LE byte-encoder round trip over every scalar decoded |
247 | | // from the input. Unlike Path 4 (which discards the encoder's bytes), this |
248 | | // drives Transcoder::encodeUTF16BE/LE directly -- the exact #659 site -- with |
249 | | // real code points, including supplementary-plane scalars that form surrogate |
250 | | // pairs, and verifies each survives a decode. Portable across LOG4CXX_CHAR |
251 | | // configurations because it reads scalars through the UTF-8 scalar decoder |
252 | | // rather than the platform wchar_t path. |
253 | 1.16k | { |
254 | 1.16k | std::string::const_iterator it = bytes.begin(); |
255 | 5.15M | while (it != bytes.end()) |
256 | 5.15M | { |
257 | 5.15M | auto old_it = it; |
258 | 5.15M | unsigned int sv = Transcoder::decode(bytes, it); |
259 | 5.15M | if (sv == 0xFFFF) |
260 | 2.24M | { |
261 | | // mirror decodeUTF8's recovery advance on a bad sequence |
262 | 2.24M | if (old_it == it) |
263 | 2.24M | ++it; |
264 | 2.24M | continue; |
265 | 2.24M | } |
266 | 2.90M | checkUTF16RoundTrip(sv); |
267 | 2.90M | } |
268 | 1.16k | } |
269 | | |
270 | 1.16k | #if LOG4CXX_WCHAR_T_API || LOG4CXX_LOGCHAR_IS_WCHAR || defined(WIN32) || defined(_WIN32) |
271 | | // Path 5: wide round trip, covering the UTF-16 surrogate-pair handling |
272 | | // that produced #659. `sanitized` holds no surrogate-range scalars, so |
273 | | // encoding to wchar_t and decoding back must be a fixed point. |
274 | 1.16k | std::wstring wide; |
275 | 1.16k | Transcoder::encode(sanitized, wide); |
276 | 1.16k | LogString fromWide; |
277 | 1.16k | Transcoder::decode(wide, fromWide); |
278 | 1.16k | requireInvariant(sanitized == fromWide, |
279 | 1.16k | "wchar_t encode/decode round trip is not idempotent"); |
280 | 1.16k | #endif |
281 | | |
282 | 1.16k | return 0; |
283 | 1.16k | } |