Coverage Report

Created: 2026-09-14 07:03

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/dovecot/src/lib-index/mail-index-map-hdr.c
Line
Count
Source
1
/* Copyright (c) Dovecot authors, see top-level COPYING file */
2
3
#include "lib.h"
4
#include "array.h"
5
#include "mail-transaction-log-private.h"
6
#include "mail-index-private.h"
7
8
int mail_index_map_parse_extensions(struct mail_index_map *map)
9
0
{
10
0
  struct mail_index *index = map->index;
11
0
  const struct mail_index_ext_header *ext_hdr;
12
0
  unsigned int i, old_count, offset;
13
0
  const char *name, *error;
14
0
  uint32_t ext_id, ext_map_idx, ext_offset;
15
16
  /* extension headers always start from 64bit offsets, so if base header
17
     doesn't happen to be 64bit aligned we'll skip some bytes */
18
0
  offset = MAIL_INDEX_HEADER_SIZE_ALIGN(map->hdr.base_header_size);
19
0
  if (offset >= map->hdr.header_size && map->extension_pool == NULL) {
20
    /* nothing to do, skip allocations and all */
21
0
    return 0;
22
0
  }
23
24
0
  old_count = array_count(&index->extensions);
25
0
  mail_index_map_init_extbufs(map, old_count + 5);
26
27
0
  ext_id = (uint32_t)-1;
28
0
  for (i = 0; i < old_count; i++)
29
0
    array_push_back(&map->ext_id_map, &ext_id);
30
31
0
  for (i = 0; offset < map->hdr.header_size; i++) {
32
0
    ext_offset = offset;
33
34
0
    if (mail_index_map_ext_get_next(map, &offset,
35
0
            &ext_hdr, &name) < 0) {
36
0
      mail_index_set_error(index, "Corrupted index file %s: "
37
0
        "Header extension #%d (%s) goes outside header",
38
0
        index->filepath, i, name);
39
0
      return -1;
40
0
    }
41
42
0
    if (mail_index_map_ext_hdr_check(&map->hdr, ext_hdr,
43
0
             name, &error) < 0) {
44
0
      mail_index_set_error(index, "Corrupted index file %s: "
45
0
               "Broken extension #%d (%s): %s",
46
0
               index->filepath, i, name, error);
47
0
      return -1;
48
0
    }
49
0
    if (mail_index_map_lookup_ext(map, name, &ext_map_idx)) {
50
0
      mail_index_set_error(index, "Corrupted index file %s: "
51
0
        "Duplicate header extension %s",
52
0
        index->filepath, name);
53
0
      return -1;
54
0
    }
55
56
0
    (void)mail_index_map_register_ext(map, name, ext_offset, ext_hdr);
57
0
  }
58
0
  return 0;
59
0
}
60
61
int mail_index_map_parse_keywords(struct mail_index_map *map)
62
0
{
63
0
  struct mail_index *index = map->index;
64
0
  const struct mail_index_ext *ext;
65
0
  const struct mail_index_keyword_header *kw_hdr;
66
0
  const struct mail_index_keyword_header_rec *kw_rec;
67
0
  const char *name;
68
0
  unsigned int i, name_area_end_offset, old_count;
69
0
  uint32_t idx;
70
71
0
  if (!mail_index_map_lookup_ext(map, MAIL_INDEX_EXT_KEYWORDS, &idx)) {
72
0
    if (array_is_created(&map->keyword_idx_map))
73
0
      array_clear(&map->keyword_idx_map);
74
0
    return 0;
75
0
  }
76
0
  ext = array_idx(&map->extensions, idx);
77
78
  /* Extension header contains:
79
     - struct mail_index_keyword_header
80
     - struct mail_index_keyword_header_rec * keywords_count
81
     - const char names[] * keywords_count
82
83
     The mail_index_keyword_header_rec are rather unnecessary nowadays.
84
     They were originally an optimization when dovecot.index header kept
85
     changing constantly, but nowadays the changes are usually read from
86
     the .log changes, so re-reading dovecot.index header isn't common.
87
     In a later version we could even remove it.
88
  */
89
0
  i_assert(ext->hdr_offset < map->hdr.header_size);
90
0
  kw_hdr = MAIL_INDEX_MAP_HDR_OFFSET(map, ext->hdr_offset);
91
0
  kw_rec = (const void *)(kw_hdr + 1);
92
0
  name = (const char *)(kw_rec + kw_hdr->keywords_count);
93
94
0
  old_count = !array_is_created(&map->keyword_idx_map) ? 0 :
95
0
    array_count(&map->keyword_idx_map);
96
97
  /* make sure the header is valid */
98
0
  if (kw_hdr->keywords_count < old_count) {
99
0
    mail_index_set_error(index, "Corrupted index file %s: "
100
0
             "Keywords removed unexpectedly",
101
0
             index->filepath);
102
0
    return -1;
103
0
  }
104
105
0
  if ((size_t)(name - (const char *)kw_hdr) > ext->hdr_size) {
106
0
    mail_index_set_error(index, "Corrupted index file %s: "
107
0
             "keywords_count larger than header size",
108
0
             index->filepath);
109
0
    return -1;
110
0
  }
111
112
0
  name_area_end_offset = (const char *)kw_hdr + ext->hdr_size - name;
113
0
  for (i = 0; i < kw_hdr->keywords_count; i++) {
114
0
    if (kw_rec[i].name_offset > name_area_end_offset) {
115
0
      mail_index_set_error(index, "Corrupted index file %s: "
116
0
        "name_offset points outside allocated header",
117
0
        index->filepath);
118
0
      return -1;
119
0
    }
120
0
  }
121
  /* A header with no keyword names at all (e.g. keywords_count==0) has an
122
     empty name area; name_area_end_offset is then 0 and there is no
123
     terminating NUL to check. Guard against the unsigned underflow of
124
     name_area_end_offset-1, which would otherwise read ~4 GB out of
125
     bounds. */
126
0
  if (name_area_end_offset > 0 &&
127
0
      name[name_area_end_offset - 1] != '\0') {
128
0
    mail_index_set_error(index, "Corrupted index file %s: "
129
0
             "Keyword header doesn't end with NUL",
130
0
             index->filepath);
131
0
    return -1;
132
0
  }
133
134
  /* create file -> index mapping */
135
0
  if (!array_is_created(&map->keyword_idx_map))
136
0
    i_array_init(&map->keyword_idx_map, kw_hdr->keywords_count);
137
138
0
  size_t name_offset = 0;
139
  /* Check that existing headers are still the same. */
140
0
  for (i = 0; i < array_count(&map->keyword_idx_map); i++) {
141
0
    const char *keyword = name + kw_rec[i].name_offset;
142
0
    const unsigned int *old_idx;
143
0
    unsigned int kw_idx;
144
145
0
    if (kw_rec[i].name_offset != name_offset) {
146
      /* this shouldn't happen, but the old code didn't check
147
         for this so for safety keep this as a warning. */
148
0
      e_warning(index->event,
149
0
          "Corrupted index file %s: "
150
0
          "Mismatching keyword name_offset",
151
0
          index->filepath);
152
0
    }
153
0
    name_offset += strlen(keyword) + 1;
154
155
0
    old_idx = array_idx(&map->keyword_idx_map, i);
156
0
    if (!mail_index_keyword_lookup(index, keyword, &kw_idx) ||
157
0
        kw_idx != *old_idx) {
158
0
      mail_index_set_error(index, "Corrupted index file %s: "
159
0
               "Keywords changed unexpectedly",
160
0
               index->filepath);
161
0
      return -1;
162
0
    }
163
0
  }
164
165
  /* Register the newly seen keywords */
166
0
  i = array_count(&map->keyword_idx_map);
167
0
  for (; i < kw_hdr->keywords_count; i++) {
168
0
    const char *keyword = name + kw_rec[i].name_offset;
169
0
    unsigned int kw_idx;
170
171
0
    if (kw_rec[i].name_offset != name_offset) {
172
      /* this shouldn't happen, but the old code didn't check
173
         for this so for safety keep this as a warning. */
174
0
      e_warning(index->event,
175
0
          "Corrupted index file %s: "
176
0
          "Mismatching keyword name_offset",
177
0
          index->filepath);
178
0
    }
179
0
    name_offset += strlen(keyword) + 1;
180
181
0
    if (*keyword == '\0') {
182
0
      mail_index_set_error(index, "Corrupted index file %s: "
183
0
        "Empty keyword name in header",
184
0
        index->filepath);
185
0
      return -1;
186
0
    }
187
0
    mail_index_keyword_lookup_or_create(index, keyword, &kw_idx);
188
0
    array_push_back(&map->keyword_idx_map, &kw_idx);
189
0
  }
190
0
  return 0;
191
0
}
192
193
bool mail_index_hdr_check_indexid(struct mail_index *index,
194
          const struct mail_index_header *hdr)
