c++ c linux sockets strict-aliasing

c++ - lanzará sockaddr_storage y sockaddr_in para romper el alias estricto



linux sockets (2)

Siguiendo mi pregunta anterior, tengo mucha curiosidad sobre este código:

case AF_INET: { struct sockaddr_in * tmp = reinterpret_cast<struct sockaddr_in *> (&addrStruct); tmp->sin_family = AF_INET; tmp->sin_port = htons(port); inet_pton(AF_INET, addr, tmp->sin_addr); } break;

Antes de hacer esta pregunta, he buscado en SO sobre el mismo tema y tengo respuestas mixtas sobre este tema. Por ejemplo, vea esto , esta y esta publicación que dicen que de alguna manera es seguro usar este tipo de código. También hay otra publicación que dice que se usen sindicatos para esa tarea, pero nuevamente los comentarios sobre la respuesta aceptada difieren.

La documentación de Microsoft en la misma estructura dice:

Los desarrolladores de aplicaciones normalmente usan solo el miembro ss_family de SOCKADDR_STORAGE. Los miembros restantes se aseguran de que SOCKADDR_STORAGE pueda contener una dirección IPv6 o IPv4 y la estructura se rellena adecuadamente para lograr la alineación de 64 bits. Tal alineación permite que las estructuras de datos de direcciones de socket específicas del protocolo accedan a los campos dentro de una estructura SOCKADDR_STORAGE sin problemas de alineación. Con su relleno, la estructura SOCKADDR_STORAGE tiene 128 bytes de longitud.

La documentación de Opengroup dice:

El encabezado definirá la estructura sockaddr_storage. Esta estructura será:

Lo suficientemente grande como para acomodar todas las estructuras de direcciones específicas del protocolo compatibles

Alineado en un límite apropiado para que los punteros a él se puedan convertir como punteros a estructuras de direcciones específicas del protocolo y se usen para acceder a los campos de esas estructuras sin problemas de alineación

La página del hombre de socket también dice lo mismo -

Además, la API de sockets proporciona el tipo de datos struct sockaddr_storage. Este tipo es adecuado para acomodar todas las estructuras de direcciones de socket específicas de dominio soportadas; es lo suficientemente grande y está alineado correctamente. (En particular, es lo suficientemente grande como para contener direcciones de socket IPv6).

He visto implementaciones múltiples utilizando tales conversiones en C y C++ en la naturaleza y ahora no estoy seguro del hecho de que uno tiene razón, ya que hay algunas publicaciones que contradicen las afirmaciones anteriores: esto y esto .

Entonces, ¿cuál es la forma más segura y correcta de llenar una estructura sockaddr_storage ? ¿Son seguros estos moldes de puntero? o el método de unión ? También conozco el llamado a getaddrinfo() pero parece un poco complicado para la tarea anterior de simplemente llenar las estructuras. Hay otra manera recomendada con memcpy , ¿es esto seguro?


Sí, es una violación de aliasing para hacer esto. Entonces no. No hay necesidad de usar sockaddr_storage ; fue un error histórico. Pero hay algunas formas seguras de usarlo:

  1. malloc(sizeof struct sockaddr_storage) . En este caso, la memoria apuntada no tiene un tipo efectivo hasta que le almacene algo.
  2. Como parte de una unión, accede explícitamente al miembro que deseas. Pero en este caso simplemente coloque los tipos reales de sockaddr que desea ( in y dentro y quizás un ) en la unión en lugar de sockaddr_storage .

Por supuesto, en la programación moderna nunca debería necesitar crear objetos de tipo struct sockaddr_* en absoluto . Simplemente use getaddrinfo y getnameinfo para traducir direcciones entre representaciones de cadenas y objetos sockaddr , y trate a estos últimos como objetos completamente opacos .


Los compiladores C y C ++ se han vuelto mucho más sofisticados en la última década que cuando se diseñaron las interfaces sockaddr , o incluso cuando se escribió C99. Como parte de eso, el propósito entendido de "comportamiento indefinido" ha cambiado. En el pasado, el comportamiento indefinido generalmente intentaba cubrir el desacuerdo entre las implementaciones de hardware sobre cuál era la semántica de una operación. Pero hoy en día, gracias en última instancia a una serie de organizaciones que querían dejar de escribir FORTRAN y podían pagarle a los ingenieros de compilación para que eso ocurriera, los compiladores utilizan una conducta indefinida para hacer inferencias sobre el código . El desplazamiento a la izquierda es un buen ejemplo: C99 6.5.7p3,4 (reorganizado un poco para mayor claridad) dice

