[PATCH v2 1/2] rust: pin-init: internal: init: remove `#[disable_initialized_field_access]`

From: Benno Lossin

Date: Mon Mar 02 2026 - 09:11:18 EST


Gary noticed [1] that the initializer macros as well as the `[Pin]Init`
traits cannot support unaligned fields, since they use operations that
require aligned pointers. This means that any code using structs with
unaligned fields in pin-init is unsound.

By default, the `init!` macro generates references to initialized
fields, which makes the compiler check that those fields are aligned.
However, we added the `#[disable_initialized_field_access]` attribute to
avoid this behavior in ceca298c53f9 ("rust: pin-init: internal: init:
add escape hatch for referencing initialized fields"). Thus remove the
`#[disable_initialized_field_access]` attribute from `init!`, which is
the only safe way to create an initializer handling unaligned fields.

If support for in-place initializing structs with unaligned fields is
required in the future, we could figure out a solution. This is tracked
in [2].

Reported-by: Gary Guo <gary@xxxxxxxxxxx>
Link: https://rust-for-linux.zulipchat.com/#narrow/channel/561532-pin-init/topic/initialized.20field.20accessor.20detection/with/576210658 [1]
Link: https://github.com/Rust-for-Linux/pin-init/issues/112 [2]
Fixes: ceca298c53f9 ("rust: pin-init: internal: init: add escape hatch for referencing initialized fields")
Signed-off-by: Benno Lossin <lossin@xxxxxxxxxx>
---
Changelog:
* changes since v1: https://lore.kernel.org/all/20260228113713.1402110-1-lossin@xxxxxxxxxx
- improved note added to the code in patch 2
- improved commit messages
---
This commit does not need backporting, as ceca298c53f9 is not yet in any
stable tree.

However, the unsoundness still affects two stable trees, because it was
unknowingly fixed in commit 42415d163e5d ("rust: pin-init: add
references to previously initialized fields"). Before then, packed
structs compiled without any issues with pin-init and thus all prior
kernel versions with pin-init that do not contain that commit are
affected.

We introduced pin-init in 90e53c5e70a6 ("rust: add pin-init API core"),
which was included in 6.4. The affected stable trees that are still
maintained are: 6.12 and 6.6. Note that 6.18 and 6.19 already contain
42415d163e5d, so they are unaffected.

I will prepare a separate patch series to backport 42415d163e5d to each
of the affected trees, including the second patch of this series that
documents the fact that field accessors are load-bearing for soundness.

Janne Grunau has worked around [3] this problem in the AOP audio driver
downstream, which was the reason for adding the
`#[disable_initialized_field_access]` attribute in the first place.

[3]: https://lore.kernel.org/all/20260301171222.GA22561@xxxxxxxxxxxxxxxx
---
rust/pin-init/internal/src/init.rs | 39 ++++++------------------------
1 file changed, 8 insertions(+), 31 deletions(-)

diff --git a/rust/pin-init/internal/src/init.rs b/rust/pin-init/internal/src/init.rs
index 42936f915a07..da53adc44ecf 100644
--- a/rust/pin-init/internal/src/init.rs
+++ b/rust/pin-init/internal/src/init.rs
@@ -62,7 +62,6 @@ fn ident(&self) -> Option<&Ident> {

enum InitializerAttribute {
DefaultError(DefaultErrorAttribute),
- DisableInitializedFieldAccess,
}

struct DefaultErrorAttribute {
@@ -86,6 +85,7 @@ pub(crate) fn expand(
let error = error.map_or_else(
|| {
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 {
@@ -145,15 +145,7 @@ 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 = Ident::new("__data", Span::mixed_site());
- let init_fields = init_fields(
- &fields,
- pinned,
- !attrs
- .iter()
- .any(|attr| matches!(attr, InitializerAttribute::DisableInitializedFieldAccess)),
- &data,
- &slot,
- );
+ let init_fields = init_fields(&fields, pinned, &data, &slot);
let field_check = make_field_check(&fields, init_kind, &path);
Ok(quote! {{
// We do not want to allow arbitrary returns, so we declare this type as the `Ok` return
@@ -236,7 +228,6 @@ fn get_init_kind(rest: Option<(Token![..], Expr)>, dcx: &mut DiagCtxt) -> InitKi
fn init_fields(
fields: &Punctuated<InitializerField, Token![,]>,
pinned: bool,
- generate_initialized_accessors: bool,
data: &Ident,
slot: &Ident,
) -> TokenStream {
@@ -272,13 +263,6 @@ fn init_fields(
unsafe { &mut (*#slot).#ident }
}
};
- let accessor = generate_initialized_accessors.then(|| {
- quote! {
- #(#cfgs)*
- #[allow(unused_variables)]
- let #ident = #accessor;
- }
- });
quote! {
#(#attrs)*
{
@@ -286,7 +270,9 @@ fn init_fields(
// SAFETY: TODO
unsafe { #write(::core::ptr::addr_of_mut!((*#slot).#ident), #value_ident) };
}
- #accessor
+ #(#cfgs)*
+ #[allow(unused_variables)]
+ let #ident = #accessor;
}
}
InitializerKind::Init { ident, value, .. } => {
@@ -326,20 +312,15 @@ fn init_fields(
},
)
};
- let accessor = generate_initialized_accessors.then(|| {
- quote! {
- #(#cfgs)*
- #[allow(unused_variables)]
- let #ident = #accessor;
- }
- });
quote! {
#(#attrs)*
{
let #init = #value;
#value_init
}
- #accessor
+ #(#cfgs)*
+ #[allow(unused_variables)]
+ let #ident = #accessor;
}
}
InitializerKind::Code { block: value, .. } => quote! {
@@ -466,10 +447,6 @@ 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"))
}

base-commit: 6de23f81a5e08be8fbf5e8d7e9febc72a5b5f27f
--
2.53.0