• 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

18.08
/lua/lua.c
1
/**
2
 * @file
3
 * Integrated Lua scripting
4
 *
5
 * @authors
6
 * Copyright (C) 2016-2017 Bernard Pratz <z+mutt+pub@m0g.net>
7
 * Copyright (C) 2017-2025 Richard Russon <rich@flatcap.org>
8
 * Copyright (C) 2018 Victor Fernandes <criw@pm.me>
9
 * Copyright (C) 2019 Ian Zimmerman <itz@no-use.mooo.com>
10
 * Copyright (C) 2019-2020 Pietro Cerutti <gahr@gahr.ch>
11
 * Copyright (C) 2023 Rayford Shireman
12
 *
13
 * @copyright
14
 * This program is free software: you can redistribute it and/or modify it under
15
 * the terms of the GNU General Public License as published by the Free Software
16
 * Foundation, either version 2 of the License, or (at your option) any later
17
 * version.
18
 *
19
 * This program is distributed in the hope that it will be useful, but WITHOUT
20
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
21
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
22
 * details.
23
 *
24
 * You should have received a copy of the GNU General Public License along with
25
 * this program.  If not, see <http://www.gnu.org/licenses/>.
26
 */
27

28
/**
29
 * @page lua_lua Integrated Lua scripting
30
 *
31
 * Integrated Lua scripting
32
 */
33

34
#ifndef LUA_COMPAT_ALL
35
#define LUA_COMPAT_ALL
36
#endif
37
#ifndef LUA_COMPAT_5_1
38
#define LUA_COMPAT_5_1
39
#endif
40

41
#include "config.h"
42
#include <lauxlib.h>
43
#include <lua.h>
44
#include <lualib.h>
45
#include <stdbool.h>
46
#include <stdint.h>
47
#include <stdio.h>
48
#include "mutt/lib.h"
49
#include "config/lib.h"
50
#include "core/lib.h"
51
#include "parse/lib.h"
52
#include "muttlib.h"
53
#include "version.h"
54

55
/**
56
 * lua_handle_panic - Handle a panic in the Lua interpreter
57
 * @param l Lua State
58
 * @retval -1 Always
59
 */
UNCOV
60
static int lua_handle_panic(lua_State *l)
×
61
{
UNCOV
62
  mutt_debug(LL_DEBUG1, "lua runtime panic: %s\n", lua_tostring(l, -1));
×
63
  mutt_error("Lua runtime panic: %s", lua_tostring(l, -1));
×
UNCOV
64
  lua_pop(l, 1);
×
65
  return -1;
×
66
}
67

68
/**
69
 * lua_handle_error - Handle an error in the Lua interpreter
70
 * @param l Lua State
71
 * @retval -1 Always
72
 */
UNCOV
73
static int lua_handle_error(lua_State *l)
×
74
{
UNCOV
75
  mutt_debug(LL_DEBUG1, "lua runtime error: %s\n", lua_tostring(l, -1));
×
76
  mutt_error("Lua runtime error: %s", lua_tostring(l, -1));
×
UNCOV
77
  lua_pop(l, 1);
×
78
  return -1;
×
79
}
80

81
/**
82
 * lua_cb_global_call - Call a NeoMutt command by name
83
 * @param l Lua State
84
 * @retval >=0 Success
85
 * @retval -1 Error
86
 */
UNCOV
87
static int lua_cb_global_call(lua_State *l)
×
88
{
UNCOV
89
  mutt_debug(LL_DEBUG2, "enter\n");
×
90
  struct Buffer *buf = buf_pool_get();
×
UNCOV
91
  struct ParseContext *pc = parse_context_new();
×
92
  struct ParseError *pe = parse_error_new();
×
93
  const struct Command *cmd = NULL;
94
  int rc = 0;
95

UNCOV
96
  if (lua_gettop(l) == 0)
×
97
  {
UNCOV
98
    buf_pool_release(&buf);
×
99
    parse_context_free(&pc);
×
UNCOV
100
    parse_error_free(&pe);
×
101
    luaL_error(l, "Error command argument required");
×
102
    return -1;
×
103
  }
104

105
  cmd = commands_get(&NeoMutt->commands, lua_tostring(l, 1));
×
UNCOV
106
  if (!cmd)
×
107
  {
108
    buf_pool_release(&buf);
×
109
    parse_context_free(&pc);
×
UNCOV
110
    parse_error_free(&pe);
×
111
    luaL_error(l, "Error command %s not found", lua_tostring(l, 1));
×
112
    return -1;
×
113
  }
114

115
  for (int i = 2; i <= lua_gettop(l); i++)
×
116
  {
UNCOV
117
    buf_addstr(buf, lua_tostring(l, i));
×
118
    buf_addch(buf, ' ');
×
119
  }
120
  buf_seek(buf, 0);
×
121

UNCOV
122
  if (cmd->parse(cmd, buf, pc, pe))
×
123
  {
UNCOV
124
    luaL_error(l, "NeoMutt error: %s", buf_string(pe->message));
×
125
    rc = -1;
126
  }
127
  else
128
  {
UNCOV
129
    if (!lua_pushstring(l, buf_string(pe->message)))
×
UNCOV
130
      lua_handle_error(l);
×
131
    else
132
      rc++;
133
  }
134

UNCOV
135
  buf_pool_release(&buf);
×
UNCOV
136
  parse_context_free(&pc);
×
UNCOV
137
  parse_error_free(&pe);
×
138
  return rc;
×
139
}
140