El resultado de E1 << E2 es E1 posiciones de bit E2 desplazadas a la izquierda; los bits vacíos se rellenan con ceros. Si el valor de [ E2 ] es negativo o es mayor o igual que el ancho del [ E1 ] promocionado, el comportamiento no está definido.

Entonces, por ejemplo, 1u << 33 es UB en una plataforma donde unsigned int tiene 32 bits de ancho. El comité hizo esto indefinido porque las diferentes instrucciones de desplazamiento de la izquierda de las arquitecturas de CPU hacen cosas diferentes en este caso: algunas producen cero consistentemente, otras reducen el módulo de recuento de cambios del ancho del tipo (x86), algunas reducen el módulo de recuento de turnos un número mayor (ARM), y al menos una arquitectura históricamente común atraparía (no sé cuál, pero es por eso que no está definido y no está especificado). Pero hoy en día, si escribes

unsigned int left_shift(unsigned int x, unsigned int y) { return x << y; }

en una plataforma con unsigned int 32 bits, el compilador, conociendo la regla UB anterior, deducirá que y debe tener un valor en el rango de 0 a 32 cuando se llama a la función. Alimentará ese rango en el análisis interprocedural y lo usará para hacer cosas como eliminar las verificaciones de rango innecesarias en las personas que llaman. Si el programador tiene motivos para pensar que no son innecesarios, bueno, ahora comienza a ver por qué este tema es una lata de gusanos.

Para obtener más información sobre este cambio en el propósito del comportamiento indefinido, consulte el ensayo de tres partes de LLVM sobre el tema ( 1 2 3 ).

Ahora que lo entiendes, puedo responder tu pregunta.

Estas son las definiciones de struct sockaddr , struct sockaddr_in y struct sockaddr_storage sockaddr_storage, después de eludir algunas complicaciones irrelevantes:

struct sockaddr { uint16_t sa_family; }; struct sockaddr_in { uint16_t sin_family; uint16_t sin_port; uint32_t sin_addr; }; struct sockaddr_storage { uint16_t ss_family; char __ss_storage[128 - (sizeof(uint16_t) + sizeof(unsigned long))]; unsigned long int __ss_force_alignment; };

Esta es la subclasificación del pobre hombre. Es un modismo omnipresente en C. Usted define un conjunto de estructuras que tienen el mismo campo inicial, que es un número de código que le dice qué estructura le pasaron. Allá en el día, todos esperaban que si asignabas y completabas una struct sockaddr_in sockaddr_in, la modificabas para struct sockaddr sockaddr y la pasabas a, por ejemplo, connect , la implementación de connect podía desreferenciar el puntero struct sockaddr forma segura para recuperar el campo sa_family , aprender que estaba mirando un sockaddr_in , lo echó hacia atrás y avanzó. El estándar C siempre ha dicho que desreferenciar el puntero struct sockaddr desencadena un comportamiento indefinido, esas reglas no han cambiado desde C89, pero todos esperaban que fuera seguro en este caso porque sería la misma instrucción "cargar 16 bits" sin importar qué estructura estabas realmente trabajando con. Es por eso que POSIX y la documentación de Windows hablan sobre la alineación; las personas que escribieron esas especificaciones, en la década de 1990, pensaron que la principal forma en que esto podría ser realmente un problema sería si terminas emitiendo un acceso a la memoria mal alineado.

Pero el texto de la norma no dice nada sobre las instrucciones de carga, ni la alineación. Esto es lo que dice (C99 §6.5p7 + nota al pie):

