[PATCH 12/12] rust: pin-init: internal: init: add escape hatch for referencing initialized fields

Benno Lossin posted 13 patches 1 month ago
There is a newer version of this series
[PATCH 12/12] rust: pin-init: internal: init: add escape hatch for referencing initialized fields
Posted by Benno Lossin 1 month ago
The initializer macro emits mutable references for already initialized
fields, which allows modifying or accessing them later in code blocks or
when initializing other fields. This behavior results in compiler errors
when combining with packed structs, since those do not permit creating
references to misaligned fields. For example:

    #[repr(C, packed)]
    struct Foo {
        a: i8,
        b: i32,
    }

    fn main() {
        let _ = init!(Foo { a: -42, b: 42 });
    }

This will lead to an error like this:

    error[E0793]: reference to field of packed struct is unaligned
      --> tests/ui/compile-fail/init/packed_struct.rs:10:13
       |
    10 |     let _ = init!(Foo { a: -42, b: 42 });
       |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       |
       = note: this struct is 1-byte aligned, but the type of this field may require higher alignment
       = note: creating a misaligned reference is undefined behavior (even if that reference is never dereferenced)
       = help: copy the field contents to a local variable, or replace the reference with a raw pointer and use `read_unaligned`/`write_unaligned` (loads and stores via `*p` must be properly aligned even when using raw pointers)
       = note: this error originates in the macro `init` (in Nightly builds, run with -Z macro-backtrace for more info)

This was requested by Janne Grunau [1] and will most certainly be used
by the kernel when we eventually end up with trying to initialize packed
structs.

Thus add an initializer attribute `#[disable_initialized_field_access]`
that does what the name suggests: do not generate references to already
initialized fields.

There is space for future work: add yet another attribute which can be
applied on fields of initializers that ask for said field to be made
accessible. We can add that when the need arises.

Requested-by: Janne Grunau <j@jannau.net>
Link: https://lore.kernel.org/all/20251206170214.GE1097212@robin.jannau.net [1]
Signed-off-by: Benno Lossin <lossin@kernel.org>
---
 rust/pin-init/internal/src/init.rs | 75 +++++++++++++++++++++---------
 1 file changed, 52 insertions(+), 23 deletions(-)

