Skip to content

[x86/x86_64] SIGSEGV in access_reg() due to NULL/invalid ucontext during local unwinding #938

@runlevel5

Description

@runlevel5

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=0x78 at x86_64/Ginit.c:332
  • i686 + 1.2.1: uc=0x2 (garbage), addr=0x2e at x86/Ginit.c:176
  • i686 + 1.4.0: uc=0x0, addr=0x2c at x86/Ginit.c:176
  • i686 + 1.6.2: uc=0x0, addr=0x2c at x86/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
./test

Suggested 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions