DEV Community

Cover image for Calling a private Rust function from outside of its module
Tim McNamara
Tim McNamara

Posted on • Originally published at tim.mcnamara.nz

Calling a private Rust function from outside of its module

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)();
    }
}
Enter fullscreen mode Exit fullscreen mode

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)();
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

Here is foo::public again. Can you align the source code its assembly?

    pub fn public() {
        println!("Public function called");
        private();   
    }
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
thepuzzlemaker profile image
James [Undefined]

There's a small typo in the code at the top:

::<usize fn> ()>
Enter fullscreen mode Exit fullscreen mode

should be

::<usize, fn ()>
Enter fullscreen mode Exit fullscreen mode

Otherwise, good article! :)