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
- I learned about this technique from litb's post Access to private members. That's easy!
- In litb's follow up post Access to private members: Safer nastiness., there's a technique using tag structs which allows this technique to scale better.
- Herb Sutter's classic Uses and Abuses of Access Rights discusses related techniques.