195
0
{
196
0
  const char *reason;
197
198
0
  if (index->indexid == 0) {
199
    /* this happens only when .log file is lost/corrupted */
200
0
    i_assert(index->log->head == NULL);
201
0
    index->indexid = hdr->indexid;
202
0
  }
203
0
  if (hdr->indexid == index->indexid)
204
0
    return TRUE;
205
206
0
  if (mail_transaction_log_has_changed(index->log, TRUE,
207
0
               &reason) != 0) {
208
    /* Transaction log has either changed, or there was some IO
209
       error when doing the check. Either way, we can't continue. */
210
0
    return FALSE;
211
0
  }
212
213
  /* indexid has a permanent mismatch - delete the index. */
214
0
  mail_index_set_error(index, "Index file %s: "
215
0
           "indexid changed: %u -> %u - deleting",
216
0
           index->filepath, index->indexid, hdr->indexid);
217
0
  if (!index->readonly)
218
0
    i_unlink_if_exists(index->filepath);
219
0
  return FALSE;
220
0
}
221
222
bool mail_index_check_header_compat(const struct mail_index_header *hdr,
223
            uoff_t file_size, const char **error_r)
224
0
{
225
0
        enum mail_index_header_compat_flags compat_flags = 0;
226
227
0
#ifndef WORDS_BIGENDIAN
228
0
  compat_flags |= MAIL_INDEX_COMPAT_LITTLE_ENDIAN;
229
0
#endif
230
231
0
  if (hdr->major_version != MAIL_INDEX_MAJOR_VERSION) {
232
    /* major version change */
233
0
    *error_r = t_strdup_printf("Major version changed (%u != %u)",
234
0
      hdr->major_version, MAIL_INDEX_MAJOR_VERSION);
235
0
    return FALSE;
236
0
  }
237
0
  if ((hdr->flags & MAIL_INDEX_HDR_FLAG_CORRUPTED) != 0) {
238
    /* we've already complained about it */
239
0
    *error_r = "Header's corrupted flag is set";
240
0
    return FALSE;
241
0
  }
242
243
0
  if (hdr->compat_flags != compat_flags) {
244
    /* architecture change */
245
0
    *error_r = "CPU architecture changed";
246
0
    return FALSE;
247
0
  }
248
249
0
  if (hdr->base_header_size < MAIL_INDEX_HEADER_MIN_SIZE ||
250
0
      hdr->header_size < hdr->base_header_size) {
251
0
    *error_r = t_strdup_printf(
252
0
      "Corrupted header sizes (base %u, full %u)",
253
0
      hdr->base_header_size, hdr->header_size);
254
0
    return FALSE;
255
0
  }
256
0
  if (hdr->record_size < sizeof(struct mail_index_record)) {
257
0
    *error_r = t_strdup_printf(
258
0
      "record_size too small (%u < %zu)",
259
0
      hdr->record_size, sizeof(struct mail_index_record));
260
0
    return FALSE;
261
0
  }
262
0
  if (hdr->header_size > file_size) {
263
0
    *error_r = t_strdup_printf(
264
0
      "Header size is larger than file (%u > %"PRIuUOFF_T")",
265
0
      hdr->header_size, file_size);
266
0
    return FALSE;
267
0
  }
268
0
  return TRUE;
269
0
}
270
271
static void mail_index_map_clear_recent_flags(struct mail_index_map *map)
272
0
{
273
0
  struct mail_index_record *rec;
274
0
  uint32_t seq;
275
276
0
  for (seq = 1; seq <= map->hdr.messages_count; seq++) {
277
0
    rec = MAIL_INDEX_REC_AT_SEQ(map, seq);
278
0
    rec->flags &= ENUM_NEGATE(MAIL_RECENT);
279
0
  }
280
0
}
281
282
int mail_index_map_check_header(struct mail_index_map *map,
283
        const char **error_r)
