Come scrivere un gestore di segnali per catturare SIGSEGV?

Voglio scrivere un gestore di segnale per catturare SIGSEGV. Proteggo un blocco di memoria per leggere o scrivere usando

char *buffer; char *p; char a; int pagesize = 4096; mprotect(buffer,pagesize,PROT_NONE) 

Questo protegge i byte di memoria della pagina a partire dal buffer rispetto a qualsiasi lettura o scrittura.

Secondo, cerco di leggere il ricordo:

 p = buffer; a = *p 

Questo genererà un SIGSEGV e il mio gestore verrà chiamato. Fin qui tutto bene. Il mio problema è che, una volta chiamato il gestore, voglio modificare la scrittura di accesso della memoria facendo

 mprotect(buffer,pagesize,PROT_READ); 

e continuare il normale funzionamento del mio codice. Non voglio uscire dalla funzione. In futuro scriverò nella stessa memoria, voglio prendere di nuovo il segnale e modificare i diritti di scrittura e quindi registrare quell’evento.

Ecco il codice :

 #include  #include  #include  #include  #include  #include  #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); } 

Il problema è che solo il gestore di segnale gira e non posso tornare alla funzione principale dopo aver catturato il segnale.

Quando il tuo gestore di segnale ritorna (assumendo che non chiami exit o longjmp o qualcosa che gli impedisca di ritornare), il codice continuerà nel punto in cui si è verificato il segnale, rieseguendo la stessa istruzione. Dato che a questo punto, la protezione della memoria non è stata cambiata, si limiterà a lanciare nuovamente il segnale e si tornerà indietro nel gestore del segnale in un loop infinito.

Quindi, per farlo funzionare, devi chiamare mprotect nel gestore del segnale. Sfortunatamente, come osserva Steven Schansker, mprotect non è async-safe, quindi non puoi chiamarlo tranquillamente dal gestore del segnale. Quindi, per quanto riguarda POSIX, sei fregato.

Fortunatamente sulla maggior parte delle implementazioni (tutte le varianti moderne UNIX e Linux per quanto ne so), mprotect è una chiamata di sistema, quindi è sicuro chiamare da un gestore di segnale , in modo da poter fare la maggior parte di ciò che si desidera. Il problema è che se vuoi cambiare le protezioni dopo la lettura, dovrai farlo nel programma principale dopo la lettura.

Un’altra possibilità è di fare qualcosa con il terzo argomento al gestore del segnale, che punta a una struttura specifica dell’OS e dell’arco che contiene informazioni su dove si è verificato il segnale. Su Linux, questa è una struttura ucontext , che contiene informazioni specifiche del computer sull’indirizzo $ PC e altri contenuti del registro in cui si è verificato il segnale. Se si modifica questo, si modifica il punto in cui il gestore del segnale ritornerà, quindi è ansible modificare il $ PC in modo che sia subito dopo l’istruzione di errore, in modo che non venga rieseguito dopo il ritorno del gestore. Questo è molto difficile da ottenere (e anche non portatile).

modificare

La struttura di ucontext è definita in . All’interno di ucontext il campo uc_mcontext contiene il contesto macchina, e all’interno di esso , l’array gregs contiene il contesto generale del registro. Quindi nel gestore del segnale:

 ucontext *u = (ucontext *)unused; unsigned char *pc = (unsigned char *)u->uc_mcontext.gregs[REG_RIP]; 

ti darà il pc dove si è verificata l’eccezione. Puoi leggerlo per capire quali istruzioni è stata criticata e fare qualcosa di diverso.

Per quanto riguarda la portabilità di chiamare mprotect nel gestore del segnale, qualsiasi sistema che segua le specifiche SVID o BSD4 dovrebbe essere sicuro – consente di chiamare qualsiasi chiamata di sistema (qualsiasi cosa nella sezione 2 del manuale) in un segnale handler.

Sei caduto nella trappola che tutte le persone fanno quando cercano per la prima volta di gestire i segnali. La trappola? Pensando di poter effettivamente fare qualcosa di utile con i gestori di segnale. Da un gestore di segnali, è ansible chiamare solo chiamate di libreria asincrone e rientranti.

Vedi questo CERT advisory sul perché e un elenco delle funzioni POSIX che sono sicure.

Nota che printf (), che stai già chiamando, non è in quella lista.

Né è mprotect. Non sei autorizzato a chiamarlo da un gestore di segnale. Potrebbe funzionare, ma posso promettere che ti imbatterai in problemi lungo la strada. Stai molto attento con i gestori del segnale, sono difficili da ottenere!

MODIFICARE

Dato che al momento sono già un douchebag per la portabilità, farò notare che non dovresti scrivere anche su variabili condivise (cioè globali) senza prendere le dovute precauzioni.

Puoi recuperare da SIGSEGV su linux. Inoltre puoi ripristinare da errori di segmentazione su Windows (vedrai un’eccezione strutturata invece di un segnale). Ma lo standard POSIX non garantisce il recupero , quindi il tuo codice sarà molto non portatile.

Dai un’occhiata a libsigsegv .

Non si dovrebbe tornare dal gestore del segnale, poiché il comportamento non è definito. Piuttosto, saltane fuori con longjmp.

Questo va bene solo se il segnale viene generato in una funzione di sicurezza del segnale asincrono. In caso contrario, il comportamento non è definito se il programma chiama mai un’altra funzione non sicura con segnale asincrono. Pertanto, il gestore del segnale deve essere stabilito immediatamente prima che sia necessario e disinnestato il prima ansible.

In effetti, conosco pochissimi usi di un gestore SIGSEGV:

  • utilizzare una libreria di backtrace sicuro da segnali asincroni per registrare un backtrace, quindi morire.
  • in una macchina virtuale come JVM o CLR: controlla se SIGSEGV si è verificato nel codice compilato con JIT. Se no, muori; in tal caso, quindi lanciare un’eccezione specifica della lingua ( non un’eccezione C ++), che funziona perché il compilatore JIT sapeva che la trappola poteva accadere e generava appropriati dati di unwind frame.
  • clone () ed exec () un debugger ( non usare fork () – che chiama i callback registrati da pthread_atfork ()).

Infine, si noti che qualsiasi azione che innesca SIGSEGV è probabilmente UB, poiché questo accede alla memoria non valida. Tuttavia, questo non sarebbe il caso se il segnale fosse, per esempio, SIGFPE.

C’è un problema di compilazione usando ucontext_t o ucontext_t ucontext (presente in /usr/include/sys/ucontext.h )

http://www.mail-archive.com/[email protected]/msg13853.html