4

POSIX intends pointers to variations of struct sockaddr to be castable, however depending on the interpretation of the C standard this may be a violation of the strict aliasing rule and therefore UB. (See this answer with comments below it.) I can, at least, confirm that there may at least be a problem with gcc: this code prints Bug! with optimization enabled, and Yay! with optimization disabled:

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

Observe this behavior on an online IDE.

To workaround this problem this answer proposes the use of type punning with unions:

/*! 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;

However, apparently things are still not as simple as they look… Quoting this comment by @zwol:

That can work but takes a fair bit of care. More than I can fit into this comment box.

What kind of fair bit of care does it take? What are the pitfalls of the use of type punning with unions to cast between variations of struct sockaddr?

I prefer to ask than to run into UB.

  • It is not clear what your actualy probpem with the `union` is. How about a [mcve] and more details about your concerns? Why don't you ask zwol what he means? We are no clairvoyants. – too honest for this site May 29 '17 at 15:20
  • @Olaf Why not ask zwol? Because as I quoted him, he already stated he did not wish to talk about this in comments. What about a Minimal, Complete and Verfiable example? Well, I'm asking this question precisely because I want to avoid falling into a pitfall unknown to me that would make it necessary for me to craft such a Minimal, Complete and Verifiable example. When it comes to UB in C, I think the phrase "better prevent than cure" holds completely. –  May 29 '17 at 15:29
  • I'm not sure you can do anything without revamping the whole bunch of interfaces that involve sockaddr. Whatever you do existing function still expect struct sockaddr* and not any kind of union. – n. m. could be an AI May 29 '17 at 17:18

2 Answers2

1

Using a union like this is safe,

from C11 §6.5.2.3:

  1. A postfix expression followed by the . operator and an identifier designates a member of a structure or union object. The value is that of the named member,95) and is an lvalue if the first expression is an lvalue. If the first expression has qualified type, the result has the so-qualified version of the type of the designated member.

95) If the member used to read the contents of a union object is not the same as the member last used to store a value in the object, the appropriate part of the object representation of the value is reinterpreted as an object representation in the new type as described in 6.2.6 (a process sometimes called ‘‘type punning’’). This might be a trap representation.

and

  1. One special guarantee is made in order to simplify the use of unions: if a union contains several structures that share a common initial sequence (see below), and if the union object currently contains one of these structures, it is permitted to inspect the common initial part of any of them anywhere that a declaration of the completed type of the union is visible. Two structures share a common initial sequence if corresponding members have compatible types (and, for bit-fields, the same widths) for a sequence of one or more initial members

(highlighted what I think is most important)

With accessing the struct sockaddr member, you will read from the common initial part.


Note: This will not make it safe to pass pointers to the members around anywhere and expect the compiler knows they refer to the same stored object. So the literal version of your example code might still break because in your test() the union is not known.

Example:

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

Here, test() might break, but test2() will be correct.

  • Judging from the code shown, OP seems to not access the common part (which would imply identical names **and types**). It is not clear how OP intends to handle the `union` just having a `union` object does not imply pointers to the members are safe for punning. – too honest for this site May 29 '17 at 15:31
  • As I read the standard, only the types have to be compatible (not necessarily identical) and the names do not matter. But see my edited note: it's actually important the union is known. –  May 29 '17 at 15:34
  • What I want is to be able to retrieve the IP address from the `struct sockaddr` returned by `recvfrom` or `accept` without triggering UB, and less importantly, to pass a manually filled `struct sockaddr_in` to `sendto` or `connect`, again without triggering UB. Without this `union` I’d have to do some pointer casting, and IIUC I might trigger UB as soon as I write sth like `(struct sockaddr*)(&sa_in)` or `(struct sockaddr_in*)(&sa)`. I hope I’ll at least be able to pass pointers to union members to functions like `sento`, `recvfrom`, `accept`, `connect`, etc. –  May 29 '17 at 15:41
  • See my edit with an example. If you take care and use your `union` type everywhere it is relevant, you won't have UB. –  May 29 '17 at 15:54
  • @gaazkam regarding your comment, using these functions and casting the pointers is safe anyways, as you access the common part and from that, you *know* which type it really is. As long as you don't pass around a second pointer of another type, there's no risk for aliasing... –  May 29 '17 at 16:04
  • Interesting. I was believing that the problem was that according to C standard it was UB to access an object or its any member through a pointer of type incompatible with the type of that object, and whether types like `struct sockaddr` and `struct sockaddr_in` are compatible was unclear, but compilers tended to assume they’re not. So, strictly speaking, I was believing that it might’ve been UB either when I accessed the `sin_port` field field of the `struct sockaddr` returned by `recvfrom` pointer-casted to `struct sockaddr_in`,… (cont.) –  May 29 '17 at 16:23
  • … or when I accessed the `sa_family` field of this `struct sockaddr` returned by `recvfrom`. Since the implementation must’ve declared this object as either `struct sockaddr` or `struct sockaddr_in` and I have to access it through pointers of both types (first I must examine `sa_family` to know to which type I have to cast), I will have to at least once access the object through a pointer of incompatible type, therefore triggering UB. –  May 29 '17 at 16:26
  • @FelixPalmen: "As I read the standard, only the types have to be compatible" - from the example you seem to be right. I was after the "compatible types" rule, but that seems to be not necessary. From a practical view it sounds resonable, as the compiled code does not even know the member names. For self-defined str5ucture/unions combinations, I strongly recommend moving the common parts to the `struct`, though. Remember that whole socket stuff is quite legacy from before C89. – too honest for this site May 29 '17 at 16:27
  • @gaazkam: The edited answer is very clear (I'd spent one of my rare upvotes if I had votes left), it **is** UB! You **have to alias through the `union` exlicitly**! – too honest for this site May 29 '17 at 16:29
  • The answer I linked to links to [this paper](http://trust-in-soft.com/wp-content/uploads/2017/01/vmcai.pdf) as its resource, and this paper seems to exactly say this what I said above. –  May 29 '17 at 16:30
  • @Olaf, I was responding to [this comment](https://stackoverflow.com/questions/44245618/how-to-legally-use-type-punning-with-unions-to-cast-between-variations-of-struct/44245922?noredirect=1#comment75501645_44245922) of Felix Palmen, which seems to say something opposite. –  May 29 '17 at 16:31
  • @gaazkam: That's exactly why we ask for a [mcve]! It is **you** wanting an answer, so you have to provide all required information. And code is often the best explanation. After all it is the most unambiguous way to tell what you intend. – too honest for this site May 29 '17 at 16:34
  • I have to clarify here: yes, this whole interface is ill-defined and reading the first field from the "base type" is UB, just saying you won't run into aliasing problems in practice, as you don't pass around pointers. The solution without UB indeed needs the union. "Safe" was not the best wording in my comment... –  May 29 '17 at 16:36
1

Given the address_t union you propose

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

and a variable declared as address_t,

address_t addr; 

you can safely initialize addr.sa.sa_family and then read addr.sa_in.sin_family (or any other pair of aliased _family fields). You can also safely use addr in a call to recvfrom, recvmsg, accept, or any other socket primitive that takes a struct sockaddr * out-parameter, e.g.

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
}

And you can also go in the other direction,

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

It is also okay to allocate address_t buffers with malloc, or embed it in a larger structure.

What's not safe is to pass pointers to individual sub-structures of an address_t union to functions that you write. For instance, your test function ...

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

... may not be called with (void *)a equal to (void *)b, even if this happens because the callsite passed &addr.sa and &addr.sa_in as the arguments. Some people used to argue that this should be allowed when a complete declaration of address_t was in scope when test was defined, but that's too much like "spukhafte Fernwirkung" for the compiler devs; the interpretation of the "common initial subsequence" rule (quoted in Felix's answer) adopted by the current generation of compilers is that it only applies when the union type is statically and locally involved in a particular access. You must write instead

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

You might be wondering why it's okay to pass &addr.sa to connect then. Very roughly, connect has its own internal address_t union, and it begins with something like

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

at which point it can safely inspect xaddr.sa.sa_family and then xaddr.sa_in.sin_addr or whatever.

Whether it would be okay for connect to just cast its addr argument to address_t *, when the caller might not have used such a union itself, is unclear to me; I can imagine arguments both ways from the text of the standard (which is ambiguous on certain key points having to do with the exact meanings of the words "object", "access", and "effective type"), and I don't know what compilers would actually do. In practice connect has to do a copy anyway, because it's a system call and almost all memory blocks passed across the user/kernel boundary have to be copied.

zwol
  • 135,547
  • 38
  • 252
  • 361