284
0
{
285
0
  struct mail_index *index = map->index;
286
0
  const struct mail_index_header *hdr = &map->hdr;
287
288
0
  if (!mail_index_check_header_compat(hdr, UOFF_T_MAX, error_r))
289
0
    return 0;
290
291
  /* following some extra checks that only take a bit of CPU */
292
0
  if (hdr->record_size < sizeof(struct mail_index_record)) {
293
0
    *error_r = t_strdup_printf(
294
0
      "record_size too small (%u < %zu)",
295
0
      hdr->record_size, sizeof(struct mail_index_record));
296
0
    return -1;
297
0
  }
298
299
0
  if (hdr->uid_validity == 0 && hdr->next_uid != 1) {
300
0
    *error_r = t_strdup_printf(
301
0
      "uidvalidity=0, but next_uid=%u", hdr->next_uid);
302
0
    return 0;
303
0
  }
304
0
  if (hdr->next_uid == 0) {
305
0
    *error_r = "next_uid=0";
306
0
    return 0;
307
0
  }
308
0
  if (hdr->messages_count > map->rec_map->records_count) {
309
0
    *error_r = t_strdup_printf(
310
0
      "messages_count is higher in header than record map (%u > %u)",
311
0
      hdr->messages_count, map->rec_map->records_count);
312
0
    return 0;
313
0
  }
314
315
0
  if (hdr->seen_messages_count > hdr->messages_count) {
316
0
    *error_r = t_strdup_printf(
317
0
      "seen_messages_count %u > messages_count %u",
318
0
      hdr->seen_messages_count, hdr->messages_count);
319
0
    return 0;
320
0
  }
321
0
  if (hdr->deleted_messages_count > hdr->messages_count) {
322
0
    *error_r = t_strdup_printf(
323
0
      "deleted_messages_count %u > messages_count %u",
324
0
      hdr->deleted_messages_count, hdr->messages_count);
325
0
    return 0;
326
0
  }
327
0
  switch (hdr->minor_version) {
328
0
  case 0:
329
    /* upgrade silently from v1.0 */
330
0
    map->hdr.unused_old_recent_messages_count = 0;
331
0
    if (hdr->first_recent_uid == 0)
332
0
      map->hdr.first_recent_uid = 1;
333
0
    if (index->need_recreate == NULL)
334
0
      index->need_recreate = i_strdup("Upgrading from index version 1.0");
335
    /* fall through */
336
0
  case 1:
337
    /* pre-v1.1.rc6: make sure the \Recent flags are gone */
338
0
    mail_index_map_clear_recent_flags(map);
339
    /* fall through */
340
0
  case 2:
341
    /* pre-v2.2 (although should have been done in v2.1 already):
342
       make sure the old unused fields are cleared */
343
0
    map->hdr.unused_old_sync_size_part1 = 0;
344
0
    map->hdr.log2_rotate_time = 0;
345
0
    map->hdr.last_temp_file_scan = 0;
346
0
  }
347
  /* The old index format is now updated, set minor_version to latest
348
     value. However, if minor_version is larger than what we know of,
349
     keep it as it is to avoid breaking anything. */
350
0
  if (map->hdr.minor_version < MAIL_INDEX_MINOR_VERSION)
351
0
    map->hdr.minor_version = MAIL_INDEX_MINOR_VERSION;
352
0
  if (hdr->first_recent_uid == 0) {
353
0
    *error_r = "first_recent_uid=0";
354
0
    return 0;
355
0
  }
356
0
  if (hdr->first_recent_uid > hdr->next_uid) {
357
0
    *error_r = t_strdup_printf(
358
0
      "first_recent_uid %u > next_uid %u",
359
0
      hdr->first_recent_uid, hdr->next_uid);
360
0
    return 0;
361
0
  }
362
0
  if (hdr->first_unseen_uid_lowwater > hdr->next_uid) {
363
0
    *error_r = t_strdup_printf(
364
0
      "first_unseen_uid_lowwater %u > next_uid %u",
365
0
      hdr->first_unseen_uid_lowwater, hdr->next_uid);
366
0
    return 0;
367
0
  }
368
0
  if (hdr->first_deleted_uid_lowwater > hdr->next_uid) {
369
0
    *error_r = t_strdup_printf(
370
0
      "first_deleted_uid_lowwater %u > next_uid %u",
371
0
      hdr->first_deleted_uid_lowwater, hdr->next_uid);
372
0
    return 0;
373
0
  }
374
375
0
  if (hdr->messages_count > 0) {
376
    /* last message's UID must be smaller than next_uid.
377
       also make sure it's not zero. */
378
0
    const struct mail_index_record *rec;
379
380
0
    rec = MAIL_INDEX_REC_AT_SEQ(map, hdr->messages_count);
381
0
    if (rec->uid == 0) {
382
0
      *error_r = "last message has uid=0";
383
0
      return -1;
384
0
    }
385
0
    if (rec->uid >= hdr->next_uid) {
386
0
      *error_r = t_strdup_printf(
387
0
        "last message uid %u >= next_uid %u",
388
0
        rec->uid, hdr->next_uid);
389
0
      return 0;
390
0
    }
391
0
  }
392
0
  return 1;
393
0
}