Soundness and interior immutability for no-alloc distributed data structures

I want to implement a node-based data structure (suppose, for example, that it is a linked list), where the presence of each node in the list is restricted by the lifetime of some pinned object; when the object is dropped, it removes its node from the data structure. (The motivation here is an internal detail of an embedded async/await task wakeup system, which will be relevant later.) This naturally requires a good amount of internal unsafe code but should result in a safe API.

A naive approach using dynamically allocated nodes is as follows.

Code
#![no_std]

extern crate alloc;
extern crate critical_section;

use alloc::boxed::Box;
use core::{
    marker::PhantomPinned,
    ptr::NonNull,
};
use critical_section::CriticalSection;

struct List<T: Send + Sync> {
    head: *mut Node<T>,
}

struct Node<T: Send + Sync> {
    /// Pointer to pointer to this node; either points to [`List::head`] or
    /// [`Self::next`].
    prev_ptr: NonNull<*mut Node<T>>,
    /// Pointer to next node; null if empty.
    next: *mut Node<T>,
    value: T,
}

struct Entry<T: Send + Sync> {
    my_node: NonNull<Node<T>>,
    _pin: PhantomPinned,
}

impl<T: Send + Sync> Entry<T> {
    pub fn new(value: T, list: &mut List<T>, _cs: CriticalSection) -> Self {
        // Insert a node at the head of the list.
        let node = Box::new(Node {
            prev_ptr: NonNull::from(&mut list.head),
            next: list.head,
            value,
        });
        let node = Box::leak(node);
        if let Some(head) = unsafe { list.head.as_mut() } {
            head.prev_ptr = NonNull::from(&mut node.next);
        }
        list.head = node as _;
        Entry {
            my_node: NonNull::from(node),
            _pin: PhantomPinned,
        }
    }
    
    pub fn access(&mut self, _cs: CriticalSection) -> &mut T {
        &mut unsafe { self.my_node.as_mut() }.value
    }
}

impl<T: Send + Sync> Drop for Entry<T> {
    fn drop(&mut self) {
        critical_section::with(|_cs| unsafe {
            let node = self.my_node.as_mut();
            // Remove the node from the list.
            node.prev_ptr.write(node.next);
            if let Some(next) = node.next.as_mut() {
                next.prev_ptr = node.prev_ptr;
            }
            // End the scope of `node` to ensure no aliasing.
            #[allow(dropping_references)]
            drop(node);
            // Destruct and deallocate the node.
            drop(Box::from_raw(self.my_node.as_ptr()));
        });
    }
}

The issue is, though, that we shouldn't need dynamic memory allocation here; if we correctly design an API around Entry being pinned, we should be able to put Node inside Entry directly (i.e., have my_node not be a pointer). Avoiding any dynamic memory allocation, especially within a critical path, would be very helpful for embedded systems.

However, what type can we actually make my_node in that case? Initially, I went with just Node<T>, but then I realized this violated aliasing rules, since plausibly an exclusive reference to the Entry<T> (or to a data structure in which it is contained) may exist when the list is being traversed (not shown in the above code). I then thought of making it UnsafeCell<Node<T>>, but I realized that this does nothing in the case where we have an exclusive reference; UnsafeCell::get_mut is safe, after all.

As I noted earlier, the motivation here is part of a wakeup primitive in an embedded async/await executor; thus, I am partially constrained by needing to fit into the Future API. This means that things will interact with Entry via a pinned exclusive reference; thus, I cannot force the API to restrict everything to shared references. Essentially, it feels like what I need is some sort of interior immutability or interior aliasability, so I can avoid breaking aliasing rules without unnecessary indirection, but I don't see how to do this within the constraints imposed by the Future API.

For reference, here is a C++ implementation of this idea with a usage example; obviously, much more care is needed in this case by a library consumer to enusre it is being used correctly, which is a big part of why I prefer using Rust, but I can't figure out how to replicate this in Rust without dynamic memory allocaiton.

Code
#include <cassert>
#include <iostream>
#include <optional>
#include <utility>

template<class T>
struct Node {
    Node<T> **prev_ptr;
    Node<T> *next;
    T value;
};

template<class T>
struct List {
    Node<T> *head = nullptr;
    
    void print() {
        std::cout << "List:";
        auto cur = head;
        while (cur) {
            std::cout << " " << cur->value;
            cur = cur->next;
        }
        std::cout << std::endl;
    }
};

template<class T>
class Entry {
    std::optional<Node<T>> my_node;

public:
    void insert_into(T &&value, List<T> &list) {
        assert(!my_node);
        my_node.emplace(Node<T>{&list.head, list.head, std::move(value)});
        if (list.head) {
            list.head->prev_ptr = &my_node->next;
        }
        list.head = &*my_node;
    }

    T *operator*() {
        return &my_node->value;
    }

    ~Entry() {
        // Assume this uses some sort of critical section to ensure atomicity of
        // interaction with the overall list.
        if (my_node) {
            *my_node->prev_ptr = my_node->next;
            if (my_node->next) {
                my_node->next->prev_ptr = my_node->prev_ptr;
            }
        }
    }
};

/*
Output:

List:
List: 5
List: 10 5
List: 5
List:
*/
int main() {
    List<int> l;
    l.print();
    
    {
        Entry<int> e1;
        e1.insert_into(5, l);
        l.print();
        {
            Entry<int> e2;
            e2.insert_into(10, l);
            l.print();
        }
        l.print();
    }
    l.print();

    return 0;
}

Questions:

  1. Is this currently possible to do this soundly in Rust (even with unsafe code)? If so, how?
  2. If not, would it be reasonable to propose a new special (i.e., lang_item) cell type, say AliasCell, such that &mut AliasCell<T> is approximately &UnsafeCell<T> (or alternatively, *mut T)? I know this is would be a fairly intrusive addition, but I don't see how else to enable this sort of data structure without dynamic memory allocation.
1 Like

What you're looking for is UnsafePinned (already linked). Unfortunately, it isn't implemented yet.

There is currently a kludge in the compiler to make this possible at all, for the benefit of async-block types: the compiler does not apply noalias (asserting uniqueness) for exclusive references to types which are !Unpin. This is not the proper solution and it is not guaranteed to exist in the future, but it’s the interim solution we have got.

2 Likes

Cool, thanks for the quick replies!

That makes sense to me. I had though that there must be something similar for the purposes of async blocks but I couldn't quite figure out how that mapped onto what I was looking for.

Goals of UnsafePinned today are reached through PhantomPinned "somewhere" in your struct, but it is undocumented anywhere. Basically, if your struct is !Unpin, &mut are treated as *mut. This will probably change in the future, when UnsafePinned will be stabilized. In my code, I defined struct UnsafePinned<T: ?Sized>(PhantomPinned, UnsafeCell<T>) and use like that.