This article is about the unsound api which I found in owning_ref. Owning_ref is a library that has 11 million all-time downloads and 60 reverse dependencies.

Overview

Unsoundness in owning_ref

This article is about the unsound api which I found in owning_ref. Owning_ref is a library that has 11 million all-time downloads and 60 reverse dependencies. In this article I will introduce owning_ref, explain the problems and discuss solving them.

tl;dr and some links

I found a few unsound functions in the owning_ref library. Two of them have been found in the past but weren't fixed. The issue is here and my fork fixing the problems is here with a pull request here. The current status regarding unsafety in owning_ref is detailed here. The examples of unsoundness can be found in this repository in the src folder and are runnable by cargo run.

Introduction to owning_ref

This section is an introduction to owning_ref. If you already know the library, you can skip it. Owning_ref is a nice library with a very cool idea. It allows you to keep a reference bundled together with its owner. This means that like an owned value, it can have an unlimited lifetime. And like a reference, it can be processed without reallocation.

In the next section I will go over the most important functions from the library that are needed in order to understand the article. In order to learn more, here's owning_ref's documentation.

Reading the api

More specifically, an owning reference of type OwningRef consists of an owned value of type O and a reference of type &T. There's also a version for a unique reference type &mut T which is OwningRefMut . First of all, since these types represent references, they implement Deref and DerefMut:

impl
   Sized> 
   Deref 
   for 
   OwningRef
   

    impl
    
     Sized> 
     Deref 
     for 
     OwningRefMut
     

      impl
      
       Sized> 
       DerefMut 
       for 
       OwningRefMut
       
      
     
    
   
  

By "processing" in the introduction section I mean that you can call map-like methods, like Ref::map, that have these signatures:

