2

I want to derive a structure from the plain old trivial C structure ::sockaddr_storage so I can extend it with methods to handle its own data. I can type cast the derived structure as usual to get access to different socket addresses AF_INET6 or AF_INET stored in the structure. But I do not understand a problem when using a virtual destructor as shown below:

#include <netinet/in.h>
#include <iostream>

namespace myns {

struct Ssockaddr_storage1 : public ::sockaddr_storage {
    Ssockaddr_storage1() : ::sockaddr_storage() {}
    ~Ssockaddr_storage1() {}
};

// This is only different by using a virtual destructor.
struct Ssockaddr_storage2 : public ::sockaddr_storage {
    Ssockaddr_storage2() : ::sockaddr_storage() {}
    virtual ~Ssockaddr_storage2() {}
};

} // namespace myns

int main() {
    {
        myns::Ssockaddr_storage1 ss;

        std::cout << "Ssockaddr_storage1:\n";
        std::cout << "ss_family   = " << ss.ss_family << "\n";
        sockaddr_in6* sa_in6 = (sockaddr_in6*)&ss;
        std::cout << "sin6_family = " << sa_in6->sin6_family << "\n";
    }
    {
        myns::Ssockaddr_storage2 ss;

        std::cout << "\nSsockaddr_storage2 with virtual destructor:\n";
        std::cout << "ss_family   = " << ss.ss_family << "\n";
        sockaddr_in6* sa_in6 = (sockaddr_in6*)&ss;
        std::cout << "sin6_family = " << sa_in6->sin6_family << "\n";
    }
}

I compile it with:

~$ g++ -std=c++11 -Wall -Wpedantic -Wextra -Werror -Wuninitialized -Wsuggest-override -Wdeprecated example.cpp

The output is:

Ssockaddr_storage1:
ss_family   = 0
sin6_family = 0

Ssockaddr_storage2 with virtual destructor:
ss_family   = 0
sin6_family = 15752

As shown when using the reference ss to the inherited sockaddr_storage it always works. But when using the type casted pointer sa_in6 to access the AF_INET6 data it only works if not using a virtual destructor. If declaring a virtual destructor I get a random undefined address family number that differs from call to call. Obviously the pointer does not point to the right location.

Why the casted pointer sa_in6 does not point to the begin of the inherited sockaddr_storage structure when declaring a virtual destructor?

