Reducing Raw Pointer Footguns: Preventing Reference Aliasing Violations at Compile Time

That code avoids the point because it avoids mixing raw pointers and references, that is outside the scope of the discussion. The goal here is to explore mixing raw pointers and references in safer way, not to avoid mixing them. We want when we combine them we have stronger protections against accidental aliasing violations

The example above is a simulation of mixed raw pointer and reference usage. If we remove the raw pointer and replace everything with references, then we are no longer simulating the actual problem space. The entire point is that some operations fundamentally require raw pointers, such as FFI boundaries, low level memory manipulation, unsafe third party APIs, and similar systems level code

So the body of the example is not important by itself. The important part is: “assume this operation requires a raw pointer”. The example exists to model that situation

Another problem is that correct usage heavily depends on deep understanding of Rust’s aliasing and pointer invalidation rules. The code becomes extremely fragile because even changing the order of a few lines can silently introduce UB. That alone demonstrates there is room for improvement

The issue is that once raw pointers and references are involved, we must manually reason about:

  • when references become invalidated,
  • when raw pointers become invalidated,
  • whether aliasing rules are violated,
  • whether a later &mut creation silently invalidates previous pointers
  • whether a write through one path invalidates another path
  • etc maybe things that I do not know yet

That is extremely easy to get wrong that general user will get the footgun easily, compared to we have compile time error so general user will change the code

To design APIs that are practical and safer for real world users, then we should evaluate them from the perspective of average users, not only people with expert level understanding of Rust aliasing

The example gives compile time error. Did you run with the code in the opening post? The newest code is in the reply. I will put it in the opening post

Where 1 one the example is indeed invalidate the head. That is because I didn't wrap the head pointer with the guard. After I wrap all will guard, it now prevents the head pointer invalidation

I simply replaced your AliasableGuardMut with a mutable reference and made no changes to avoid mixing raw pointers and references. This means that your code didn't really need the capabilities of raw pointers in the first place and is thus not a valid example of where AliasableGuardMut would help.

Assumptions are ok only as long as they don't matter for the point you're making. They matter in your examples though.

The examples themselves were incomplete so I didn't run them.

No, the head is still invalidated because you use the node mutable reference after creating the head. It's another example where your AliasableGuardMut does not prevent this kind of issues.

1 Like

The AliasGuardMut definition is

pub struct AliasingGuardMut<'a, T: ?Sized> {
    ptr: NonNull<T>,
    _marker: PhantomData<&'a mut T>,
}

Aka it is alias to the real raw pointer that is used in the original code. Changing all of them to references is equivalent to changing the raw pointers in the original code to references. So it defeats the purpose of simulating the mixing of raw pointers and references

Eg:

struct Arena<T> {
    ptr: *mut T,
}

Becomes:

struct Arena<'a, T> {
    guard: &'a mut T,
}
struct Buffer {
    ptr: NonNull<Vec<i32>>,
}

Becomes:

struct Buffer<'a> {
    guard: &'a mut Vec<i32>,
}
struct SlotMap<T> {
    slots: Vec<NonNull<T>>,
}

Becomes:

struct SlotMap<'a, T> {
    slot: Option<&'a mut T>,
}

The examples are intentionally made with human mistakes included, to demonstrate that the guard shows compile time error where the raw direct raw pointers and references interactions are human error prone

The body of the example is not important, it is just a simulation of raw pointers and references interaction which is definetely exist in real world

Please elaborate more maybe with Miri to show the UB. Because the method .mutable_reference() gives mutable reference from the raw pointer, not the original data, so it does not invalidate previous pointers permanently, only when the mutable reference is active, that the guard will prevent a use of pointer while said mutable reference is still active via the method .with_mutable_pointer(). After said mutable reference is inactive, the pointer is valid again

The full code with intentional human mistake is like this, to demonstrate that it is able to show compile time error

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

