10

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(\
    ".text\n"\
    ".byte 0\n"\
    ".align 16\n"\
    "__" #name ":\n"\
    "   movq $" #syscall ", %rax\n"\
    "   syscall\n"\
);
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
Dan Beaulieu
  • 19,406
  • 19
  • 101
  • 135
Will
  • 103
  • 1
  • 8
  • Why `+=6`? Second, a read will (say) set a register value. I don't see anything about that in your code, are you doing it? You say you 'send the data read/write to simulation', but nothing about getting any data back, is that intended? – Yakk - Adam Nevraumont Jun 18 '15 at 18:04
  • The `+=6` seems to be the instruction width for x86_64, at least for the simplest case. Unfortunately I cannot provide the source of this knowledge, but it does seem to work. When I say 'send data read/write...', I mean I issue a custom instruction (with the data attached) to my hardware simulation (i.e. load/store). Are you saying I need to pinpoint the processor register that needs the data and load it up? That would make sense... – Will Jun 18 '15 at 18:14
  • Wrap your hardware in an abstraction layer and only make calls to the abstraction layer. As an added bonus when you port the software to new hardware you only have to update the abstraction layer. (And deal with any bugs in your code exposed by new timing and probably a new compiler) – user4581301 Jun 18 '15 at 18:25
  • I am not sure how I would add a global level of abstraction to an arbitrary user program. I would greatly appreciate any links to resources discussing this. It may be good to note that the hardware simulation is running in its own process, where custom instructions (and data) are sent via socket or shared memory space. – Will Jun 18 '15 at 18:32
  • My apologies. I read that too quickly with a bad opening assumption. What you have in mind sounds more like a virtual machine than building a test bench. – user4581301 Jun 18 '15 at 19:00
  • I have created a library, which allows to catch sigsegv and recover by restarting a process. I used the DeathHandler project as starting point. You may find some inspiration here https://github.com/vmarkovtsev/DeathHandler – Jens Munk Jun 18 '15 at 20:40
  • @Will: I think the source of your problem may be the `+= 6`. x86 instructions are variable-length. It may just turn out that a common `mov` instruction that writes to a memory address may be 6 bytes, but other instruction encodings can write to memory as well. I'm not sure the best way to fix this, but if you could figure out the right amount to step forward, then you might be able to get it to work. – Jason R Sep 29 '15 at 19:17

1 Answers1

6

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...

Chris Dodd
  • 119,907
  • 13
  • 134
  • 226
  • 1
    Thanks for the information regarding setting the trap flag. I should have paid more attention to this question [link](http://stackoverflow.com/questions/21068714/trap-all-accesses-to-an-address-range-linux). Perhaps I could utilize the SIGSEGV+SIGTRAP sequence of signals with a small memory region functioning as a sort-of swap to achieve my intended functionality. – Will Jun 18 '15 at 21:29
  • I was able to build a "processor cache" emulator that functions at block-granularity using SIGSEGV, SIGTRAP, and mprotect. Unfortunately trapping every read/write at an address causes huge performance drawbacks, but the implementation of the concept is valid. Thanks again, and apologies for the delayed acceptance. – Will Sep 29 '15 at 22:30