Coverage Report

Created: 2026-07-30 07:12

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}