Twitter iPhone pliant OnePlus 11 PS5 Disney+ Orange Livebox Windows 11

optimisation vs securite

18 réponses
Avatar
Thierry B.
Bonjour les codeurz...

En me promenant dans le grand ouèbe mondial, je suis tombé sur
cet article: http://lwn.net/Articles/278137/ qui évoque des
problèmes de potentiels buffer-overflow quand Gcc (et d'autres)
optimisent un peu trop sur certaines condition/tests.

Certains commentaires sont assez bons :)

tTh.


--
Une cinquième astuce pour pas passer pour une imbécile sur usenet : Ne
jamais hésiter à donner dans l'auto-dérision.
--{ SC, in fr.misc.divers }--

10 réponses

1 2
Avatar
Xavier Roche
problèmes de potentiels buffer-overflow quand Gcc (et d'autres)
optimisent un peu trop sur certaines condition/tests.


De toute manière, c'est le test <pointeur>+<taille venant de
l'extérieur> qui est potentiellement dangereux.

C'est le cas classique dans beaucoup (trop) de "parseurs" d'objets
structurés (fichiers GIF, PNG, documents divers, etc.) qui ont souvent
une structure du genre:

void parseFoo(const unsigned char *buffer, size_t buffSize) {
/* current pointer */
const unsigned char *ptr;
/* parse all "foo" blocks */
for(ptr = buffer ; ptr + 4 <= buff + buffSize ; ) {
/* next block size */
unsigned int size = ( ptr[0] << 24 )
+ ( ptr[1] << 16 )
+ ( ptr[2] << 8 )
+ ptr[3];
ptr += 4; /* skip size ob next chunk */
/* block not too large/corrupted/truncated ? */
if (ptr + size <= buffer + buffSize) {
..
} else {
break ;
}
ptr += size;
}
}

Une forme moins dangereuse qui évite que l'information extérieure "size"
ne puisse provoquer un débordement (par exemple avec size == 0xfffffffc
qui provoque une boucle rigolote) et de séparer dans la comparaison à
gauche ce qui provient de l'extérieur, et à droite, le reste:

- if (ptr + size <= buffer + buffSize) {
+ if (size <= buffer + buffSize - ptr) {

Avatar
Vincent Lefevre
Dans l'article ,
Thierry B. écrit:

En me promenant dans le grand ouèbe mondial, je suis tombé sur
cet article: http://lwn.net/Articles/278137/ qui évoque des
problèmes de potentiels buffer-overflow quand Gcc (et d'autres)
optimisent un peu trop sur certaines condition/tests.


Je ne pense pas que "This behavior is allowed by the C standard,
which states that, in a correct program, pointer addition will not
yield a pointer value outside of the same object." soit correct,
surtout que cette phrase ne précise pas de quel objet il s'agit.
Par exemple, je pense que le programme suivant est correct bien
que l'on sorte de l'objet v.b, puisqu'on est aussi dans l'objet v,
qui peut être assimilé à un tableau de caractères:

#include <stdio.h>

int main (void)
{
struct { char a[8], b[8]; } v;
char *p;

p = &v.b[0];
printf ("%pn", (void *) p);
p--;
printf ("%pn", (void *) p);
return 0;
}

En revanche, l'optimisation vient du fait que si on ajoute un nombre
positif ou nul à un pointeur, on ne peut pas se retrouver avec un
pointeur plus petit.

--
Vincent Lefèvre - Web: <http://www.vinc17.org/>
100% accessible validated (X)HTML - Blog: <http://www.vinc17.org/blog/>
Work: CR INRIA - computer arithmetic / Arenaire project (LIP, ENS-Lyon)

Avatar
espie
In article ,
Thierry B. wrote:
Bonjour les codeurz...

En me promenant dans le grand ouèbe mondial, je suis tombé sur
cet article: http://lwn.net/Articles/278137/ qui évoque des
problèmes de potentiels buffer-overflow quand Gcc (et d'autres)
optimisent un peu trop sur certaines condition/tests.


Ca fait un moment que le zele des constructeurs de compilateurs a faire
des trucs qui vont le plus vite possible aux benchmarks posent quelques
problemes aux programmes reels.

Le chiendent, c'est que certains tests et certaines instructions, rajoutees
pour securiser du code, ne font pas forcement partie des `defined behavior'.

C, initialement, c'est cense etre un assembleur portable. Ca veut entre
autres dire qu'il y a des bouts qui sont censes etre dependants de
l'architecture. Lorsque GCC cesse de fournir des constructions supplementaires
propres pour traiter ces cas en plus, ou ne possede pas une representation
fiable de l'archi en question, ca commence a merder.

L'ennui, c'est qu'il n'y a aucun autre compilo `libre' digne de ce nom
susceptible de le remplacer pour l'instant.

L'article en question correspond a une vision des pointeurs qui ne correspond
pas forcement a la realite de la machine, et aux cas ou ca merde.

Je pourrais citer le strict-aliasing, qui a pose bien des problemes (et les
enormes difficultes de comprehension de restrict qui vont avec).

Ou alors, la capacite des gcc recents a faire disparaitre des memset a 0,
s'ils touchent des variables non utilisees ensuite (pratique commune pour
virer des mot de passe en clair avant de poursuivre le programme), sachant
qu'il n'y a PAS de construction raisonnable permettant d'avoir la
semantique voulue (volatile n'en est pas une, puisqu'elle va generalement
plomber le temps de calcul lie au mot de passe en question).

Ou alors, les histoires d'asm et de volatile asm dans les gcc recents
qui merdoient un tantinet, en particulier qui sont quand meme susceptibles
de pas mal `flotter' dans le code et donc de pourrir pas mal de trucs.

Avatar
Thierry B.
--{ Vincent Lefevre a plopé ceci: }--

Par exemple, je pense que le programme suivant est correct bien
que l'on sorte de l'objet v.b, puisqu'on est aussi dans l'objet v,
qui peut être assimilé à un tableau de caractères:


C'est un peu capillotracté, ton raisonnement...

#include <stdio.h>

int main (void)
{
struct { char a[8], b[8]; } v;
char *p;

p = &v.b[0];
printf ("%pn", (void *) p);
p--;


Je n'aime pas trop cette décrémentation. Un bon compilo/lint
arriverait-il à la détecter ?

printf ("%pn", (void *) p);
return 0;
}




--
+++ Dénoncez les quotes de http://la.buvette.org/goret.txt +++

Avatar
Vincent Lefevre
Dans l'article ,
Thierry B. écrit:

--{ Vincent Lefevre a plopé ceci: }--

Par exemple, je pense que le programme suivant est correct bien
que l'on sorte de l'objet v.b, puisqu'on est aussi dans l'objet v,
qui peut être assimilé à un tableau de caractères:


C'est un peu capillotracté, ton raisonnement...


Pourtant les fonctions du style memcpy font ce genre de chose.

--
Vincent Lefèvre - Web: <http://www.vinc17.org/>
100% accessible validated (X)HTML - Blog: <http://www.vinc17.org/blog/>
Work: CR INRIA - computer arithmetic / Arenaire project (LIP, ENS-Lyon)


Avatar
Vincent Lefevre
Dans l'article <fvuhhm$aj2$,
Marc Espie écrit:

Ca fait un moment que le zele des constructeurs de compilateurs a faire
des trucs qui vont le plus vite possible aux benchmarks posent quelques
problemes aux programmes reels.


Il n'y a pas que les benchmarks, il y a aussi des programmes réels.
Même si certaines optimisations peuvent sembler inutiles pour certains
programmes, elles peuvent être importantes pour d'autres.

Le chiendent, c'est que certains tests et certaines instructions, rajoutees
pour securiser du code, ne font pas forcement partie des `defined behavior'.


Dans la mesure où un programme se base sur un undefined behavior,
il ne peut y avoir aucune sécurité. Un compilateur peut toujours
décrire comment ça se comporte ou avoir des options du style -fwrap,
mais alors ce n'est plus portable.

Note: j'ai déjà vu du code supposer que toutes les variables (y compris
automatiques) étaient initialisées à 0. Ça risque de poser des problèmes
de performance si GCC doit supposer cela.

Ou alors, la capacite des gcc recents a faire disparaitre des memset a 0,
s'ils touchent des variables non utilisees ensuite (pratique commune pour
virer des mot de passe en clair avant de poursuivre le programme), sachant
qu'il n'y a PAS de construction raisonnable permettant d'avoir la
semantique voulue (volatile n'en est pas une, puisqu'elle va generalement
plomber le temps de calcul lie au mot de passe en question).


Et tu acceptes de plomber de temps de calcul d'autres programmes parce
que certains codes sont mal écrits?

Est-ce qu'avec des casts avec volatile seulement dans certains cas,
cela serait une solution acceptable?

Et puis le volatile ne garantit pas tout: par exemple, une partie du
mot de passe pourrait être conservé dans un registre. Bref, c'est à
la norme C de définir un moyen d'effacer toute info relative à telle
ou telle donnée (ce qui ne doit pas être si simple à spécifier).

--
Vincent Lefèvre - Web: <http://www.vinc17.org/>
100% accessible validated (X)HTML - Blog: <http://www.vinc17.org/blog/>
Work: CR INRIA - computer arithmetic / Arenaire project (LIP, ENS-Lyon)

Avatar
Pierre Maurette

[...]

Est-ce qu'avec des casts avec volatile seulement dans certains cas,
cela serait une solution acceptable?


Dans une implémentation raisonnable de volatile [cui cui] que peut-on
attendre de:

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
int a, b, c, test; /* non volatile data*/
(volatile void)test;
(volatile void)(a = 0);
(volatile void)(b = 5);
(volatile void)(c = 10);
(volatile void)(a = b + c);
printf("%dn", a);
return EXIT_SUCCESS;
}

Je n'ai pas trouvé la syntaxe pour rendre un bloc {...} volatile. Un
#pragma serait utile, peut-être en existe-t-il, mais ce ne serait pas
portable...

En fait le C est portable et justement ne peut être un "assembleur
portable".

--
Pierre Maurette

Avatar
espie
In article ,
Pierre Maurette wrote:

En fait le C est portable et justement ne peut être un "assembleur
portable".


Sauf que, historiquement, le C est d'abord un assembleur portable.
C'est bien de mettre des regles dans la norme, mais lorsque le compilateur
devient trop agressif, le code C historique ne marche plus.

Et vous pourrez toujours dire `oui, mais c'est pas propre', il y a des
coins du systeme ou c'est un sacre casse-tete a corriger...

Avatar
Vincent Lefevre
Dans l'article ,
Pierre Maurette écrit:

int main(void)
{
int a, b, c, test; /* non volatile data*/
(volatile void)test;


(volatile void), ça a un sens? J'aurais plutôt écrit:

(void) (volatile int) test;

(volatile void)(a = 0);


Et là j'aurais écrit:

* (volatile int *) &a = 0;

et ainsi de suite.

Maintenant, concernant le code suivant:

int main(void)
{
int a, b, c;
(volatile void)(a = 17);
* (volatile int *) &b = 289;
c = 4913;
return 0;
}

GCC ne génère aucune affectation. Et si on sort le

int a, b, c;

afin que a, b et c deviennent des variables externes, les 3 affectations
sont générées (ce qui est normal, vu que main() peut être appelé par du
code compilé séparément, et les variables a, b et c sont accessibles).

En fait, je ne vois pas le problème dont parle Marc concernant la mise
à 0 de la mémoire: il suffit de mettre le code dans une fonction avec
le pointeur passé à cette fonction, et de compiler cette fonction
séparément du reste: le compilateur ne peut pas deviner ce que va faire
le reste du programme et ne pourra donc pas optimiser (i.e. il sera
forcé de mettre à 0 la mémoire comme voulu).

--
Vincent Lefèvre - Web: <http://www.vinc17.org/>
100% accessible validated (X)HTML - Blog: <http://www.vinc17.org/blog/>
Work: CR INRIA - computer arithmetic / Arenaire project (LIP, ENS-Lyon)

Avatar
espie
In article <20080508232434$,
Vincent Lefevre <vincent+ wrote:

En fait, je ne vois pas le problème dont parle Marc concernant la mise
à 0 de la mémoire: il suffit de mettre le code dans une fonction avec
le pointeur passé à cette fonction, et de compiler cette fonction
séparément du reste: le compilateur ne peut pas deviner ce que va faire
le reste du programme et ne pourra donc pas optimiser (i.e. il sera
forcé de mettre à 0 la mémoire comme voulu).


Effectivement, tu passes completement a cote du vrai probleme.

Dans le temps, gcc n'optimisait pas le memset en question.

Qui te dit que bientot, il n'optimisera pas ce qui se passe entre
unites de compilation ? certains compilos le font bien...

Et la mise a zero de la memoire n'est toujours pas comportement observable
(et volatile n'est pas une solution). Et je n'ai toujours pas de solution
dont je suis sur qu'elle continuera a marcher avec la version suivante
de gcc.

Si tu preferes: chaque nouvelle version de gcc apporte son nouveau lot
de surprises qui cassent des comportements limites. Dans certains cas,
les comportements limites sont vraiment du code crade, et n'ont pas ma
sympathie. Dans d'autres cas, un peu plus pathologiques, ca n'est pas
le cas. Ca entraine des modifications de semantique surprenante, et
ca casse du code reel, pour lequel il n'y a pas vraiment de solution
portable qui marche... et ca pose probleme pour upgrader le compilo.

Enfin bon, dans le cas de gcc, vu comment les versions recentes se
trainent par rapport aux anciennes, ca pose d'enormes problemes pour
tout ce qui n'est pas i386/amd64...

1 2