pub struct AliasingGuardMut<'a, T: ?Sized> {
    ptr: NonNull<T>,

    // SAFETY:
    // This models exclusive mutable ownership over `T` for lifetime `'a`.
    //
    // The guard conceptually behaves like it owns an `&'a mut T`,
    // which prevents aliasing mutable borrows through Rust's borrow checker.
    //
    // `PhantomData<&'a mut T>` is important because:
    // - it enforces invariance over `T`
    // - it tells the compiler this type semantically contains `&mut T`
    // - it enables borrow checking rules for aliasing/exclusivity
    // - it prevents multiple mutable guards existing simultaneously in safe code
    _marker: PhantomData<&'a mut T>,
}

impl<'a, T: ?Sized> AliasingGuardMut<'a, T> {
    #[inline(always)]
    pub fn from_reference(value: &'a mut T) -> Self {
        Self {
            // SAFETY:
            // `NonNull::from` is safe because `&mut T` is guaranteed:
            // - non-null
            // - properly aligned
            // - valid for reads/writes for `'a`
            ptr: NonNull::from(value),

            _marker: PhantomData,
        }
    }

    #[inline(always)]
    pub fn immutable_reference(&self) -> &T {
        // SAFETY:
        // The original `&mut T` guarantees:
        // - pointer validity
        // - proper alignment
        // - initialized memory
        //
        // Returning `&T` from `&self` is safe because:
        // - immutable references may alias other immutable references
        // - Rust reference rules prevent obtaining `&mut self` simultaneously with this reference in safe code
        unsafe { self.ptr.as_ref() }
    }

    #[inline(always)]
    pub fn mutable_reference(&mut self) -> &mut T {
        // SAFETY:
        // `&mut self` guarantees exclusive access to the guard.
        //
        // Because the guard semantically owns an exclusive `&mut T`,
        // this ensures no competing mutable references can exist
        // through this API in safe Rust.
        //
        // WARNING:
        // Raw pointers previously extracted from this guard may still
        // exist and can violate aliasing rules if used incorrectly.
        // Safe Rust callers cannot trigger UB here, but unsafe callers can.
        unsafe { self.ptr.as_mut() }
    }

    #[inline(always)]
    pub fn with_immutable_reference<R>(&self, f: impl FnOnce(&T) -> R) -> R {
        // SAFETY:
        // Same reasoning as `immutable_reference`.
        //
        // The reference is scoped to the closure call,
        // preventing it from escaping accidentally.
        unsafe { f(self.ptr.as_ref()) }
    }

    #[inline(always)]
    pub fn with_mutable_reference<R>(&mut self, f: impl FnOnce(&mut T) -> R) -> R {
        // SAFETY:
        // Same reasoning as `mutable_reference`.
        //
        // The mutable reference is scoped to the closure execution,
        // which helps reduce accidental misuse duration.
        unsafe { f(self.ptr.as_mut()) }
    }

    #[inline(always)]
    pub fn with_immutable_pointer<R>(&self, f: impl FnOnce(*const T) -> R) -> R {
        // SAFETY:
        // - Rust reference rules prevent obtaining `&mut self` simultaneously with this reference in safe code
        //
        // In particular:
        // - The immutable raw pointer is scoped to the closure execution
        // which makes able to create `&mut` without invalidating the pointers
        // - It prevents calling immutable raw pointer while `&mut` is still active because it violates the aliasing rules
        f(self.ptr.as_ptr())
    }

    #[inline(always)]
    pub fn with_mutable_pointer<R>(&mut self, f: impl FnOnce(*mut T) -> R) -> R {
        // SAFETY:
        // - Rust reference rules prevent obtaining `&mut self` simultaneously with this reference in safe code
        //
        // In particular:
        // - The mutable raw pointer is scoped to the closure execution
        // which makes able to create `&` or `&mut` without invalidating the pointers
        // - It prevents calling mutable raw pointer while `&` or `&mut` is still active because it violates the aliasing rules
        f(self.ptr.as_ptr())
    }

    #[inline(always)]
    pub unsafe fn as_ptr(&mut self) -> *mut T {
        // SAFETY:
        // This exists to make if closure based pointer is not enough, then this unsafe method can be used
        // Returning raw pointers is safe by itself.
        //
        // However, once the pointer escapes, this type can no longer
        // enforce aliasing guarantees.
        //
        // The caller must ensure:
        // - no invalid reference/raw-pointer combinations are used
        // - no aliasing UB occurs
        // - do not write to the pointer while `&` or `&mut` to same memory is still active
        // - do not read the pointer while `&mut` to same memory is still active
        // - be aware that `&mut` creation that points to same address of this pointer will invalidate this pointer
        // - pointer is not used after underlying value becomes invalid
        self.ptr.as_ptr()
    }
    