impl OwningRef
    {
  
   pub 
   fn 
   map
   
    Sized>(
    self, f: F) -> OwningRef
    
    
     where
        O: StableAddress,
        F: 
     FnOnce(
     &T) -> 
     &U;

  
     pub 
     fn 
     map_with_owner
     
      Sized>(
      self, f: F) -> OwningRef
      
    
       where
        O: StableAddress,
        F: for<
       'a> 
       FnOnce(&
       'a O, &
       'a T) -> &
       'a U;
}
      
     
    
   
  

And mutable counterparts:

impl OwningRefMut
    {
  
   pub 
   fn 
   map
   
    Sized>(
    self, f: F) -> OwningRef
    
    
     where
        O: StableAddress,
        F: 
     FnOnce(
     &
     mut T) -> 
     &U;

  
     pub 
     fn 
     map_mut
     
      Sized>(
      self, f: F) -> OwningRefMut
      
    
       where
        O: StableAddress,
        F: 
       FnOnce(
       &
       mut T) -> 
       &
       mut U;
}
      
     
    
   
  

StableAddress is an unsafe marker trait that encapsulates the following idea: when our owning reference moves, our owner moves with it. This usually invalidates the original reference that was borrowed from the owner. However, we want it to stay valid. For example, we know that when a Box is moved, its inner value is still at the same place. So, a trait StableAddress encapsulates this guarantee.

(This currently conflicts with StackedBorrows, and thus optimizations might change the meaning of code because of this. However, this is outside the scope of the article)

O: StableAddress is required when creating new owning references, and the owner is immediately dereferenced on creation, to prevent the reference from pointing directly to the owner. These are the signatures:

impl OwningRef
    {
  
   // Create a new `OwningRef`. Requires `O: StableAddress` to ensure that moving the owner
  
   // doesn't invalidate the reference.
  
   pub 
   fn 
   new(o: O) -> 
   Self
    
   where
        O: StableAddress,
        O: Deref
   
    ;
}


    impl 
    OwningRefMut
    
      {
  
     // Create a new `OwningRef`. Requires `O: StableAddress` to ensure that moving the owner
  
     // doesn't invalidate the reference.
  
     pub 
     fn 
     new(o: O) -> 
     Self
    
     where
        O: StableAddress,
        O: DerefMut
     
      ;
}
     
    
   
  

In addition, here are a few more important methods from owning_ref, which access the owner. Their signatures are:

impl OwningRef
    {
  
   pub 
   fn 
   as_owner(
   &
   self) -> 
   &O;

  
   pub 
   fn 
   into_owner(
   self) -> O;
}
  

And mutable counterparts:

impl OwningRefMut
    {
  
   pub 
   fn 
   as_owner(
   &
   self) -> 
   &O;

  
   pub 
   fn 
   as_owner_mut(
   &
   mut 
   self) -> 
   &
   mut O;

  
   pub 
   fn 
   into_owner(
   self) -> O;
}
  

Unstable Address

map_with_owner is weird. The callback's signature is FnOnce(&O, &T) -> &U. What if I just give back the owner's reference? The owner could just be moved away.

fn unstable_address() {
    println!("unstable_address");
    let ow_ref = OwningRef::new(Box::new(5));
    let new_ow_ref = ow_ref.map_with_owner(|owner, _my_ref| owner);
    println!("Reading memory that was moved from: {}", *new_ow_ref);
}

After all of that hard work to prevent the reference from pointing to the owner by requiring O: StableAddress!

Reading the owner

The first thing that occured to me was this: How come we can change the owner while we still have a unique reference into the owner? Even though we can't access the reference at the exact same time we're mutating the owner, this is still sketchy. And sure enough, it's unsound. One way to show unsoundness is to cause the owner to deallocate, so that the reference now points to invalid memory.

// Original credit: [https://github.com/comex/owning_ref_bug/blob/master/src/main.rs]
fn ref_mut_as_owner_mut() {
    println!("ref_mut_as_owner_mut");
    let mut ow_ref = OwningRefMut::new(Box::new(5));
    *ow_ref.as_owner_mut() = Box::new(9);
    println!("Reading deallocated memory: {}", *ow_ref);
}

It turns out this unsoundness was already found beforehand (but not fixed yet).

So we've determined OwningRefMut::as_owner_mut is unsound. Is removing it enough? Well, say we try to use OwningRefMut::as_owner instead. It's only a shared reference to the owner. But it's "aliasing" a mutable reference, so it's still sketchy. Let's try to do the same thing: use the shared reference to the owner in order to deallocate whatever our reference is pointing at. We want a shared reference to the owner to deallocate something. Sounds like a case for interior mutability:

fn ref_mut_as_owner_failed() {
    use core::cell::RefCell;
    println!("ref_mut_as_owner");
    let ow_ref = OwningRefMut::new(Box::new(RefCell::new(Box::new(5))));
    let new_ow_ref = ow_ref.map_mut(|cell| &mut cell.borrow_mut());
    // We could call `as_owner_mut`, but we already saw that `as_owner_mut` is inherently unsound.
    *new_ow_ref.as_owner().borrow_mut() = Box::new(9);
    println!("Reading deallocated memory: {}", *new_ow_ref);
}

So that doesn't work, since we can't manage to convince OwningRefMut::map_mut to move our reference into the RefCell instead of pointing at it from the outside... However, inside the map, we have unique access to the cell, which allows us to use RefCell::get_mut:

// Original credit: [https://github.com/comex/owning_ref_bug/blob/master/src/main.rs]
fn ref_mut_as_owner() {
    use core::cell::RefCell; // `Cell` and any other kind of cell also works
    println!("ref_mut_as_owner");
    let ow_ref = OwningRefMut::new(Box::new(RefCell::new(Box::new(5))));
    let new_ow_ref = ow_ref.map_mut(|cell| &mut **cell.get_mut());
    // We could call `as_owner_mut`, but we already saw that `as_owner_mut` is inherently unsound.
    *new_ow_ref.as_owner().borrow_mut() = Box::new(9);
    println!("Reading deallocated memory: {}", *new_ow_ref);
}

This unsoundness was also already found beforehand (but not fixed yet). But now we get to more unsoundness that hasn't been found yet.

Let's say we also rule out the OwningRefMut::as_owner method as inherently unsound. Is there any other way we can exploit this? We can try to use the OwningRef::as_owner method instead:

fn ref_mut_to_ref() {
    use core::cell::RefCell; // `Cell` and any other kind of cell also works
    println!("ref_mut_to_ref");
    let ow_ref = OwningRefMut::new(Box::new(RefCell::new(Box::new(5))));
    let new_ow_ref = ow_ref.map(|cell| &**cell.get_mut());
    // We could call `as_owner_mut`, but we already saw that `as_owner_mut` is inherently unsound.
    // Have to convert to `OwningRef` In order to use `OwningRef::as_owner`
    // instead of `OwningRefMut::as_owner`.
    *new_ow_ref.as_owner().borrow_mut() = Box::new(9);
    println!("Reading deallocated memory: {}", *new_ow_ref);
}

In addition, another method that enables reading the owner is OwningRef::map_with_owner, which doesn't have any equivalent for OwningRefMut (I wonder why?). It goes like this:

fn ref_mut_to_ref_2() {
    use core::cell::RefCell; // `Cell` and any other kind of cell also works
    println!("ref_mut_to_ref_2");
    let ow_ref = OwningRefMut::new(Box::new(RefCell::new(0)));
    let new_ow_ref = ow_ref.map(|cell| &mut *cell.get_mut());
    // We could call `as_owner_mut`, but we already saw that `as_owner_mut` is inherently unsound.
    // Have to convert to `OwningRef` In order to use `OwningRef::map_with_owner`.
    new_ow_ref.map_with_owner(|owner, x| {
        // Now we have both mutable and immutable access to the same value
        println!("First read of reference {}", x);
        *owner.borrow_mut() = 1;
        println!("Second read of reference {}", x);
        x
    });
}

Now we don't have to do a deallocation to show definite unsoundness, because we have a unique reference that's definitely aliasing another reference.

Safety discussion

So, what do we do now? What is and isn't safe? How can we fix this?

First of all, fixing the problems described in this library won't make owning_ref completely clean and safe. This is because of this Conflict with StackedBorrows' rules that might cause your program to change meaning when compiled with optimizations. However, this is out of the scope of this article: this is about plain unsoundness, no optimization and tricky language semantics needed.

Unstable address

The "unstable address" problem by itself can be fixed quite easily. Just don't give out a reference to the owner! I would be surprised if anyone really needed a direct reference to the owner itself in order to map their reference, instead of just using whatever the owner was pointing at. The owner is required to be a Deref value for the owning_ref to be created in the first place. So, just replace the old function with this new version:

pub fn map_with_owner
   Sized>(
   self, f: F) -> OwningRef<
   't, O, U>
    
   where O: StableAddress + Deref,
        F: for<
   'a> 
   FnOnce(&
   'a O::Target, &
   'a T) -> &
   'a U;
  

And now the new reference can't point to the owner anymore.

OwningRefMut::{as_owner, as_owner_mut}

The OwningRefMut::{as_owner, as_owner_mut} problems are more difficult, but in the end, straightforward. OwningRefMut means it contains a unique reference that (may) borrow from the owner. That means that nothing else can access the owner. That's it. There's no workaround. The solution has to be removing these two functions.

OwningRef::as_owner

So how come we can cause the same unsoundness using OwningRef::as_owner? OwningRef has an invariant: it contains a shared reference that may only borrow immutably from the owner. So clearly, we can share the owner too. So OwningRef::as_owner and OwningRef::map_with_owner (the new version of course) are valid according to this invariant. What went wrong?

When we convert from a OwningRefMut to an OwningRef , we know that that reference might be borrowing mutably from the owner. Converting to an OwningRef necessitates converting our reference from mutable to immutable. Intuitively, this ensures the reference now only immutably borrows from the owner, which is necessary to comply with OwningRef's invariant.

But that isn't true! This is the heart of the issue. Say we do

use std::cell::RefCell;
let mut o = RefCell::new(7);
let immutable_ref : &i32 = &*(&mut o).get_mut();
&o; // read from the owner
&immutable_ref; // ensure `immutable_ref` is still live here

Which is basically equivalent to the unsoundness examples. immutable_ref was created through &mut o. Even though it's an immutable reference, it still borrows mutably from o. And indeed, rust complains that this code is unsafe.

The OwningRef::as_owner, OwningRef::map_with_owner functions assume that the owner may only be shared. But the conversions from OwningRefMut into OwningRef imply that the reference borrows from the owner mutably. And these two assumptions contradict each other.

Three options

I can see three options for fixing this unsoundness:

  1. Disallow accessing the owner for OwningRef as well, but keep the conversions.
  2. Allow acessing the owner for OwningRef, but disallow conversions (The conversions are the OwningRef : From > instance that is too strict anyway, and [OwningRefMut]::{map, try_map}`).
  3. Weird compromise: Have two distinct types for OwningRef depending on if the owner is mutably borrowed.

The question is really about the meaning of OwningRef. The first option says, "The reference in OwningRef may borrow from the owner either immutably or mutably". The second option says "The reference in OwningRef may only borrow from the owner immutably". Really, these two interpretations are two distinct types, and the third option says, "These are both possible, let's duplicate all of our code twice just in case".

My fork

I've created a fork to create a version that fixes these problems. First of all, I'm building on this pull request, that already solves the problems that were already found: it removes OwningRefMut::{as_owner, as_owner_mut}, and adds a lifetime to fix the tricky existing unsoundness in map.

I've decided to choose the second option: OwningRef::as_owner, and disallow converting from OwningRefMut to OwningRef. In order to preserve backwards compatibility as much as possible, I've kept the old functions as unsafe, #[deprecated] functions, although I'm not sure if that's really necessary.

Previous status

Some unsoundness issues in owning_ref were already found in the past. These were fixed:

This is the original finding of the OwningRefMut::{as_owner, as_owner_mut} unsoundness that I rediscovered:

which isn't fixed at the time of this writing.

And these are also not fixed at the time of this writing:

  • Conflict with StackedBorrows' rules that turns correct code into incorrect code when optimizing. It seems impossible or very hard to fix without language-level intervention, and it's uncertain if Rust should intervene.
  • map is unsound because of very tricky implicit bounds.

And also, this is the First claim I found that OwningRefMut::as_owner_mut is unsound, although the given example wasn't really an example of unsoundness. The issue was ignored for some time and then the OP closed his own issue.

Conclusion

In conclusion:

  • Owning_ref is a cool library that implements a useful Rust primitive. It's popular: it has 11 million all-time downloads and 60 reverse dependencies. It's also used in rustc as part of the rustc data structures library.

  • Writing safe rust primitives is hard. We in the Rust community should encourage developers to check libraries for safety and review each other's code. I welcome feedback on my own rust primitive library.

  • Users of this library (and all Rust libraries) rely on its safety. If safety issues are uncovered it's important to give them the attention they deserve and make sure they get fixed. Safety problems inOwning_ref were discovered almost 2 years ago but have yet to be fixed.

  • I submitted a PR which is based on this fork and fixes the issues I discussed here. I hope this will help the maintainers of OwningRef implement a solution.

You might also like...
A digital audio workstation for all platforms that is libre, gratis, and awesome

🎹 Dawsome [WIP] A DAW build on React + Rust + WASM Available Scripts In the project directory, you can run: pnpm dev Tauri will open a new window wit

Create virtual serial ports, connect them to physical serial ports, and create routes between them all.

Virtual Serial Port Router (vsp-router) Create virtual serial ports, connect them to physical serial ports, and create routes between them all. vsp-ro

This contract is to provide vesting account feature for the both cw20 and native tokens, which is controlled by a master address

Token Vesting This contract is to provide vesting account feature for the both cw20 and native tokens, which is controlled by a master address. Instan

Sampling profiler and tracer for Ruby (CRuby) which runs in BPF

rbperf rbperf is a low-overhead sampling profiler and tracer for Ruby (CRuby) which runs in BPF Build To build rbperf you would need a Linux machine w

🦀 Rust library for printing human readable, relative time differences

🦀 Rust library for printing human readable, relative time differences

Rust library for one-time async initialization

async-lazy An async version of once_cell::sync::Lazy, std::sync::OnceLock or lazy_static. Uses tokio's sychronization primitives. This crate offers an

Utilities to gather data out of roms. Written in Rust. It (should) support all types.

snesutilities Utilities to gather data out of roms. Written in Rust. It (should) support all types. How Have a look at main.rs: use snesutilities::Sne

Generate a THIRDPARTY file with all licenses in a cargo project.

cargo-bundle-licenses Bundle all third-party licenses into a single file. NOTE This tools is not a lawyer and no guarantee of correctness can be made

A cli utility written in Rust that allows fetching all the labels of a project, save those as a YAML file

A cli utility written in Rust that allows fetching all the labels of a project, save those as a YAML file that you can easily edit or save as backup and apply a saved preset to new repositories.

Owner
Noam Ta Shma
Noam Ta Shma
The Dutch secret service (AIVD) has a yearly puzzle challenge around Christmas

AIVD kerstpuzzel 2021 18 solver The Dutch secret service (AIVD) has a yearly puzzle challenge around Christmas, called the 'AIVD kerstpuzzel'. This re

Tim Visée 2 Mar 7, 2022
Ferris has to escape the creepers using a modified version of Dijkstra called A*

Ferris has to escape the creepers using a modified version of Dijkstra called A*

Security Union 13 Oct 30, 2022
A rust implementation of the reverse-engineered Glorious mouse protocol

gloryctl This project is an implementation of the vendor-specific HID protocol in use by Glorious mice used to configure parameters such as DPI profil

Samuel Čavoj 6 Oct 17, 2022
my attempt at compromise between unwrapping and bullying my dependencies' authors for Error impl

string-eyre Has this happened to you? error[E0599]: the method `wrap_err` exists for enum `Result<(), tauri::Error>`, but its trait bounds were not sa

Michał Sidor 1 Nov 25, 2021
Detect and remove unused dependencies from Cargo.toml

Cargo Shear ✂️ ?? Detect and remove unused dependencies from Cargo.toml in Rust projects. Installation cargo binstall cargo-shear # OR cargo install c

Boshen 49 Jul 25, 2024
Keep your dependencies up-to-date

Deps.rs - Dependency status at a glance Deps.rs is a service that shows you at a glance if any of your dependencies are out of date or insecure. This

deps.rs 369 Jan 5, 2023
Error context library with support for type-erased sources and backtraces, targeting full support of all features on stable Rust

Error context library with support for type-erased sources and backtraces, targeting full support of all features on stable Rust, and with an eye towards serializing runtime errors using serde.

Findora Foundation 1 Feb 12, 2022
A rust interval arithmetic library which provides flags that detect domain errors.

intervals-good A Rust interval arithmetic library which provides flags that detect domain errors, supports more functions than any other interval arit

Oliver Flatt 3 Jul 27, 2022
Massayo is a small proof-of-concept Rust library which removes AV/EDR hooks in a given system DLL

Massayo Massayo is a small proof-of-concept Rust library based on UnhookingPOC, which removes AV/EDR hooks in a given system DLL. I tried to reduce fi

null 53 Dec 21, 2022
Fast, compact and all-around subdomain enumeration tool written in Rust

Fast, compact and all-around subdomain enumeration tool written in Rust, which uses dns bruteforce, internet search and recursive http content search.

foreseon 16 Dec 10, 2022