Gotchas and footguns

Rust gotchas and footguns #

This section provides a checklist that can be used during manual Rust code reviews. The list represents common issues we have encountered during our audits. It is not comprehensive, but it is a good starting point to quickly bootstrap an audit.

For safe code #

  • Check string comparisons.
    • Often partial-match (starts_with, ends_with, contains) is used instead of equality.
    • Case (in)sensitivity of comparisons often results in issues.
  • Check string conversions to other data types (like Vec) and vice versa. These may come with UTF-8 encoding issues. Options for handling bytes that may not be valid UTF-8:
    • from_utf8 with unwrap—strict, panics on non-convertible data
    • from_utf8_lossy—lossy, replaces invalid UTF-8 sequences with U+FFFD (replacement character)
    • OsStr/OsString—for platform-native strings that may not be valid UTF-8 (e.g., paths, environment variables, and argv); it is not a UTF-8/bytes converter, and there is no portable way to get its raw bytes (as_bytes is Unix-only; as_encoded_bytes uses a non-portable encoding)
  • Verify that the with_capacity method of the Vec, HashMap, HashSet, and indexmap::IndexSet types (and possibly other types) is not called with user-controlled data. Large values can lead to denial of service.
  • Verify that users cannot create arbitrarily deep recursive structs. A drop of such a struct can lead to a stack overflow. (See “If a Tree Falls in a Forest, Does It Overflow the Stack?” for an example.)
  • Verify that std::process::exit is used sparingly. Calling this function causes a process to exit immediately, thereby sidestepping all registered drop handlers.
  • Verify that proper bounds checks are performed before array accesses. An out-of-bounds array access can lead to denial of service.
  • Verify that proper checks are performed before type conversions to prevent loss of precision (e.g., u64 to f64, as the mantissa of type f64 is only 53 bits wide).
  • Check that the number of fields passed into the serialize_struct method matches the actual number of serialized fields. Some serialization formats, such as serde-binary, could truncate the serialized data if the number of fields is incorrect. This would mean that deserializing the data would result in a different value than the original.
  • Review all methods and actions that may cause panics. Other sections of the Handbook describe tools that can help with reviewing operations that could lead to panics.
    • unwrap and expect (the most common panicking methods)
    • todo!, unimplemented!, assert! and unreachable! macros
    • Out-of-bounds accesses
    • Large allocations (MAX limits cause panics, OOM errors cause aborts)
    • String slicing at non-character boundaries
    • RefCell
      • This struct enforces borrowing rules at runtime, and borrow/borrow_mut calls may panic.
    • HeaderMap
      • This struct panics after more than 32,768 (2^15) elements are added.
    • Duration::from_secs_f{32,64} and Duration::new
      • Duration::from_secs_f{32,64} panics with negative inputs; Duration::new normalizes excess nanoseconds into seconds and panics only if that carry overflows the seconds counter.
  • Verify that keys aren’t mutated while inside a collection in a way that changes their hash and equality (HashMap) or ordering (BinaryHeap). Doing so is a logic error and can cause panics or incorrect results.
  • Verify that debug_assert! and other debug macros are not used for actual data validation. Such macros are removed from production builds.
  • Check uses of file descriptors
    • Verify that raw file descriptors are explicitly closed in all execution flow paths. Raw descriptors are not closed on Drop.
    • Verify that owned file descriptors are not closed two times: automatically on Drop and explicitly via the close method.
    • Review that uses of raw file descriptors are I/O safe.
  • Explicitly flush BufWriters to get flush errors; errors are ignored on automatic flushing when values are dropped.
  • Ensure that absolute paths are not used with PathBuf::join, as this may lead to path traversal issues.
  • Verify that functions used only in tests are guarded by #[cfg(test)].
  • Verify that each use of #![allow(...)] is justified and that #[allow(...)] is not used excessively.
  • Operator precedence of bitwise operators (&, ^, |) compared to comparison operators (==, !=) differs between Rust and C. This is something to be aware of when rewriting C code.
  • Verify that test-only Cargo features (like mocks) are not included in [dependencies] and are not part of the default feature set. Use cargo tree -e features to validate your project.
  • Review code against possible issues resulting from operating system interactions (see the C/C++ chapter for ideas). Any syscall, libc function call, and other interaction with the operating system should be checked against known gotchas.
  • Review the “Secure Rust Guidelines checklist”.

For unsafe code #

Common issues to check for in unsafe code are given below. For more information, read the Rustonomicon.

  • Any union access is unsafe in Rust. Verify that the union field that matches the underlying data is used.
  • Look for uses of libc APIs like memset or memcpy. Most of them can be replaced with safe Rust counterparts.
  • If #[repr(packed)] is used on a struct, then check that read_unaligned/ write_unaligned is used for unaligned fields.
  • Check for uses of std::mem::uninitialized and mem::zeroed.
  • Review uses of MaybeUninit to ensure that all calls to assume_init are preceded by initialization.
    • Ensure that dropping of partially initialized data is implemented correctly.
  • Check for uses of std::mem::forget.
  • Check for uses of transmute or cast from a non-mutable reference & to a mutable &mut (likely undefined behavior).
  • Review uses of static mut and recommend using synchronization instead.
  • Review uses of unsafe attributes like #[unsafe(no_mangle)].
This content is licensed under a Creative Commons Attribution 4.0 International license.