    #[inline(always)]
    pub fn clone_guard(&self) -> Self {
        Self {
            ptr: self.ptr,
            _marker: PhantomData,
        }
    }

    #[inline(always)]
    pub fn close(self) {
        // SAFETY:
        // Consuming `self` ends the guard lifetime early.
        //
        // This can be useful to release the conceptual mutable borrow
        // before the surrounding scope ends.
    }
}

struct Node<'a> {
    next: Option<AliasingGuardMut<'a, Node<'a>>>,
    value: i32,
}

struct List<'a> {
    head: Option<AliasingGuardMut<'a, Node<'a>>>,
    guard: Option<AliasingGuardMut<'a, Node<'a>>>,
}

impl<'a> List<'a> {
    fn new() -> Self {
        Self {
            head: None,
            guard: None,
        }
    }

    fn push_front(&mut self, node: &'a mut Node<'a>) {
        node.next = self.head.take();

        self.head = Some(
            AliasingGuardMut::from_reference(node)
        );

        self.guard = Some(
            AliasingGuardMut::from_reference(
                node  
            )
        );
    }

    fn first_mut(&mut self) -> Option<&mut Node<'a>> {
        self.guard.as_mut().map(|guard| {
            guard.mutable_reference()
        })
    }
}

fn increment(node: &mut Node<'_>) {
    node.value += 1;
}

fn main() {
    let mut node = Box::new(Node {
        next: None,
        value: 10,
    });

    let mut list = List::new();

    list.push_front(&mut *node);

    let a = list.first_mut().unwrap();

    increment(a);

    let b = list.first_mut().unwrap();

    b.value += 10;

    println!("{}", a.value);
}

To find the newest AliasGuard code, it will be updated in bottom section of the opening post everytime there is change

I just added the #[guard] attribute and the guard_block!({ ... }) macro to improve the guard

Previously, the guard did not cover this. Now, it covers this:

#[guard]
fn a(guard: &AliasingGuardMut<Vec<i32>>) {
    let direct_ptr = guard.as_ptr();
    let mutable_reference = guard.mutable_reference();
    unsafe { *direct_ptr = vec![1] };
    *mutable_reference = vec![1];
}

fn main() {

    let mut s = vec![1, 2, 3];

    let mut guard = AliasingGuardMut::from_reference(&mut s);
   
    guard_block!({
        let b = &raw mut s;
        let a = guard.mutable_reference();
        unsafe { *b = vec![1] };
        *a = vec![1];
    });

}

It produces the following compile time error:

There is still case that can not be covered yet, it is when the declaration is ouside the guard. This is because, during macro expansion, we do not know whether the dereferenced variable is a pointer or a reference. Once compile time reflection is available, we will be able to determine the variable’s type at compile time and cover this case as well

fn main() {

    let mut s = vec![1, 2, 3];

    let mut guard = AliasingGuardMut::from_reference(&mut s);
    let b = &raw mut s;
    
    guard_block!({
        let a = guard.mutable_reference();
        unsafe { *b = vec![1] };
        *a = vec![1];
    });

}

I now get that you're trying to make a tool for reducing the likelihood of (accidentally) intermixing references and raw pointers. I do think it's worth noting that, AFAICT, the warning "don't mix accesses between references and raw pointers" is very common. It doesn't seem anywhere near to being the most complicated part of the aliasing rules, so I'm not sure that this tool has a good target audience.

People who haven't even heard this warning should probably learn much more about the aliasing rules before writing unsafe pointer manipulation; meanwhile, for people who do already know the warning, I'm not sure that pulling in a dependency for this is worth it.

Instead...... how hard would it be to make a clippy lint for intermixed pointer and reference accesses? Is this something which we can programmatically look for in a sufficient variety of simple cases? (Or is Miri considered good enough for any case that matters?)

1 Like

