• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

neomutt / neomutt / 24648356273

18 Apr 2026 01:24PM UTC coverage: 42.851% (+0.5%) from 42.375%
24648356273

push

github

flatcap
merge: fix security issues

Raised by evilrabbit on Mutt devel mailing list.

 * security: fix GSSAPI buffer underflow on short unwrapped tokens
 * security: reject percent-encoded NUL bytes in URL decoding
 * security: skip CN fallback when SAN dNSName entries exist (RFC6125)
 * security: cap POP3 UIDL responses to prevent OOM from malicious server

3 of 7 new or added lines in 2 files covered. (42.86%)

3465 existing lines in 53 files now uncovered.

12428 of 29003 relevant lines covered (42.85%)

5272.14 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

4.3
/pattern/message.c
1
/**
2
 * @file
3
 * Pattern handling for messages
4
 *
5
 * @authors
6
 * Copyright (C) 2023-2024 Richard Russon <rich@flatcap.org>
7
 * Copyright (C) 2025 Thomas Klausner <wiz@gatalith.at>
8
 *
9
 * @copyright
10
 * This program is free software: you can redistribute it and/or modify it under
11
 * the terms of the GNU General Public License as published by the Free Software
12
 * Foundation, either version 2 of the License, or (at your option) any later
13
 * version.
14
 *
15
 * This program is distributed in the hope that it will be useful, but WITHOUT
16
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
17
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
18
 * details.
19
 *
20
 * You should have received a copy of the GNU General Public License along with
21
 * this program.  If not, see <http://www.gnu.org/licenses/>.
22
 */
23

24
/**
25
 * @page pattern_message Pattern handling for messages
26
 *
27
 * Pattern handling for messages
28
 */
29

30
#include "config.h"
31
#include <stdbool.h>
32
#include <stdlib.h>
33
#include <string.h>
34
#include "private.h"
35
#include "mutt/lib.h"
36
#include "core/lib.h"
37
#include "gui/lib.h"
38
#include "lib.h"
39
#include "menu/lib.h"
40
#include "module_data.h"
41

42
#define KILO 1024    ///< 1024 bytes (1 kibibyte)
43
#define MEGA 1048576 ///< 1048576 bytes (1 mebibyte)
44

45
/**
46
 * enum EatRangeError - Error codes for eat_range_by_regex()
47
 */
48
enum EatRangeError
49
{
50
  RANGE_E_OK,     ///< Range is valid
51
  RANGE_E_SYNTAX, ///< Range contains syntax error
52
  RANGE_E_MVIEW,  ///< Range requires MailboxView, but none available
53
};
54

55
/**
56
 * report_regerror - Create a regex error message
57
 * @param regerr Regex error code
58
 * @param preg   Regex pattern buffer
59
 * @param err    Buffer for error messages
60
 * @retval #RANGE_E_SYNTAX Always
61
 */
UNCOV
62
static int report_regerror(int regerr, regex_t *preg, struct Buffer *err)
×
63
{
UNCOV
64
  size_t ds = err->dsize;
×
65

66
  if (regerror(regerr, preg, err->data, ds) > ds)
×
UNCOV
67
    mutt_debug(LL_DEBUG2, "warning: buffer too small for regerror\n");
×
68
  /* The return value is fixed, exists only to shorten code at callsite */
UNCOV
69
  return RANGE_E_SYNTAX;
×
70
}
71

72
/**
73
 * is_menu_available - Do we need a MailboxView for this Pattern?
74
 * @param s      String to check
75
 * @param pmatch Regex matches
76
 * @param kind   Range type, e.g. #RANGE_K_REL
77
 * @param err    Buffer for error messages
78
 * @param menu   Current Menu
79
 * @retval false MailboxView is required, but not available
80
 * @retval true  Otherwise
81
 */
UNCOV
82
static bool is_menu_available(struct Buffer *s, regmatch_t pmatch[], int kind,
×
83
                              struct Buffer *err, const struct Menu *menu)
84
{
UNCOV
85
  const char *context_req_chars[] = {
×
86
    [RANGE_K_REL] = ".0123456789",
87
    [RANGE_K_ABS] = ".",
88
    [RANGE_K_LT] = "",
89
    [RANGE_K_GT] = "",
90
    [RANGE_K_BARE] = ".",
91
  };
92

93
  /* First decide if we're going to need the menu at all.
94
   * Relative patterns need it if they contain a dot or a number.
95
   * Absolute patterns only need it if they contain a dot. */
96
  char *context_loc = strpbrk(s->dptr + pmatch[0].rm_so, context_req_chars[kind]);
×
UNCOV
97
  if (!context_loc || (context_loc >= &s->dptr[pmatch[0].rm_eo]))
×
98
    return true;
99

100
  /* We need a current message.  Do we actually have one? */
UNCOV
101
  if (menu)
×
102
    return true;
103

104
  /* Nope. */
105
  buf_strcpy(err, _("No current message"));
×
UNCOV
106
  return false;
×
107
}
108

