1#![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
18const ZEROIZE_ATTR: &str = "zeroize";
20
21#[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#[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#[derive(Default)]
104struct ZeroizeAttrs {
105 bound: Option<Bounds>,
107 auto_params: Vec<Ident>,
109}
110
111struct 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 syn::visit::visit_path(self, path);
142 }
143}
144
145impl ZeroizeAttrs {
146 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 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 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 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 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 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}