Safe reinterpret_cast with sockaddr?

Viewed 1565

Assume I'm doing some socket programming:

struct sockaddr_in sa;
inet_pton(AF_INET, "127.0.0.1", &(sa.sin_addr));
auto *resa = reinterpret_cast<struct sockaddr*>(&sa);
bind(sfd, resa, sizeof(sa));

Now the question is: we do reinterpret_cast (or C-style (struct sockaddr *) cast like in tutorials or man) yet the standard does not guarantee that to work, right? On the other hand there does not seem to be a way to do that differently, bind() requires struct sockaddr* (and it has to access the underlying struct to determine what it received).

So is it safe to do reinterpret_cast between different types in this case? If yes then why?

5 Answers

The ability to support many kinds of pointer manipulation is regarded by the Standard as a Quality of Implementation issue. The Standard does not mandate that all implementations be suitable for low-level or systems programming, but quality implementations suitable for such purpose on e.g. Unix should support the kinds of semantics typically used by systems code on such platform. An implementation could be incapable of handling code that treats shared parts of structures in type-agnostic fashion and yet still be a high-quality implementation for some specialized purposes that don't involve any low-level or systems programming. On the other hand, a quality implementation suitable for low-level programming should have no trouble handling such code. Any implementation that can't handle such code should be viewed as being a low-quality implementation and/or unsuitable for low-level programming, and a low-level program's inability to work with such implementations is not a defect.

So is it safe to do reinterpret_cast between different types in this case?

No, not really. You use a pointer to a sockaddr in order to point to an object of type sockaddr_in. These are unrelated types and so it implies something that isn't true: two unrelated objects are pointed to, but only one object is allocated.

If you work on the most constrained of systems then yes, you might be happy with this and as @supercat says, your implementor might have your back. But your code will not be portable.

there does not seem to be a way to do that differently

The prescribed solution is to allocate memory for both objects and to use std::memcpy to make them equal:

sockaddr sa2;
std::memcpy(&sa2, &sa, sizeof(sa));
bind(sfd, &sa2, sizeof(sa));

From cppreference.com:

Where strict aliasing prohibits examining the same memory as values of two different types, std::memcpy may be used to convert the values.

It is important that the two objects (sockaddr_in and sockaddr) are the same size. You can assert that this is the case:

static_assert(sizeof(sa2) == sizeof(sa));

The call to std::memcpy is not always free but often it is. (example)

Contrary to the top answer, I would say that bind CAN and MUST be written in a way such that reinterpret_cast is safe to use here. For instance, bind could be implemented as:

int bind(SOCKET s, const sockaddr* addr, int addrlen) {
    std::uint16_t address_family;
    std::memcpy(&address_family, addr, sizeof(address_family));
    if (address_family == AF_INET) {
        const sockaddr_in* sin = reinterpret_cast<const sockaddr_in*>(addr);
        // Accessing sin->sin_addr is safe here
        ...
    } else if (address_family == AF_INET6) {
        const sockaddr_in6* sin6 = reinterpret_cast<const sockaddr_in6*>(addr);
        ...
    }
}

The key point is that the reinterpret_cast itself is not UB, it's trying to access the data that's UB (see Type Aliasing):

Whenever an attempt is made to read or modify the stored value of an object of type DynamicType through a glvalue of type AliasedType, the behavior is undefined[...]

In the code above, we never try to read the contents of addr through a pointer of type sockaddr*. We examine the value representation (raw bytes) to get the address family, which tells us the exact type of struct to use. Then we can safely cast back to the original type. Casting to a pointer of a different type then back to the original type is allowed by the standard.

Taking it one step further, I would say that with sockaddr_in6 the implementation must handle reinterpret_cast correctly. Since sizeof(sockaddr_in6) > sizeof(sockaddr), the memcpy trick no longer works. The API is specifically asking for a pointer to an object of the wrong type, so the onus is on the API implementers to use the pointer correctly.

Related