Is it possible to call a private function from outside its module in safe Rust?

Title says it all really. I know you should never do this, I'm just curious if safe Rust's enforcement is air tight. With unsafe code you can jump to any address so it's definitely possible there.

No you can't.

3 Likes

It can’t be called by name, but there are more roundabout ways. Some code within the privacy boundary could pass it as a function pointer to external code, for example. As far as I know, any such workaround will require the cooperation of some code inside the module.

2 Likes

Just had the same idea, see below.

mod m {
    fn private() {
        println!("I'm private.");
    }
    pub fn get_private(password: &str) -> fn() {
        if password == "sesam" {
            private
        } else {
            panic!("ACCESS DENIED!")
        }
    }
}

fn main() {
    let p = m::get_private("sesam");
    p();
}

(Playground)

2 Likes

Privacy in Rust relates to safety. Private fields of structs often must be inaccessible as otherwise safe code could modify – say – some pointers in ways that breaks assumptions that some unsafe code relied on. (Or put differently, unsafe code can rely on private fields not being accessible from safe code outside of where they are visible.)

The same limitations apply to all kinds of private items: private fields, private functions/methods, private traits and their implementations, private structs/enums and their trait implementations.

I wouldn’t even know any reliable way how unsafe code can call a private function. As an extreme (yet probably very common) example, private functions need not exist at all in the compiled and optimized code. If they’re small enough or only used once (or however LLVM decides these things), they might be inlined at every call-site, and the original function literally doesn’t exist anymore. If they aren’t used at all, they typically won’t exist anymore.

Even if they are used, if their use-sites are less general than their signature allows (e.g. one argument always stays the same) then their ABI might be changed to skip and hard-code such an argument. Their ABI might also change depending on the definition of the function: E.g. if they take a &i32 and immediately dereference it (so they don’t inspect the address of the &i32), their ABI might be changed to take i32 by value… etc.

(In light of previous answers, of course such optimizations can no longer happen, if you ever create and use a function pointer derived from such a function. (And the usage of that function pointer can’t be optimized out.))

5 Likes

Maybe in some cases, an unsafe interface could make sense (but no idea if there's really such a use case):

mod m {
    fn private() {
        println!("I'm private.");
    }
    pub unsafe fn private_unsafe() {
        private()
    }
}

fn main() {
    // SAFETY: I know what I'm doing.
    unsafe { m::private_unsafe() }
}

(Playground)

1 Like

Some compiler explorer examples:

All calls are the same, so the argument is hard-wired: Compiler Explorer

Different calls, however it’s still passing the i32 by value: Compiler Explorer

Create a function pointer indirectly (i.e. using an intermediate closure); the calls still happen by-value: Compiler Explorer

Create a function pointer directly and expose it in a public function; now the calls happen by actually passing an &i32: Compiler Explorer

Make the function that creates the function pointer private; all usages of the function pointer can be optimized away to a direct call to foo and the ABI is by-value again, passing i32s: Compiler Explorer


If you remove the #[inline(never)], then foo doesn’t exist anymore at all in all of the above examples except for the second-to-last one where a function pointer is created directly, and exposed via a public function. (And of course, even in that example, foo every other usage of foo still gets inlined.)


By the way: With regards to those optimizations, public functions in private modules (that aren’t publicly re-exported) seem to behave the same as private functions.

1 Like

That's undefined behaviour, at that point everything becomes possible and nothing guaranteed.

1 Like

If the function comes from a different crate, you can sometimes be sneaky and link to the function directly using its mangled symbol.

// code in some other crate

pub mod hidden_internals {
    fn print(msg: &str) {
        println!("{msg}");
    }

    pub fn high_level_function() {
        print("Hello, World!");
    }
}


// in your crate

fn main() {
    hidden_internals::high_level_function();

    extern "Rust" {
        #[link_name = "_ZN15some_dependency16hidden_internals5print17h3a4d9ee4c28a1bd6E"]
        fn print(msg: &str);
    }

    unsafe {
        print("Skipped the high level function");
    }
}

This is pretty brittle though (and really easy to screw up) because it relies on how the some_dependency::hidden_internals::print()'s name was mangled, and rustc adds a hash to the end.

2 Likes

What is used to generate the hash?

It's derived from the internal data structures representing a crate. Probably the Hash implementation for the HIR or something similarly unstable.

I found that symbol name by using nm on the compiled binary and grepping for the names I wanted.

The rustc-demangle crate has logic for parsing mangled symbols from rustc, so in theory you could use it to programmatically find the mangled name of a particular item.

2 Likes