Skip to content
Advertisement

How to trap memory reads and writes using sigsegv?

How do I trick linux into thinking a memory read/write was successful? I am writing a C++ library such that all reads/writes are redirected and handled transparently to the end user. Anytime a variable is written or read from, the library will need to catch that request and shoot it off to a hardware simulation which will handle the data from there.

Note that my library is platform dependent on:

Linux ubuntu 3.16.0-39-generic #53~14.04.1-Ubuntu SMP x86_64 GNU/Linux

gcc (Ubuntu 4.8.2-19ubuntu1) 4.8.2

Current Approach: catch SIGSEGV and increment REG_RIP

My current approach involves getting a memory region using mmap() and shutting off access using mprotect(). I have a SIGSEGV handler to get the info containing the memory address, export the read/write elsewhere, then increment context REG_RIP.

void handle_sigsegv(int code, siginfo_t *info, void *ctx)
{
    void *addr = info->si_addr;
    ucontext_t *u = (ucontext_t *)ctx;
    int err = u->uc_mcontext.gregs[REG_ERR];
    bool is_write = (err & 0x2);
    // send data read/write to simulation...
    // then continue execution of program by incrementing RIP
    u->uc_mcontext.gregs[REG_RIP] += 6;
}

This works for very simple cases, such as:

int *num_ptr = (int *)nullptr;
*num_ptr = 10;                          // write segfault

But for anything even slightly more complex, I receive a SIGABRT:

30729 Illegal instruction (core dumped) ./$target

Using mprotect() within SIGSEGV handler

If I were to not increment REG_RIP, handle_sigsegv() will be called over and over again by the kernel until the memory region becomes available for reading or writing. I could run mprotect() for that specific address, but that has multiple caveats:

  • Subsequent memory access will not trigger a SIGSEGV due to the memory region now having PROT_WRITE ability. I have tried to create a thread that continuously marks the region as PROT_NONE, but that does not elude the next point:
  • mprotect() will, at the end of the day, perform the read or write into memory, invalidating the use case of my library.

Writing a device driver

I have also attempted to write a device module such that the library can call mmap() on the char device, where the driver will handle the reads and writes from there. This makes sense in theory, but I have not been able to (or do not have the knowledge to) catch every load/store the processor issues to the device. I have attempted overwrite the mapped vm_operations_struct and/or the inode’s address_space_operations struct, but that will only call reads/writes when a page is faulted or a page is flushed into backing store.

Perhaps I could use mmap() and mprotect(), like explained above, on the device that writes data nowhere (similar to /dev/null), then have a process that recognizes the reads/writes and routes the data from there (?).

Utilize syscall() and provide a restorer assembly function

The following was pulled from the segvcatch project1 that converts segfaults into exceptions.

#define RESTORE(name, syscall) RESTORE2(name, syscall)
#define RESTORE2(name, syscall)
asm(
    ".textn"
    ".byte 0n"
    ".align 16n"
    "__" #name ":n"
    "   movq $" #syscall ", %raxn"
    "   syscalln"
);
RESTORE(restore_rt, __NR_rt_sigreturn)
void restore_rt(void) asm("__restore_rt") __attribute__
((visibility("hidden")));

extern "C" {
    struct kernel_sigaction {
        void (*k_sa_sigaction)(int, siginfo_t *, void *); 
        unsigned long k_sa_flags;
        void (*k_sa_restorer)(void);
        sigset_t k_sa_mask;
    };  
}

// then within main ...
struct kernel_sigaction act;
act.k_sa_sigaction = handle_sigegv;
sigemptyset(&act.k_sa_mask);
act.k_sa_flags = SA_SIGINFO|0x4000000;
act.k_sa_restorer = restore_rt;
syscall(SYS_rt_sigaction, SIGSEGV, &act, NULL, _NSIG / 8); 

But this ends up functioning no different than a regular sigaction() configuration. If I do not set the restorer function the signal handler is not called more than once, even when the memory region is still not available. Perhaps there is some other trickery I could do with the kernel signal here.


Again, the entire objective of the library is to transparently handle reads and writes to memory. Perhaps there is a much better way of doing things, maybe with ptrace() or even updating the kernel code that generates the segfault signal, but the important part is that the end-user’s code does not require changes. I have seen examples using setjmp() and longjmp() to continue after a segfault, but that would require adding those calls to every memory access. The same goes for converting a segfault to a try/catch.


1 segvcatch project

Advertisement

Answer

You can use mprotect and avoid the first problem you note by also having the SIGSEGV handler set the T flag in the flags register. Then, you add a SIGTRAP handler that restores the mprotected memory and clears the T flag.

The T flag causes the processor to single step, so when the SEGV handler returns it will execute that single instruction, and then immediately TRAP.

This still leaves you with your second problem — the read/write instruction will actually occur. You may be able to get around that problem by carefully modifying the memory before and/or after the instruction in the two signal handlers…

User contributions licensed under: CC BY-SA
10 People found this is helpful
Advertisement