diff --git a/rust/pin-init/internal/src/init.rs b/rust/pin-init/internal/src/init.rs
index c20be37e7fef..148f43bf3c7b 100644
--- a/rust/pin-init/internal/src/init.rs
+++ b/rust/pin-init/internal/src/init.rs
@@ -58,6 +58,7 @@ fn ident(&self) -> Option<&Ident> {
 
 enum InitializerAttribute {
     DefaultError(DefaultErrorAttribute),
+    DisableInitializedFieldAccess,
 }
 
 struct DefaultErrorAttribute {
@@ -80,7 +81,6 @@ pub(crate) fn expand(
     let mut errors = TokenStream::new();
     let mut error = error.map(|(_, err)| err);
     if let Some(default_error) = attrs.iter().fold(None, |acc, attr| {
-        #[expect(irrefutable_let_patterns)]
         if let InitializerAttribute::DefaultError(DefaultErrorAttribute { ty }) = attr {
             Some(ty.clone())
         } else {
@@ -142,7 +142,15 @@ fn assert_zeroable<T: ?::core::marker::Sized>(_: *mut T)
     };
     // `mixed_site` ensures that the data is not accessible to the user-controlled code.
     let data = format_ident!("__data", span = Span::mixed_site());
-    let init_fields = init_fields(&fields, pinned, &data, &slot);
+    let init_fields = init_fields(
+        &fields,
+        pinned,
+        !attrs
+            .iter()
+            .any(|attr| matches!(attr, InitializerAttribute::DisableInitializedFieldAccess)),
+        &data,
+        &slot,
+    );
     let field_check = make_field_check(&fields, init_kind, &path);
     quote! {{
         // We do not want to allow arbitrary returns, so we declare this type as the `Ok` return
@@ -225,6 +233,7 @@ fn get_init_kind(rest: Option<(Token![..], Expr)>, errors: &mut TokenStream) ->
 fn init_fields(
     fields: &Punctuated<InitializerField, Token![,]>,
     pinned: bool,
+    generate_initialized_accessors: bool,
     data: &Ident,
     slot: &Ident,
 ) -> TokenStream {
@@ -255,6 +264,13 @@ fn init_fields(
                         unsafe { &mut (*#slot).#ident }
                     }
                 };
+                let accessor = generate_initialized_accessors.then(|| {
+                    quote! {
+                        #(#attrs)*
+                        #[allow(unused_variables)]
+                        let #ident = #accessor;
+                    }
+                });
                 quote! {
                     #(#attrs)*
                     {
@@ -262,37 +278,31 @@ fn init_fields(
                         // SAFETY: TODO
                         unsafe { #write(::core::ptr::addr_of_mut!((*#slot).#ident), #value_ident) };
                     }
-                    #(#attrs)*
-                    #[allow(unused_variables)]
-                    let #ident = #accessor;
+                    #accessor
                 }
             }
             InitializerKind::Init { ident, value, .. } => {
                 // Again span for better diagnostics
                 let init = format_ident!("init", span = value.span());
-                if pinned {
+                let (value_init, accessor) = if pinned {
                     let project_ident = format_ident!("__project_{ident}");
-                    quote! {
-                        #(#attrs)*
-                        {
-                            let #init = #value;
+                    (
+                        quote! {
                             // SAFETY:
                             // - `slot` is valid, because we are inside of an initializer closure, we
                             //   return when an error/panic occurs.
                             // - We also use `#data` to require the correct trait (`Init` or `PinInit`)
                             //   for `#ident`.
                             unsafe { #data.#ident(::core::ptr::addr_of_mut!((*#slot).#ident), #init)? };
-                        }
-                        #(#attrs)*
-                        // SAFETY: TODO
-                        #[allow(unused_variables)]
-                        let #ident = unsafe { #data.#project_ident(&mut (*#slot).#ident) };
-                    }
+                        },
+                        quote! {
+                            // SAFETY: TODO
+                            unsafe { #data.#project_ident(&mut (*#slot).#ident) }
+                        },
+                    )
                 } else {
-                    quote! {
-                        #(#attrs)*
-                        {
-                            let #init = #value;
+                    (
+                        quote! {
                             // SAFETY: `slot` is valid, because we are inside of an initializer
                             // closure, we return when an error/panic occurs.
                             unsafe {
@@ -301,12 +311,27 @@ fn init_fields(
                                     ::core::ptr::addr_of_mut!((*#slot).#ident),
                                 )?
                             };
-                        }
+                        },
+                        quote! {
+                            // SAFETY: TODO
+                            unsafe { &mut (*#slot).#ident }
+                        },
+                    )
+                };
+                let accessor = generate_initialized_accessors.then(|| {
+                    quote! {
                         #(#attrs)*
-                        // SAFETY: TODO
                         #[allow(unused_variables)]
-                        let #ident = unsafe { &mut (*#slot).#ident };
+                        let #ident = #accessor;
+                    }
+                });
+                quote! {
+                    #(#attrs)*
+                    {
+                        let #init = #value;
+                        #value_init
                     }
+                    #accessor
                 }
             }
             InitializerKind::Code { block: value, .. } => quote! {
@@ -436,6 +461,10 @@ fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
                 if a.path().is_ident("default_error") {
                     a.parse_args::<DefaultErrorAttribute>()
                         .map(InitializerAttribute::DefaultError)
+                } else if a.path().is_ident("disable_initialized_field_access") {
+                    a.meta
+                        .require_path_only()
+                        .map(|_| InitializerAttribute::DisableInitializedFieldAccess)
                 } else {
                     Err(syn::Error::new_spanned(a, "unknown initializer attribute"))
                 }
-- 
2.51.2
Re: [PATCH 12/12] rust: pin-init: internal: init: add escape hatch for referencing initialized fields
Posted by Gary Guo 1 month ago
On Thu Jan 8, 2026 at 1:50 PM GMT, Benno Lossin wrote:
> The initializer macro emits mutable references for already initialized
> fields, which allows modifying or accessing them later in code blocks or
> when initializing other fields. This behavior results in compiler errors
> when combining with packed structs, since those do not permit creating
> references to misaligned fields. For example:
>
>     #[repr(C, packed)]
>     struct Foo {
>         a: i8,
>         b: i32,
>     }
>
>     fn main() {
>         let _ = init!(Foo { a: -42, b: 42 });
>     }
>
> This will lead to an error like this:
>
>     error[E0793]: reference to field of packed struct is unaligned
>       --> tests/ui/compile-fail/init/packed_struct.rs:10:13
>        |
>     10 |     let _ = init!(Foo { a: -42, b: 42 });
>        |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>        |
>        = note: this struct is 1-byte aligned, but the type of this field may require higher alignment
>        = note: creating a misaligned reference is undefined behavior (even if that reference is never dereferenced)
>        = help: copy the field contents to a local variable, or replace the reference with a raw pointer and use `read_unaligned`/`write_unaligned` (loads and stores via `*p` must be properly aligned even when using raw pointers)
>        = note: this error originates in the macro `init` (in Nightly builds, run with -Z macro-backtrace for more info)
>
> This was requested by Janne Grunau [1] and will most certainly be used
> by the kernel when we eventually end up with trying to initialize packed
> structs.
>
> Thus add an initializer attribute `#[disable_initialized_field_access]`
> that does what the name suggests: do not generate references to already
> initialized fields.
>
> There is space for future work: add yet another attribute which can be
> applied on fields of initializers that ask for said field to be made
> accessible. We can add that when the need arises.

An alternative might be checking if the specific field is actually used later
and only generate references if so. Although, that might be "too much magic"?

Best,
Gary

>
> Requested-by: Janne Grunau <j@jannau.net>
> Link: https://lore.kernel.org/all/20251206170214.GE1097212@robin.jannau.net [1]
> Signed-off-by: Benno Lossin <lossin@kernel.org>
Re: [PATCH 12/12] rust: pin-init: internal: init: add escape hatch for referencing initialized fields
Posted by Benno Lossin 1 month ago
On Fri Jan 9, 2026 at 2:58 PM CET, Gary Guo wrote:
> On Thu Jan 8, 2026 at 1:50 PM GMT, Benno Lossin wrote:
>> The initializer macro emits mutable references for already initialized
>> fields, which allows modifying or accessing them later in code blocks or
>> when initializing other fields. This behavior results in compiler errors
>> when combining with packed structs, since those do not permit creating
>> references to misaligned fields. For example:
>>
>>     #[repr(C, packed)]
>>     struct Foo {
>>         a: i8,
>>         b: i32,
>>     }
>>
>>     fn main() {
>>         let _ = init!(Foo { a: -42, b: 42 });
>>     }
>>
>> This will lead to an error like this:
>>
>>     error[E0793]: reference to field of packed struct is unaligned
>>       --> tests/ui/compile-fail/init/packed_struct.rs:10:13
>>        |
>>     10 |     let _ = init!(Foo { a: -42, b: 42 });
>>        |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>>        |
>>        = note: this struct is 1-byte aligned, but the type of this field may require higher alignment
>>        = note: creating a misaligned reference is undefined behavior (even if that reference is never dereferenced)
>>        = help: copy the field contents to a local variable, or replace the reference with a raw pointer and use `read_unaligned`/`write_unaligned` (loads and stores via `*p` must be properly aligned even when using raw pointers)
>>        = note: this error originates in the macro `init` (in Nightly builds, run with -Z macro-backtrace for more info)
>>
>> This was requested by Janne Grunau [1] and will most certainly be used
>> by the kernel when we eventually end up with trying to initialize packed
>> structs.
>>
>> Thus add an initializer attribute `#[disable_initialized_field_access]`
>> that does what the name suggests: do not generate references to already
>> initialized fields.
>>
>> There is space for future work: add yet another attribute which can be
>> applied on fields of initializers that ask for said field to be made
>> accessible. We can add that when the need arises.
>
> An alternative might be checking if the specific field is actually used later
> and only generate references if so. Although, that might be "too much magic"?

Sounds like a good idea, I'll consider for a future series.

Cheers,
Benno
Re: [PATCH 12/12] rust: pin-init: internal: init: add escape hatch for referencing initialized fields
Posted by kernel test robot 1 month ago
Hi Benno,

kernel test robot noticed the following build warnings:

[auto build test WARNING on rust/rust-next]
[also build test WARNING on linus/master v6.19-rc4 next-20260109]
[cannot apply to rust/pin-init-next]
[If your patch is applied to the wrong git tree, kindly drop us a note.
And when submitting patch, we suggest to use '--base' as documented in
https://git-scm.com/docs/git-format-patch#_base_tree_information]

url:    https://github.com/intel-lab-lkp/linux/commits/Benno-Lossin/rust-pin-init-allow-the-crate-to-refer-to-itself-as-pin-init-in-doc-tests/20260108-234753
base:   https://github.com/Rust-for-Linux/linux rust-next
patch link:    https://lore.kernel.org/r/20260108135127.3153925-13-lossin%40kernel.org
patch subject: [PATCH 12/12] rust: pin-init: internal: init: add escape hatch for referencing initialized fields
config: x86_64-rhel-9.4-rust (https://download.01.org/0day-ci/archive/20260109/202601091003.kjIJiuUn-lkp@intel.com/config)
compiler: clang version 20.1.8 (https://github.com/llvm/llvm-project 87f0227cb60147a26a1eeb4fb06e3b505e9c7261)
rustc: rustc 1.88.0 (6b00bc388 2025-06-23)
reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20260109/202601091003.kjIJiuUn-lkp@intel.com/reproduce)

If you fix the issue in a separate patch/commit (i.e. not just a new version of
the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <lkp@intel.com>
| Closes: https://lore.kernel.org/oe-kbuild-all/202601091003.kjIJiuUn-lkp@intel.com/

All warnings (new ones prefixed by >>):

>> warning: large size difference between variants
   --> rust/pin-init/internal/src/init.rs:59:1
   |
   59 | / enum InitializerAttribute {
   60 | |     DefaultError(DefaultErrorAttribute),
   | |     ----------------------------------- the largest variant contains at least 224 bytes
   61 | |     DisableInitializedFieldAccess,
   | |     ----------------------------- the second-largest variant carries no data at all
   62 | | }
   | |_^ the entire enum is at least 224 bytes
   |
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#large_enum_variant
   = note: `-W clippy::large-enum-variant` implied by `-W clippy::all`
   = help: to override `-W clippy::all` add `#[allow(clippy::large_enum_variant)]`
   help: consider boxing the large fields to reduce the total size of the enum
   |
   60 -     DefaultError(DefaultErrorAttribute),
   60 +     DefaultError(Box<DefaultErrorAttribute>),
   |

-- 
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki