redoubt_zero_derive/
lib.rs1#![warn(missing_docs)]
14
15#[cfg(all(
17 test,
18 any(
19 target_arch = "x86_64",
20 target_arch = "x86",
21 target_arch = "aarch64",
22 target_arch = "loongarch64"
23 )
24))]
25mod tests;
26
27use proc_macro::TokenStream;
28use proc_macro_crate::{FoundCrate, crate_name};
29use proc_macro2::{Span, TokenStream as TokenStream2};
30use quote::{format_ident, quote};
31use syn::{
32 Attribute, Data, DeriveInput, Fields, Ident, Index, LitStr, Meta, Type, parse_macro_input,
33};
34
35#[proc_macro_derive(RedoubtZero, attributes(fast_zeroize))]
103pub fn derive_redoubt_zero(input: TokenStream) -> TokenStream {
104 let input = parse_macro_input!(input as DeriveInput);
105 expand(input).unwrap_or_else(|e| e).into()
106}
107
108pub(crate) fn find_root_with_candidates(candidates: &[&'static str]) -> TokenStream2 {
113 for &candidate in candidates {
114 if let Some((crate_part, path_part)) = candidate.split_once("::") {
116 match crate_name(crate_part) {
117 Ok(FoundCrate::Itself) => {
118 let path: TokenStream2 = path_part.parse().unwrap_or_else(|_| quote!());
119 return quote!(crate::#path);
120 }
121 Ok(FoundCrate::Name(name)) => {
122 let crate_id = Ident::new(&name, Span::call_site());
123 let path: TokenStream2 = path_part.parse().unwrap_or_else(|_| quote!());
124 return quote!(#crate_id::#path);
125 }
126 Err(_) => continue,
127 }
128 } else {
129 match crate_name(candidate) {
130 Ok(FoundCrate::Itself) => return quote!(crate),
131 Ok(FoundCrate::Name(name)) => {
132 let id = Ident::new(&name, Span::call_site());
133 return quote!(#id);
134 }
135 Err(_) => continue,
136 }
137 }
138 }
139
140 let msg = "RedoubtZero: could not find redoubt-zero or redoubt-zero-core. Add redoubt-zero to Cargo.toml.";
141 let lit = LitStr::new(msg, Span::call_site());
142 quote! { compile_error!(#lit); }
143}
144
145pub(crate) fn is_zeroize_on_drop_sentinel_type(ty: &Type) -> bool {
149 matches!(
150 ty,
151 Type::Path(type_path)
152 if type_path.path.segments.last()
153 .map(|seg| seg.ident == "ZeroizeOnDropSentinel")
154 .unwrap_or(false)
155 )
156}
157
158pub(crate) fn is_mut_reference_type(ty: &Type) -> bool {
163 if let Type::Reference(r) = ty {
164 r.mutability.is_some()
165 } else {
166 false
167 }
168}
169
170pub(crate) fn is_immut_reference_type(ty: &Type) -> bool {
174 if let Type::Reference(r) = ty {
175 r.mutability.is_none()
176 } else {
177 false
178 }
179}
180
181fn has_fast_zeroize_skip(attrs: &[Attribute]) -> bool {
183 attrs.iter().any(|attr| match &attr.meta {
184 Meta::List(meta_list) => {
185 meta_list.path.is_ident("fast_zeroize") && meta_list.tokens.to_string().contains("skip")
186 }
187 _ => false,
188 })
189}
190
191fn has_fast_zeroize_drop(attrs: &[Attribute]) -> bool {
193 attrs.iter().any(|attr| match &attr.meta {
194 Meta::List(meta_list) => {
195 meta_list.path.is_ident("fast_zeroize") && meta_list.tokens.to_string().contains("drop")
196 }
197 _ => false,
198 })
199}
200
201struct SentinelState {
203 index: usize,
204 access: TokenStream2,
205}
206
207fn expand(input: DeriveInput) -> Result<TokenStream2, TokenStream2> {
209 let struct_name = &input.ident;
210 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
211
212 let root = find_root_with_candidates(&["redoubt-zero-core", "redoubt-zero", "redoubt::zero"]);
214
215 let all_fields: Vec<(usize, &syn::Field)> = match &input.data {
217 Data::Struct(data) => match &data.fields {
218 Fields::Named(named) => named.named.iter().enumerate().collect(),
219 Fields::Unnamed(unnamed) => unnamed.unnamed.iter().enumerate().collect(),
220 Fields::Unit => vec![],
221 },
222 _ => {
223 return Err(syn::Error::new_spanned(
224 &input.ident,
225 "RedoubtZero can only be derived for structs (named or tuple).",
226 )
227 .to_compile_error());
228 }
229 };
230
231 let sentinel_ident = format_ident!("__sentinel");
233 let mut maybe_sentinel_state: Option<SentinelState> = None;
234
235 for (i, f) in &all_fields {
236 let is_sentinel = if let Some(ident) = &f.ident {
237 if *ident == sentinel_ident {
239 maybe_sentinel_state = Some(SentinelState {
240 index: *i,
241 access: quote! { self.#sentinel_ident },
242 });
243 true
244 } else {
245 false
246 }
247 } else {
248 if is_zeroize_on_drop_sentinel_type(&f.ty) {
250 let idx = Index::from(*i);
251 maybe_sentinel_state = Some(SentinelState {
252 index: *i,
253 access: quote! { self.#idx },
254 });
255 true
256 } else {
257 false
258 }
259 };
260
261 if is_sentinel {
262 break;
263 }
264 }
265
266 let sentinel_idx = maybe_sentinel_state.as_ref().map(|s| s.index);
271
272 for (i, f) in &all_fields {
273 if Some(*i) == sentinel_idx {
274 continue;
275 }
276
277 if is_immut_reference_type(&f.ty) && !has_fast_zeroize_skip(&f.attrs) {
278 let field_name = if let Some(ident) = &f.ident {
279 format!("field `{}`", ident)
280 } else {
281 format!("field at index {}", i)
282 };
283
284 return Err(syn::Error::new_spanned(
285 &f.ty,
286 format!(
287 "{} has type `&T` (immutable reference) which cannot be zeroized. \
288 Add `#[fast_zeroize(skip)]` to exclude it from zeroization.",
289 field_name
290 ),
291 )
292 .to_compile_error());
293 }
294 }
295
296 let (immut_refs_without_sentinel, _): (Vec<TokenStream2>, Vec<TokenStream2>) = all_fields
303 .iter()
304 .filter(|(i, f)| Some(*i) != sentinel_idx && !has_fast_zeroize_skip(&f.attrs))
305 .map(|(i, f)| {
306 let is_mut_ref = is_mut_reference_type(&f.ty);
307
308 if let Some(ident) = &f.ident {
309 let immut_ref = if is_mut_ref {
310 quote! { self.#ident }
311 } else {
312 quote! { &self.#ident }
313 };
314 (immut_ref, quote! { &mut self.#ident })
315 } else {
316 let idx = Index::from(*i);
317 let immut_ref = if is_mut_ref {
318 quote! { self.#idx }
319 } else {
320 quote! { &self.#idx }
321 };
322 (immut_ref, quote! { &mut self.#idx })
323 }
324 })
325 .unzip();
326
327 let (_, mut_refs_with_sentinel): (Vec<TokenStream2>, Vec<TokenStream2>) = all_fields
330 .iter()
331 .filter(|(_, f)| !has_fast_zeroize_skip(&f.attrs))
332 .map(|(i, f)| {
333 let is_mut_ref = is_mut_reference_type(&f.ty);
334
335 if let Some(ident) = &f.ident {
336 let mut_ref = if is_mut_ref {
337 quote! { self.#ident }
338 } else {
339 quote! { &mut self.#ident }
340 };
341 (quote! { &self.#ident }, mut_ref)
342 } else {
343 let idx = Index::from(*i);
344 let mut_ref = if is_mut_ref {
345 quote! { self.#idx }
346 } else {
347 quote! { &mut self.#idx }
348 };
349 (quote! { &self.#idx }, mut_ref)
350 }
351 })
352 .unzip();
353
354 let len_without_sentinel = immut_refs_without_sentinel.len();
356 let len_without_sentinel_lit =
357 syn::LitInt::new(&len_without_sentinel.to_string(), Span::call_site());
358
359 let len_with_sentinel = mut_refs_with_sentinel.len();
360 let len_with_sentinel_lit = syn::LitInt::new(&len_with_sentinel.to_string(), Span::call_site());
361
362 let should_generate_drop = has_fast_zeroize_drop(&input.attrs);
364
365 let drop_impl = if should_generate_drop {
367 quote! {
368 impl #impl_generics Drop for #struct_name #ty_generics #where_clause {
369 fn drop(&mut self) {
370 #root::FastZeroizable::fast_zeroize(self);
371 }
372 }
373 }
374 } else {
375 quote! {}
376 };
377
378 let output = quote! {
379 impl #impl_generics #root::ZeroizeMetadata for #struct_name #ty_generics #where_clause {
380 const CAN_BE_BULK_ZEROIZED: bool = false;
381 }
382
383 impl #impl_generics #root::FastZeroizable for #struct_name #ty_generics #where_clause {
384 fn fast_zeroize(&mut self) {
385 let fields: [&mut dyn #root::FastZeroizable; #len_with_sentinel_lit] = [
386 #( #root::collections::to_fast_zeroizable_dyn_mut(#mut_refs_with_sentinel) ),*
387 ];
388 #root::collections::zeroize_collection(&mut fields.into_iter())
389 }
390 }
391
392 impl #impl_generics #root::ZeroizationProbe for #struct_name #ty_generics #where_clause {
393 fn is_zeroized(&self) -> bool {
394 let fields: [&dyn #root::ZeroizationProbe; #len_without_sentinel_lit] = [
395 #( #root::collections::to_zeroization_probe_dyn_ref(#immut_refs_without_sentinel) ),*
396 ];
397 #root::collections::collection_zeroed(&mut fields.into_iter())
398 }
399 }
400
401 #drop_impl
402 };
403
404 let assert_impl = if let Some(sentinel_state) = maybe_sentinel_state {
406 let sentinel_access = sentinel_state.access;
407 quote! {
408 impl #impl_generics #root::AssertZeroizeOnDrop for #struct_name #ty_generics #where_clause {
409 fn clone_sentinel(&self) -> #root::ZeroizeOnDropSentinel {
410 #sentinel_access.clone()
411 }
412
413 fn assert_zeroize_on_drop(self) {
414 #root::assert::assert_zeroize_on_drop(self);
415 }
416 }
417 }
418 } else {
419 quote! {}
420 };
421
422 let full_output = quote! {
423 #output
424 #assert_impl
425 };
426
427 Ok(full_output)
428}