//! Custom derive support for `zeroize` #![crate_type = "proc-macro"] #![forbid(unsafe_code)] #![warn(rust_2018_idioms, trivial_casts, unused_qualifications)] use proc_macro2::TokenStream; use quote::quote; use syn::{ parse::{Parse, ParseStream}, punctuated::Punctuated, token::Comma, Attribute, Lit, Meta, NestedMeta, Result, WherePredicate, }; use synstructure::{decl_derive, AddBounds, BindStyle, BindingInfo, VariantInfo}; decl_derive!( [Zeroize, attributes(zeroize)] => /// Derive the `Zeroize` trait. /// /// Supports the following attributes: /// /// On the item level: /// - `#[zeroize(drop)]`: *deprecated* use `ZeroizeOnDrop` instead /// - `#[zeroize(bound = "T: MyTrait")]`: this replaces any trait bounds /// inferred by zeroize-derive /// /// On the field level: /// - `#[zeroize(skip)]`: skips this field or variant when calling `zeroize()` derive_zeroize ); decl_derive!( [ZeroizeOnDrop, attributes(zeroize)] => /// Derive the `ZeroizeOnDrop` trait. /// /// Supports the following attributes: /// /// On the field level: /// - `#[zeroize(skip)]`: skips this field or variant when calling `zeroize()` derive_zeroize_on_drop ); /// Name of zeroize-related attributes const ZEROIZE_ATTR: &str = "zeroize"; /// Custom derive for `Zeroize` fn derive_zeroize(mut s: synstructure::Structure<'_>) -> TokenStream { let attributes = ZeroizeAttrs::parse(&s); if let Some(bounds) = attributes.bound { s.add_bounds(AddBounds::None); for bound in bounds.0 { s.add_where_predicate(bound); } } // NOTE: These are split into named functions to simplify testing with // synstructure's `test_derive!` macro. if attributes.drop { derive_zeroize_with_drop(s) } else { derive_zeroize_without_drop(s) } } /// Custom derive for `ZeroizeOnDrop` fn derive_zeroize_on_drop(mut s: synstructure::Structure<'_>) -> TokenStream { let zeroizers = generate_fields(&mut s, quote! { zeroize_or_on_drop }); let drop_impl = s.gen_impl(quote! { gen impl Drop for @Self { fn drop(&mut self) { use zeroize::__internal::AssertZeroize; use zeroize::__internal::AssertZeroizeOnDrop; match self { #zeroizers } } } }); let zeroize_on_drop_impl = impl_zeroize_on_drop(&s); quote! { #drop_impl #zeroize_on_drop_impl } } /// Custom derive attributes for `Zeroize` #[derive(Default)] struct ZeroizeAttrs { /// Derive a `Drop` impl which calls zeroize on this type drop: bool, /// Custom bounds as defined by the user bound: Option, } /// Parsing helper for custom bounds struct Bounds(Punctuated); impl Parse for Bounds { fn parse(input: ParseStream<'_>) -> Result { Ok(Self(Punctuated::parse_terminated(input)?)) } } impl ZeroizeAttrs { /// Parse attributes from the incoming AST fn parse(s: &synstructure::Structure<'_>) -> Self { let mut result = Self::default(); for attr in s.ast().attrs.iter() { result.parse_attr(attr, None, None); } for v in s.variants().iter() { // only process actual enum variants here, as we don't want to process struct attributes twice if v.prefix.is_some() { for attr in v.ast().attrs.iter() { result.parse_attr(attr, Some(v), None); } } for binding in v.bindings().iter() { for attr in binding.ast().attrs.iter() { result.parse_attr(attr, Some(v), Some(binding)); } } } result } /// Parse attribute and handle `#[zeroize(...)]` attributes fn parse_attr( &mut self, attr: &Attribute, variant: Option<&VariantInfo<'_>>, binding: Option<&BindingInfo<'_>>, ) { let meta_list = match attr .parse_meta() .unwrap_or_else(|e| panic!("error parsing attribute: {:?} ({})", attr, e)) { Meta::List(list) => list, _ => return, }; // Ignore any non-zeroize attributes if !meta_list.path.is_ident(ZEROIZE_ATTR) { return; } for nested_meta in &meta_list.nested { if let NestedMeta::Meta(meta) = nested_meta { self.parse_meta(meta, variant, binding); } else { panic!("malformed #[zeroize] attribute: {:?}", nested_meta); } } } /// Parse `#[zeroize(...)]` attribute metadata (e.g. `drop`) fn parse_meta( &mut self, meta: &Meta, variant: Option<&VariantInfo<'_>>, binding: Option<&BindingInfo<'_>>, ) { if meta.path().is_ident("drop") { assert!(!self.drop, "duplicate #[zeroize] drop flags"); match (variant, binding) { (_variant, Some(_binding)) => { // structs don't have a variant prefix, and only structs have bindings outside of a variant let item_kind = match variant.and_then(|variant| variant.prefix) { Some(_) => "enum", None => "struct", }; panic!( concat!( "The #[zeroize(drop)] attribute is not allowed on {} fields. ", "Use it on the containing {} instead.", ), item_kind, item_kind, ) } (Some(_variant), None) => panic!(concat!( "The #[zeroize(drop)] attribute is not allowed on enum variants. ", "Use it on the containing enum instead.", )), (None, None) => (), }; self.drop = true; } else if meta.path().is_ident("bound") { assert!(self.bound.is_none(), "duplicate #[zeroize] bound flags"); match (variant, binding) { (_variant, Some(_binding)) => { // structs don't have a variant prefix, and only structs have bindings outside of a variant let item_kind = match variant.and_then(|variant| variant.prefix) { Some(_) => "enum", None => "struct", }; panic!( concat!( "The #[zeroize(bound)] attribute is not allowed on {} fields. ", "Use it on the containing {} instead.", ), item_kind, item_kind, ) } (Some(_variant), None) => panic!(concat!( "The #[zeroize(bound)] attribute is not allowed on enum variants. ", "Use it on the containing enum instead.", )), (None, None) => { if let Meta::NameValue(meta_name_value) = meta { if let Lit::Str(lit) = &meta_name_value.lit { if lit.value().is_empty() { self.bound = Some(Bounds(Punctuated::new())); } else { self.bound = Some(lit.parse().unwrap_or_else(|e| { panic!("error parsing bounds: {:?} ({})", lit, e) })); } return; } } panic!(concat!( "The #[zeroize(bound)] attribute expects a name-value syntax with a string literal value.", "E.g. #[zeroize(bound = \"T: MyTrait\")]." )) } } } else if meta.path().is_ident("skip") { if variant.is_none() && binding.is_none() { panic!(concat!( "The #[zeroize(skip)] attribute is not allowed on a `struct` or `enum`. ", "Use it on a field or variant instead.", )) } } else { panic!("unknown #[zeroize] attribute type: {:?}", meta.path()); } } } fn generate_fields(s: &mut synstructure::Structure<'_>, method: TokenStream) -> TokenStream { s.bind_with(|_| BindStyle::RefMut); s.filter_variants(|vi| { let result = filter_skip(vi.ast().attrs, true); // check for duplicate `#[zeroize(skip)]` attributes in nested variants for field in vi.ast().fields { filter_skip(&field.attrs, result); } result }) .filter(|bi| filter_skip(&bi.ast().attrs, true)) .each(|bi| quote! { #bi.#method(); }) } fn filter_skip(attrs: &[Attribute], start: bool) -> bool { let mut result = start; for attr in attrs.iter().filter_map(|attr| attr.parse_meta().ok()) { if let Meta::List(list) = attr { if list.path.is_ident(ZEROIZE_ATTR) { for nested in list.nested { if let NestedMeta::Meta(Meta::Path(path)) = nested { if path.is_ident("skip") { assert!(result, "duplicate #[zeroize] skip flags"); result = false; } } } } } } result } /// Custom derive for `Zeroize` (without `Drop`) fn derive_zeroize_without_drop(mut s: synstructure::Structure<'_>) -> TokenStream { let zeroizers = generate_fields(&mut s, quote! { zeroize }); s.bound_impl( quote!(zeroize::Zeroize), quote! { fn zeroize(&mut self) { match self { #zeroizers } } }, ) } /// Custom derive for `Zeroize` and `Drop` fn derive_zeroize_with_drop(s: synstructure::Structure<'_>) -> TokenStream { let drop_impl = s.gen_impl(quote! { gen impl Drop for @Self { fn drop(&mut self) { self.zeroize(); } } }); let zeroize_impl = derive_zeroize_without_drop(s); quote! { #zeroize_impl #[doc(hidden)] #drop_impl } } fn impl_zeroize_on_drop(s: &synstructure::Structure<'_>) -> TokenStream { #[allow(unused_qualifications)] s.bound_impl(quote!(zeroize::ZeroizeOnDrop), Option::::None) } #[cfg(test)] mod tests { use super::*; use syn::parse_str; use synstructure::{test_derive, Structure}; #[test] fn zeroize_without_drop() { test_derive! { derive_zeroize_without_drop { struct Z { a: String, b: Vec, c: [u8; 3], } } expands to { #[allow(non_upper_case_globals)] #[doc(hidden)] const _DERIVE_zeroize_Zeroize_FOR_Z: () = { extern crate zeroize; impl zeroize::Zeroize for Z { fn zeroize(&mut self) { match self { Z { a: ref mut __binding_0, b: ref mut __binding_1, c: ref mut __binding_2, } => { { __binding_0.zeroize(); } { __binding_1.zeroize(); } { __binding_2.zeroize(); } } } } } }; } no_build // tests the code compiles are in the `zeroize` crate } } #[test] fn zeroize_with_drop() { test_derive! { derive_zeroize_with_drop { struct Z { a: String, b: Vec, c: [u8; 3], } } expands to { #[allow(non_upper_case_globals)] #[doc(hidden)] const _DERIVE_zeroize_Zeroize_FOR_Z: () = { extern crate zeroize; impl zeroize::Zeroize for Z { fn zeroize(&mut self) { match self { Z { a: ref mut __binding_0, b: ref mut __binding_1, c: ref mut __binding_2, } => { { __binding_0.zeroize(); } { __binding_1.zeroize(); } { __binding_2.zeroize(); } } } } } }; #[doc(hidden)] #[allow(non_upper_case_globals)] const _DERIVE_Drop_FOR_Z: () = { impl Drop for Z { fn drop(&mut self) { self.zeroize(); } } }; } no_build // tests the code compiles are in the `zeroize` crate } } #[test] fn zeroize_with_skip() { test_derive! { derive_zeroize_without_drop { struct Z { a: String, b: Vec, #[zeroize(skip)] c: [u8; 3], } } expands to { #[allow(non_upper_case_globals)] #[doc(hidden)] const _DERIVE_zeroize_Zeroize_FOR_Z: () = { extern crate zeroize; impl zeroize::Zeroize for Z { fn zeroize(&mut self) { match self { Z { a: ref mut __binding_0, b: ref mut __binding_1, .. } => { { __binding_0.zeroize(); } { __binding_1.zeroize(); } } } } } }; } no_build // tests the code compiles are in the `zeroize` crate } } #[test] fn zeroize_with_bound() { test_derive! { derive_zeroize { #[zeroize(bound = "T: MyTrait")] struct Z(T); } expands to { #[allow(non_upper_case_globals)] #[doc(hidden)] const _DERIVE_zeroize_Zeroize_FOR_Z: () = { extern crate zeroize; impl zeroize::Zeroize for Z where T: MyTrait { fn zeroize(&mut self) { match self { Z(ref mut __binding_0,) => { { __binding_0.zeroize(); } } } } } }; } no_build // tests the code compiles are in the `zeroize` crate } } #[test] fn zeroize_only_drop() { test_derive! { derive_zeroize_on_drop { struct Z { a: String, b: Vec, c: [u8; 3], } } expands to { #[allow(non_upper_case_globals)] const _DERIVE_Drop_FOR_Z: () = { impl Drop for Z { fn drop(&mut self) { use zeroize::__internal::AssertZeroize; use zeroize::__internal::AssertZeroizeOnDrop; match self { Z { a: ref mut __binding_0, b: ref mut __binding_1, c: ref mut __binding_2, } => { { __binding_0.zeroize_or_on_drop(); } { __binding_1.zeroize_or_on_drop(); } { __binding_2.zeroize_or_on_drop(); } } } } } }; #[allow(non_upper_case_globals)] #[doc(hidden)] const _DERIVE_zeroize_ZeroizeOnDrop_FOR_Z: () = { extern crate zeroize; impl zeroize::ZeroizeOnDrop for Z {} }; } no_build // tests the code compiles are in the `zeroize` crate } } #[test] fn zeroize_on_struct() { parse_zeroize_test(stringify!( #[zeroize(drop)] struct Z { a: String, b: Vec, c: [u8; 3], } )); } #[test] fn zeroize_on_enum() { parse_zeroize_test(stringify!( #[zeroize(drop)] enum Z { Variant1 { a: String, b: Vec, c: [u8; 3] }, } )); } #[test] #[should_panic(expected = "#[zeroize(drop)] attribute is not allowed on struct fields")] fn zeroize_on_struct_field() { parse_zeroize_test(stringify!( struct Z { #[zeroize(drop)] a: String, b: Vec, c: [u8; 3], } )); } #[test] #[should_panic(expected = "#[zeroize(drop)] attribute is not allowed on struct fields")] fn zeroize_on_tuple_struct_field() { parse_zeroize_test(stringify!( struct Z(#[zeroize(drop)] String); )); } #[test] #[should_panic(expected = "#[zeroize(drop)] attribute is not allowed on struct fields")] fn zeroize_on_second_field() { parse_zeroize_test(stringify!( struct Z { a: String, #[zeroize(drop)] b: Vec, c: [u8; 3], } )); } #[test] #[should_panic(expected = "#[zeroize(drop)] attribute is not allowed on enum fields")] fn zeroize_on_tuple_enum_variant_field() { parse_zeroize_test(stringify!( enum Z { Variant(#[zeroize(drop)] String), } )); } #[test] #[should_panic(expected = "#[zeroize(drop)] attribute is not allowed on enum fields")] fn zeroize_on_enum_variant_field() { parse_zeroize_test(stringify!( enum Z { Variant { #[zeroize(drop)] a: String, b: Vec, c: [u8; 3], }, } )); } #[test] #[should_panic(expected = "#[zeroize(drop)] attribute is not allowed on enum fields")] fn zeroize_on_enum_second_variant_field() { parse_zeroize_test(stringify!( enum Z { Variant1 { a: String, b: Vec, c: [u8; 3], }, Variant2 { #[zeroize(drop)] a: String, b: Vec, c: [u8; 3], }, } )); } #[test] #[should_panic(expected = "#[zeroize(drop)] attribute is not allowed on enum variants")] fn zeroize_on_enum_variant() { parse_zeroize_test(stringify!( enum Z { #[zeroize(drop)] Variant, } )); } #[test] #[should_panic(expected = "#[zeroize(drop)] attribute is not allowed on enum variants")] fn zeroize_on_enum_second_variant() { parse_zeroize_test(stringify!( enum Z { Variant1, #[zeroize(drop)] Variant2, } )); } #[test] #[should_panic( expected = "The #[zeroize(skip)] attribute is not allowed on a `struct` or `enum`. Use it on a field or variant instead." )] fn zeroize_skip_on_struct() { parse_zeroize_test(stringify!( #[zeroize(skip)] struct Z { a: String, b: Vec, c: [u8; 3], } )); } #[test] #[should_panic( expected = "The #[zeroize(skip)] attribute is not allowed on a `struct` or `enum`. Use it on a field or variant instead." )] fn zeroize_skip_on_enum() { parse_zeroize_test(stringify!( #[zeroize(skip)] enum Z { Variant1, Variant2, } )); } #[test] #[should_panic(expected = "duplicate #[zeroize] skip flags")] fn zeroize_duplicate_skip() { parse_zeroize_test(stringify!( struct Z { a: String, #[zeroize(skip)] #[zeroize(skip)] b: Vec, c: [u8; 3], } )); } #[test] #[should_panic(expected = "duplicate #[zeroize] skip flags")] fn zeroize_duplicate_skip_list() { parse_zeroize_test(stringify!( struct Z { a: String, #[zeroize(skip, skip)] b: Vec, c: [u8; 3], } )); } #[test] #[should_panic(expected = "duplicate #[zeroize] skip flags")] fn zeroize_duplicate_skip_enum() { parse_zeroize_test(stringify!( enum Z { #[zeroize(skip)] Variant { a: String, #[zeroize(skip)] b: Vec, c: [u8; 3], }, } )); } #[test] #[should_panic(expected = "duplicate #[zeroize] bound flags")] fn zeroize_duplicate_bound() { parse_zeroize_test(stringify!( #[zeroize(bound = "T: MyTrait")] #[zeroize(bound = "")] struct Z(T); )); } #[test] #[should_panic(expected = "duplicate #[zeroize] bound flags")] fn zeroize_duplicate_bound_list() { parse_zeroize_test(stringify!( #[zeroize(bound = "T: MyTrait", bound = "")] struct Z(T); )); } #[test] #[should_panic( expected = "The #[zeroize(bound)] attribute is not allowed on struct fields. Use it on the containing struct instead." )] fn zeroize_bound_struct() { parse_zeroize_test(stringify!( struct Z { #[zeroize(bound = "T: MyTrait")] a: T, } )); } #[test] #[should_panic( expected = "The #[zeroize(bound)] attribute is not allowed on enum variants. Use it on the containing enum instead." )] fn zeroize_bound_enum() { parse_zeroize_test(stringify!( enum Z { #[zeroize(bound = "T: MyTrait")] A(T), } )); } #[test] #[should_panic( expected = "The #[zeroize(bound)] attribute is not allowed on enum fields. Use it on the containing enum instead." )] fn zeroize_bound_enum_variant_field() { parse_zeroize_test(stringify!( enum Z { A { #[zeroize(bound = "T: MyTrait")] a: T, }, } )); } #[test] #[should_panic( expected = "The #[zeroize(bound)] attribute expects a name-value syntax with a string literal value.E.g. #[zeroize(bound = \"T: MyTrait\")]." )] fn zeroize_bound_no_value() { parse_zeroize_test(stringify!( #[zeroize(bound)] struct Z(T); )); } #[test] #[should_panic(expected = "error parsing bounds: LitStr { token: \"T\" } (expected `:`)")] fn zeroize_bound_no_where_predicate() { parse_zeroize_test(stringify!( #[zeroize(bound = "T")] struct Z(T); )); } fn parse_zeroize_test(unparsed: &str) -> TokenStream { derive_zeroize(Structure::new( &parse_str(unparsed).expect("Failed to parse test input"), )) } }