Skip to content

Latest commit

 

History

History
45 lines (38 loc) · 5.55 KB

deliberate-ub.md

File metadata and controls

45 lines (38 loc) · 5.55 KB

When a crate explicitly acknowledges that what it does is UB, but prefers keeping that code over UB-free alternatives (or there are no UB-free alternatives), that is always a concerning sign. We should evaluate whether there truly is some use-case here that is not currently achievable in well-defined Rust, and work with crate authors on providing a UB-free solution.

Known cases of deliberate UB

Cases related to concurrency

  • crossbeam's AtomicCell "fallback path" uses a SeqLock, which is well-known to not be compatible with the C++ memory model. Specifically the problem is this non-atomic volatile read which can cause data races and hence UB. This would be fine if we either (a) adopted LLVM's handling of memory races (then the problematic read would merely return undef instead of UB due to a data race), or (b) added bytewise atomic memcpy and used that instead of the non-atomic volatile load. This is currently not UB in the LLVM IR we generate, due to LLVM's different handling of read-write races and because the code carefully uses MaybeUninit.
  • crossbeam's AtomicCell "fast path" uses the standard library Atomic* types to do atomic reads and writes of any type that has the right size. However, doing an AtomicU32 read (returning a u32) on a (u16, u8) is unsound because the padding byte can be uninitialized. (Also, pointer provenance is lost, so AtomicCell<*const T> does not behave the way people expect.) To fix this we need to be able to perform atomic loads at type MaybeUninit<u32>. The LLVM IR we generate here contains noundef so this UB is not just a specification artifact.
    Furthermore, compare_exchange will compare padding bytes, which leads to UB. It is not clear how to best specify a useful compare_exchange that can work on padding bytes, see the discussion here.
    The alternative is to not use the "fast path" for problematic types (and fall back to the SeqLock), but that requires some way to query at const-time whether the type contains padding (or provenance). (Or of course one can use inline assembly, but it would be better if that was not required.)
  • tokio causes race conditions between atomic and non-atomic loads which are not permitted; see this issue for details.

Cases related to aliasing

Former cases of deliberate UB that have at least a work-in-progress solution to them

  • Various offset_of implementations caused UB by using mem::uninitialized(), or they used &(*base).field or addr_of!((*base).field) to project a dummy pointer to the field which is UB due to out-of-bounds pointer arithmetic. The memoffset crate has a sound implementation that however causes stack allocations which the compiler must optimize away. This will be fixed properly by the native offset_of! macro, which is currently in nightly.
  • It used to be common to unwind out of extern "C" functions which is UB, see this discussions. This is fixed by extern "C-unwind", which is stable since Rust 1.71.