Prevent field projection through references?

This question relates to this proposal.

TLDR: Introduce a project! macro that supports arbitrary field projection by desugaring an expression like project!(&mut container.field_a.field_b.field_c) into an addr_of! expression that computes the byte offset of the projected field.

Given a type like MaybeUninit<(u8, u16)>, project!(&m.1) would return a &MaybeUninit<u16>.

Currently, the macro supports more projections than we want. In particular, it supports any sequence of token trees that addr_of! supports, which means that it supports references. This in turn means that, given a type like MaybeUninit<&'static (u8, u16)>, project!(&m.1) will happily produce a & &'static MaybeUninit<u16>, which is unsound (in particular, it treats the possibly-garbage &'static (u8, u16) as a valid reference and uses it to compute the address of the &'static MaybeUninit<u16>).

Here's a concrete example that causes Miri to fail

I ran this code through Miri:

let inner = (0u8, 1u16);
let mut m = MaybeUninit::new(&inner);
m = MaybeUninit::uninit();

project!(&m.1);

Here's the output:

test tests::test_project_reference ... error: Undefined Behavior: using uninitialized data, but this operation requires initialized memory
   --> project/src/lib.rs:518:9
    |
518 |         project!(&m.1);
    |         ^^^^^^^^^^^^^^ using uninitialized data, but this operation requires initialized memory
    |
    = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior

My question is: Does anyone have suggestions for how I could modify project! such that it does not support fields with reference types?

There is alas no standalone / reliable way to avoid .-deref operations from occurring when inside a ptr::addr_of{,_mut}!(…) invocation.

The usual workaround is to be less ambitious w.r.t. projections, and allow the macro to handle them one by one.

From there, by requiring that the intermediary type (your P::Inner) be specified to the macro as well, the macro can then make the necessary previous checks to make it impossible for the caller to "smuggle" derefs.

Demo


In that demo, the syntax I have chosen is:

project!(
    & <mut>? <expr> => <P::Inner> => .<field>
)
  • e.g., for a tuple:

    project!(&m => (_, _) => .1)
    
  • and for a (tuple) struct:

    project!(&m => StructName<...> => .some_field)
    

The key idea is that, for (tuple) structs, emitting:

let StructName { field_name: _, .. };

is:

  • valid Rust code you can emit anywhere;
    • even for numerical/indexed field names! let Wrapper { 0: _, .. }.
  • and which compile-time asserts the existence of .field_name for that type, thereby guaranteeing lack of *-deref shenanigans being involved.

For actual Rust tuples, whilst we cannot do this, it turns out that we do know they do not implement Deref.

Finally, by emitting the given type in the input arg signature of the closure expected to work with P::Inner, we are effectively asserting that these two match, so that a ManuallyDrop<& ...> with thus a P::Inner of & ... will correctly reject any of these two usages.

3 Likes

Hey, thanks for the suggestion, and sorry for not replying earlier! I've ended up deciding to just keep the macro unsafe for the time being, but I've listed your proposed solution as a potential direction once we're ready to tackle making it safe again.

1 Like

The unstable offset_of! macro can detect this kind of issue.

This works:

#![feature(offset_of)]

#[repr(C)]
struct NestedA {
    b: NestedB
}

#[repr(C)]
struct NestedB(u8);

fn main() {
    println!("{}", core::mem::offset_of!(NestedA, b.0));
}

This fails to compile:

#![feature(offset_of)]

#[repr(C)]
struct NestedA {
    b: &'static NestedB
}

#[repr(C)]
struct NestedB(u8);

fn main() {
    println!("{}", core::mem::offset_of!(NestedA, b.0));
}
error[E0609]: no field `0` on type `&'static NestedB`
  --> src/main.rs:12:53
   |
12 |     println!("{}", core::mem::offset_of!(NestedA, b.0));
   |                                                     ^
   |
1 Like

Yeah that's fair. One thing you could do is still offer a non-unsafe version for the single/non-chained projection case.

While the macro duplication may be a bit annoying, I wouldn't understate the usefulness of not being unsafe


Nice!

Long-term, I definitely want the macro to be safe. In the short-term, it's important to unblock this design, and it's okay for it to be unsafe in that context (it's pretty easy to make sure we're not generating unsound uses of project!). But long-term, I definitely want it to be safe.