Comment utiliser légalement le typage avec des unions pour répartir des variations de struct sockaddr sans enfreindre la règle de crénelage ssortingcte?

POSIX souhaite que les pointeurs sur les variations de struct sockaddr soient castables. Toutefois, en fonction de l’interprétation du standard C, cela peut constituer une violation de la règle de repliement ssortingct et donc de UB. (Voir cette réponse avec les commentaires ci-dessous.) Je peux, au moins, confirmer qu’il peut au moins y avoir un problème avec gcc: ce code affiche Bug! avec optimisation activée, et Yay! avec optimisation désactivée:

 #include  #include  #include  sa_family_t test(struct sockaddr *a, struct sockaddr_in *b) { a->sa_family = AF_UNSPEC; b->sin_family = AF_INET; return a->sa_family; // AF_INET please! } int main(void) { struct sockaddr addr; sa_family_t x = test(&addr, (struct sockaddr_in*)&addr); if(x == AF_INET) printf("Yay!\n"); else if(x == AF_UNSPEC) printf("Bug!\n"); return 0; } 

Observez ce comportement sur un IDE en ligne .

Pour résoudre ce problème, cette réponse propose l’utilisation du type punning avec les syndicats:

 /*! Multi-family socket end-point address. */ typedef union address { struct sockaddr sa; struct sockaddr_in sa_in; struct sockaddr_in6 sa_in6; struct sockaddr_storage sa_stor; } address_t; 

Cependant, apparemment, les choses ne sont toujours pas aussi simples qu’elles le paraissent… Citant ce commentaire de @zwol:

Cela peut fonctionner mais prend pas mal de soin. Plus que je peux entrer dans cette boîte de commentaire.

Quel genre de soin juste faut-il? Quels sont les pièges de l’utilisation du type punning avec les unions pour répartir les variations de struct sockaddr ?

Je préfère demander que de tomber sur UB.

Utiliser un union comme celui-ci est sûr,

à partir de C11 § 6.5.2.3:

  1. Une expression postfixée suivie du. opérateur et un identifiant désigne un membre d’une structure ou d’un object d’union. La valeur est celle du membre nommé, 95) et est une lvalue si la première expression est une lvalue. Si la première expression a un type qualifié, le résultat a la version qualifiée du type du membre désigné.

95) Si le membre utilisé pour lire le contenu d’un object d’union n’est pas identique au dernier membre utilisé pour stocker une valeur dans l’object, la partie appropriée de la représentation d’object de la valeur est réinterprétée en tant que représentation d’object dans le nouveau. taper comme décrit au 6.2.6 (un processus parfois appelé ” type punning ”). Cela pourrait être une représentation de piège.

et

  1. Une garantie spéciale est donnée afin de simplifier l’utilisation des unions: si une union contient plusieurs structures partageant une séquence initiale commune (voir ci-dessous), et si l’object union contient actuellement l’une de ces structures, il est autorisé à inspecter la structure commune. partie initiale de l’un d’entre eux où qu’une déclaration du type complet de l’union soit visible. Deux structures partagent une séquence initiale commune si les membres correspondants ont des types compatibles (et, pour les champs de bits, les mêmes largeurs) pour une séquence d’un ou plusieurs membres initiaux

(a souligné ce que je pense est le plus important)

En accédant au membre struct sockaddr , vous lirez à partir de la partie initiale commune .


Remarque : Cela ne compromettra pas la sécurité des pointeurs vers les membres et attendra du compilateur qu’il sache qu’ils se réfèrent au même object stocké. Donc, la version littérale de votre exemple de code peut quand même se rompre car, dans votre test() l’ union n’est pas connue.

Exemple:

 #include  struct foo { int fooid; char x; }; struct bar { int barid; double y; }; union foobar { struct foo a; struct bar b; }; int test(struct foo *a, struct bar *b) { a->fooid = 23; b->barid = 42; return a->fooid; } int test2(union foobar *a, union foobar *b) { a->a.fooid = 23; b->b.barid = 42; return a->a.fooid; } int main(void) { union foobar fb; int result = test(&fb.a, &fb.b); printf("%d\n", result); result = test2(&fb, &fb); printf("%d\n", result); return 0; } 

Ici, test() peut casser, mais test2() sera correct.

Étant donné l’ address_t vous proposez

 typedef union address { struct sockaddr sa; struct sockaddr_in sa_in; struct sockaddr_in6 sa_in6; struct sockaddr_storage sa_stor; } address_t; 

et une variable déclarée comme address_t ,

 address_t addr; 

vous pouvez initialiser en toute sécurité addr.sa.sa_family puis lire addr.sa_in.sin_family (ou toute autre paire de champs alias _family ). Vous pouvez également utiliser addr en toute sécurité dans un appel à recvfrom , recvmsg , accept ou à n’importe quelle autre primitive de socket prenant un paramètre de struct sockaddr * out, par exemple:

 bytes_read = recvfrom(sockfd, buf, sizeof buf, &addr.sa, sizeof addr); if (bytes_read < 0) goto recv_error; switch (addr.sa.sa_family) { case AF_INET: printf("Datagram from %s:%d, %zu bytes\n", inet_ntoa(addr.sa_in.sin_addr), addr.sa_in.sin_port, (size_t) bytes_read); break; case AF_INET6: // etc } 

Et vous pouvez aussi aller dans l'autre sens,

 memset(&addr, 0, sizeof addr); addr.sa_in.sin_family = AF_INET; addr.sa_in.sin_port = port; inet_aton(address, &addr.sa_in.sin_addr); connect(sockfd, &addr.sa, sizeof addr.sa_in); 

Il est également correct d'allouer des tampons address_t avec malloc ou de l'intégrer dans une structure plus grande.

Ce qui n'est pas sûr, c'est de faire passer des pointeurs sur des sous-structures individuelles d'une union address_t vers des fonctions que vous écrivez. Par exemple, votre fonction de test ...

 sa_family_t test(struct sockaddr *a, struct sockaddr_in *b) { a->sa_family = AF_UNSPEC; b->sin_family = AF_INET; return a->sa_family; // AF_INET please! } 

... ne peut pas être appelé avec (void *)a égale à (void *)b , même si cela se produit car le site d' &addr.sa &addr.sa_in les arguments &addr.sa et &addr.sa_in . Certaines personnes soutenaient que cela devrait être autorisé lorsqu'une déclaration complète de address_t était dans la scope du test , mais cela ressemble trop à " spukhafte Fernwirkung " pour les développeurs du compilateur; L'interprétation de la règle de "sous-séquence initiale commune" (citée dans la réponse de Felix) adoptée par la génération actuelle de compilateurs est qu'elle ne s'applique que lorsque le type d'union est impliqué de manière statique et locale dans un access particulier. Vous devez écrire à la place

 sa_family_t test2(address_t *x) { x->sa.sa_family = AF_UNSPEC; x->sa_in.sa_family = AF_INET; return x->sa.sa_family; } 

Vous vous demandez peut-être pourquoi il est normal de passer &addr.sa pour se connect alors. En gros, connect a sa propre union interne address_t , et commence par quelque chose comme:

 int connect(int sock, struct sockaddr *addr, socklen_t len) { address_t xaddr; memcpy(xaddr, addr, len); 

à ce stade, il peut inspecter xaddr.sa.sa_family toute sécurité, puis xaddr.sa_in.sin_addr ou autre.

Il n'est pas clair pour moi de savoir si connect se contentera de lancer son argument addr à address_t * , alors que l'appelant n'aurait peut-être pas utilisé une telle union, Je peux imaginer des arguments allant dans les deux sens à partir du texte de la norme (ce qui est ambigu sur certains points essentiels liés au sens exact des mots "object", "access" et "type effectif"), et je ne le fais pas. savoir ce que les compilateurs feraient réellement. En pratique, connect doit quand même effectuer une copie, car il s'agit d'un appel système et presque tous les blocs de mémoire transmis à travers la limite utilisateur / kernel doivent être copiés.