DynamoRIO can have unbounded signal delivery time when a signal arrives during cache entry
Created by: egrimley
You probably knew that, but here is a program that demonstrates it, repeatably.
The way I have solved this problem in other projects is as follows:
If a signal arrives while the PC is in the fragment cache, we handle it immediately. If a signal arrives while we are in the runtime, we set a flag which gets checked as we return to the fragment cache. DynamoRIO does this, I think, with dcontext->signals_pending being the flag.
The problem, of course, is a signal arriving after you've checked the flag but before you arrive in the fragment cache. The solution is to write this part, from checking the flag up to the final branch into the fragment cache, in assembler so that the master signal handler can recognise those PC values.
What does the master signal handler do when it recognises a PC value in that region? Two possibilities:
- It knows the code was loading the register state from some structure, so it just gets the state from that structure and then longjmps somewhere or something...
- It knows that the code region is restartable, so it sets the flag, resets the PC to the start of the region, and returns (from the signal handler). The assembler code then sees that the flag is set and returns to the C runtime. I call a restartable code region like this a "ramp". Its advantage is simplicity: a signal that arrives while you're on the ramp can be handled in exactly the same way as a signal that arrives while you're in the C runtime, so you don't have a rarely visited code path that needs separate testing.
I'm not sure how practicable it would be to combine fully race-free signal handling with client instrumentation...
/* **********************************************************
* Copyright (c) 2016 ARM Limited. All rights reserved.
* **********************************************************/
/*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of ARM Limited nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL ARM LIMITED OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
* DAMAGE.
*/
/* Repeatedly set a short-duration timer, adjusting it to arrive immediately
* after the return from the system call. This can expose a race condition.
*/
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
volatile int polling_started;
volatile int signal_arrived;
timer_t timer;
void fail(const char *s)
{
perror(s);
exit(1);
}
void handler(int signum)
{
signal_arrived = 1;
}
void setup(void)
{
struct sigaction act;
struct sigevent sevp;
memset(&act, 0, sizeof(act));
act.sa_handler = handler;
if (sigaction(SIGUSR1, &act, 0))
fail("sigaction");
memset(&sevp, 0, sizeof(sevp));
sevp.sigev_notify = SIGEV_SIGNAL;
sevp.sigev_signo = SIGUSR1;
if (timer_create(CLOCK_REALTIME, &sevp, &timer))
fail("timer_create");
}
int try(uint64_t time)
{
struct itimerspec spec = {
{ 0, 0 },
{ time / 1000000000, time % 1000000000 }
};
polling_started = 0;
signal_arrived = 0;
if (timer_settime(timer, 0, &spec, 0))
fail("timer_settime");
if (!signal_arrived) {
polling_started = 1;
while (!signal_arrived)
;
}
return polling_started;
}
int main()
{
int counts[2] = { 0, 0 };
uint64_t time = 1;
uint64_t step = 1;
int direction = 0;
int count_max = 4;
int count = count_max;
int i;
setup();
for (i = 0; i < 10000; i++) {
printf("%8d %llu\n", i, (unsigned long long)time);
int r = try(time);
++counts[r];
/* Count number of successive steps in same direction. */
if (r == direction)
count = count >= count_max ? count_max : count + 1;
else
count = 0;
direction = r;
/* Halve or double step. */
if (count < count_max - 1)
step = step >> 1 ? step >> 1 : 1;
else if (count >= count_max)
step = step << 1 ? step << 1 : step;
/* Adjust time, avoiding zero and overflow. */
if (direction) {
if (step < time)
time -= step;
else {
time = 1;
step = 1;
}
}
else {
if (time + step > time)
time += step;
else {
time = -1;
step = 1;
}
}
}
printf("Summary: %d %d %d\n", counts[0], counts[1], (int)time);
return 0;
}