• Aucun résultat trouvé

Protection de l'accès â la mémoire

Dans le document Programmation système en C sous (Page 191-195)

Nous voyons ainsi une méthode assez simple et amusante de partager de la mémoire entre deux processus père et fils. Bien entendu, nous observerons une autre technique, plus conventionnelle, dans le chapitre consacré aux communications entre processus.

Dans notre dernier exemple, nous ne nous sommes pas soucié du contenu effectif du fichier projeté. La seule chose qui nous intéressait était que la zone mémoire de la projection soit partagée avec le processus fils. Parfois au contraire, les modifications apportées au fichier doivent être visibles de l'extérieur, que ce soit pour la lecture directe du fichier ou pour d'autres programmes qui en effectuent une projection dans leur propre mémoire.

Ce n'est, par défaut, que lorsqu'une projection SHARED est supprimée avec munmap( ) ou à la fin du processus, que le noyau nous garantit que les modifications apportées à la zone de projection seront répercutées sur le fichier. Si nous désirons nous en assurer à un autre moment, l'appel-système msync( ) fournit plusieurs possibilités. Son prototype est le suivant :

int msync (const void * debut, size_t longueur, int attribut);

Lorsqu'on invoque msync( ), on lui transmet le pointeur sur la zone de projection qu'on désire mettre à jour, ainsi que la longueur de la zone. Naturellement, il est conseillé de ne demander que la mise à jour des portions effectivement modifiées de la projection, et pas nécessairement tout le fichier. L'attribut fourni en troisième argument peut contenir les constantes symboliques suivantes, liées par un OU binaire :

MS_ASYNC : cette option demande au noyau de se préparer à mettre à jour les zones indiquées. Néanmoins, l'écriture n'a pas lieu tout de suite, l'appel-système revenant immédiatement.

MS_SYNC : avec cet attribut, le noyau effectue tout de suite la mise à jour. Lorsque l'appel-système revient, nous savons qu'une lecture directe du fichier concerné nous renverrait les informations mises à jour. Toutefois, rien ne nous assure que les données aient été réellement écrites sur le disque, celui-ci peut gérer un cache plus ou moins important et retarder les écritures effectives.

MS_INVALIDATE : qu'on utilise une mise à jour synchrone ou asynchrone, le noyau nous assure uniquement qu'une lecture directe du fichier renverra nos données mises à jour.

Malgré tout, d'autres processus, totalement indépendants du nôtre, peuvent avoir effectué une projection du même fichier dans leur espace mémoire. Cet attribut garantit que leurs pages seront invalidées et que le noyau les reprendra sur le disque lors du prochain accès aux données.

On ne doit évidemment pas utiliser les options MS_ASYNC et MS_SYNC simultanément.

Il existe également sous Linux un appel-système supplémentaire. Il n'est disponible qu'en utilisant l'option _GNU_SOURCE lors de la compilation. Il s'agit de mremap( ) , qui permet à

On transmet en argument un pointeur sur la zone de projection en cours, ainsi que sa longueur, suivi de la nouvelle longueur désirée. L'attribut peut éventuellement prendre comme unique valeur celle de la constante symbolique MREMAP_MAYMOVE, auquel cas mremap( ) sera autorisé à déplacer la zone de projection dans l'espace d'adressage.

L'appel-système renvoie un pointeur sur la nouvelle zone, ou MAP_FAILED en cas d'échec.

Bien entendu, les relations avec le fichier sous-jacent sont conservées. Si on agrandit une zone de projection, il est possible d'accéder à une plus grande partie du fichier.

Avant de conclure ce chapitre sur la gestion de la mémoire, nous allons nous intéresser au principe de la protection des pages mémoire, sujet que nous avons effleuré sans entrer dans les détails en présentant le troisième argument d'invocation de mmap( ).

Protection de l'accès â la mémoire

L'appel-système mprotect( ) permet de limiter les possibilités d'accès à certaines pages mémoire. Son prototype est le suivant :

