CVE-2020-15945 (C, published 2020-07-24T21:15Z)
Description: value: Lua through 5.4.0 has a segmentation fault in changedline in ldebug.c (e.g., when called by luaG_traceexec) because it incorrectly expects that an oldpc value is always updated upon a return of the flow of control to a function.
Severity: LOW (CVSS v3 base score: 5.5)
Weakness: NVD-CWE-Other \u2014 Other
NVD is only using a subset of CWE for mapping instead of the entire CWE, and the weakness type is not covered by that subset.
Repository: https://github.com/lua/lua
Files changed:
ldebug.c
ldebug.h
ldo.c
lstate.c
lstate.h
lvm.c
Commit message: Fixed bug: invalid 'oldpc' when returning to a function

The field 'L->oldpc' is not always updated when control returns to a
function; an invalid value can seg. fault when computing 'changedline'.
(One example is an error in a finalizer; control can return to
'luaV_execute' without executing 'luaD_poscall'.) Instead of trying to
fix all possible corner cases, it seems safer to be resilient to invalid
values for 'oldpc'. Valid but wrong values at most cause an extra call
to a line hook.


Vulnerable code:

/* Active Lua function (given call info) */
#define ci_func(ci)		(clLvalue(s2v((ci)->func)))

** Fields 'oldpc', 'basehookcount', and 'hookcount' (set by
** 'resethookcount') are for debug only, and it is no problem if they
** get arbitrary values (causes at most one wrong hook call). 'hookmask'
** is an atomic value. We assume that pointers are atomic too (e.g., gcc
** ensures that for all platforms where it runs). Moreover, 'hook' is
** always checked before being called (see 'luaD_hook').
  if (isLua(L->ci))
    L->oldpc = L->ci->u.l.savedpc;
    const Proto *p = ci_func(ci)->p;
        pc <= L->oldpc ||  /* when jump back (loop), or when */
        changedline(p, pcRel(L->oldpc, p), npci)) {  /* enter new line */
    L->oldpc = pc;  /* 'pc' of last call to line hook */
    Proto *p = clLvalue(s2v(ci->func))->p;
  if (isLua(ci->previous))
    L->oldpc = ci->previous->u.l.savedpc;  /* update 'oldpc' */
  const Instruction *oldpc;  /* last pc traced */
          L->oldpc = pc + 1;  /* next opcode will be seen as a "new" line */

Fixed code:
/* inverse of 'pcRel' */
#define invpcRel(pc, p)		((p)->code + (pc) + 1)
** Fields 'basehookcount' and 'hookcount' (set by 'resethookcount')
** are for debug only, and it is no problem if they get arbitrary
** values (causes at most one wrong hook call). 'hookmask' is an atomic
** value. We assume that pointers are atomic too (e.g., gcc ensures that
** for all platforms where it runs). Moreover, 'hook' is always checked
** before being called (see 'luaD_hook').
/*
** Traces the execution of a Lua function. Called before the execution
** of each opcode, when debug is on. 'L->oldpc' stores the last
** instruction traced, to detect line changes. When entering a new
** function, 'npci' will be zero and will test as a new line without
** the need for 'oldpc'; so, 'oldpc' does not need to be initialized
** before. Some exceptional conditions may return to a function without
** updating 'oldpc'. In that case, 'oldpc' may be invalid; if so, it is
** reset to zero.  (A wrong but valid 'oldpc' at most causes an extra
** call to a line hook.)
*/
  const Proto *p = ci_func(ci)->p;
  /* 'L->oldpc' may be invalid; reset it in this case */
  int oldpc = (L->oldpc < p->sizecode) ? L->oldpc : 0;
        pc <= invpcRel(oldpc, p) ||  /* when jump back (loop), or when */
        changedline(p, oldpc, npci)) {  /* enter new line */
    L->oldpc = npci;  /* 'pc' of last call to line hook */

/* Active Lua function (given call info) */
#define ci_func(ci)		(clLvalue(s2v((ci)->func)))


    Proto *p = ci_func(ci)->p;
  if (isLua(ci = ci->previous))
    L->oldpc = pcRel(ci->u.l.savedpc, ci_func(ci)->p);  /* update 'oldpc' */
  L->oldpc = 0;
  int oldpc;  /* last pc traced */
          L->oldpc = 1;  /* next opcode will be seen as a "new" line */