Je veux écrire un gestionnaire de signal pour attraper SIGSEGV. Je protège un bloc de mémoire en lecture ou en écriture avec
char *buffer;
char *p;
char a;
int pagesize = 4096;
mprotect(buffer,pagesize,PROT_NONE)
Cela protège les octets de la taille d'une page commençant par la mémoire tampon des lectures et des écritures.
Deuxièmement, j'essaie de lire la mémoire:
p = buffer;
a = *p
Cela générera un SIGSEGV et mon gestionnaire sera appelé. Jusqu'ici tout va bien. Mon problème est que, une fois que le gestionnaire est appelé, je veux changer l'accès en écriture de la mémoire en faisant
mprotect(buffer,pagesize,PROT_READ);
et continuer le fonctionnement normal de mon code. Je ne veux pas quitter la fonction . Lors de futures écritures dans la même mémoire, je veux récupérer le signal, modifier les droits d'écriture, puis enregistrer cet événement.
Voici le code :
#include <signal.h>
#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/mman.h>
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
char *buffer;
int flag=0;
static void handler(int sig, siginfo_t *si, void *unused)
{
printf("Got SIGSEGV at address: 0x%lx\n",(long) si->si_addr);
printf("Implements the handler only\n");
flag=1;
//exit(EXIT_FAILURE);
}
int main(int argc, char *argv[])
{
char *p; char a;
int pagesize;
struct sigaction sa;
sa.sa_flags = SA_SIGINFO;
sigemptyset(&sa.sa_mask);
sa.sa_sigaction = handler;
if (sigaction(SIGSEGV, &sa, NULL) == -1)
handle_error("sigaction");
pagesize=4096;
/* Allocate a buffer aligned on a page boundary;
initial protection is PROT_READ | PROT_WRITE */
buffer = memalign(pagesize, 4 * pagesize);
if (buffer == NULL)
handle_error("memalign");
printf("Start of region: 0x%lx\n", (long) buffer);
printf("Start of region: 0x%lx\n", (long) buffer+pagesize);
printf("Start of region: 0x%lx\n", (long) buffer+2*pagesize);
printf("Start of region: 0x%lx\n", (long) buffer+3*pagesize);
//if (mprotect(buffer + pagesize * 0, pagesize,PROT_NONE) == -1)
if (mprotect(buffer + pagesize * 0, pagesize,PROT_NONE) == -1)
handle_error("mprotect");
//for (p = buffer ; ; )
if(flag==0)
{
p = buffer+pagesize/2;
printf("It comes here before reading memory\n");
a = *p; //trying to read the memory
printf("It comes here after reading memory\n");
}
else
{
if (mprotect(buffer + pagesize * 0, pagesize,PROT_READ) == -1)
handle_error("mprotect");
a = *p;
printf("Now i can read the memory\n");
}
/* for (p = buffer;p<=buffer+4*pagesize ;p++ )
{
//a = *(p);
*(p) = 'a';
printf("Writing at address %p\n",p);
}*/
printf("Loop completed\n"); /* Should never happen */
exit(EXIT_SUCCESS);
}
Le problème est que seul le gestionnaire de signal s'exécute et que je ne peux pas revenir à la fonction principale après avoir capté le signal.
Lorsque votre gestionnaire de signal revient (en supposant qu'il n'appelle pas exit ou longjmp ni quelque chose qui l'empêche de revenir), le code continue au point où le signal s'est produit, en réexécutant la même instruction. Comme à ce stade, la protection de la mémoire n’a pas été modifiée, le signal sera renvoyé et vous serez de retour dans votre gestionnaire de signaux dans une boucle infinie.
Donc, pour que cela fonctionne, vous devez appeler mprotect dans le gestionnaire de signaux. Malheureusement, comme le note Steven Schansker, mprotect n'est pas asynchrone, vous ne pouvez donc pas l'appeler en toute sécurité depuis le gestionnaire de signaux. Donc, en ce qui concerne POSIX, vous êtes foutu.
Heureusement, pour la plupart des implémentations (toutes les variantes UNIX et Linux modernes, autant que je sache), mprotect est un appel système. Il est donc sûr que est sûr d'appeler depuis un gestionnaire de signaux , afin que vous puissiez faire ce que vous voulez. Le problème est que si vous souhaitez modifier les protections après la lecture, vous devrez le faire dans le programme principal après la lecture.
Une autre possibilité consiste à utiliser le troisième argument du gestionnaire de signal, qui pointe vers une structure spécifique à l'OS et à Arch qui contient des informations sur l'emplacement du signal. Sous Linux, il s’agit d’une structure ucontext, qui contient des informations propres à la machine concernant l’adresse $ PC et d’autres contenus de registres dans lesquels le signal s’est produit. Si vous modifiez cela, vous modifiez le lieu de retour du gestionnaire de signaux afin que vous puissiez modifier le $ PC pour qu'il se trouve juste après l'instruction défaillante afin qu'il ne soit pas réexécuté après le retour du gestionnaire. C’est très délicat à faire (et aussi non portable).
modifier
La structure ucontext
est définie dans <ucontext.h>
. Dans ucontext
, le champ uc_mcontext
contient le contexte de la machine et dans that, le tableau gregs
contient le contexte général du registre. Donc, dans votre gestionnaire de signal:
ucontext *u = (ucontext *)unused;
unsigned char *pc = (unsigned char *)u->uc_mcontext.gregs[REG_RIP];
vous donnera le pc où l'exception s'est produite. Vous pouvez le lire pour comprendre l’instruction qu’il a reçue, et faire quelque chose de différent.
En ce qui concerne la portabilité de l'appel de mprotect dans le gestionnaire de signaux, tout système respectant les spécifications SVID ou BSD4 doit être sûr: il permet d'appeler n'importe quel appel système (tout élément de la section 2 du manuel) dans un signal. gestionnaire.
Vous êtes tombé dans le piège que font tous les gens quand ils essaient pour la première fois de gérer les signaux. Le piège? Penser que vous pouvez réellement faire n'importe quoi utile avec les gestionnaires de signaux. À partir d'un gestionnaire de signal, vous n'êtes autorisé à appeler que des appels de bibliothèque asynchrones et réentrants sûrs.
Voir cet avis CERT pour savoir pourquoi et une liste des fonctions POSIX sûres.
Notez que printf (), que vous appelez déjà, ne figure pas dans cette liste.
Mprotect n'est pas non plus. Vous n'êtes pas autorisé à appeler cela depuis un gestionnaire de signal. Cela fonctionne peut-être, mais je peux vous promettre que vous rencontrerez des problèmes plus tard. Soyez très prudent avec les gestionnaires de signaux, ils sont difficiles à obtenir!
MODIFIER
Étant donné que je suis déjà un douchebag de portabilité, je ferai remarquer que vous ne devriez pas écrire non plus dans des variables partagées (c'est-à-dire globales) sans prendre les précautions appropriées.
Vous pouvez récupérer de SIGSEGV sur Linux. Vous pouvez également récupérer les erreurs de segmentation sous Windows (vous verrez une exception structurée au lieu d'un signal). Mais le standard POSIX ne garantit pas la récupération , votre code sera donc très peu portable.
Jetez un coup d'oeil à libsigsegv .
Vous ne devez pas revenir du gestionnaire de signal car le comportement n'est pas défini. Sautez plutôt avec LongJmp.
Cela n’est acceptable que si le signal est généré dans une fonction async-signal-safe. Sinon, le comportement n'est pas défini si le programme appelle une autre fonction async-signal-unsafe. Par conséquent, le gestionnaire de signaux ne devrait être établi que juste avant que cela soit nécessaire, et désactivé dès que possible.
En fait, je connais très peu d'utilisations d'un gestionnaire SIGSEGV:
Enfin, notez que toute action qui déclenche SIGSEGV est probablement UB, car elle accède à une mémoire invalide. Cependant, cela ne serait pas le cas si le signal était, par exemple, SIGFPE.
Il y a un problème de compilation avec ucontext_t
ou struct ucontext
(présent dans /usr/include/sys/ucontext.h
)