Why does my program crash in libloading's FreeLibrary when I use a dangling pointer instead of std::ptr::copy_nonoverlapping?

use libc::{size_t, wchar_t};

const MYDLL: &str = "mydll.dll";
const MAX_PATH: usize = 260;

pub fn test() -> Result<size_t, Box<dyn std::error::Error>> {
    let mut dll_path = std::env::current_dir().unwrap();
    dll_path.push(MYDLL);

    unsafe {
        // load library
        let lib: libloading::Library = libloading::Library::new(dll_path)?;
        
        // dangling pointers 
        let company = vec![1;MAX_PATH].as_mut_ptr();
        //println!("{:?}", company);

        let mut content: [u16; MAX_PATH] = [1; MAX_PATH];
        let company2 = &mut content as *mut _ as *mut wchar_t;
       
        // this line std::ptr::copy_nonoverlapping
        std::ptr::copy_nonoverlapping(company2, company, 300);

        println!("before Library close");
        
        lib.close()?;

        println!("after Library close");

        Ok(1)
    }
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
    std::env::set_current_dir(std::env::current_exe()?.parent().unwrap())?;

    println!("before call test");

    test()?;
    
    println!("after call test");
    
    Ok(())
}

E:\TestProject\rust\rust_load\rust_load>cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target\debug\rust_load.exe`
before call test
before Library close
error: process didn't exit successfully: `target\debug\rust_load.exe` (exit code: 0xc0000005, STATUS_ACCESS_VIOLATION)


if i use let mut company = [1; MAX_PATH].as_mut_ptr();will not crash,why crash in close,FreeLibrary

Writing to a dangling pointer is undefined behavior. A program with undefined behavior may do literally anything, and it may behave differently when compiled with different settings or different Rust toolchains.

For example, when you write to memory that has been freed, you might overwrite data that is owned by some other variable, causing problems in unrelated parts of your program (even in library code that you didn’t write).

By the way, even if this were not a dangling pointer, it would be undefined behavior because you are reading and writing 300 elements, but you only allocated space for MAX_PATH = 260 elements:

std::ptr::copy_nonoverlapping(company2, company, 300);

Also, note that u16 may not have the same size and alignment as wchar_t, making company2 an invalid pointer:

let mut content: [u16; MAX_PATH] = [1; MAX_PATH];
let company2 = &mut content as *mut _ as *mut wchar_t;

You could avoid all these problems by using safe Rust instead of unsafe Rust for most of the operations here. If you do use unsafe then you must not write code that has undefined behavior. In safe Rust, the compiler can check for undefined behavior and prevent it. In unsafe Rust, the programmer must know and follow all of the rules, all of the time.

3 Likes

As you can see from windbg's screenshot, the questioner's intention is for the program to crash when executing std::ptr::copy_nonoverlapping(company2, company, 300), not when executing lib.close()? The program should not crash when releasing dynamic libraries.

When program hits UB, "program should (not)" becomes meaningless anyway.

1 Like

One of the nasty properties of Undefined Behaviour is that the behaviour of the binary is completely unconstrained if an execution of the source code would invoke UB. This means that once you have shown that a code path containing UB is executed, you can't make any statements about the program's behaviour at all - there is no concept of "when" to reason with, since an execution of UB says that any part of the program, including parts of the program that come before the UB in program order, can do anything.

This is why Rust constrains itself to only permitting UB when you're in an unsafe block, because once you've got UB in your code, you can no longer reason about the program. And that makes us all very sad programmers, since reasoning about programs is what we do.

In practice, if you fix enough of the environment (OS, compiler version, CPU, compiler flags etc), you can sometimes reason a little bit about UB, because compilers don't tend to take full advantage of the freedom to do anything that UB is defined as giving them, and so for a tightly constrained system, you can reason about UB. However, you should not expect to be able to do this in the general case, since any change to the environment can invalidate everything about your reasoning (for example, if you reason based on the idea that the compiler optimizes within a crate, turning on LTO breaks your reasoning completely, as does changes to inlining thresholds).

3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.