What kind of warning do you mean? Because none of the code containing aliasing violations that I posted in the replies produces any warning from the compiler. And it would be better for this to become a compile time error rather than just a warning, because warnings do not stop UB code from compiling

Having safety mechanisms that catch mistakes at compile time is better than only deeply studying unsafe Rust, because there is no guarantee that someone will immediately understand everything and never make mistakes afterward. That is similar to saying manually reasoning about C and C++ is better than having compile time safety checks that catch mistakes when they slip through. I do not understand how manual reasoning, which is highly prone to human error, could suddenly be considered better than compile time errors

No dependencies are being pulled in because this is just pure Rust code plus a proc macro library, which is we already uses the proc macro library when we uses the built in proc macros in Rust such as #[derive()], #[inline], #[test], and others

The question is whether Clippy has enough context to fully understand Rust aliasing rules? If it does, then it could be worth trying. But compile time errors are still preferable to warnings because this involves UB. As for Miri, I already answered that in my previous reply yesterday :]

Rust's goal is to move as many errors as possible to compile time, that’s the whole reason we have the borrow checker, to prevent mistakes at compile time. One aspect of this now is gradually giving unsafe Rust better ways to prevent mistakes, specifically regarding raw pointer to reference conversions that violate aliasing rules in this case. It's like having a way to prevent data races at compile time so we don't need Miri. If something can be checked at compile time, it's better than relying on Miri, because Miri needs to trigger every execution path to achieve 100% coverage, which is time consuming for large codebases. If there's a massive code update, we'd also need a massive update to the tests to ensure they run and maintain 100% coverage. With compile time checks, none of that is necessary. This is why Rust's advantage as a language with extensive compile time checks is so significant

The correct way to fix this is to give raw pointers a lifetime, and have them borrow the reference from which they were created. One potential downside of this approach is that people might interpret the lifetime on the raw pointer as being the lifetime of the object it points to, rather than the lifetime of the reference (if any) from which it was created – but it has the huge upside of preventing a wide range of provenance-related mistakes and thus making unsafe code a bit safer. There are still a couple of types of mistakes it won't catch (creating multiple mutable references from the same pointer, and offsetting the pointer outside the range of the original object and dereferencing it, but it would be much safer).

I suspect that long-term, it would be a good approach to deprecate raw pointers in favour of using types that represent the specific invarians you want to relax (e.g. most *mut T could instead be represented as &'a UnsafeCell<MaybeUninit<T>> – possibly all, given that &UnsafeCell doesn't actually require its target to be allocated).

Warning in the literal “people warn each other about this” sense, not the “compiler warning” sense.

If it were made into a clippy lint, if the analysis reliably catches UB with very few false positives, it could be made deny-by-default (resulting in a compilation error). Perhaps a second clippy lint with slightly broader analysis and more false positives could be warn-by-default.

Don’t give too much trust to your AI. Pure Rust crates count as dependencies. Moreover, your proc macro pulls in a well-known crate commonly considered to be a “heavy” dependency, syn.

Yeah, I have no clue.

Rust’s Send and Sync system is fantastic for safe code. But I think you severely overestimate how much is possible to check. When I make an unsafe concurrent data structure like a skiplist with lock-free reads, then forget relying on compile-time checks, forget relying on only Miri; I’m going to do exhaustive loom tests. (I’ll probably also start using shuttle for my next such project.)

The invariants of a structure like that are simply too complicated to be checked by the compiler. Sure, “the checks can’t be perfect” isn’t an argument to add no new checks, but…

More precisely, I’d say that “a major goal of Rust is to move as many errors as reasonably possible to compile time”. It is not a full formal verification system. Ergonomics, compiler performance, and no doubt many other factors influence whether Rust is willing to check for some class of errors. I’m fairly sure that “as many errors as possible” is “all of them”. But I worry that such a programming language might be fit for use only by mathematicians, and productivity would be quite low.

So, since Rust leaves some errors to runtime and some are not caught at all (which can include UB), yes, some part of the code gated behind unsafe will need careful manual reasoning. The advantage over C and C++ is (supposed to be) that the safety invariants of the code are precisely defined and need to be carefully checked only for unsafe operations.

1 Like

Do you have an example of this sort of code? I've been looking into "what sort of features would safe Rust need to be able to allow it to replicate all/most unsafe Rust" with a focus on avoiding race conditions in nontrivial cases, so I'd love to be able to see a really difficult example (because both the outcome of "the safe Rust extensions I was thinking about are sufficient to make this work", and "this is impossible to do even with my extensions, because of X" would be very useful results). Even an outcome of "this can't be made safe, but at least it's possible to reduce the amount of testing required" would be both interesting and useful.

Having more of the matrix of invariants would certainly be nice. unsafe<'_> binders, for lifetime erasure. MaybeDangling. UnsafePinned or whatever it ends up being called.

Don’t forget pointer tagging, though. Is there currently any other way to soundly read and manipulate the numeric value of bytes which may have providence (without exposing the pointer) other than raw pointers? Designing a non-raw-pointer-type for ergonomic pointer tagging might be interesting, I suppose. (Ah, maybe it could just be an enum, possibly with some repr(ptr) or something if needed for clarity, in some specific scenario guaranteed to be optimized into a single pointer, a bit like how the null optimization of Option-like enums is guaranteed.)

Edit: hrmm I think you might be able to make “we have raw pointers at home” with references to ZSTs.

I fear raw pointers won’t be able to be deprecated at this point, but I hope Rust serves as good research material for a successor language that might avoid needing them.

This is exactly what I was planning – an AtomicEnum that gets packed down to a sufficiently small size that it can be stored in an atomic. This gives an enormous amount of power (e.g. it is enough to write Mutex in entirely safe code, as long as you have access to a safe futex or other comparable blocking primitive). For Mutex you just need the existing optimisation on Option, but for more complicated situations you would probably want a #[repr(…)] that's guaranteed to be able to pack multiple types of pointer into a single atomic-sized word, as long as they have sufficient alignment to allow enough of a niche.

So right now, there is still no warning for it. A ready to use compile time check is better than a compile time check that requires a complex Clippy setup, because realistically, most general users not using that setup anyway, which means the end result for the majority of users is effectively the same

I just learned that Rust built in macros do not use syn :]. In that case, this could be implemented as built in macro using the method like the other built in macros, since it would have access to much broader contextual information than user space code that relies on syn

I mean, as long as a check can be performed at compile time, Rust usually performs it. Even having two &mut references to the same memory is checked at compile time, so why not also check for two pointer and reference pointing to the same memory, especially if it is already possible to detect it?

If the macro were implemented the same way as Rust’s built in proc macros, this check should have overhead characteristics similar to existing built in attribute macros like #[test], #[derive], and others, because it would likely have direct access to the token context at the compiler level

And this check does not limit the language’s flexibility, because we can leave the checked area at any time simply by writing code outside the guard. So I do not really understand what you mean by “limiting productivity”. This should improve productivity instead, because errors appear immediately at compile time so feedback is fast, errors are not silent

Unless the respondents to this survey are extremely unrepresentative, the vast majority of Rust users do in fact run cargo clippy (and roughly a quarter, including myself, do so after every code change): Rust compiler performance survey 2025 results | Rust Blog

Running that two-word cargo clippy command currently has 67 deny-by-default lints (Clippy Lints) and a little over 400 warn-by-default lints. Incredibly useful, even without a more thorough setup.

I was referring to a language where "as many errors as possible are checked at compile time" -- namely, where all errors are checked at compile time -- not just your suggestion. Imagine a language which lets you use all the powers you could in C or unsafe Rust but requires you to provide a rigorous computer-readable mathematical proof that your program is sound. That's an extreme which would be difficult to work with. Presumably, it could still be worth its cost in rare cases, but not usually.

I think detecting it in Miri would be quite feasible. I'm not sure how hard it would be to check at compile time, I've never tried to implement a lint.

