Coverage Report

Created: 2026-08-18 06:34

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/nss/fuzz/targets/lib/tls/mutators.cc
Line
Count
Source
1
/* This Source Code Form is subject to the terms of the Mozilla Public
2
 * License, v. 2.0. If a copy of the MPL was not distributed with this
3
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5
#include "mutators.h"
6
7
#include <algorithm>
8
#include <cstddef>
9
#include <cstdint>
10
#include <cstring>
11
#include <random>
12
#include <vector>
13
14
#include "tls_parser.h"
15
16
// TLS record header: ContentType(1) + ProtocolVersion(2) + length(2) = 5 bytes.
17
// DTLS adds epoch(2) + sequence_number(6) before the length field.
18
// See RFC 8446 Section 5.1 (TLS) and RFC 9147 Section 4 (DTLS).
19
constexpr size_t kHeaderBytes = 5 + EXTRA_HEADER_BYTES;
20
constexpr size_t kLengthOffset = 3 + EXTRA_HEADER_BYTES;
21
22
// RFC 8446 Section 5.1: change_cipher_spec(20), alert(21),
23
// handshake(22), application_data(23). Also heartbeat(24, RFC 6520),
24
// tls12_cid(25, RFC 9146), and ACK(26, RFC 9147 DTLS 1.3).
25
constexpr uint8_t kContentTypes[] = {20, 21, 22, 23, 24, 25, 26};
26
27
struct Record {
28
  uint8_t* data;
29
  size_t size;
30
  size_t remaining;
31
32
0
  uint8_t contentType() { return data[0]; }
33
};
34
35
0
static std::vector<Record> ParseRecords(uint8_t* data, size_t size) {
36
0
  std::vector<Record> records;
37
0
  nss_test::TlsParser parser(data, size);
38
39
0
  while (parser.remaining()) {
40
0
    size_t offset = parser.consumed();
41
42
0
    if (!parser.Skip(kLengthOffset)) {
43
0
      break;
44
0
    }
45
46
    // Read the 2-byte length field, then skip that many payload bytes.
47
0
    if (!parser.SkipVariable(2)) {
48
0
      break;
49
0
    }
50
51
0
    records.push_back(
52
0
        {data + offset, parser.consumed() - offset, parser.remaining()});
53
0
  }
54
55
0
  return records;
56
0
}
57
58
static size_t DropRecord(uint8_t* data, size_t size, size_t maxSize,
59
0
                         std::mt19937& rng) {
60
0
  std::vector<Record> records = ParseRecords(data, size);
61
0
  if (records.empty()) {
62
0
    return 0;
63
0
  }
64
65
0
  std::uniform_int_distribution<size_t> dist(0, records.size() - 1);
66
0
  Record& record = records.at(dist(rng));
67
68
0
  memmove(record.data, record.data + record.size, record.remaining);
69
0
  return size - record.size;
70
0
}
71
72
static size_t SwapRecords(uint8_t* data, size_t size, size_t maxSize,
73
0
                          std::mt19937& rng) {
74
0
  std::vector<Record> records = ParseRecords(data, size);
75
0
  if (records.size() < 2) {
76
0
    return 0;
77
0
  }
78
79
0
  std::uniform_int_distribution<size_t> dist(0, records.size() - 1);
80
0
  size_t i = dist(rng);
81
0
  size_t j;
82
0
  do {
83
0
    j = dist(rng);
84
0
  } while (j == i);
85
86
  // Read from buf (snapshot), write to data. Do not remove the copy;
87
  // without it, in-place reordering of variable-size records aliases.
88
0
  std::vector<uint8_t> buf(data, data + size);
89
0
  std::swap(records.at(i), records.at(j));
90
91
0
  uint8_t* dest = data;
92
0
  for (Record& rec : records) {
93
0
    memcpy(dest, buf.data() + (rec.data - data), rec.size);
94
0
    dest += rec.size;
95
0
  }
96
97
0
  return size;
98
0
}
99
100
// Copy a random record and insert the duplicate before a random record.
101
static size_t DuplicateRecord(uint8_t* data, size_t size, size_t maxSize,
102
0
                              std::mt19937& rng) {
103
0
  std::vector<Record> records = ParseRecords(data, size);
104
0
  if (records.empty()) {
105
0
    return 0;
106
0
  }
107
108
0
  std::uniform_int_distribution<size_t> dist(0, records.size() - 1);
109
0
  Record& record = records.at(dist(rng));
110
111
0
  if (size + record.size > maxSize) {
112
0
    return 0;
113
0
  }
114
115
0
  Record& other = records.at(dist(rng));
116
117
0
  memmove(other.data + record.size, other.data, other.size + other.remaining);
118
119
0
  uint8_t* src =
120
0
      (record.data < other.data) ? record.data : record.data + record.size;
121
0
  memcpy(other.data, src, record.size);
122
123
0
  return size + record.size;
124
0
}
125
126
// Shorten a random record's payload to a random length.
127
static size_t TruncateRecord(uint8_t* data, size_t size, size_t maxSize,
128
0
                             std::mt19937& rng) {
129
0
  std::vector<Record> records = ParseRecords(data, size);
130
0
  if (records.empty()) {
131
0
    return 0;
132
0
  }
133
134
0
  std::uniform_int_distribution<size_t> dist(0, records.size() - 1);
135
0
  Record& record = records.at(dist(rng));
136
137
0
  if (record.size <= kHeaderBytes) {
138
0
    return 0;
139
0
  }
140
141
0
  std::uniform_int_distribution<size_t> dist2(kHeaderBytes, record.size - 1);
142
0
  size_t length = dist2(rng);
143
144
0
  size_t payloadLength = length - kHeaderBytes;
145
0
  record.data[kLengthOffset] = (payloadLength >> 8) & 0xff;
146
0
  record.data[kLengthOffset + 1] = payloadLength & 0xff;
147
148
0
  memmove(record.data + length, record.data + record.size, record.remaining);
149
150
0
  return size + length - record.size;
151
0
}
152
153
// Split a random record into two records of the same ContentType.
154
// RFC 8446 Section 5.1: "Multiple fragments of a Handshake message may be
155
// coalesced into a single TLSPlaintext record" -- the inverse is also valid.
156
// For DTLS the second record inherits the first's epoch/seqno; real DTLS
157
// fragments at the handshake layer, but duplicate seqnos exercise anti-replay.
158
static size_t FragmentRecord(uint8_t* data, size_t size, size_t maxSize,
159
0
                             std::mt19937& rng) {
160
0
  if (size + kHeaderBytes > maxSize) {
161
0
    return 0;
162
0
  }
163
164
0
  std::vector<Record> records = ParseRecords(data, size);
165
0
  if (records.empty()) {
166
0
    return 0;
167
0
  }
168
169
0
  std::uniform_int_distribution<size_t> dist(0, records.size() - 1);
170
0
  Record& record = records.at(dist(rng));
171
0
  size_t contentLength = record.size - kHeaderBytes;
172
173
0
  if (contentLength < 2) {
174
0
    return 0;
175
0
  }
176
177
0
  std::uniform_int_distribution<size_t> dist2(1, contentLength - 1);
178
0
  size_t firstLength = dist2(rng);
179
0
  size_t secondLength = contentLength - firstLength;
180
181
  // Update first record's length.
182
0
  record.data[kLengthOffset] = (firstLength >> 8) & 0xff;
183
0
  record.data[kLengthOffset + 1] = firstLength & 0xff;
184
185
  // Make room for the second record's header at the split point.
186
0
  uint8_t* second = record.data + kHeaderBytes + firstLength;
187
0
  memmove(second + kHeaderBytes, second, record.remaining + secondLength);
188
189
  // Write second header: copy type/version from first, set new length.
190
0
  memcpy(second, record.data, kLengthOffset);
191
0
  second[kLengthOffset] = (secondLength >> 8) & 0xff;
192
0
  second[kLengthOffset + 1] = secondLength & 0xff;
193
194
0
  return size + kHeaderBytes;
195
0
}
196
197
// Insert a zero-length record with a random content type before a random
198
// record. RFC 8446 Section 5.4: "Application Data records may contain a
199
// zero-length TLSInnerPlaintext.content".
200
static size_t InjectEmptyRecord(uint8_t* data, size_t size, size_t maxSize,
201
0
                                std::mt19937& rng) {
202
0
  if (size + kHeaderBytes > maxSize) {
203
0
    return 0;
204
0
  }
205
206
0
  std::vector<Record> records = ParseRecords(data, size);
207
0
  if (records.empty()) {
208
0
    return 0;
209
0
  }
210
211
0
  std::uniform_int_distribution<size_t> dist(0, records.size() - 1);
212
0
  Record& record = records.at(dist(rng));
213
214
0
  uint8_t* dest = record.data;
215
0
  memmove(dest + kHeaderBytes, dest, record.size + record.remaining);
216
217
  // Copy ProtocolVersion (+ epoch/seqno for DTLS) from the adjacent
218
  // record, pick a random ContentType, and set length to zero.
219
0
  memcpy(dest + 1, dest + kHeaderBytes + 1, kLengthOffset - 1);
220
221
0
  std::uniform_int_distribution<size_t> dist2(0, sizeof(kContentTypes) - 1);
222
0
  dest[0] = kContentTypes[dist2(rng)];
223
224
0
  dest[kLengthOffset] = 0;
225
0
  dest[kLengthOffset + 1] = 0;
226
227
0
  return size + kHeaderBytes;
228
0
}
229
230
// Merge two adjacent records that share a ContentType into one.
231
// Inverse of FragmentRecord. RFC 8446 Section 5.1: "Handshake messages may
232
// be fragmented over several records".
233
static size_t MergeRecords(uint8_t* data, size_t size, size_t maxSize,
234
0
                           std::mt19937& rng) {
235
0
  std::vector<Record> records = ParseRecords(data, size);
236
0
  if (records.size() < 2) {
237
0
    return 0;
238
0
  }
239
240
0
  std::vector<size_t> candidates;
241
0
  for (size_t i = 0; i + 1 < records.size(); i++) {
242
0
    if (records[i].contentType() == records[i + 1].contentType()) {
243
0
      candidates.push_back(i);
244
0
    }
245
0
  }
246
247
0
  if (candidates.empty()) {
248
0
    return 0;
249
0
  }
250
251
0
  std::uniform_int_distribution<size_t> dist(0, candidates.size() - 1);
252
0
  size_t idx = candidates.at(dist(rng));
253
254
0
  Record& first = records.at(idx);
255
0
  Record& second = records.at(idx + 1);
256
257
0
  size_t combinedPayload =
258
0
      (first.size - kHeaderBytes) + (second.size - kHeaderBytes);
259
260
  // RFC 8446 Section 5.1: TLSPlaintext.length MUST NOT exceed 2^14.
261
  // We use the uint16 max here to also exercise the overflow path.
262
0
  if (combinedPayload > 0xffff) {
263
0
    return 0;
264
0
  }
265
266
0
  first.data[kLengthOffset] = (combinedPayload >> 8) & 0xff;
267
0
  first.data[kLengthOffset + 1] = combinedPayload & 0xff;
268
269
0
  uint8_t* secondHeader = second.data;
270
0
  memmove(secondHeader, secondHeader + kHeaderBytes,
271
0
          second.size - kHeaderBytes + second.remaining);
272
273
0
  return size - kHeaderBytes;
274
0
}
275
276
// Extend a random record's payload with random bytes.
277
// Inverse of TruncateRecord. Exercises overlong-message handling and
278
// "trailing data after message" code paths.
279
static size_t ExtendRecord(uint8_t* data, size_t size, size_t maxSize,
280
0
                           std::mt19937& rng) {
281
0
  std::vector<Record> records = ParseRecords(data, size);
282
0
  if (records.empty()) {
283
0
    return 0;
284
0
  }
285
286
0
  std::uniform_int_distribution<size_t> dist(0, records.size() - 1);
287
0
  Record& record = records.at(dist(rng));
288
289
0
  size_t available = maxSize - size;
290
0
  if (available == 0) {
291
0
    return 0;
292
0
  }
293
294
0
  size_t currentPayload = record.size - kHeaderBytes;
295
0
  if (currentPayload >= 0xffff) {
296
0
    return 0;
297
0
  }
298
299
0
  size_t maxExtend = std::min(available, (size_t)0xffff - currentPayload);
300
0
  std::uniform_int_distribution<size_t> dist2(1, maxExtend);
301
0
  size_t extendBy = dist2(rng);
302
303
  // Make room after the record's current payload.
304
0
  uint8_t* insertPoint = record.data + record.size;
305
0
  memmove(insertPoint + extendBy, insertPoint, record.remaining);
306
307
  // Fill extension with random bytes.
308
0
  std::uniform_int_distribution<uint8_t> dist3(0, 255);
309
0
  for (size_t i = 0; i < extendBy; ++i) {
310
0
    insertPoint[i] = dist3(rng);
311
0
  }
312
313
  // Update the length field.
314
0
  size_t newPayload = currentPayload + extendBy;
315
0
  record.data[kLengthOffset] = (newPayload >> 8) & 0xff;
316
0
  record.data[kLengthOffset + 1] = newPayload & 0xff;
317
318
0
  return size + extendBy;
319
0
}
320
321
using Mutator = size_t (*)(uint8_t*, size_t, size_t, std::mt19937&);
322
323
constexpr Mutator kMutators[] = {
324
    DropRecord,     SwapRecords,       DuplicateRecord, TruncateRecord,
325
    FragmentRecord, InjectEmptyRecord, MergeRecords,    ExtendRecord,
326
};
327
328
namespace TlsMutators {
329
330
extern "C" size_t LLVMFuzzerMutate(uint8_t* data, size_t size, size_t maxSize);
331
332
// Pick a random TLS-aware mutator or fall back to libFuzzer's default.
333
size_t CustomMutator(uint8_t* data, size_t size, size_t maxSize,
334
0
                     unsigned int seed) {
335
0
  std::mt19937 rng(seed);
336
0
  std::bernoulli_distribution coin;
337
338
0
  if (coin(rng)) {
339
0
    std::uniform_int_distribution<size_t> dist(
340
0
        0, (sizeof(kMutators) / sizeof(kMutators[0])) - 1);
341
0
    return kMutators[dist(rng)](data, size, maxSize, rng);
342
0
  }
343
344
0
  return LLVMFuzzerMutate(data, size, maxSize);
345
0
}
346
347
// Merge records from two transcripts, shuffle, and write to `out`.
348
// const_cast is safe: records are only read via memcpy to `out`.
349
size_t CustomCrossOver(const uint8_t* data1, size_t size1, const uint8_t* data2,
350
                       size_t size2, uint8_t* out, size_t maxOutSize,
351
0
                       unsigned int seed) {
352
0
  std::vector<Record> records1 =
353
0
      ParseRecords(const_cast<uint8_t*>(data1), size1);
354
0
  if (records1.empty()) {
355
0
    return 0;
356
0
  }
357
358
0
  std::vector<Record> records2 =
359
0
      ParseRecords(const_cast<uint8_t*>(data2), size2);
360
0
  if (records2.empty()) {
361
0
    return 0;
362
0
  }
363
364
  // Append `records2` to the back of `records1`.
365
0
  std::move(records2.begin(), records2.end(), std::back_inserter(records1));
366
367
0
  std::mt19937 rng(seed);
368
0
  std::shuffle(records1.begin(), records1.end(), rng);
369
370
0
  size_t total = 0;
371
0
  for (Record& record : records1) {
372
0
    size_t length = record.size;
373
0
    if (total + length > maxOutSize) {
374
0
      break;
375
0
    }
376
377
0
    memcpy(out + total, record.data, length);
378
0
    total += length;
379
0
  }
380
381
0
  return total;
382
0
}
383
384
}  // namespace TlsMutators