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 avoidingunsafe
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
to0_u16
will work as expected, but it would not work for the entire range of values ofu32
. 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.