2017-05-29 3 views
4

POSIX a l'intention de pointer vers des variantes de struct sockaddr pour être castable, cependant cela dépend de l'interprétation de la norme C, ce qui peut être une violation de la règle stricte d'aliasing et par conséquent UB. (Voir this answer avec des commentaires en dessous.) Je peux, au moins, confirme qu'il peut y avoir au moins un problème avec gcc: ce code imprime Bug! avec l'optimisation activée et Yay! avec l'optimisation disabled:Comment utiliser légalement le type-punning avec des syndicats pour convertir entre les variantes de struct sockaddr sans violer la règle stricte d'aliasing?

#include <sys/types.h> 
#include <netinet/in.h> 
#include <stdio.h> 

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; 
} 

Observer cette comportement sur un online IDE.

Pour résoudre ce problème this answer propose l'utilisation du type calembour 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 pas aussi simples qu'ils regardent ... this comment par @zwol Citation:

Que peut travail, mais prend un peu de soin. Plus que je peux entrer dans cette boîte de commentaire.

Quel genre de peu juste des soins faut-il? Quels sont les pièges de l'utilisation de type punning avec les syndicats à jeter entre les variations de struct sockaddr?

Je préfère demander que de courir dans UB.

+0

Il n'est pas clair quel est votre problème actuel avec le «union». Que diriez-vous d'un [mcve] et plus de détails sur vos préoccupations? Pourquoi ne demandez-vous pas à zwol ce qu'il veut dire? Nous ne sommes pas clairvoyants. – Olaf

+0

@Olaf Pourquoi ne pas demander zwol? Parce que, comme je l'ai cité, il a déjà déclaré qu'il ne souhaitait pas en parler dans les commentaires. Qu'en est-il un exemple minimal, complet et vérifiable? Eh bien, je pose cette question précisément parce que je veux éviter de tomber dans un piège inconnu de moi qui me rendrait nécessaire d'élaborer un exemple aussi minime, complet et vérifiable. Quand il s'agit de UB en C, je pense que l'expression "mieux vaut prévenir que guérir" tient complètement. – gaazkam

+0

Je ne suis pas sûr que vous pouvez faire quelque chose sans réorganiser tout le tas d'interfaces qui impliquent sockaddr. Quoi que vous fassiez, la fonction existante attend toujours struct sockaddr * et pas n'importe quel type d'union. –

Répondre

2

L'utilisation d'un union comme celui-ci est sûr,

de C11 §6.5.2.3:

  1. Une expression postfix suivie par la. opérateur et un identificateur désigne un membre de une structure ou un objet 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 le 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 objet syndical n'est pas le même que le dernier membre utilisé pour stocker une valeur dans l'objet, la partie appropriée de la représentation de l'objet de la valeur est réinterprété comme une représentation d'objet dans le nouveau type comme décrit dans 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 faite afin de simplifier l'utilisation des syndicats: si un syndicat contient plusieurs structures qui partagent une séquence initiale commune (voir ci-dessous), et si l'union objet contient actuellement l'une de ces structures, il est permis d'inspecter la partie initiale commune de l'un d'eux partout où une déclaration du type complété de l'union est 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

(mis en évidence ce que je penser est le plus important)

Avec l'accès au membre struct sockaddr, vous sera lecture de la première partie commune.


Remarque: Cela ne le rendre sûr de passer des pointeurs vers les membres autour de partout et attendre le compilateur sait qu'ils se réfèrent au même objet stocké. Donc, la version littérale de votre code d'exemple pourrait encore casser parce que dans votre test() le union n'est pas connu.

Exemple:

#include <stdio.h> 

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() pourrait briser, mais test2() sera correcte.

+0

A en juger par le code affiché, OP ne semble pas accéder à la partie commune (ce qui impliquerait des noms et des types ** identiques). Il n'est pas clair comment OP a l'intention de gérer le 'union' simplement en ayant un objet' union' n'implique pas que les pointeurs sur les membres sont sûrs pour le jeu de mots. – Olaf

+0

En lisant la norme, seuls les types doivent être compatibles (pas nécessairement identiques) et les noms n'ont pas d'importance. Mais voyez ma note éditée: il est important que le syndicat soit connu. –

+0

Ce que je veux, c'est pouvoir récupérer l'adresse IP à partir de la 'struct sockaddr' retournée par' recvfrom' ou 'accept' sans déclencher UB, et moins important, de passer une' struct sockaddr_in' manuellement à 'sendto' ou 'connect', encore une fois sans déclencher UB. Sans cette 'union' je devrais faire un cast de pointeur, et IIUC je pourrais déclencher UB dès que j'écrirais sth comme '(struct sockaddr *) (& sa_in)' ou '(struct sockaddr_in *) (& sa)'.J'espère pouvoir au moins passer des pointeurs aux membres de l'union vers des fonctions comme 'sento',' recvfrom', 'accept',' connect', etc. – gaazkam

1

Étant donné l'union address_t vous proposes

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 crénelage _family des champs). Vous pouvez également utiliser en toute sécurité addr dans un appel à recvfrom, recvmsg, accept ou toute autre primitive de socket prenant un paramètre de sortie struct sockaddr *, par ex.

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 l'intégrer dans une structure plus grande.

Qu'est-ce que pas est sûr de passer des pointeurs vers des sous-structures individuelles d'un syndicat address_t aux fonctions que vous écrivez. Par exemple, votre fonction 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! 
} 

... peut pas être appelé avec (void *)a égal à (void *)b, même si cela se produit parce que le callsite passé &addr.sa et &addr.sa_in les arguments. Certaines personnes ont fait valoir que cela devrait être autorisé lorsqu'une déclaration complète de address_t était dans la portée lorsque test a été définie, 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é statiquement et localement dans un accès 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 demandez peut-être pourquoi il est normal de passer &addr.sa-connect alors. Très gros, connect a son propre syndicat address_t interne, et il commence par quelque chose comme

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

à quel point il peut en toute sécurité, puis inspecter xaddr.sa.sa_familyxaddr.sa_in.sin_addr ou autre.

Que ce serait bien pour connect juste casting son argument addr à address_t *, lorsque l'appelant pourrait ne pas avoir utilisé un tel syndicat lui-même, ne sait pas à moi; Je peux imaginer des arguments dans les deux sens à partir du texte de la norme (qui est ambigu sur certains points clés ayant à voir avec les significations exactes des mots "objet", "accès", et "type effectif"), et je ne savoir ce que les compilateurs feraient réellement. En pratique, connect doit quand même faire une copie, car c'est un appel système et presque tous les blocs de mémoire passés à travers la limite utilisateur/noyau doivent être copiés.