Stealing Private Variables in C++

This week I learned that C++'s private is not actually so private:

#include <iostream>
#include <functional>

struct MyStruct {
   MyStruct(int secret) : secret_(secret) {}
private:
   int secret_;
};

using SecretAccessor = int MyStruct::*;

SecretAccessor get_secret_accessor();

template <SecretAccessor Instance> struct Robber {
    friend SecretAccessor get_secret_accessor() {
        return Instance;
    }
};
template struct Robber<&MyStruct::secret_>;

int main() {
    MyStruct my_struct(42);
    std::cout
        << "Proof: "
        << std::invoke(get_secret_accessor(), my_struct)
        << std::endl;
}

And of course:

$ g++ --std=c++17 ./proof.cpp
$ ./a.out
Proof: 42

So what's going on?

First, consider the type SecretAccessor:

using SecretAccessor = int MyStruct::*;

This is a MyStruct member function pointer. Member function pointers work just like function pointers, but need an instance (in this case of MyStruct) to be evaluated.

In C++17, we can use std::invoke(get_secrets_accessor(), my_struct) to evaluate the member function pointer on the instance my_struct. For older versions of C++, we have to use the rather opaque syntax my_struct.*get_secret_accessor().

Next, note that &MyStruct::secret_ is an instance of SecretAccessor. If we get this function pointer, we can use it to access secret_ on an arbitrary MyStruct instance.

There's only one problem: secret_ is a private member variable. Therefore, &MyStruct::secret_ needs to be given to us by MyStruct or by one of its friends (of which it has none—sad).

Enter: the Robber.

It turns out that during explicit template specialization, access rules are not enforced. Yes, you read that right. Yes, that's in the C++ spec. No, it's not undefined behavior.

Since the Robber is defined as:

template <SecretAccessor Instance> struct Robber {
    friend SecretAccessor get_secret_accessor() {
        return Instance;
    }
};

When we have the explicit template instantiation:

template struct Robber<&MyStruct::secret_>;

We create an instance of Robber which looks like:

struct Robber {
    friend SecretAccessor get_secret_accessor() {
        return &MyStruct::secret_;
    }
};

If we wrote this out directly, it would be a compile error. (Robber isn't a friend of MyStruct!) But since we created it using an explicit template instantiation, we're all good.

But wait, there's more!

Although we've now created a version of Robber which has access to the internals of MyStruct, we can't directly access this version of Robber.

For instance, if we wrote

int main() {
    Robber<&MyStruct::secret_>::get_secret_accessor();
}

we'd get a compile error: we can't use &MyStruct::secret_ in main().

However, declaring get_secret_accessor() as a friend function moves it out of the Robber<&MyStruct::secret_> namespace and into the surrounding namespace. Thus, all we need to do is add a forward declaration:

SecretAccessor get_secret_accessor();

And we can run the code from the Robber explicit specialization without referencing how Robber was specialized!

int main() {
    MyStruct my_struct(42);
    std::cout
        << "Proof: "
        << std::invoke(get_secret_accessor(), my_struct)
        << std::endl;
}

And there you have it: we can now access a private member variable without violating the C++ spec!

Prior Art