Safety and Undefined-Behavior in Rust

Rust is known to be a safety first language and that holds true most of the time but Rust also allows for an “escape-hatch” in the form of unsafe {}.

unsafe was introduced for the sake of flexibility and performance and it allows programmers to write code that override some of Rust’s guarantees, especially since unsafe isn’t verified by the borrow-checker (yet!).

Since unsafe is an advanced language feature, I recommend following the rust-nomicon whenever you are dealing with things that may produce any undefined behavior or are outside of the safety guarantees of the Rust type-system or borrow-checker.

Popular unsafe operations are:

  • Raw pointers
  • Transmutation between values of different types by byte reinterpretation
  • Dealing with manual memory management, writing a custom allocator, for example
  • Non-local data handling, i.e., handling data that your program doesn’t own
  • Any form of direct FFI (Foreign Function Interface)

DeepSource can detect various safety issues in your code, the following steps may come in handy to help resolve such issues:

  • Generally safety issues are raised when dealing with unsafe operations like the ones mentioned above. The best solution is try and avoid them. I know we all need to use them sometimes and I am with you on that but raw pointers are references to data without any consistency and synchronization guarantees. This makes them extremely dubious for use in larger projects, much less in critical parts of your code, without a complete understanding of them. FFI is one such situation where avoiding unsafe is impossible, because Rust cannot guarantee memory-safety of foreign code! Here’s a short Rust FFI Guide on using Rust with FFI.

  • The Rust Analyzer only highlights unsafe code as issues when they are likely to break contractual agreements between the compiler and the platform you are developing for, such as dealing with undefined behavior. In our tests, our lints are fairly accurate, so we recommend taking a deeper look at them as described in the issue description even if they seem to be part of working code.

  • The above generally means that the issues we highlight are critical errors in very-specific scenarios but may work fine rest of the time. Understanding & debugging such issues is difficult because they may often be hard to reproduce. Is is important to revisit such code despite it being accurate “most of the time”, because such issues can be critical

  • UB (undefined-behavior) are critical issues and can cause your code to break in unexpected ways or leave vulnerabilities that maybe really easy to exploit but tricky to find. Most security vulnerabilities are the caused by undefined-behavior such as out-of-bounds access, overflows and etc.

Some examples of seemingly valid unsafe code are:

  • Casting a *const to a *mut. All pointers are simply chunks of memory, and such casts may work most of the time, but platforms and compilers don’t guarantee a non-mut memory location would be in a writeable section of the memory. When using raw-pointers Rust type-system cannot provide such guarantees either, causing the program to break if the *const was on a read-only memory section.
  • Transmutation between two “non-same sized” values, especially down casting. Down casting can often produce the correct value owing to the fact the system never internally produces a larger value. Transmuting a 0_u32 to 0_u16 will work as expected, but it would not work for the entire range of values of u32. Down casting would output lower bits of the successive values, and minor mistakes such as reading the high bits over the low bits because of platform differences can further cause issues.

Collection of resources for Understanding Safety in Rust

1 Like