I remember that survey, but I forgot whether it included running Clippy or not. But in my daily use case, I do not run Clippy :[ I run it previously using the Clippy command to auto fix, and the result wasn't what I expected then I don't use it again. Maybe the safest usage of it is the Clippy check command to only give messages not edit

I think there is a reason why this was implemented in Miri instead of Clippy. Maybe, within Clippy’s context, it is not possible to accurately prove aliasing rule violations?

That is an imagination based concern that does not apply here, because we can still opt out of the guard checks simply by writing code outside the guard’s scope

If Miri were already sufficient, then we probably would not still see aliasing rule UB in Rust projects. Miri runs at runtime, which means every problematic code path must actually be executed, usually through tests. The tests must trigger every piece of code that could potentially violate aliasing rules, and that is exactly where human error comes in, some violations slip through simply because nobody realized that the code violated aliasing rules. And when the codebase becomes large, the number of tests required to trigger all those checks also increases. If the code gets refactored, the tests also need to be refactored so they still execute the relevant paths and trigger the checks. Human error in failing to trigger every problematic path is what allows aliasing related UB to slip through

Anyway, do you have any technical feedback? For example, what kinds of code would not work with this guard?

Oh, yeah, absolutely. I use neither cargo clippy --fix (I think that's the command?) nor even cargo fmt (I may agree with the linter, but I disagree with the formatter).

Plus, the immediate/direct problem found by clippy may be a symptom of the actual problem. Applying patchwork fixes to the direct problems won't actually fix the code in those cases, no matter how correct those patchwork fixes may or may not be.

I think there's large enough escape hatches that anything could work with the guard, but maybe not well. Either way... I probably know the aliasing rules well enough that I'm not the target audience to begin with.

I think this word is popular in the Rust community

"Even the best programmers can make mistakes"

That acknowledges we are just human. That is also the reason why raw pointers are avoided as much as possible

But regardless of what you think about it, I need some clear technical feedback, which is specific and not generic like "I think there are many", what are those "many" things? because I don't know what they are

What I expect is : "Oh this lacks A because of reason B", "Oh, if this faces case C, it will cause D", etc. The message is clear about what it means

Let me provide a specific technical claim: AliasingGuardMut's intended property is trivially defeatable. Nothing prevents this, which is the kind of mixing that you want to prevent:

let ptr = guard.with_mutable_pointer(|ptr| ptr);
guard.with_mutable_reference(|r| {
    // do something with `r` and `ptr` here
});

This means that, if AliasingGuardMut provides value, the value will be in the subjective realm of “using this type as intended will help people make fewer mistakes”.

One powerful escape hatch is “you can take the mutable/immutable reference in the guard’s callback and do anything you could do to a normal mutable reference, within that scope, including mixing raw pointers and references”. The callback could be arbitrarily complicated, potentially the equivalent of a full fn main.

This would be an utterly ineffective use of the guard which I don’t thing anybody would want to do, but still possible.

Thank you, I just realized that. I explored some ways to prevent the pointer from being returned, ran into difficulties because of the current limitations in the stable channel, then eventually found the best way :]

  • The reference case has no problem when I was in the process of creating the new code, I just added a lifetime to the reference in the closure

Now the pointer :

  • I tried to implement a trait for detection, but the orphan rule prevents this solution. A trait based solution in stable is only possible by manually implementing the trait for all non pointer types and pointer types
  • The trait based solution becomes nicer in the unstable nightly channel with the auto_traits and negative_impls features. Or the specialization feature to override the trait implementation
  • However, all trait based solutions still allow an escape via wrapping the raw pointer inside a custom struct. This requires manually implementing the detection trait for the struct, eg via a manually implement the trait or macro syntax sugar that wraps the impl trait. At least now it explicitly makes one aware that it can be dangerous, but not ergonomic because it requires manual ritual
  • A proc macro based solution is only aware of raw tokens, not types. It checks if the type returned in the closure is a raw pointer. If the raw pointer is wrapped inside a struct, then adding #[derive(Guard)] to the struct allows the proc macro to check if there is a raw pointer inside. But it can still escape if forgot to use the derive guard
  • The best approach that I found is using the compile time reflection feature available in nightly. This one guarantees detection of the raw pointer and can not be tricked

I have updated the opening post with the latest code that uses compile time reflection :]

Now we can not escape the pointer ouside the clossure:

    let ptr = guard.with_mutable_pointer(|ptr| ptr);
    guard.with_mutable_reference(|r| {

    });

It will cause compile time error: