Soundness of write-only pointers contravariant in the pointed type

⚓ Rust    📅 2026-06-08    👤 surdeus    👁️ 1      

surdeus

Hi, I am fighting with lifetimes.

Due to a limit in an HRBT (see after for the X problem) I need to make a type that holds some data contravariant over the held type. I do believe that this is sound as the only thing you can do with the type is to write a value to it, so a function with signature fn(T).

I wrote a basic playground to show a simplified implementation of what I am writing, but am not enough at ease with raw pointer to be sure that's sound.

use std::ptr::NonNull;
use std::mem::MaybeUninit;
use std::marker::PhantomData;


/// Pointer that can be only written
///
/// Covariant in 'a, contravariant in T
pub struct ContravariantPtr<'a, T> {
    /// Raw pointer or the compiler deduce covariance
    ptr: NonNull<()>,
    _phantom: PhantomData<&'a fn(T)>,
}

impl<'a, T> ContravariantPtr<'a, T> {
    /// Create a new pointer that can be only written
    pub fn new(slot: &'a mut MaybeUninit<T>) -> Self {
        Self {
            ptr: NonNull::from(slot).cast(),
            _phantom: PhantomData
        }
    }   
    
    /// Write to the pointer
    ///
    /// This will discard the previous written value, exactly like 
    /// [`std::mem::forget`]
    pub fn write(&mut self, value: T) {
        let ptr = self.ptr.cast();
        unsafe {
            // Safety: 
            // We know the slot is live as `self` is covariant in 'a, so:
            //     'a_creation: 'a_now
            //
            // We also know that `value` is a valid value for the slot as 
            // `self` is controvariant in `T`, so:
            //     T_now: T_creation
            
            ptr.write(MaybeUninit::new(value)) 
        };
    }   
}


fn main() {
    // Allocation to have a lifetime smaller than 'static
    let finite_lived: Box<u8> = Box::new(13);
    // Slot where to put something that's 'finite_lived or more
    let mut slot: MaybeUninit<&u8> = MaybeUninit::new(&*finite_lived);
    // Pointer to the slot, write only
    let ptr: ContravariantPtr<'_, &u8> = ContravariantPtr::new(&mut slot);
    // Force the pointer to point to a smaller type - we can do, as the pointer
    // is write only
    let mut ptr_with_narrowed_type: ContravariantPtr<'_, &'static u8> = ptr;
    // Write a static to it
    ptr_with_narrowed_type.write(&42);
    // Safe to get it, as we only casted a type to a larger one: 
    //      'static: 'short_lived
    assert_eq!(unsafe { slot.assume_init() }, &42)
}

(Playground)

It is sound? I am playing around passing miri over it and it does not seem to break using only the pub interface

X problem: need to pass a impl for<'a> Fn(&mut Trait::Associated<'a>) to a function and being &mut invariant, plus associated types too that requires that the type pointed to does live for static. At least I think. Anyway that nerdsniped me and now i am shaving this yak.

1 post - 1 participant

Read full topic

🏷️ Rust_feed