Have you ever needed to use some obscure function only for it to be private? If you were in a higher-level language you could just use reflection, but it’s in a third party library and you can’t modify that.
Enter public_cast. This abomination can see everything except constructors and destructors.
Did I mention it’s 100% legal and standard-compliant?
If that makes you uncomfortable? Good, you’re still sane. Leave now before I decide to vary your invariants.
The ideas behind this come from Logan Smith’s Cursed Casts, and this implementation is heavily based on his.
The entry point
There is one place in all of C++ where access checking is ignored:
The usual access checking rules do not apply to names in a declaration of an explicit instantiation or explicit specialization.
[temp.spec.general]/6
In short, as long as we explicitly instantiate a template, we can reference private members at will. The tricky part then becomes getting this data out of the template, to a place where we can access it anywhere. Once instantiated we can’t directly reference that template, because then we would have to reference the private member.
class C
{
private:
int myVar;
}
template<auto Member>
struct AccessBreaker
{
static const inline auto val = Member;
};
template struct AccessBreaker<&C::myVar>; //No error
int main()
{
C c;
//Error: myVar is private
c.myVar = 3;
//Error: we're no longer declaring an explicit instantiation, we're using one
auto ptr = AccessBreaker<&C::myVar>::val;
c.*ptr = 3;
}We can use a triple assignment to exfiltrate a pointer to the private member to another template, and that downstream template only requires a pointer-to-member as a type parameter. As long as both the owner and the member’s declared type are public, we now have free access.
template<typename Ty, typename Key>
struct MemberAccessor
{
static inline Ty val;
};
template<auto Member, typename Key>
struct AccessBreaker
{
static const inline auto val = MemberAccessor<decltype(Member), Key>::val = Member;
};Now whenever we instantiate AccessBreaker with a private member and a key, we can use MemberAccessor to look up that private member later provided we have its type and the key:
struct _C_myVar {};
template struct AccessBreaker<&C::myVar, _C_myVar>;
int main()
{
C c;
auto ptr = MemberAccessor<int C::*, _C_myVar>::val;
c.*ptr = 3; //No error
}Minimizing room for error
Unfortunately this is easy to break. If the type changes, we aren’t guaranteed much safety–our plumbing might start implicitly casting or otherwise fail invisibly. We can go a step further and make our accessor easier to read by storing our type when we declare our AccessBreaker, and add some type safety with static asserts.
template<typename Key>
struct MemberType {};
template<auto Member, typename Key>
struct AccessBreaker
{
static const inline auto val = MemberAccessor<decltype(Member), Key>::val = Member;
static_assert(std::is_same_v<typename MemberType<Key>::ptr_t, decltype(Member)>); //Type safety
};
struct _C_myVar {};
template<> struct MemberType<_C_myVar> { using ptr_t = int C::*; }; //Lets us quickly lookup a member's type
template struct AccessBreaker<&C::myVar, _C_myVar>;In Logan Smith’s original video, he describes the syntax required to use a public_cast’ed private member as “so long to type, you’ll plenty of time to rethink your life”. But this is the Cursed Code series, an idea looking bad on paper is no deterrent. If we want to get rid of some of the unwieldiness, we can clean it up with either a type alias or a macro:
template<typename Key>
constexpr auto public_cast = MemberAccessor<typename MemberType<Key>::ptr_t, Key>::val;
int main()
{
C c;
c.val = 3; //Error: private member
c.*public_cast<_C_myVar> = 3; //No error
}#define public_cast(Key) typename MemberAccessor<typename MemberType<Key>::ptr_t, Key>::val;
int main()
{
C c;
c.val = 3; //Error: private member
c.*public_cast(_C_myVar) = 3; //No error
}Uses
This same technique will work on functions. Casting to a private inherited class doesn’t even need public_cast: C-style casts ignore inheritance access specifiers. However, C-style casting is dangerous enough that you may want to wrap it and add some asserts to prevent an accidental implicit reinterpret_cast or const_cast.
With this in hand, you can get access to internals of third-party libraries as long as the field you want is in the headers. Library maintainers, if you want to avoid this kind of intrusion you can use the pImpl idiom or a handle system, although that won’t stop a dedicated reverse engineer. If header-only is a requirement, enclosing the implementation in an anonymous namespace can also work.
Last but certainly not least, the reason I did all this in the first place: my reflection library, Sanable Type Interrogation eXtensions. In order to support Sanable Engine’s goal of hot-reloading, STIX uses process of elimination on any user-declared fields in order to infer the locations and values of vtable pointers. It can then update them to the correct value on reload, identify objects, or change their type at runtime.
Huge thank you to my friend Nick, who sent me Logan’s video with perfect timing as I was first developing STIX. This was a problem I had been chewing on for weeks, and was about to to use a much worse solution to.
Further improvements
There are a few cases where this public_cast implementation breaks:
- When dealing with constructors or destructors that are private, because we need a pointer for public_cast and it’s illegal to get a pointer to a constructor or destructor.
- When the type of a variable is private, or when dealing a function that takes/returns private types. We can’t exfiltrate a type with public_cast, only members that can be referenced through pointers. Pointers and references to forward-declared types are still OK.
- When the owning type is private or in an anonymous namespace, since we need to be able to reference it to create a pointer-to-member. For example, we couldn’t write
int B::C::*if type C were hidden by B. - When the referenced type is defined in a source file, but public_cast is being used outside that source file
- public_cast can’t be declared inside a class, because only functions can have explicit complete specializations within classes. It also doesn’t work inside namespaces out-of-the-box, but we can make it work by redeclaring our plumbing.
Ideally we’d be able to make our AccessBreaker instantiation associate that key with the correct type in MemberType. Unfortunately I was only able to make some small improvements on Logan’s work, such as making this more maintainable by incorporating it into automated codegen, type checking, and cleaning up the interface.