Unsafe code using dylib

I've decided to learn a bit about rust's dynamic libraries (#![crate_type="dylib"]) and it seems to me that using them can be very very unsafe, even though no "unsafe" blocks are involved.

This is opposed to FFI dynamic linking that is always wrapped in "unsafe" (and it makes sense!)

I couldn't find any proper documentation regarding this issue or on the topic of dylibs in general. Quick google search also gave no indication that dylibs are potentially unsafe.

Consider the following example (I used rustc 1.75.0 (82e1608df 2023-12-21) on windows 11, commands are from PowerShell 7, but you can rewrite them for cmd):

file structure:

unsafebin\
    unsafebin.rs
unsafelib\
    unsafelib_1.rs
    unsafelib_2.rs
    unsafelib_3.rs
// unsafebin.rs

extern crate unsafelib;

fn main() {
    let res = func();
    dbg!(res);
}

fn func() -> &'static str {
    let s = String::from("Unsafe string");
    dbg!(unsafelib::return_str(&s))
}
// unsafelib_1.rs
#![crate_type="dylib"]

#[no_mangle]
pub fn return_str(input: &str) -> &'static str {
    "This is safe"
}
// unsafelib_2.rs
#![crate_type="dylib"]

#[no_mangle]
pub fn return_str(input: &str) -> &str {
    input
}
// unsafelib_3.rs
#![crate_type="dylib"]

#[no_mangle]
pub fn return_str(x: i32) -> &'static str {
    dbg!(x);
    "Signature changed"
}
> # compile the dynamic library
> rustc --edition 2021 --crate-name unsafelib --out-dir unsafelib\out -C prefer-dynamic unsafelib\unsafelib_1.rs
> # compile the binary that uses the dynamic library
> rustc --edition 2021 --out-dir unsafebin\out -L "unsafelib\out" unsafebin\unsafebin.rs
> # add compiled library to PATH for dynamic linking
> $env:PATH = "$($pwd)\unsafelib\out;$($env:PATH)"
> # add rust libraries (std and others) to PATH for dynamic linking
> $env:PATH = "$(rustc --print=target-libdir);$($env:PATH)"
> # run executable (everything is ok so far)
> unsafebin\out\unsafebin.exe

[unsafebin\unsafebin.rs:13] unsafelib::return_str(&s) = "This is safe"
[unsafebin\unsafebin.rs:8] res = "This is safe"

> # now compile a different version of the library with different lifetimes in the function's signature
> rustc --edition 2021 --crate-name unsafelib --out-dir unsafelib\out -C prefer-dynamic unsafelib\unsafelib_2.rs
> # run executable - this is not memory safe!
> unsafebin\out\unsafebin.exe

[unsafebin\unsafebin.rs:13] unsafelib::return_str(&s) = "Unsafe string"
[unsafebin\unsafebin.rs:8] res = "Unsafe string"

> # here, res is a dangling pointer. Obviously, you might get different results like gibberish or a crash.

> # even though results look fine in this case (it's not always like that),
> # we know the code is unsafe because if we attempt to compile it we will get a borrowck error:
> rustc --edition 2021 --out-dir unsafebin\out -L "unsafelib\out" unsafebin\unsafebin.rs

error[E0515]: cannot return value referencing local variable `s`
  --> unsafebin\unsafebin.rs:12:5
   |
12 |     dbg!(unsafelib::return_str(&s))
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^--^^
   |     |                          |
   |     |                          `s` is borrowed here
   |     returns a value referencing data owned by the current function
   |
   = note: this error originates in the macro `dbg` (in Nightly builds, run with -Z macro-backtrace for more info)

error: aborting due to previous error

For more information about this error, try `rustc --explain E0515`.

> # Another example, where the function's argument is of a different type
> rustc --edition 2021 --crate-name unsafelib --out-dir unsafelib\out -C prefer-dynamic unsafelib\unsafelib_3.rs
> # Running the executable results in gibberish (variable x)
> unsafebin\out\unsafebin.exe

[unsafelib\unsafelib_3.rs:6] x = 1387282608
[unsafebin\unsafebin.rs:13] unsafelib::return_str(&s) = "Signature changed"
[unsafebin\unsafebin.rs:8] res = "Signature changed"

While it is hard to create this situation on accident (you'd have to use no_mangle, change the signature of a public function and not attempt to compile the binary after the changes), but it's also not impossible. And at the very least, this feels like a security risk.

I understand why this happens and how to avoid this, so I guess my questions are:

  1. Is this a bug? If yes, would it make sense to report it? If no, then is there any documentation about dylibs and their safety that goes deeper into how to use them, and how does it make sense within Rust's guarantee/philosophy of safety/correctness.
  2. Is it only a windows thing or would the same happen on other platforms?

Yes, dynamic linking in general may load symbols that are not the expected ones. There are plans to make dynamic linking between Rust crates more robust (see for example https://github.com/rust-lang/rfcs/pull/3435) but in the worst case the dynamically linked library may not even be produced by rustc but by a malicious actor trying to imitate it, in which case you really can't guarantee that the types match up.

Also, #[no_mangle] alone is unsafe even without using dylibs, see for example #[no_mangle] is unsafe · Issue #28179 · rust-lang/rust · GitHub There are plans to add the concept of unsafe attributes (and thus eventually make no_mangle one of them), see unsafe attributes by RalfJung · Pull Request #3325 · rust-lang/rfcs · GitHub

1 Like