int mprotect (const void * debut_zone, size_t longueur, int protection);

La norme Posix. lb précise que la zone de mémoire à protéger doit avoir été obtenue avec l'appel-système mmap( ). Sous Linux, la contrainte est légèrement relâchée puisqu'il suffit que l'adresse fournie soit alignée sur une frontière de page. Nous détaillerons tout ceci plus bas.

La protection qu'on réclame en troisième argument est du même genre que celle de l'appel mmap( ), avec une composition par OU binaire des constantes suivantes :

Constante Signification

PROT_NONE Aucune autorisation d'accès PROT_READ Autorisation de lire dans la zone

PROT_WRITE Autorisation d'écrire dans la zone mémoire PROT_EXEC Possibilité d'y exécuter du code

Lorsque l'appel mprotect( ) réussit, il renvoie 0 et remplace complètement les protections originales de la zone mémoire, sinon il renvoie -1.

L'autorisation PROT_EXEC ne concerne normalement pas le programmeur applicatif. De toute manière, sur les architectures x86 par exemple, PROT_EXEC et PROT_READ ont exactement le même effet. Peut-être y aura-t-il malgré tout une évolution future. Aussi on utilise correcte-ment ces attributs pour assurer la pérennité et la portabilité d'un programme. Il est dommage que PROT_EXEC ne soit pas réellement utilisé par la gestion mémoire des processeurs x86, car cela permettrait de déjouer une partie des attaques de sécurité en empêchant formellement d'exécuter des instructions en dehors du segment de code initialisé au chargement du programme. Nous avons déjà vu que de nombreux piratages se basaient sur le débordement de chaînes de caractères locales pour placer des instructions personnelles dans la pile des utilitaires système Set-UID.

Le fait d'interdire tout accès avec l'autorisation vierge PROT_NONE peut servir au débogage d'un programme pour contrôler tous les accès d'une application à une zone de mémoire allouée dynamiquement. Par exemple, on peut placer l'autorisation PROT_NONE dès l'allocation, et ne

permettre la lecture qu'à partir du moment où l'initialisation a lieu. Si un autre module tente d'utiliser la variable avant la fin de l'initialisation, le processus sera tué par un signal, et nous pourrons employer gdb et le fichier core pour remonter jusqu'à l'utilisation fautive.

Sur les processeurs x86, le fait de demander une autorisation d'écriture PROT_WRITE entraîne également la disponibilité en lecture (et donc en exécution), mais cela n'est pas nécessairement portable sur les autres architectures.

Nous avons précisé que l'adresse de début de zone doit être alignée sur une frontière de page. C'est automatiquement le cas avec les zones mémoire allouées par mmap( ); aussi est-ce la manière la plus simple d'allouer les zones qu'on protégera ultérieurement. La fonction malloc( ) utilise en interne l'appel-système mmap( ) dans certaines conditions, mais on ne peut pas en être certain. S'il y a suffisamment de place libre dans le segment de données qui n'a pas été rendu au système, elle est employée avant toute chose.

Pour assurer la portabilité de notre application, on se conformera donc au standard Posix.1b en allouant les zones mémoire avec mmap( ) en projection ANONYMOUS. Il faudra simplement se méfier de la différence entre malloc( ), qui renvoie NULL en cas d'échec, et mmap( ), qui renvoie MAP_FAILED (qui vaut normalement -1). Pour éviter toute ambiguïté, on pourra se redéfinir une fonction d'allocation semblable à malloc( ). La seule contrainte est de conserver la taille de la zone allouée car on doit la transmettre à mprotect( ).

Lorsqu'un processus tente d'accéder de manière illégale à une zone de mémoire protégée, il est tué par le signal SIGSEGV. En voici une illustration :

exemple_mprotect_l.c #include <stdio.h>

#include <stdlib.h>

#include <sys/mman.h>

#define TAILLE_CHAINE 128 void *

