-
Notifications
You must be signed in to change notification settings - Fork 323
Description
Environment
- libunwind versions: 1.2.1, 1.4.0, 1.6.2 (also checked master - same code)
- Architectures: i686 (all versions broken), x86_64 (1.4.0 broken)
- OS: Linux (Ubuntu 22.04)
Description
When performing local stack unwinding using unw_step(), libunwind crashes with a SIGSEGV in access_reg(). The crash occurs because cursor->uc is NULL or invalid, but access_reg() unconditionally dereferences it.
Affected Versions/Architectures
| Architecture | 1.2.1 | 1.4.0 | 1.6.2 | master |
|---|---|---|---|---|
| x86_64 | OK | SIGSEGV | OK | OK |
| i686 | SIGSEGV | SIGSEGV | SIGSEGV | likely broken |
| aarch64 | OK | OK | OK | OK |
Crash details:
- x86_64 + 1.4.0:
uc=0x0,addr=0x78atx86_64/Ginit.c:332 - i686 + 1.2.1:
uc=0x2(garbage),addr=0x2eatx86/Ginit.c:176 - i686 + 1.4.0:
uc=0x0,addr=0x2catx86/Ginit.c:176 - i686 + 1.6.2:
uc=0x0,addr=0x2catx86/Ginit.c:175
Root Cause
In src/x86/Ginit.c (and src/x86_64/Ginit.c), the access_reg() function:
static int
access_reg (unw_addr_space_t as, unw_regnum_t reg, unw_word_t *val, int write,
void *arg)
{
unw_word_t *addr;
ucontext_t *uc = ((struct cursor *)arg)->uc; // uc can be NULL or invalid!
if (unw_is_fpreg (reg))
goto badreg;
if (!(addr = x86_r_uc_addr (uc, reg))) // computes offset into invalid pointer
goto badreg;
// ...
*val = *(unw_word_t *) addr; // CRASH: addr is just an offset (0x2c, 0x78, etc.)When libunwind is built as a shared library (not UNW_LOCAL_ONLY), DWARF unwinding uses DWARF_REG_LOC() which creates locations with DWARF_LOC_TYPE_REG. During dwarf_get(), when it encounters such a location, it calls access_reg() via the accessor function pointer. However, access_reg() assumes cursor->uc is valid, which is not true during stepping through frames.
Stack Trace (i686 + 1.6.2)
#0 0xf7fa1095 in access_reg (reg=6, val=0xf7bf80ac, write=0, arg=0xf7bf7940) at x86/Ginit.c:175
addr = 0x2c
uc = 0x0
#1 0xf7fa20af in dwarf_get (c=0xf7bf8504, loc=..., val=0xf7bf80ac) at ../include/tdep-x86/libunwind_i.h:208
#2 0xf7fa24ab in _Ux86_access_reg (c=0xf7bf8504, reg=6, valp=0xf7bf80ac, write=0) at x86/Gregs.c:118
loc = {val = 6, type = 2}
#3 ... in apply_reg_state ()
#4 ... in _Ux86_dwarf_step ()
#5 ... in _Ux86_step ()
#6 ... in user code calling unw_step()
Reproduction Steps
Simple C test case:
#define UNW_LOCAL_ONLY
#include <libunwind.h>
#include <stdio.h>
void show_backtrace() {
unw_cursor_t cursor;
unw_context_t context;
unw_getcontext(&context);
unw_init_local(&cursor, &context);
while (unw_step(&cursor) > 0) { // SIGSEGV here on i686
unw_word_t ip;
unw_get_reg(&cursor, UNW_REG_IP, &ip);
printf("ip = %lx\n", (long)ip);
}
}
void foo() { show_backtrace(); }
void bar() { foo(); }
int main() { bar(); return 0; }Compile and run:
gcc -m32 test.c -o test -lunwind
./testSuggested Fix
Add a NULL check for uc in access_reg():
static int
access_reg (unw_addr_space_t as, unw_regnum_t reg, unw_word_t *val, int write,
void *arg)
{
unw_word_t *addr;
ucontext_t *uc = ((struct cursor *)arg)->uc;
if (!uc)
return -UNW_EBADREG; // or appropriate error
// ... rest of function
}Or ensure that DWARF register locations are handled differently when uc is not available.
Notes
- This issue is specific to i686 when built as a shared library
- The same bug pattern likely exists in
access_fpreg()as well