141
/**
142
 * lua_cb_global_set - Set a NeoMutt variable
143
 * @param l Lua State
144
 * @retval  0 Success
145
 * @retval -1 Error
146
 */
UNCOV
147
static int lua_cb_global_set(lua_State *l)
×
148
{
UNCOV
149
  const char *param = lua_tostring(l, -2);
×
150
  mutt_debug(LL_DEBUG2, "%s\n", param);
×
151

152
  struct Buffer *err = buf_pool_get();
×
153
  struct HashElem *he = cs_subset_lookup(NeoMutt->sub, param);
×
UNCOV
154
  if (!he)
×
155
  {
156
    // In case it is a my_var, we have to create it
157
    if (mutt_str_startswith(param, "my_"))
×
158
    {
UNCOV
159
      struct ConfigDef my_cdef = { 0 };
×
160
      my_cdef.name = param;
×
UNCOV
161
      my_cdef.type = DT_MYVAR;
×
162
      he = cs_create_variable(NeoMutt->sub->cs, &my_cdef, err);
×
163
      if (!he)
×
164
        return -1;
×
165
    }
166
    else
167
    {
UNCOV
168
      luaL_error(l, "NeoMutt parameter not found %s", param);
×
UNCOV
169
      return -1;
×
170
    }
171
  }
172

UNCOV
173
  struct ConfigDef *cdef = he->data;
×
174

175
  int rc = 0;
176

UNCOV
177
  switch (CONFIG_TYPE(cdef->type))
×
178
  {
UNCOV
179
    case DT_ADDRESS:
×
180
    case DT_ENUM:
181
    case DT_EXPANDO:
182
    case DT_MBTABLE:
183
    case DT_MYVAR:
184
    case DT_PATH:
185
    case DT_REGEX:
186
    case DT_SLIST:
187
    case DT_SORT:
188
    case DT_STRING:
189
    {
UNCOV
190
      const char *value = lua_tostring(l, -1);
×
UNCOV
191
      size_t val_size = lua_rawlen(l, -1);
×
UNCOV
192
      struct Buffer *value_buf = buf_pool_get();
×
193
      buf_strcpy_n(value_buf, value, val_size);
×
194
      if (CONFIG_TYPE(he->type) == DT_PATH)
×
195
        expand_path(value_buf, false);
×
196

197
      int rv = cs_subset_he_string_set(NeoMutt->sub, he, buf_string(value_buf), err);
×
198
      buf_pool_release(&value_buf);
×
UNCOV
199
      if (CSR_RESULT(rv) != CSR_SUCCESS)
×
200
        rc = -1;
201
      break;
202
    }
UNCOV
203
    case DT_LONG:
×
204
    case DT_NUMBER:
205
    case DT_QUAD:
206
    {
UNCOV
207
      const intptr_t value = lua_tointeger(l, -1);
×
UNCOV
208
      int rv = cs_subset_he_native_set(NeoMutt->sub, he, value, err);
×
UNCOV
209
      if (CSR_RESULT(rv) != CSR_SUCCESS)
×
210
        rc = -1;
211
      break;
212
    }
UNCOV
213
    case DT_BOOL:
×
214
    {
UNCOV
215
      const intptr_t value = lua_toboolean(l, -1);
×
216
      int rv = cs_subset_he_native_set(NeoMutt->sub, he, value, err);
×
UNCOV
217
      if (CSR_RESULT(rv) != CSR_SUCCESS)
×
218
        rc = -1;
219
      break;
220
    }
UNCOV
221
    default:
×
UNCOV
222
      luaL_error(l, "Unsupported NeoMutt parameter type %d for %s",
×
223
                 CONFIG_TYPE(cdef->type), param);
224
      rc = -1;
225
      break;
×
226
  }
227

228
  buf_pool_release(&err);
×
UNCOV
229
  return rc;
×
230
}
231

232
/**
233
 * lua_cb_global_get - Get a NeoMutt variable
234
 * @param l Lua State
235
 * @retval  1 Success
236
 * @retval -1 Error
237
 */
UNCOV
238
static int lua_cb_global_get(lua_State *l)
×
239
{
UNCOV
240
  const char *param = lua_tostring(l, -1);
×
241
  mutt_debug(LL_DEBUG2, "%s\n", param);
×
242

243
  struct HashElem *he = cs_subset_lookup(NeoMutt->sub, param);
×
244
  if (!he)
×
245
  {
246
    mutt_debug(LL_DEBUG2, "error\n");
×
247
    luaL_error(l, "NeoMutt parameter not found %s", param);
×
UNCOV
248
    return -1;
×
249
  }
250

251
  struct ConfigDef *cdef = he->data;
×
252

UNCOV
253
  switch (CONFIG_TYPE(cdef->type))
×
254
  {
UNCOV
255
    case DT_ADDRESS:
×
256
    case DT_ENUM:
257
    case DT_EXPANDO:
258
    case DT_MBTABLE:
259
    case DT_MYVAR:
260
    case DT_PATH:
261
    case DT_REGEX:
262
    case DT_SLIST:
263
    case DT_SORT:
264
    case DT_STRING:
265
    {
UNCOV
266
      struct Buffer *value = buf_pool_get();
×
UNCOV
267
      int rc = cs_subset_he_string_get(NeoMutt->sub, he, value);
×
UNCOV
268
      if (CSR_RESULT(rc) != CSR_SUCCESS)
×
269
      {
270
        buf_pool_release(&value);
×
271
        return -1;
×
272
      }
273

274
      struct Buffer *escaped = buf_pool_get();
×
UNCOV
275
      escape_string(escaped, buf_string(value));
×
UNCOV
276
      lua_pushstring(l, buf_string(escaped));
×
277
      buf_pool_release(&value);
×
278
      buf_pool_release(&escaped);
×
279
      return 1;
×
280
    }
281
    case DT_QUAD:
×
282
      lua_pushinteger(l, (unsigned char) cdef->var);
×
UNCOV
283
      return 1;
×
284
    case DT_LONG:
×
285
      lua_pushinteger(l, (signed long) cdef->var);
×
286
      return 1;
×
287
    case DT_NUMBER:
×
288
      lua_pushinteger(l, (signed short) cdef->var);
×
289
      return 1;
×
290
    case DT_BOOL:
×
291
      lua_pushboolean(l, (bool) cdef->var);
×
292
      return 1;
×
293
    default:
×
294
      luaL_error(l, "NeoMutt parameter type %d unknown for %s", cdef->type, param);
×
295
      return -1;
×
296
  }
297
}
298

299
/**
300
 * lua_cb_global_enter - Execute NeoMutt config from Lua
301
 * @param l Lua State
302
 * @retval >=0 Success
303
 * @retval -1  Error
304
 */
UNCOV
305
static int lua_cb_global_enter(lua_State *l)
×
306
{
UNCOV
307
  mutt_debug(LL_DEBUG2, "enter\n");
×
308
  struct Buffer *line = buf_pool_get();
×
UNCOV
309
  struct ParseContext *pc = parse_context_new();
×
310
  struct ParseError *pe = parse_error_new();
×
311

312
  buf_strcpy(line, lua_tostring(l, -1));
×
313
  int rc = 0;
314

315
  if (parse_rc_line(line, pc, pe))
×
316
  {
UNCOV
317
    luaL_error(l, "NeoMutt error: %s", buf_string(pe->message));
×
318
    rc = -1;
319
  }
320
  else
321
  {
UNCOV
322
    if (!lua_pushstring(l, buf_string(pe->message)))
×
UNCOV
323
      lua_handle_error(l);
×
324
    else
325
      rc++;
326
  }
327

UNCOV
328
  buf_pool_release(&line);
×
UNCOV
329
  parse_context_free(&pc);
×
UNCOV
330
  parse_error_free(&pe);
×
331

332
  return rc;
×
333
}
334

335
/**
336
 * lua_cb_global_message - Display a message in NeoMutt
337
 * @param l Lua State
338
 * @retval 0 Always
339
 */
UNCOV
340
static int lua_cb_global_message(lua_State *l)
×
341
{
UNCOV
342
  mutt_debug(LL_DEBUG2, "enter\n");
×
343
  const char *msg = lua_tostring(l, -1);
×
UNCOV
344
  if (msg)
×
345
    mutt_message("%s", msg);
×
346
  return 0;
×
347
}
348

349
/**
350
 * lua_cb_global_error - Display an error in NeoMutt
351
 * @param l Lua State
352
 * @retval 0 Always
353
 */
UNCOV
354
static int lua_cb_global_error(lua_State *l)
×
355
{
UNCOV
356
  mutt_debug(LL_DEBUG2, "enter\n");
×
357
  const char *msg = lua_tostring(l, -1);
×
UNCOV
358
  if (msg)
×
359
    mutt_error("%s", msg);
×
360
  return 0;
×
361
}
362

363
/**
364
 * lua_expose_command - Expose a NeoMutt command to the Lua interpreter
365
 * @param l   Lua state
366
 * @param cmd NeoMutt Command
367
 */
368
static void lua_expose_command(lua_State *l, const struct Command *cmd)
218✔
369
{
370
  char buf[1024] = { 0 };
218✔
371
  snprintf(buf, sizeof(buf), "mutt.command.%s = function (...); mutt.call('%s', ...); end",
372
           cmd->name, cmd->name);
218✔
373
  (void) luaL_dostring(l, buf);
218✔
374
}
218✔
375

376
/**
377
 * LuaMuttCommands - List of Lua commands to register
378
 *
379
 * In NeoMutt, run:
380
 *
381
 * `:lua mutt.message('hello')`
382
 *
383
 * and it will call lua_cb_global_message()
384
 */
385
static const luaL_Reg LuaMuttCommands[] = {
386
  // clang-format off
387
  { "set",     lua_cb_global_set },
388
  { "get",     lua_cb_global_get },
389
  { "call",    lua_cb_global_call },
390
  { "enter",   lua_cb_global_enter },
391
  { "print",   lua_cb_global_message },
392
  { "message", lua_cb_global_message },
393
  { "error",   lua_cb_global_error },
394
  { NULL, NULL },
395
  // clang-format on
396
};
397

398
/**
399
 * lua_expose_commands - Declare some NeoMutt types to the Lua interpreter
400
 * @param l Lua State
401
 * @retval 1 Always
402
 */
403
static int lua_expose_commands(lua_State *l)
2✔
404
{
405
  mutt_debug(LL_DEBUG2, "enter\n");
2✔
406
  luaL_newlib(l, LuaMuttCommands);
2✔
407
  int lib_idx = lua_gettop(l);
2✔
408

409
  // clang-format off
410
  lua_pushstring(l, "VERSION");     lua_pushstring(l, mutt_make_version()); lua_settable(l, lib_idx);
2✔
411
  lua_pushstring(l, "QUAD_YES");    lua_pushinteger(l, MUTT_YES);           lua_settable(l, lib_idx);
2✔
412
  lua_pushstring(l, "QUAD_NO");     lua_pushinteger(l, MUTT_NO);            lua_settable(l, lib_idx);
2✔
413
  lua_pushstring(l, "QUAD_ASKYES"); lua_pushinteger(l, MUTT_ASKYES);        lua_settable(l, lib_idx);
2✔
414
  lua_pushstring(l, "QUAD_ASKNO");  lua_pushinteger(l, MUTT_ASKNO);         lua_settable(l, lib_idx);
2✔
415
  // clang-format on
416

417
  return 1;
2✔
418
}
419

420
/**
421
 * lua_expose_mutt - Expose a 'Mutt' object to the Lua interpreter
422
 * @param l Lua State
423
 */
424
static void lua_expose_mutt(lua_State *l)
2✔
425
{
426
  luaL_requiref(l, "mutt", lua_expose_commands, 1);
2✔
427
  (void) luaL_dostring(l, "mutt.command = {}");
2✔
428

429
  const struct Command **cp = NULL;
430
  ARRAY_FOREACH(cp, &NeoMutt->commands)
220✔
431
  {
432
    const struct Command *cmd = *cp;
218✔
433

434
    lua_expose_command(l, cmd);
218✔
435
  }
436
}
2✔
437

438
/**
439
 * lua_init_state - Initialise a Lua State
440
 * @param[out] l Lua State
441
 * @retval true Successful
442
 */
443
bool lua_init_state(lua_State **l)
2✔
444
{
445
  if (!l)
2✔
446
    return false;
447
  if (*l)
2✔
448
    return true;
449

450
  mutt_debug(LL_DEBUG2, "enter\n");
2✔
451
  *l = luaL_newstate();
2✔
452

453
  if (!*l)
2✔
454
  {
UNCOV
455
    mutt_error(_("Error: Couldn't load the lua interpreter"));
×
UNCOV
456
    return false;
×
457
  }
458

459
  lua_atpanic(*l, lua_handle_panic);
2✔
460

461
  /* load various Lua libraries */
462
  luaL_openlibs(*l);
2✔
463
  lua_expose_mutt(*l);
2✔
464

465
  return true;
2✔
466
}
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