109
/**
110
 * scan_range_num - Parse a number range
111
 * @param s      String to parse
112
 * @param pmatch Array of regex matches
113
 * @param group  Index of regex match to use
114
 * @param kind   Range type, e.g. #RANGE_K_REL
115
 * @param mv     Mailbox view
116
 * @retval num Parse number
117
 */
UNCOV
118
static int scan_range_num(struct Buffer *s, regmatch_t pmatch[], int group,
×
119
                          int kind, struct MailboxView *mv)
120
{
121
  int num = (int) strtol(&s->dptr[pmatch[group].rm_so], NULL, 0);
×
122
  unsigned char c = (unsigned char) (s->dptr[pmatch[group].rm_eo - 1]);
×
123
  if (mutt_toupper(c) == 'K')
×
124
    num *= KILO;
×
125
  else if (mutt_toupper(c) == 'M')
×
126
    num *= MEGA;
×
UNCOV
127
  switch (kind)
×
128
  {
UNCOV
129
    case RANGE_K_REL:
×
130
    {
131
      struct Mailbox *m = mv->mailbox;
×
132
      struct Menu *menu = mv->menu;
×
133
      struct Email *e = mutt_get_virt_email(m, menu_get_index(menu));
×
UNCOV
134
      if (!e)
×
135
        return num;
UNCOV
136
      return num + email_msgno(e);
×
137
    }
138
    case RANGE_K_LT:
×
139
      return num - 1;
×
140
    case RANGE_K_GT:
×
UNCOV
141
      return num + 1;
×
142
    default:
143
      return num;
144
  }
145
}
146

147
/**
148
 * scan_range_slot - Parse a range of message numbers
149
 * @param s      String to parse
150
 * @param pmatch Regex matches
151
 * @param grp    Which regex match to use
152
 * @param side   Which side of the range is this?  #RANGE_S_LEFT or #RANGE_S_RIGHT
153
 * @param kind   Range type, e.g. #RANGE_K_REL
154
 * @param mv     Mailbox view
155
 * @retval num Index number for the message specified
156
 */
UNCOV
157
static int scan_range_slot(struct Buffer *s, regmatch_t pmatch[], int grp,
×
158
                           int side, int kind, struct MailboxView *mv)
159
{
160
  struct Mailbox *m = mv->mailbox;
×
UNCOV
161
  struct Menu *menu = mv->menu;
×
162

163
  /* This means the left or right subpattern was empty, e.g. ",." */
UNCOV
164
  if ((pmatch[grp].rm_so == -1) || (pmatch[grp].rm_so == pmatch[grp].rm_eo))
×
165
  {
UNCOV
166
    if (side == RANGE_S_LEFT)
×
167
      return 1;
168
    if (side == RANGE_S_RIGHT)
×
UNCOV
169
      return m->msg_count;
×
170
  }
171
  /* We have something, so determine what */
172
  unsigned char c = (unsigned char) (s->dptr[pmatch[grp].rm_so]);
×
UNCOV
173
  switch (c)
×
174
  {
175
    case RANGE_CIRCUM:
176
      return 1;
177
    case RANGE_DOLLAR:
×
178
      return m->msg_count;
×
UNCOV
179
    case RANGE_DOT:
×
180
    {
181
      struct Email *e = mutt_get_virt_email(m, menu_get_index(menu));
×
UNCOV
182
      if (!e)
×
183
        return 1;
UNCOV
184
      return email_msgno(e);
×
185
    }
UNCOV
186
    case RANGE_LT:
×
187
    case RANGE_GT:
188
      return scan_range_num(s, pmatch, grp + 1, kind, mv);
×
UNCOV
189
    default:
×
190
      /* Only other possibility: a number */
UNCOV
191
      return scan_range_num(s, pmatch, grp, kind, mv);
×
192
  }
193
}
194

195
/**
196
 * order_range - Put a range in order
197
 * @param pat Pattern to check
198
 */
199
static void order_range(struct Pattern *pat)
200
{
UNCOV
201
  if (pat->min <= pat->max)
×
202
    return;
203
  long num = pat->min;
204
  pat->min = pat->max;
×
UNCOV
205
  pat->max = num;
×
206
}
207

208
/**
209
 * eat_range_by_regex - Parse a range given as a regex
210
 * @param pat  Pattern to store the range in
211
 * @param s    String to parse
212
 * @param kind Range type, e.g. #RANGE_K_REL
213
 * @param err  Buffer for error messages
214
 * @param mv   Mailbox view
215
 * @retval num EatRangeError code, e.g. #RANGE_E_OK
216
 */
UNCOV
217
static int eat_range_by_regex(struct Pattern *pat, struct Buffer *s, int kind,
×
218
                              struct Buffer *err, struct MailboxView *mv)
219
{
220
  struct PatternModuleData *mod_data = neomutt_get_module_data(NeoMutt, MODULE_ID_PATTERN);
×
221
  int regerr;
UNCOV
222
  regmatch_t pmatch[RANGE_RX_GROUPS] = { 0 };
×
223
  struct RangeRegex *pspec = &mod_data->range_regexes[kind];
224

225
  /* First time through, compile the big regex */
226
  if (!pspec->ready)
×
227
  {
228
    regerr = regcomp(&pspec->cooked, pspec->raw, REG_EXTENDED);
×
229
    if (regerr != 0)
×
UNCOV
230
      return report_regerror(regerr, &pspec->cooked, err);
×
UNCOV
231
    pspec->ready = true;
×
232
  }
233

234
  /* Match the pattern buffer against the compiled regex.
235
   * No match means syntax error. */
236
  regerr = regexec(&pspec->cooked, s->dptr, RANGE_RX_GROUPS, pmatch, 0);
×
UNCOV
237
  if (regerr != 0)
×
238
    return report_regerror(regerr, &pspec->cooked, err);
×
239

240
  struct Mailbox *m = mv->mailbox;
×
UNCOV
241
  struct Menu *menu = mv->menu;
×
UNCOV
242
  if (!is_menu_available(s, pmatch, kind, err, menu))
×
243
    return RANGE_E_MVIEW;
244

245
  /* Snarf the contents of the two sides of the range. */
246
  pat->min = scan_range_slot(s, pmatch, pspec->lgrp, RANGE_S_LEFT, kind, mv);
×
UNCOV
247
  pat->max = scan_range_slot(s, pmatch, pspec->rgrp, RANGE_S_RIGHT, kind, mv);
×
UNCOV
248
  mutt_debug(LL_DEBUG1, "pat->min=%ld pat->max=%ld\n", pat->min, pat->max);
×
249

250
  /* Special case for a bare 0. */
251
  if ((kind == RANGE_K_BARE) && (pat->min == 0) && (pat->max == 0))
×
252
  {
253
    if (!m || !menu)
×
254
    {
UNCOV
255
      buf_strcpy(err, _("No current message"));
×
256
      return RANGE_E_MVIEW;
×
257
    }
UNCOV
258
    struct Email *e = mutt_get_virt_email(m, menu_get_index(menu));
×
UNCOV
259
    if (!e)
×
260
      return RANGE_E_MVIEW;
261

UNCOV
262
    pat->max = email_msgno(e);
×
UNCOV
263
    pat->min = pat->max;
×
264
  }
265

266
  /* Since we don't enforce order, we must swap bounds if they're backward */
267
  order_range(pat);
268

269
  /* Slide pointer past the entire match. */
UNCOV
270
  s->dptr += pmatch[0].rm_eo;
×
UNCOV
271
  return RANGE_E_OK;
×
272
}
273

274
/**
275
 * eat_message_range - Parse a range of message numbers - Implements ::eat_arg_t - @ingroup eat_arg_api
276
 * @param pat   Pattern to store the results in
277
 * @param flags Flags, e.g. #MUTT_PC_PATTERN_DYNAMIC
278
 * @param s     String to parse
279
 * @param err   Buffer for error messages
280
 * @param mv    Mailbox view
281
 * @retval true The pattern was read successfully
282
 */
283
bool eat_message_range(struct Pattern *pat, PatternCompFlags flags,
2✔
284
                       struct Buffer *s, struct Buffer *err, struct MailboxView *mv)
285
{
286
  if (!mv || !mv->mailbox || !mv->menu)
2✔
287
  {
288
    // We need these for pretty much anything
289
    buf_strcpy(err, _("No mailbox is open"));
2✔
290
    return false;
2✔
291
  }
292

293
  bool skip_quote = false;
294

295
  /* If simple_search is set to "~m %s", the range will have double quotes
296
   * around it...  */
297
  if (*s->dptr == '"')
×
298
  {
UNCOV
299
    s->dptr++;
×
300
    skip_quote = true;
301
  }
302

303
  for (int i_kind = 0; i_kind != RANGE_K_INVALID; i_kind++)
×
304
  {
UNCOV
305
    switch (eat_range_by_regex(pat, s, i_kind, err, mv))
×
306
    {
307
      case RANGE_E_MVIEW:
308
        /* This means it matched syntactically but lacked context.
309
         * No point in continuing. */
310
        break;
311
      case RANGE_E_SYNTAX:
×
312
        /* Try another syntax, then */
313
        continue;
×
314
      case RANGE_E_OK:
×
315
        if (skip_quote && (*s->dptr == '"'))
×
UNCOV
316
          s->dptr++;
×
UNCOV
317
        SKIPWS(s->dptr);
×
318
        return true;
319
    }
320
  }
321
  return false;
322
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc