kAworu's avatar

calloc(3) iz good 4 u

publié par kAworu
il y a presque deux ans

Pour ceux qui ont oublié le C (pas bien!) voilà un petit rappel:

#include <stdlib.h>
void * malloc(size_t size);
void * calloc(size_t number, size_t size);

malloc

Alloue simplement un buffer de taille size et retourne l'adresse. Le contenu du buffer est indéterminé, il est donc d'usage de l'initialiser juste après l'allocation.

calloc

Alloue un buffer de taille (number * size) et retourne l'adresse. Le contenu du buffer est initialisé avec des 0 par calloc. Le résultat est généralement équivalent à:

void *ptr = malloc(number * size);
if (ptr != NULL)
    (void)memset(ptr, 0, number * size);

Mais alors, pourquoi calloc(3) prend deux arguments?

I can haz overflow

Si on a besoin d'un tableau d'éléments de taille dynamique, c'est que généralement l'utilisateur peut influencer plus ou moins directement sur la taille du tableau en question. S'il influence le programme afin d'allouer un très grand nombre d'éléments, il peut faire un overflow de la représentation de size_t. Un petit exemple pour la route:

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

struct dummy {
    int garbage[42];
};

int
main(int argc, char *argv[])
{
    size_t  max_count, too_much;
    struct dummy    *array;

    /* max count of an array of struct dummy to not overflow size_t */
    max_count = SIZE_MAX / sizeof(struct dummy);
    too_much  = max_count + 1;

    (void)printf("too_much = %zu\n", too_much);
    (void)printf("too_much * sizeof(struct dummy) = %zu\n",
        too_much * sizeof(struct dummy));

    array = calloc(too_much, sizeof(struct dummy));
    (void)printf("calloc %s.\n", (array == NULL ? "fail" : "ok"));

    array = malloc(too_much * sizeof(struct dummy));
    (void)printf("malloc %s.\n", (array == NULL ? "fail" : "ok"));

    return (0);
}

Sous FreeBSD ou linusk i386 ça donne:

too_much = 25565282
too_much * sizeof(struct dummy) = 80
calloc fail.
malloc ok.

Ce qui se passe ici, c'est que calloc(3) a détecté l'overflow et a retourné NULL, alors qu'avec malloc(3) la multiplication est faite dans notre code, et aucune précaution n'a été prise pour éviter l'overflow.

C'est problématique car dans le cas de malloc(3) on croit vraiment avoir too_much éléments de type struct dummy. On va faire quelque chose comme:

for (i = 0; i < too_much; i++)
    /* write something in array[i] */

Et là c'est la cata assurée, car en réalité on a alloué seulement 80, ce qui fait même pas 1 * sizeof(struct dummy) ! On va faire des écritures invalides, ce qui peux mener à une faille de sécurité, perte de cheveux, fuite de l'être aimé, et mauvaise haleine constante.

gimme good stuff

Une implémentation décente de calloc(3) se doit de vérifier que la multiplication ne fait pas un overflow. Du coté de chez la serviette orange ils ont un calloc(3) bien lisible:

 1 void *
 2 calloc(size_t num, size_t size)
 3 {
 4     void *ret;
 5 
 6     if (size != 0 && (num * size) / size != num) {
 7         /* size_t overflow. */
 8         errno = ENOMEM;
 9         return (NULL);
10     }
11 
12     ret = pubrealloc(NULL, num * size, " in calloc():");
13 
14     if (ret != NULL)
15         memset(ret, 0, num * size);
16 
17     return ret;
18 }

On remarque que les lignes 6 à 10 servent à vérifier un potentiel overflow, et si c'est le cas NULL est retourné. J'ai choisi celle de NetBSD car c'est de loin la plus lisible, mais les autres implémentations (*BSD, Glibc) font également le check de l'overflow.

What's next?

Maintenant qu'on a compris pourquoi calloc(3) prend deux arguments et fait la multiplication tout seul comme un grand, on peut voir les bons et les mauvais élèves, car la majorité des projets ont leur fonctions qui répondent souvent au doux nom de xmalloc, xcalloc etc. qui ne retournent jamais NULL.

bien!

ouvert-sécurité-console c'est le bon élève. En même temps c'est codé par les magiciens du poisson qui pique qui pondent du code secure™. Il fait lui-même le check de l'overflow au cas où il est dans un environnent hostile avec un calloc(3) en carton, et évite d'essayer d'allouer 0. Comme il a besoin d'un calloc très robuste, c'est adapté, autrement on peut compter sur la libc pour le check de l'overflow, parce que là ils ont pratiquement recodé calloc(3).

void *
xcalloc(size_t nmemb, size_t size)
{
        void *ptr;

        if (size == 0 || nmemb == 0)
                fatal("xcalloc: zero size");
        if (SIZE_T_MAX / nmemb < size)
                fatal("xcalloc: nmemb * size > SIZE_T_MAX");
        ptr = calloc(nmemb, size);
        if (ptr == NULL)
                fatal("xcalloc: out of memory (allocating %lu bytes)",
                    (u_long)(size * nmemb));
        return ptr;
}

Mais ce qui est encore plus brillant c'est d'appliquer ce principe aussi pour realloc(3). En effet quand on veut realloc on est obligé d'utiliser:

void * realloc(void *ptr, size_t size);

Et là plus de check d'overflow de notre gentille libc. Heureusement, xmalloc.c made in OpenSSH en a fait un bien convi pour nous. Voilà la version de tmux qui figure aussi aux rangs des bons élèves (dont le xmalloc.c semble très inspiré de celui d'OpenSSH, ce qui n'est pas étonnant quand on sait que tmux à été intégré dans la codebase OpenBSD):

void *
xrealloc(void *oldptr, size_t nmemb, size_t size)
{
        size_t   newsize = nmemb * size;
        void    *newptr;

        if (newsize == 0)
                fatalx("zero size");
        if (SIZE_MAX / nmemb < size)
                fatalx("nmemb * size > SIZE_MAX");
        if ((newptr = realloc(oldptr, newsize)) == NULL)
                fatal("xrealloc failed");

        return (newptr);
}

pas bien!

Dans le genre pas malin, i3 a un scalloc en carton. Dans util.c on trouve:

void *scalloc(size_t size) {
        void *result = calloc(size, 1);
        exit_if_null(result, "Error: out of memory (calloc(%zd))\n", size);
        return result;
}

et après dans table.c:

/* ... */
workspace->table = realloc(workspace->table, sizeof(Container**) * workspace->cols);
workspace->table[workspace->cols-1] = scalloc(sizeof(Container*) * workspace->rows);
/* ... */

Dans ce cas il se tire une balle dans le pied, calloc(3) n'a aucun moyen de vérifier la multiplication.

conclusion

  • Pour allouer plusieurs éléments, toujours utiliser calloc(3). Piquer le xrealloc de tmux ou OpenSSH est une bonne idée, vu que la libc ne nous donne pas de bonne solution.

  • Pour allouer des C-string, comme (sizeof(char) == 1) on peut parfaitement utiliser malloc(3).

  • on en parle par et ici aussi

un commentaire

écrire un commentaire

  1. philpep il y a presque deux ans philpep's avatar

    Très instructif, à se demander pourquoi malloc n'est pas codée avec le

    if ((num * size) / size != num)
    

    J'imagine qu'ils ne pensaient pas encore au size_t overflow quand ils ont défini le prototype, et c'est le genre de fonctions qu'on touche pas.

    M'en vais checker tout ça dans mon code. Merci.

écrire un commentaire:


(utilisé pour gravatar, ne sera pas affiché)



tu peux utiliser la syntaxe markdown :)