mon_malloc_avec_mmap (size_t taille) {

void * retour;

retour = mmap (NULL, taille, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);

if (retour = MAP_FAILED) return (NULL);

return (retour);

int main (void) {

char * chaine = NULL;

fprintf (stdout, "Allocation de %d octets \n", TAILLE_CHAINE);

chaine = mon_malloc_avec_mmap (TAILLE_CHAINE);

if (chaine = NULL) { perror ("mmap");

exit (1);

}

fprintf (stdout, "Protections par défaut \n");

fprintf (stdout, " Écriture ...");

strcpy (chaine, "0k");

fprintf (stdout, "0k\n");

fprintf (stdout, " Lecture ...");

fprintf (stdout, "%s\n", chaine);

fprintf (stdout, "Interdiction d'écriture \n");

if (mprotect (chaine, TAILLE_CHAINE, PROT_READ) < 0) { perror ("mprotect");

exit (1);

}

fprintf (stdout, " Lecture ...");

fprintf (stdout, "%s\n", chaine);

fprintf (stdout, " Écriture ...");

strcpy (chaine, "Non");

/* ici on doit déjà être arrêté par un signal */

return (0);

}

Nous exécutons le programme, puis nous invoquons gdb pour rechercher d'où vient l'erreur :

$ ./exemple_mprotect_1 Allocation de 128 octets Protections par défaut Écriture ...0k Lecture ...Ok Interdiction d'écriture Lecture ...0k

Segmentation fault (core dumped)

$ gdb exemple_mprotect_1 core

GNU gdb 4.17.0.11 with Linux support [...]

Core was generated by './exemple_mprotect_1'.

Program terminated with signal 11, Erreur de segmentation.

Reading symbols from /lib/libc.so.6...done.

Reading symbols from /lib/ld-linux.so.2...done.

#0 strcpy (dest=0x40015000 <Address 0x40015000 out of bounds>, src=0x8048782 "Non") at ../sysdeps/generic/strcpy.c:38

../sysdeps/generic/strcpy.c:38: Aucun fichier ou répertoire de ce type.

(gdb) bt

#0 strcpy (dest=0x40015000 <Address 0x40015000 out of bounds>, src=0x8048782 "Non") at ../sysdeps/generic/strcpy.c:38

#1 0x8048692 in main ( ) at exemple_mprotect_1.c:47

#2 0x40030cb3 in libc_start_main (main=0x804853c <main>, argc=1, argv=0xbffffdl4, init=0x8048378 <_init>, fini=0x80486dc <_fini>, rtld_fini=0x4000a350 <_dl_fini>, stack_end=0xbffffd0c)

at ../sysdeps/generic/libc-start.c:78 (gdb) quit

$ rm core

$

Le processus est bien tué par un signal SIGSEGV, avec création d'une image disque core.

L'invocation de gdb nous apprend que l'erreur s'est produite dans la routine strcpy( ), dont le fichier source était «../sysdeps/generic/strcpy.c» lors de la compilation de la bibliothèque C. Cela ne nous est pas d'un grand secours, aussi utilisons-nous la commande bt (pour backtrace, suivi en arrière) qui nous permet de voir que strcpy( ) a été invoquée en ligne 47 du fichier exemple_mprotect_1.c.

Nous observons que la ligne «Écriture... » n'a pas été transmise sur stdout, car le buffer n'a pas été vidé par fflush( ) ni par un retour à la ligne avant l'arrêt brutal du programme.

Le comportement du processeur face à un accès illégal à la mémoire peut paraître déroutant de prime abord. En effet, lorsqu'une faute d'accès est détectée, le noyau est prévenu. Il envoie alors un signal au processus. Par contre, le compteur d'instruction du programme n'est pas incrémenté. Cela signifie que si le processus n'est pas tué par le signal, au retour du gestionnaire de SIGSEGV, il retentera la même opération d'accès interdite. En voici une démonstration.

exemple_mprotect_2.c : #include <signal.h>

#include <stdio.h>

#include <stdlib.h>

#include <sys/mman.h>

#define TAILLE_CHAINE 128 void

gestionnaire_sigsegv (int numero) {

fprintf (stderr, "Signal SIGSEGV reçu \n");

} int main (void) {

char * chaine = NULL;

if (signal (SIGSEGV, gestionnaire_sigsegv) < 0) { perror ("signal");

exit (1);

}

fprintf (stdout, "Allocation de %d octets \n", TAILLE_CHAINE);

chaine = mon_mallocavec _mmap (TAILLE_CHAINE);

if (chaine = NULL) { perror ("mmap");

exit (1);

}

fprintf (stdout, "Protections par défaut \n");

fprintf (stdout, " Écriture ...");

strcpy (chaine, "Ok");

fprintf (stdout, "Ok\n");

fprintf (stdout, "Interdiction de lecture \n");

if (mprotect (chaine, TAILLE_CHAINE, PROT_NONE) < 0) {

perror ("mprotect");

exit (1);

}

fprintf (stdout, " Lecture ...\n"):

fflush (stdout);

fprintf (stdout, "%s\n", chaine);

/* ici on doit déjà être arrêté par un signal */

return (0);

}

Cette fois-ci nous observons l'effet d'une protection NONE sur une tentative de lecture. Par contre, nous interceptons le signal SIGSEGV et nous affichons simplement un message sur le flux d'erreur standard.

$ ./exemple_mprotect_2 Allocation de 128 octets Protections par défaut Écriture ...Ok

Interdiction de lecture Lecture ...

Signal SIGSEGV reçu Signal SIGSEGV reçu Signal SIGSEGV reçu Signal SIGSEGV reçu Signal SIGSEGV reçu Signal SIGSEGV reçu Signal SIGSEGV reçu Signal SIGSEGV reçu Signal SIGSEGV reçu Signal SIGSEGV reçu Signal SIGSEGV reçu Signal SIGSEGV reçu Signal SIGSEGV reçu Signal SIGSEGV reçu Signal SIGSEGV reçu Signal SIGSEGV reçu Signal SIGSEGV reçu Signal SIGSEGV reçu Signal SIGSEGV reçu (Contrôle-C)

$

Nous sommes obligé d'arrêter avec Contrôle-C le processus qui part autrement dans un cycle infini d'interception de signal et de tentative de lecture sur une zone verrouillée. Un moyen de se libérer de ce problème serait d'utiliser un saut non local depuis le gestionnaire pour revenir à un emplacement plus sûr du programme. Malgré tout, le fait de détecter une erreur d'accès mémoire doit plutôt être considéré comme un dysfonctionnement à corriger immédiatement à l'aide du fichier d'image core et d'un débogueur. Le comportement par défaut de SIGSEGV est donc plus approprié.

Conclusion

Nous avons étudié longuement la gestion de la mémoire sous Linux. Ceci nous permet – espérons-le – d'avoir une vision claire des mécanismes mis en oeuvre lors des allocations, projections ou accès aux zones de la mémoire tant physique que virtuelle.

Le programmeur applicatif a rarement besoin de se soucier des techniques sous-jacentes aux allocations dynamiques de mémoire. Toutefois, une bonne compréhension de ces phénomènes permet de diagnostiquer plus facilement les problèmes lorsqu'un programme se comporte de manière a priori surprenante.

Pour étudier en détail l'espace mémoire d'un processus, les meilleures informations proviennent de l'étude directe des sources du noyau Linux. Toutefois, on trouvera des éléments intéressants dans [BACH 1989] Conception du système Unix.

Le prochain chapitre nous permettra d'étudier ce que nous pouvons faire avec les blocs de mémoire ainsi obtenus, y compris les traitements concernant les chaînes de caractères.

15

Utilisation des blocs

Dans le document Programmation système en C sous (Page 191-195)