Ingo
  • 588
  • 9
  • 21
  • What is `sockaddr_in6`? I assume if you wouldn't use C-style pointer casts this problem would have been caught at compile time. You are probably reading the vtable pointer of the `Ssockaddr_storage2` object in the second case when you cast its address to `sockaddr_in6*`. The vtable pointer usually lies at memory offset 0 within the object – chrysante May 29 '23 at 20:38
  • Since you never initialize the fields of `ss`, you get unspecified values when you access them. – Chris Dodd May 29 '23 at 20:54
  • @ChrisDodd I initialize `ss` with calling the base struct default constructor `Ssockaddr_storage1() : ::sockaddr_storage() {}`. – Ingo May 29 '23 at 21:17
  • @chrysante `sockaddr_in6` is the AF_INET6 socket address structure as listed for example at https://stackoverflow.com/a/16010670/5014688 – Ingo May 29 '23 at 21:21
  • sockaddr_in6 is _not_ a base class of either of your classes. `::sockaddr_storage()` default initializes its first member, an unsigned int type - which is a no-op. (It may have other members in some implementations.) As for sockaddr_in6's other members - You don't have one and such members do not exist in a sockaddr_storage. Since sockaddr_in6 and sockaddr_storage have the identical initial element, if you actually had an sockaddr_in6, which you don't, in C you could access that first element only, via a pointer to sockaddr_storage. Different situation then your post. – Avi Berger May 29 '23 at 22:00
  • @AviBerger Yes, the structure is a union of `::sockaddr_storage`, `::sockaddr`, `::sockaddr_in` and `sockaddr_in6`. The first member of all of them is the address family `AF_INET6` or `AF_INET`. It is the only common member of the union, means it is the same memory location on all structures. So if I initialize `sockaddr_storage.ss_family` to 0 (AF_UNSPEC) I will see it also at `sockaddr_in6.sin6_family` and at all first members of the other structures. If I find a random number there then something is wrong with the pointer to the begin of the structures. That's what I've used here. – Ingo May 29 '23 at 22:31
  • I do not see a union in your post or in [these docs.](https://manpages.ubuntu.com/manpages/lunar/man3/sockaddr.3type.html) Beyond that, sin6_addr, which you are attempting to access is, as far as I can tell, not a member of sockaddr_storage at all, never mind part of a common initial sequence. – Avi Berger May 29 '23 at 23:07
  • Rather than doing inheritance from a C style struct, why not have a member variable to the C style struct, or a pointer member variable to the C style struct? A lot less sharp edges to hurt yourself upon. – Eljay May 29 '23 at 23:11
  • @AviBerger Sorry, I do not see your point. The question is how to get the right pointer. Please have a look at the accepted answer. – Ingo May 29 '23 at 23:13
  • The memory offset of the sin6_family member of a sockaddr_in6 could be larger than the size of a sockaddr_storage or a myns::Ssockaddr_storage1. Potential off the end memory access. There isn't necessarily a "right" pointer. Now if you were going the other way, from a sockaddr_in6 to a sockaddr_storage, it might not have the language seal of approval, but it is more reasonable that you could get away with it. – Avi Berger May 29 '23 at 23:24
  • @Eljay Yes, that is a valid alternative and I had used it before as member variable. But then the `::sockaddr_storage` structure is a member of a another class. Its members are not direct (inherited) part of the derived structure. My intention is to specialize it to reengineer old C code to C++, e.g. using `Ssockaddr_storage ss; if(ss.ss_family == AF_INET6) {..}` instead of `CSockAddr saddr; if(saddr.ss.ss_family == AF_INET) {..}` and other issues to use it as member. Inheritance is made for specialization. – Ingo May 29 '23 at 23:46
  • @AviBerger quote: *"The memory offset of the sin6_family member of a sockaddr_in6 could be larger than the size of a sockaddr_storage*" - No, never - Please read the part about **sockaddr_storage** at [sys_socket.h(0p) - Linux manual page](https://man7.org/linux/man-pages/man0/sys_socket.h.0p.html). But this discussion becomes off topic. – Ingo May 30 '23 at 00:05
  • Thanks, I see the text in that link. Not reflected in the example code I was looking at. – Avi Berger May 30 '23 at 00:34
  • @AviBerger As already said: casting of `sockaddr_storage` in general is not the question and was assumed by me to be well known. If you are interested in that you may have a look at [cast sockaddr_storage](https://duckduckgo.com/?q=cast+sockaddr_storage) and many other resources on the web. – Ingo May 30 '23 at 09:13

3 Answers3

9

That's because this is undefined behavior.

sockaddr_in6* sa_in6 = (sockaddr_in6*)&ss;

C++ is not C (obviously). An analogous cast would be valid in C, but not in C++. In C++ this is undefined behavior, because C++'s type checking is more strict. ss is Ssockaddr_storage2, which is not related to sockaddr_in6, in any way.

sockaddr_storage *sas= &ss;

This conversion does not require a cast, since sockaddr_storage is just a superclass of Ssockaddr_storage2.

And now that you have a POD C struct, you can close your eyes and pretend that it's really a sockaddr_in6:

sockaddr_in6* sa_in6 = (sockaddr_in6*)sas;

Now that we got this out of the way, the reason for your observed behavior is that once you introduce virtual inheritance most C++ implementations add an extra, hidden pointer to objects with virtual inheritance, to handle all the implementation details. This hidden pointer is typically placed at the beginning of the class's instances, and this invalid cast now ends up pointing not at sockaddr_storage, which now starts later in the class, but at the virtual table pointer. Hillarity ensues.

Sam Varshavchik
  • 114,536
  • 5
  • 94
  • 148
1

The other answers (by Ingo and Sam) work in C++, but to clearly document the difference between the casts you can use named casts in C++:

sockaddr_in6* sa_in6 = reinterpret_cast<sockaddr_in6*>(static_cast<::sockaddr_storage*>(&ss));

This is recommended by the C++ core guidelines https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#es49-if-you-must-use-a-cast-use-a-named-cast

Hans Olsson
  • 11,123
  • 15
  • 38
0

As Sam Varshavchik pointed out in his answer, the problem is the usual type cast done with the C programming language. This has undefined behavior on C++.

sockaddr_in6* sa_in6 = (sockaddr_in6*)&ss;

To summarize the explanation from Sam I only have to tell the compiler to use the reference address &ss as pointer:

myns::Ssockaddr_storage2 ss;
sockaddr_in6* sa_in6 = (sockaddr_in6*)(::sockaddr_storage*)&ss;
std::cout << "sin6_family = " << sa_in6->sin6_family << "\n";
Ingo
  • 588
  • 9
  • 21