Skip to main content

zeroize_derive/
zeroize_derive.rs

1//! Custom derive support for `zeroize`
2
3#![crate_type = "proc-macro"]
4#![forbid(unsafe_code)]
5#![warn(trivial_casts, unused_qualifications)]
6
7use proc_macro2::{Ident, TokenStream};
8use quote::{format_ident, quote};
9use syn::{
10    Attribute, Data, DeriveInput, Expr, ExprLit, Field, Fields, Lit, Meta, Result, Variant, WherePredicate,
11    parse::{Parse, ParseStream},
12    parse_quote,
13    punctuated::Punctuated,
14    token::Comma,
15    visit::Visit,
16};
17
18/// Name of zeroize-related attributes
19const ZEROIZE_ATTR: &str = "zeroize";
20
21/// Derive the `Zeroize` trait.
22///
23/// Supports the following attributes:
24///
25/// On the item level:
26/// - `#[zeroize(bound = "T: MyTrait")]`: this replaces any trait bounds
27///   inferred by zeroize-derive
28///
29/// On the field level:
30/// - `#[zeroize(skip)]`: skips this field or variant when calling `zeroize()`
31#[proc_macro_derive(Zeroize, attributes(zeroize))]
32pub fn derive_zeroize(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
33    derive_zeroize_impl(syn::parse_macro_input!(input as DeriveInput)).into()
34}
35
36fn derive_zeroize_impl(input: DeriveInput) -> TokenStream {
37    let attributes = ZeroizeAttrs::parse(&input);
38
39    let mut generics = input.generics.clone();
40
41    let extra_bounds = match attributes.bound {
42        Some(bounds) => bounds.0,
43        None => attributes
44            .auto_params
45            .iter()
46            .map(|type_param| -> WherePredicate {
47                parse_quote! {#type_param: ::zeroize::Zeroize}
48            })
49            .collect(),
50    };
51
52    generics.make_where_clause().predicates.extend(extra_bounds);
53
54    let ty_name = &input.ident;
55
56    let (impl_gen, type_gen, where_) = generics.split_for_impl();
57
58    let zeroizers = generate_fields(&input, quote! { zeroize });
59    quote! {
60        impl #impl_gen ::zeroize::Zeroize for #ty_name #type_gen #where_ {
61            fn zeroize(&mut self) {
62                #zeroizers
63            }
64        }
65    }
66}
67
68/// Derive the `ZeroizeOnDrop` trait.
69///
70/// Supports the following attributes:
71///
72/// On the field level:
73/// - `#[zeroize(skip)]`: skips this field or variant when calling `zeroize()`
74#[proc_macro_derive(ZeroizeOnDrop, attributes(zeroize))]
75pub fn derive_zeroize_on_drop(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
76    derive_zeroize_on_drop_impl(syn::parse_macro_input!(input as DeriveInput)).into()
77}
78
79fn derive_zeroize_on_drop_impl(input: DeriveInput) -> TokenStream {
80    let zeroizers = generate_fields(&input, quote! { zeroize_or_on_drop });
81
82    let (impl_gen, type_gen, where_) = input.generics.split_for_impl();
83    let name = input.ident.clone();
84
85    let drop_impl = quote! {
86        impl #impl_gen Drop for #name #type_gen #where_ {
87            fn drop(&mut self) {
88                use ::zeroize::__internal::AssertZeroize;
89                use ::zeroize::__internal::AssertZeroizeOnDrop;
90                #zeroizers
91            }
92        }
93    };
94    let zeroize_on_drop_impl = impl_zeroize_on_drop(&input);
95
96    quote! {
97        #drop_impl
98        #zeroize_on_drop_impl
99    }
100}
101
102/// Custom derive attributes for `Zeroize`
103#[derive(Default)]
104struct ZeroizeAttrs {
105    /// Custom bounds as defined by the user
106    bound: Option<Bounds>,
107    /// Type parameters in use by fields
108    auto_params: Vec<Ident>,
109}
110
111/// Parsing helper for custom bounds
112struct Bounds(Punctuated<WherePredicate, Comma>);
113
114impl Parse for Bounds {
115    fn parse(input: ParseStream<'_>) -> Result<Self> {
116        Ok(Self(Punctuated::parse_terminated(input)?))
117    }
118}
119
120struct BoundAccumulator<'a> {
121    generics: &'a syn::Generics,
122    params: Vec<Ident>,
123}
124
125impl<'ast> Visit<'ast> for BoundAccumulator<'ast> {
126    fn visit_path(&mut self, path: &'ast syn::Path) {
127        if path.segments.len() == 1 {
128            if let Some(segment) = path.segments.first() {
129                for param in &self.generics.params {
130                    if let syn::GenericParam::Type(type_param) = param {
131                        if type_param.ident == segment.ident && !self.params.contains(&segment.ident) {
132                            self.params.push(type_param.ident.clone());
133                        }
134                    }
135                }
136            }
137        }
138
139        // Recurse into the path so that type parameters nested inside another type (e.g. the `T`
140        // in `Vec<T>` or `Option<Box<[T]>>`) are still picked up.
141        syn::visit::visit_path(self, path);
142    }
143}
144
145impl ZeroizeAttrs {
146    /// Parse attributes from the incoming AST
147    fn parse(input: &DeriveInput) -> Self {
148        let mut result = Self::default();
149        let mut bound_accumulator = BoundAccumulator {
150            generics: &input.generics,
151            params: Vec::new(),
152        };
153
154        for attr in &input.attrs {
155            result.parse_attr(attr, None, None);
156        }
157
158        match &input.data {
159            Data::Enum(enum_) => {
160                for variant in &enum_.variants {
161                    for attr in &variant.attrs {
162                        result.parse_attr(attr, Some(variant), None);
163                    }
164                    for field in &variant.fields {
165                        for attr in &field.attrs {
166                            result.parse_attr(attr, Some(variant), Some(field));
167                        }
168                        if !attr_skip(&field.attrs) {
169                            bound_accumulator.visit_type(&field.ty);
170                        }
171                    }
172                }
173            }
174            Data::Struct(struct_) => {
175                for field in &struct_.fields {
176                    for attr in &field.attrs {
177                        result.parse_attr(attr, None, Some(field));
178                    }
179                    if !attr_skip(&field.attrs) {
180                        bound_accumulator.visit_type(&field.ty);
181                    }
182                }
183            }
184            Data::Union(union_) => panic!("Unsupported untagged union {union_:?}"),
185        }
186
187        result.auto_params = bound_accumulator.params;
188
189        result
190    }
191
192    /// Parse attribute and handle `#[zeroize(...)]` attributes
193    fn parse_attr(&mut self, attr: &Attribute, variant: Option<&Variant>, binding: Option<&Field>) {
194        let meta_list = match &attr.meta {
195            Meta::List(list) => list,
196            _ => return,
197        };
198
199        // Ignore any non-zeroize attributes
200        if !meta_list.path.is_ident(ZEROIZE_ATTR) {
201            return;
202        }
203
204        for meta in attr
205            .parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)
206            .unwrap_or_else(|e| panic!("error parsing attribute: {attr:?} ({e})"))
207        {
208            self.parse_meta(&meta, variant, binding);
209        }
210    }
211
212    /// Parse `#[zeroize(...)]` attribute metadata
213    fn parse_meta(&mut self, meta: &Meta, variant: Option<&Variant>, binding: Option<&Field>) {
214        if meta.path().is_ident("bound") {
215            assert!(self.bound.is_none(), "duplicate #[zeroize] bound flags");
216
217            match (variant, binding) {
218                (_variant, Some(_binding)) => {
219                    // structs don't have a variant prefix, and only structs have bindings outside of a variant
220                    let item_kind = match variant {
221                        Some(_) => "enum",
222                        None => "struct",
223                    };
224                    panic!(
225                        concat!(
226                            "The #[zeroize(bound)] attribute is not allowed on {} fields. ",
227                            "Use it on the containing {} instead.",
228                        ),
229                        item_kind, item_kind,
230                    )
231                }
232                (Some(_variant), None) => panic!(concat!(
233                    "The #[zeroize(bound)] attribute is not allowed on enum variants. ",
234                    "Use it on the containing enum instead.",
235                )),
236                (None, None) => {
237                    if let Meta::NameValue(meta_name_value) = meta {
238                        if let Expr::Lit(ExprLit {
239                            lit: Lit::Str(lit), ..
240                        }) = &meta_name_value.value
241                        {
242                            if lit.value().is_empty() {
243                                self.bound = Some(Bounds(Punctuated::new()));
244                            } else {
245                                self.bound = Some(
246                                    lit.parse()
247                                        .unwrap_or_else(|e| panic!("error parsing bounds: {lit:?} ({e})")),
248                                );
249                            }
250
251                            return;
252                        }
253                    }
254
255                    panic!(concat!(
256                        "The #[zeroize(bound)] attribute expects a name-value syntax with a string literal value.",
257                        "E.g. #[zeroize(bound = \"T: MyTrait\")]."
258                    ))
259                }
260            }
261        } else if meta.path().is_ident("skip") {
262            assert!(
263                !(variant.is_none() && binding.is_none()),
264                concat!(
265                    "The #[zeroize(skip)] attribute is not allowed on a `struct` or `enum`. ",
266                    "Use it on a field or variant instead.",
267                )
268            );
269        } else {
270            panic!("unknown #[zeroize] attribute type: {:?}", meta.path());
271        }
272    }
273}
274
275fn field_ident(n: usize, field: &Field) -> Ident {
276    if let Some(ref name) = field.ident {
277        name.clone()
278    } else {
279        format_ident!("__zeroize_field_{}", n)
280    }
281}
282
283fn generate_fields(input: &DeriveInput, method: TokenStream) -> TokenStream {
284    let input_id = &input.ident;
285    let fields: Vec<_> = match input.data {
286        Data::Enum(ref enum_) => enum_
287            .variants
288            .iter()
289            .filter_map(|variant| {
290                if attr_skip(&variant.attrs) {
291                    assert!(
292                        !variant.fields.iter().any(|field| attr_skip(&field.attrs)),
293                        "duplicate #[zeroize] skip flags"
294                    );
295                    None
296                } else {
297                    let variant_id = &variant.ident;
298                    Some((quote! { #input_id :: #variant_id }, &variant.fields))
299                }
300            })
301            .collect(),
302        Data::Struct(ref struct_) => vec![(quote! { #input_id }, &struct_.fields)],
303        Data::Union(ref union_) => panic!("Cannot generate fields for untagged union {union_:?}"),
304    };
305
306    let arms = fields.into_iter().map(|(name, fields)| {
307        let method_field = fields.iter().enumerate().filter_map(|(n, field)| {
308            if attr_skip(&field.attrs) {
309                None
310            } else {
311                let name = field_ident(n, field);
312                Some(quote! { #name.#method() })
313            }
314        });
315
316        let field_bindings = fields.iter().enumerate().map(|(n, field)| field_ident(n, field));
317
318        let binding = match fields {
319            Fields::Named(_) => quote! {
320                #name { #(#field_bindings),* }
321            },
322            Fields::Unnamed(_) => quote! {
323                #name ( #(#field_bindings),* )
324            },
325            Fields::Unit => quote! {
326                #name
327            },
328        };
329
330        quote! {
331            #[allow(unused_variables, unused_assignments)]
332            #binding => {
333                #(#method_field);*
334            }
335        }
336    });
337
338    quote! {
339        match self {
340            #(#arms),*
341            _ => {}
342        }
343    }
344}
345
346fn attr_skip(attrs: &[Attribute]) -> bool {
347    let mut result = false;
348    for attr in attrs.iter().map(|attr| &attr.meta) {
349        if let Meta::List(list) = attr {
350            if list.path.is_ident(ZEROIZE_ATTR) {
351                for meta in list
352                    .parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)
353                    .unwrap_or_else(|e| panic!("error parsing attribute: {list:?} ({e})"))
354                {
355                    if let Meta::Path(path) = meta {
356                        if path.is_ident("skip") {
357                            assert!(!result, "duplicate #[zeroize] skip flags");
358                            result = true;
359                        }
360                    }
361                }
362            }
363        }
364    }
365    result
366}
367
368fn impl_zeroize_on_drop(input: &DeriveInput) -> TokenStream {
369    let name = input.ident.clone();
370    let (impl_gen, type_gen, where_) = input.generics.split_for_impl();
371    quote! {
372        #[doc(hidden)]
373        impl #impl_gen ::zeroize::ZeroizeOnDrop for #name #type_gen #where_ {}
374    }
375}
376
377#[cfg(test)]
378mod tests {
379    use super::*;
380
381    #[track_caller]
382    fn test_derive(f: impl Fn(DeriveInput) -> TokenStream, input: TokenStream, expected_output: TokenStream) {
383        let output = f(syn::parse2(input).unwrap());
384        assert_eq!(format!("{output}"), format!("{expected_output}"));
385    }
386
387    #[track_caller]
388    fn parse_zeroize_test(unparsed: &str) -> TokenStream {
389        derive_zeroize_impl(syn::parse_str(unparsed).expect("Failed to parse test input"))
390    }
391
392    #[test]
393    fn zeroize_without_drop() {
394        test_derive(
395            derive_zeroize_impl,
396            quote! {
397                struct Z {
398                    a: String,
399                    b: Vec<u8>,
400                    c: [u8; 3],
401                }
402            },
403            quote! {
404                impl ::zeroize::Zeroize for Z {
405                    fn zeroize(&mut self) {
406                        match self {
407                            #[allow(unused_variables, unused_assignments)]
408                            Z { a, b, c } => {
409                                a.zeroize();
410                                b.zeroize();
411                                c.zeroize()
412                            }
413                            _ => {}
414                        }
415                    }
416                }
417            },
418        );
419    }
420
421    #[test]
422    fn zeroize_with_skip() {
423        test_derive(
424            derive_zeroize_impl,
425            quote! {
426                struct Z {
427                    a: String,
428                    b: Vec<u8>,
429                    #[zeroize(skip)]
430                    c: [u8; 3],
431                }
432            },
433            quote! {
434                impl ::zeroize::Zeroize for Z {
435                    fn zeroize(&mut self) {
436                        match self {
437                            #[allow(unused_variables, unused_assignments)]
438                            Z { a, b, c } => {
439                                a.zeroize();
440                                b.zeroize()
441                            }
442                            _ => {}
443                        }
444                    }
445                }
446            },
447        );
448    }
449
450    #[test]
451    #[should_panic(expected = "unknown #[zeroize] attribute type")]
452    fn zeroize_drop_attribute_removed() {
453        parse_zeroize_test(stringify!(
454            #[zeroize(drop)]
455            struct Z {
456                a: String,
457            }
458        ));
459    }
460
461    #[test]
462    fn zeroize_with_bound() {
463        test_derive(
464            derive_zeroize_impl,
465            quote! {
466                #[zeroize(bound = "T: MyTrait")]
467                struct Z<T>(T);
468            },
469            quote! {
470                impl<T> ::zeroize::Zeroize for Z<T> where T: MyTrait {
471                    fn zeroize(&mut self) {
472                        match self {
473                            #[allow(unused_variables, unused_assignments)]
474                            Z(__zeroize_field_0) => {
475                                __zeroize_field_0.zeroize()
476                            }
477                            _ => {}
478                        }
479                    }
480                }
481            },
482        );
483    }
484
485    #[test]
486    fn zeroize_generic_nested_params() {
487        // Type parameters nested inside another type must still produce a bound, and that bound
488        // must be fully qualified so the derive works without `Zeroize` being imported.
489        let output = derive_zeroize_impl(
490            syn::parse_str("struct Z<T> { a: Vec<T>, b: Option<T>, c: [T; 2] }").expect("Failed to parse test input"),
491        )
492        .to_string();
493
494        assert!(
495            output.contains("T : :: zeroize :: Zeroize"),
496            "missing qualified `Zeroize` bound in: {output}"
497        );
498    }
499
500    #[test]
501    fn zeroize_only_drop() {
502        test_derive(
503            derive_zeroize_on_drop_impl,
504            quote! {
505                struct Z {
506                    a: String,
507                    b: Vec<u8>,
508                    c: [u8; 3],
509                }
510            },
511            quote! {
512                impl Drop for Z {
513                    fn drop(&mut self) {
514                        use ::zeroize::__internal::AssertZeroize;
515                        use ::zeroize::__internal::AssertZeroizeOnDrop;
516                        match self {
517                            #[allow(unused_variables, unused_assignments)]
518                            Z { a, b, c } => {
519                                a.zeroize_or_on_drop();
520                                b.zeroize_or_on_drop();
521                                c.zeroize_or_on_drop()
522                            }
523                            _ => {}
524                        }
525                    }
526                }
527                #[doc(hidden)]
528                impl ::zeroize::ZeroizeOnDrop for Z {}
529            },
530        );
531    }
532
533    #[test]
534    #[should_panic(
535        expected = "The #[zeroize(skip)] attribute is not allowed on a `struct` or `enum`. Use it on a field or variant instead."
536    )]
537    fn zeroize_skip_on_struct() {
538        parse_zeroize_test(stringify!(
539            #[zeroize(skip)]
540            struct Z {
541                a: String,
542                b: Vec<u8>,
543                c: [u8; 3],
544            }
545        ));
546    }
547
548    #[test]
549    #[should_panic(
550        expected = "The #[zeroize(skip)] attribute is not allowed on a `struct` or `enum`. Use it on a field or variant instead."
551    )]
552    fn zeroize_skip_on_enum() {
553        parse_zeroize_test(stringify!(
554            #[zeroize(skip)]
555            enum Z {
556                Variant1,
557                Variant2,
558            }
559        ));
560    }
561
562    #[test]
563    #[should_panic(expected = "duplicate #[zeroize] skip flags")]
564    fn zeroize_duplicate_skip() {
565        parse_zeroize_test(stringify!(
566            struct Z {
567                a: String,
568                #[zeroize(skip)]
569                #[zeroize(skip)]
570                b: Vec<u8>,
571                c: [u8; 3],
572            }
573        ));
574    }
575
576    #[test]
577    #[should_panic(expected = "duplicate #[zeroize] skip flags")]
578    fn zeroize_duplicate_skip_list() {
579        parse_zeroize_test(stringify!(
580            struct Z {
581                a: String,
582                #[zeroize(skip, skip)]
583                b: Vec<u8>,
584                c: [u8; 3],
585            }
586        ));
587    }
588
589    #[test]
590    #[should_panic(expected = "duplicate #[zeroize] skip flags")]
591    fn zeroize_duplicate_skip_enum() {
592        parse_zeroize_test(stringify!(
593            enum Z {
594                #[zeroize(skip)]
595                Variant {
596                    a: String,
597                    #[zeroize(skip)]
598                    b: Vec<u8>,
599                    c: [u8; 3],
600                },
601            }
602        ));
603    }
604
605    #[test]
606    #[should_panic(expected = "duplicate #[zeroize] bound flags")]
607    fn zeroize_duplicate_bound() {
608        parse_zeroize_test(stringify!(
609            #[zeroize(bound = "T: MyTrait")]
610            #[zeroize(bound = "")]
611            struct Z<T>(T);
612        ));
613    }
614
615    #[test]
616    #[should_panic(expected = "duplicate #[zeroize] bound flags")]
617    fn zeroize_duplicate_bound_list() {
618        parse_zeroize_test(stringify!(
619            #[zeroize(bound = "T: MyTrait", bound = "")]
620            struct Z<T>(T);
621        ));
622    }
623
624    #[test]
625    #[should_panic(
626        expected = "The #[zeroize(bound)] attribute is not allowed on struct fields. Use it on the containing struct instead."
627    )]
628    fn zeroize_bound_struct() {
629        parse_zeroize_test(stringify!(
630            struct Z<T> {
631                #[zeroize(bound = "T: MyTrait")]
632                a: T,
633            }
634        ));
635    }
636
637    #[test]
638    #[should_panic(
639        expected = "The #[zeroize(bound)] attribute is not allowed on enum variants. Use it on the containing enum instead."
640    )]
641    fn zeroize_bound_enum() {
642        parse_zeroize_test(stringify!(
643            enum Z<T> {
644                #[zeroize(bound = "T: MyTrait")]
645                A(T),
646            }
647        ));
648    }
649
650    #[test]
651    #[should_panic(
652        expected = "The #[zeroize(bound)] attribute is not allowed on enum fields. Use it on the containing enum instead."
653    )]
654    fn zeroize_bound_enum_variant_field() {
655        parse_zeroize_test(stringify!(
656            enum Z<T> {
657                A {
658                    #[zeroize(bound = "T: MyTrait")]
659                    a: T,
660                },
661            }
662        ));
663    }
664
665    #[test]
666    #[should_panic(
667        expected = "The #[zeroize(bound)] attribute expects a name-value syntax with a string literal value.E.g. #[zeroize(bound = \"T: MyTrait\")]."
668    )]
669    fn zeroize_bound_no_value() {
670        parse_zeroize_test(stringify!(
671            #[zeroize(bound)]
672            struct Z<T>(T);
673        ));
674    }
675
676    #[test]
677    #[should_panic(expected = "error parsing bounds: LitStr { token: \"T\" } (expected `:`)")]
678    fn zeroize_bound_no_where_predicate() {
679        parse_zeroize_test(stringify!(
680            #[zeroize(bound = "T")]
681            struct Z<T>(T);
682        ));
683    }
684}