Trap all accesses to an address range (Linux)
Asked Answered
A

1

9

Background

I'm writing a framework to enable co-simulation of RTL running in a simulator and un-modified host software. The host software is written to control actual hardware and typically works in one of two ways:

  1. Read/Write calls through a driver
  2. Memory mapped access using mmap

The former case is pretty straightforward - write a library that implements the same read / write calls as the driver and link against that when running a simulation. This all works wonderfully and I can run un-modified production software as stimulus for my RTL simulations.

The second case is turning out to be far more difficult than the first...

Trapping mmap

Initially I thought I could use LD_PRELOAD to intercept the mmap call. In my implementation of mmap I'd allocate some page-aligned memory and then mprotect it and set a signal handler to trap SIGSEGV.

There are numerous problems with this approach:

Read vs Write

I can determine the address of the access from siginfo_t->si_addr but not whether the access was read or write.

Catching repeat accesses

In the signal handler I need to un-protect the memory region, otherwise the I'll get repeat SIGSEGVs as soon as my handler exits and the host code can never continue. However if I unprotect the region then my signal handler won't trap subsequent accesses.

Signal handler nastiness

To block in a signal handler while the simulator drives the RTL and returns a result violates all sorts of programming rules - particularly given the simulator could trigger all sorts of other events and execute arbitrary code before returning a result from this access.

Other approaches

I was wondering if it's possible to create a file-like object that behaves like a disk rather than using mprotect on a buffer. I haven't found any information suggesting this is feasible.

Questions

Is it possible to trap all accesses to an mmap region and how?

  • Accesses need to block for an indeterminate period of time (while simulator runs)
  • Read accesses need to retrieve a new value placed by my trap

Assuming LD_PRELOAD and mprotect is the best route:

  • Can I determine whether the access was a read or a write call?
  • How do I trap subsequent accesses since I have to un-mprotect the region?

Related Questions

How to write a signal handler to catch SIGSEGV?

Possible to trap write to address (x86 - linux)

Atlantes answered 11/1, 2014 at 22:30 Comment(0)
M
8

On X86 you can set Trap flag for the caller's context to get SIGTRAP after one instruction (this flag is typically used for single-stepping). That is, when SIGSEGV is encountered, you set TF in the caller's EFLAGS (see ucontext.h), enable reading with mprotect and return. If SIGSEGV is repeated instantly with the same IP, you enable writing (and optionally disable reading, if you want to distinguish read-modify-write from write-only access). If you get SIGSEGV from the same IP for read-only and write-only protection, enable read-write.

Whenever you get SIGTRAP, you can analyze what value was written (if it was a write access), and you can also re-protect the page to trap future accesses.

Correction: if both reads and writes can have side-effects, try write-only protection first, then apply reading side-effects and try read-only protection, then enable writing and handle side-effects of writing in the final SIGTRAP handler.

UPDATE: I was deadly wrong on recommending hypothetical write-only protection which turns out not to exist on most architectures. Fortunately there's a more straightforward way to know whether the operation that failed tries to read memory, at least on x86:

Page fault exception pushes an error code to the stack, which is available in Linux SIGSEGV handler as the err member of sigcontext structure. Bit 1 of the error code is 1 for write faults and 0 otherwise. For read-modify-write operation, it will be 0 initially (here you can emulate reading, knowing exactly that it's going to happen).

Merrymaking answered 11/1, 2014 at 22:42 Comment(6)
Thanks for the suggestion. Note that X86 doesn't support write-only protection (https://mcmap.net/q/1174017/-behaviour-of-prot_read-and-prot_write-with-mprotect). Unfortunately it looks like I'll have to perform a speculative read into the simulator every SIGSEGV which will be inefficient for writes (and may make the modelled behaviour different to real hardware)Atlantes
@Chiggs, I can't believe that I was silly enough to invent this "write-only" nonsense. Fortunately, there is a solution (exception error code that gets also into sigcontext->err contains r/w bit). See update in the answer.Merrymaking
@AntonKovalenko -- could you please elaborate on how to set the TF in the caller's EFLAGS? Is this done inside the SIGSEGV handler by (1) calling swapcontext() to swap to the uc_link context, (2) setting the TF flag (via inline asm), (3) swapping back to the original SIGSEGV handler context, and (4) enabling read via mprotect()? I'm unclear what the "caller context" is inside the SIGSEGV handler.Liaison
@Liaison In linux, caller's EFLAGS are in ...->uc_mcontext.gregs[REG_EFL] (see sys/ucontext.h for related definitions). AFAIK you need no swapping and no use of uc_link, just set TF in gregs[REG_EFL].Merrymaking
@Liaison There's a (messy) example of my experimentation here, but in summary: ucontext_t *c = context; c->uc_mcontext.gregs[REG_EFL] |= X86_EFLAGS_TF; worked for me, no need to call swapcontext() since it's passed to your handler.Atlantes
Worked perfectly for me. Note that REG_EFL, REG_ERR are only defined when _GNU_SOURCE is defined on my Ubuntu system. REG_ERR is array index of the write flag mentioned in the answer.Flesh

© 2022 - 2024 — McMap. All rights reserved.