Skip to content

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

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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