Private functions should be, well, private. Let's learn how to break that! Understanding what's happening will provide you insight into how programming languages are implemented.
This party trick is attributed to LordMZTE. When I was discussing raw pointer manipulation on a live stream a few days' ago, they sent me a Rust playground link to explore. What I discovered was code doing something that I didn’t think was possible: call a function that’s private from outside of a module.
When the code from the example below is executed, it prints an interesting message:
Private function called!
Take a look at the source and see if you can figure out what’s happening:
#![allow(dead_code)]
pub mod foo {
pub fn public() {
println!("Public function called");
private();
}
fn private() {
println!("Private function called!");
}
}
pub fn main() {
// foo::public();
unsafe {
std::mem::transmute::<usize fn> ()>((foo::public as usize) + 64)();
}
}
So, how does it work? And where does 64 come from?
Video Recording
If you prefer videos - you can watch a recording of the stream.
Explanation
The magic happens here:
std::mem::transmute::<usize fn> ()>((foo::public as usize) + 64)();
This line asks Rust to treat the memory address of the foo::public
function as an unsigned integer, add 64 to that value, then treat that higher address as a function and call it.
Let’s break it apart to figure out how it works.
Functions are represented in memory as an array of bytes just like data. Unlike data, the CPU interprets them as code. To call the foo::private
function from outside of the foo
module, we need to find out where it is laid out in memory. The Rust compiler happens to lay functions right next to each other. So to find the start of foo::private
, we can start with the location of foo::public
and add its length.
Now to reveal a sleight of hand in the original example. I removed a call to the dbg!()
macro, which I had executed in a previous execution:
// finding the length of foo::public()
#![allow(dead_code)]
pub mod foo {
pub fn public() {
println!("Public function called");
private();
}
pub fn private() { // temporarily mark foo::private as public
println!("Private function called!");
}
}
pub fn main() {
dbg!((foo::private as usize) - (foo::public as usize));
}
If we run this new version in the playground, we’ll receive the length printed to the console:
[src/main.rs:16] (foo::private as usize) - (foo::public as usize) = 64
With the length handy, we now know what fn foo::private
refers to. As an implementation detail, the fn
keyword creates a function pointer. Function pointers are pointers that point to executable memory.
To treat a function pointer as an integer, the code uses <fn> as usize
. To treat a usize
as a memory address though, we need more powerful tools. The std::mem::transmute()
function is that tool. It is probably the most unsafe constructs in the Rust language. It instructs Rust to re-interpret data types. In our case, std::mem::transmute::<usize fn> ()>
asks Rust to interpret a usize
as a function pointer that takes 0 arguments and returns the “unit type” (()
).
Discussion
Is being able to call private functions a security hole?
No, not really. The program needs to be able to call private functions. We only knew which memory address to “call” because we had access to the source code. This enabled us to mark foo::private
as public and deduce the length of foo::public
.
That said, it is possible to scan a program’s address space looking for executable chunks. That’s exactly what viruses do. But that doesn’t mean that Rust programs can arbitrarily violate their own privacy guards.
What does “private” mean in the context of a programming language?
In some sense, privacy doesn’t exist. There is no guard watching modules to make sure that they’re not trespassing other modules. Privacy, at least within Rust as it’s currently implemented, is a compile-time construct. We can’t look inside the executable binary to find sections marked as private.
That said, programming language designers have the ability to define whatever compile-time they want. For example, the operating system has the ability to mark specific memory pages as illegal (look up “guard pages” for extra info). Attempts to access these specially marked sections of memory indicate a runtime fault and the process will be terminated. It would be possible to add one of these pages between private and public members of a module, but that would incur a memory overhead.
What’s in those 64 bytes?
If you’re interested in what is happening in those 64 bytes, then you can ask the playground to print out the assembly generated by rustc. Look for the playground::foo::public
section:
playground::foo::public:
subq $56, %rsp
leaq .L__unnamed_2(%rip), %rax
leaq .L__unnamed_3(%rip), %rcx
xorl %edx, %edx
movl %edx, %r8d
leaq 8(%rsp), %rdi
movq %rax, %rsi
movl $1, %edx
callq core::fmt::Arguments::new_v1
leaq 8(%rsp), %rdi
callq *std::io::stdio::_print@GOTPCREL(%rip)
callq playground::foo::private
addq $56, %rsp
retq
Here is foo::public
again. Can you align the source code its assembly?
pub fn public() {
println!("Public function called");
private();
}
Here are some hints for the curious. Many of the the opcodes are suffixed with q
meaning quad, or l
for long. Strings are passed to functions as memory addresses and the println!
macro desugars to calls to functions within core::fmt
.
Top comments (1)
There's a small typo in the code at the top:
should be
Otherwise, good article! :)