Как легально использовать наложение типов с союзами для приведения между вариантами struct sockaddr без нарушения строгого правила наложения имен?
POSIX намеревается указатели на вариации struct sockaddr
быть кастуемым, однако в зависимости от интерпретации стандарта C это может быть нарушением строгого правила псевдонимов и, следовательно, UB. (См. Этот ответ с комментариями под ним.) Я могу, по крайней мере, подтвердить, что, по крайней мере, может быть проблема с gcc: этот код печатает Bug!
с включенной оптимизацией и Yay!
с отключенной оптимизацией:
#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;
}
Наблюдайте за этим поведением в онлайн-среде IDE.
Чтобы обойти эту проблему, в этом ответе предлагается использовать тип punning с объединениями:
/*! 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;
Однако, по-видимому, все не так просто, как кажется... Цитируя этот комментарий @zwol:
Это может сработать, но потребует немало усилий. Больше, чем я могу вписаться в это поле для комментариев.
Какого рода осторожность это требует? Каковы подводные камни в использовании типа наказания союзов, чтобы бросать между вариациями struct sockaddr
?
Я предпочитаю спросить, чем сталкиваться с UB.
2 ответа
Используя union
как это безопасно,
из C11 §6.5.2.3:
- Выражение постфикса, сопровождаемое. оператор и идентификатор обозначают член структуры или объединенного объекта. Значение соответствует названному члену (95) и является lvalue, если первое выражение является lvalue. Если первое выражение имеет уточненный тип, результат имеет уточненную версию типа указанного члена.
95) Если элемент, используемый для чтения содержимого объекта объединения, не совпадает с элементом, который последний раз использовался для хранения значения в объекте, соответствующая часть представления объекта значения повторно интерпретируется как представление объекта в новом type, как описано в 6.2.6 (процесс иногда называется 'type punning' '). Это может быть представление ловушки.
а также
- Одна специальная гарантия сделана для того, чтобы упростить использование объединений: если объединение содержит несколько структур, которые имеют общую начальную последовательность (см. Ниже), и если объект объединения в настоящее время содержит одну из этих структур, разрешается проверять общие Начальная часть любого из них везде, где видна декларация о законченном типе объединения. Две структуры имеют общую начальную последовательность, если соответствующие элементы имеют совместимые типы (и, для битовых полей, одинаковой ширины) для последовательности из одного или нескольких начальных элементов
(выделено то, что я считаю наиболее важным)
С доступом к struct sockaddr
член, вы будете читать из общей начальной части.
Примечание. Это не сделает безопасным передачу указателей членам в любом месте и ожидание того, что компилятор знает, что они ссылаются на один и тот же хранимый объект. Таким образом, буквальная версия вашего примера кода может все еще сломаться, потому что в вашем test()
union
не известно
Пример:
#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;
}
Вот, test()
может сломаться, но test2()
будет правильно.
Учитывая address_t
союз, который вы предлагаете
typedef union address
{
struct sockaddr sa;
struct sockaddr_in sa_in;
struct sockaddr_in6 sa_in6;
struct sockaddr_storage sa_stor;
}
address_t;
и переменная, объявленная как address_t
,
address_t addr;
вы можете безопасно инициализировать addr.sa.sa_family
а затем прочитать addr.sa_in.sin_family
(или любая другая пара псевдонимов _family
поля). Вы также можете безопасно использовать addr
в вызове recvfrom
, recvmsg
, accept
или любой другой примитив сокета, который принимает struct sockaddr *
выходной параметр, например
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
}
И вы также можете пойти в другом направлении,
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);
Также можно выделить address_t
буферы с malloc
или встраивать его в более крупную структуру.
Что небезопасно, так это передавать указатели на отдельные подструктуры address_t
объединение функций, которые вы пишете. Например, ваш 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!
}
... нельзя вызывать с (void *)a
равно (void *)b
, даже если это произойдет, потому что пройден &addr.sa
а также &addr.sa_in
в качестве аргументов. Некоторые люди утверждали, что это должно быть разрешено, когда полная декларация address_t
был в объеме, когда test
был определен, но это слишком похоже на spukhafte Fernwirkung для разработчиков компиляторов; Интерпретация правила "общей начальной подпоследовательности" (цитируемого в ответе Феликса), принятого нынешним поколением компиляторов, заключается в том, что оно применяется только тогда, когда тип объединения статически и локально участвует в конкретном доступе. Вы должны написать вместо
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;
}
Вы можете быть удивлены, почему это нормально, чтобы пройти &addr.sa
в connect
затем. Очень грубо connect
имеет свой внутренний address_t
союз, и это начинается с чего-то вроде
int connect(int sock, struct sockaddr *addr, socklen_t len)
{
address_t xaddr;
memcpy(xaddr, addr, len);
в этот момент он может безопасно проверить xaddr.sa.sa_family
а потом xaddr.sa_in.sin_addr
или что угодно.
Будет ли это хорошо для connect
просто бросить его addr
аргумент address_t *
когда звонящий, возможно, сам не использовал такой союз, мне неясно; Я могу представить аргументы в обоих направлениях из текста стандарта (что неоднозначно в определенных ключевых моментах, связанных с точным значением слов "объект", "доступ" и "эффективный тип"), и я не знать, что на самом деле делают компиляторы. На практике connect
В любом случае необходимо выполнить копирование, потому что это системный вызов, и почти все блоки памяти, передаваемые через границу пользователя / ядра, должны быть скопированы.