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.