Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is the segfault address NULL when accessing memory that has any of the 16 most significant bits set?

Consider the following assembly program:

bits 64
global _start
_start:
    mov rax, 0x0000111111111111
    add byte [rax*1+0x0], al
    jmp _start

When you compile this with nasm and ld (on Ubuntu, kernel 5.4.0-48-generic, Ryzen 3900X), you get a segfault:

$ ./segfault-addr
[1]    107116 segmentation fault (core dumped)  ./segfault-addr

When you attach gdb you can see the address that caused this fault:

(gdb) p $_siginfo._sifields._sigfault.si_addr
$1 = (void *) 0x111111111111

However, if you set any of the 16 most significant bits to 1 like this:

bits 64
global _start
_start:
    mov rax, 0x0001111111111111
    add byte [rax*1+0x0], al
    jmp _start

You obviously still get a segfault, but now the address is NULL:

(gdb) p $_siginfo._sifields._sigfault.si_addr
$1 = (void *) 0x0

Why is this happening? Is it caused by gdb, Linux, or the CPU itself?

Is there anything I can do to prevent this behavior?

like image 291
cmpxchg8b Avatar asked Oct 11 '20 21:10

cmpxchg8b


1 Answers

It's the difference between canonical and non-canonical addresses, coming from the fact that the x86-64 doesn't have a full 64-bit virtual address space. Your second example is a non-canonical address as it isn't a sign-extended 48-bit value (you apparently don't have the 5-level page table extension on your machine or it would be 57 bits); such addresses can never resolve to a physical memory location.

Invalid accesses to canonical addresses generate a page fault (#PF), for which the CPU provides the faulting address to the kernel (in the CR2 register), and the kernel passes it along to userspace in the si_addr field of struct siginfo as you see. But accesses to non-canonical addresses are always invalid and the CPU raises a general protection exception (#GP), or in rare cases, a stack fault (#SS). The designers of the x86 architecture chose, in their infinite wisdom, not to provide the faulting address to software in case of a #GP or #SS exception, so the kernel doesn't get it and neither do you.

If you really need the address, your only choice is to decode the instruction that caused the exception, and inspect the contents of registers as needed to work out what it was trying to do.


I presume this decision was because the kernel really needs the address in case of a page fault. An access to a not-present page may be a memory violation that should kill the process; or, for instance, it may simply be a page that has been swapped out from physical memory. In the latter case, the kernel uses the fault address to find the appropriate page on disk and load it back into physical memory. Then it updates the page tables and returns from the exception handler to restart the faulting instruction, and the program can continue.

However, a general protection fault is typically unrecoverable, and the process will have to be killed, or at least signaled so it can try to clean up. In this case there is nothing actionable to be done with the faulting address, and I guess the architecture designers didn't think its potential value for debugging was worth the effort of having the CPU save it. Anyway, many possible causes of #GP don't arise from a memory access at all (e.g. trying to read or write control registers from unprivileged mode), in which case there is no faulting address.

like image 108
Nate Eldredge Avatar answered Oct 21 '22 18:10

Nate Eldredge