Un objeto debe tener acceso a su valor almacenado solo mediante una expresión lvalue que tenga uno de los siguientes tipos: 73)

  • un tipo compatible con el tipo efectivo del objeto,
  • una versión calificada de un tipo compatible con el tipo efectivo del objeto,
  • un tipo que es el tipo firmado o no firmado correspondiente al tipo efectivo del objeto,
  • un tipo que es el tipo firmado o sin firmar correspondiente a una versión calificada del tipo efectivo del objeto,
  • un agregado o tipo de unión que incluye uno de los tipos mencionados anteriormente entre sus miembros (incluido, recursivamente, un miembro de un subaggregado o sindicato contenido), o
  • un tipo de personaje

73) El propósito de esta lista es especificar las circunstancias en las que un objeto puede o no tener alias.

struct tipos struct son "compatibles" solo consigo mismos, y el "tipo efectivo" de una variable declarada es su tipo declarado. Entonces el código que mostró ...

struct sockaddr_storage addrStruct; /* ... */ case AF_INET: { struct sockaddr_in * tmp = (struct sockaddr_in *)&addrStruct; tmp->sin_family = AF_INET; tmp->sin_port = htons(port); inet_pton(AF_INET, addr, tmp->sin_addr); } break;

... tiene un comportamiento indefinido, y los compiladores pueden hacer inferencias a partir de eso, a pesar de que la generación de código ingenuo se comportaría como se esperaba. Lo que un compilador moderno probablemente infiera de esto es que el case AF_INET nunca se puede ejecutar . Eliminará todo el bloque como código muerto, y se producirá hilaridad.

Entonces, ¿cómo trabajas con sockaddr con seguridad? La respuesta más corta es "solo use getaddrinfo y getnameinfo ". Se ocupan de este problema por ti.

Pero tal vez necesite trabajar con una familia de direcciones, como AF_UNIX , que getaddrinfo no maneja. En la mayoría de los casos, puede declarar una variable del tipo correcto para la familia de direcciones, y enviarla solo al llamar a funciones que toman una struct sockaddr *

int connect_to_unix_socket(const char *path, int type) { struct sockaddr_un sun; size_t plen = strlen(path); if (plen >= sizeof(sun.sun_path)) { errno = ENAMETOOLONG; return -1; } sun.sun_family = AF_UNIX; memcpy(sun.sun_path, path, plen+1); int sock = socket(AF_UNIX, type, 0); if (sock == -1) return -1; if (connect(sock, (struct sockaddr *)&sun, offsetof(struct sockaddr_un, sun_path) + plen)) { int save_errno = errno; close(sock); errno = save_errno; return -1; } return sock; }

La implementación de connect tiene que pasar por algunos aros para que esto sea seguro, pero ese no es su problema.

En contraste con la otra respuesta, hay un caso en el que es posible que desee utilizar sockaddr_storage ; junto con getpeername y getnameinfo , en un servidor que necesita manejar direcciones IPv4 e IPv6. Es una forma conveniente de saber qué tan grande de un búfer se debe asignar.

#ifndef NI_IDN #define NI_IDN 0 #endif char *get_peer_hostname(int sock) { char addrbuf[sizeof(struct sockaddr_storage)]; socklen_t addrlen = sizeof addrbuf; if (getpeername(sock, (struct sockaddr *)addrbuf, &addrlen)) return 0; char *peer_hostname = malloc(MAX_HOSTNAME_LEN+1); if (!peer_hostname) return 0; if (getnameinfo((struct sockaddr *)addrbuf, addrlen, peer_hostname, MAX_HOSTNAME_LEN+1, 0, 0, NI_IDN) { free(peer_hostname); return 0; } return peer_hostname; }

(Podría haber escrito struct sockaddr_storage addrbuf , pero quería enfatizar que nunca necesito acceder al contenido de addrbuf directamente).

Una nota final: si la gente de BSD hubiera definido las estructuras de Sockaddr un poco diferente ...

struct sockaddr { uint16_t sa_family; }; struct sockaddr_in { struct sockaddr sin_base; uint16_t sin_port; uint32_t sin_addr; }; struct sockaddr_storage { struct sockaddr ss_base; char __ss_storage[128 - (sizeof(uint16_t) + sizeof(unsigned long))]; unsigned long int __ss_force_alignment; };

... las subidas y bajadas se habrían definido perfectamente, gracias a la regla "agregado o unión que incluye uno de los tipos mencionados anteriormente". Si se está preguntando cómo debe lidiar con este problema en el nuevo código C, aquí tiene.