scudo_proc_macros/
lib.rs

1// Copyright 2022 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! A proc-macro crate for the Rust bindings to the
16//! [Scudo allocator](https://llvm.org/docs/ScudoHardenedAllocator.html#options).
17//!
18//! The exported [`macro@set_scudo_options`] attribute macro allows to set Scudo
19//! options with an annotation on the main method:
20//!
21//! ```rust
22//! use scudo_proc_macros::set_scudo_options;
23//!
24//! #[set_scudo_options(delete_size_mismatch = false, release_to_os_interval_ms = 1)]
25//! fn main() {
26//!     // Use Scudo with the provided options.
27//! }
28//! ```
29//!
30//! For more on Scudo options, visit the official documentation
31//! [here](https://llvm.org/docs/ScudoHardenedAllocator.html#options).
32//!
33//! Please note: the proc macro exported by this crate works both with the
34//! [scudo-sys](https://crates.io/crates/scudo-sys) crate as well as with the
35//! idiomatic Rust binding crate, [scudo](https://crates.io/crates/scudo).
36
37extern crate proc_macro;
38
39use proc_macro::TokenStream;
40use proc_macro2::{Span, TokenTree};
41use quote::quote;
42use syn::{
43    parse, parse::Parse, parse_macro_input, punctuated::Punctuated, Error, Ident, ItemFn, LitStr,
44    Token,
45};
46
47/// An `Option` holds a key and a value. The value could either be an
48/// `Ident` (like "true") or a `Literal` (like "3.14").
49struct Option {
50    key: Ident,
51    value: Box<dyn ToString>,
52}
53
54/// Holds a comma seperated list of `Option`s.
55struct OptionsList {
56    content: Punctuated<Option, Token![,]>,
57}
58
59impl Parse for Option {
60    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
61        let key = input.parse()?;
62        _ = input.parse::<Token![=]>()?;
63
64        let error = Error::new(input.span(), "Expect a valid value like '3.14' or 'true'");
65
66        // Parse the value as `ToString`.
67        let value = match input.parse() {
68            Ok(TokenTree::Punct(punct)) => {
69                if punct.as_char() != '-' {
70                    return Err(error);
71                }
72
73                // Parsing a negative number, the next element needs to be a literal in this case.
74                if let TokenTree::Literal(lit) = input.parse()? {
75                    Ok(Box::new(format!("-{}", lit)) as Box<dyn ToString>)
76                } else {
77                    Err(error)
78                }
79            }
80            Ok(TokenTree::Literal(lit)) => Ok(Box::new(lit) as Box<dyn ToString>),
81            Ok(TokenTree::Ident(ident)) => {
82                if ident != "true" && ident != "false" {
83                    Err(error)
84                } else {
85                    Ok(Box::new(ident) as Box<dyn ToString>)
86                }
87            }
88            _ => Err(error),
89        }?;
90
91        Ok(Option { key, value })
92    }
93}
94
95impl Parse for OptionsList {
96    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
97        Ok(OptionsList {
98            content: input.parse_terminated(Option::parse)?,
99        })
100    }
101}
102
103fn token_stream_with_error(mut tokens: TokenStream, error: syn::Error) -> TokenStream {
104    tokens.extend(TokenStream::from(error.into_compile_error()));
105    tokens
106}
107
108/// Sets options for the Scudo Allocator. This macro takes a list of
109/// comma-seperated options, where each option is in the form
110/// key = value. The value could either be a number like '3.14' or a
111/// boolean value like 'true'. For a list of all available Scudo options,
112/// please visit the [official documentations](https://llvm.org/docs/ScudoHardenedAllocator.html#options)
113/// Pleaso note that this macro can only be used on the main method of a
114/// Rust program.
115///
116/// # Example
117///
118/// ```rust
119/// use scudo_proc_macros::set_scudo_options;
120///
121/// #[set_scudo_options(delete_size_mismatch = false, release_to_os_interval_ms = 1)]
122/// fn main() {
123///     // Use Scudo with the provided options.
124/// }
125/// ```
126#[proc_macro_attribute]
127pub fn set_scudo_options(attr: TokenStream, item: TokenStream) -> TokenStream {
128    // Check that this macro is only used on the main method.
129    let input: ItemFn = match parse(item.clone()) {
130        Ok(it) => it,
131        Err(e) => return token_stream_with_error(item, e),
132    };
133
134    if input.sig.ident != "main" {
135        let msg = "This macro is only allowed to be used on the main method";
136        return token_stream_with_error(item, syn::Error::new_spanned(&input.sig.ident, msg));
137    };
138
139    // Parse the options.
140    let options = parse_macro_input!(attr as OptionsList).content;
141
142    // Build the options string.
143    let mut option_str = String::new();
144    for option in options.iter() {
145        option_str += &format!("{}={}:", option.key, option.value.to_string());
146    }
147    // This interfaces with C, so null-terminate the String.
148    option_str += "\0";
149    let option_lit = LitStr::new(&option_str, Span::call_site());
150
151    // The resulting code defines the `__scudo_default_options` method that returns
152    // a pointer to the created string and leaves the main method untouched.
153    let options_method = TokenStream::from(quote! {
154        use libc::c_char;
155
156        #[no_mangle]
157        const extern "C" fn __scudo_default_options() -> *const c_char {
158            #option_lit.as_ptr() as *const c_char
159        }
160
161    });
162    let mut result = item;
163    result.extend(options_method);
164
165    result
166}
167
168#[cfg(test)]
169mod tests {
170
171    #[test]
172    fn test_fail() {
173        let test_cases = trybuild::TestCases::new();
174        test_cases.compile_fail("tests/ui/*_fail.rs");
175    }
176
177    #[test]
178    fn test_pass() {
179        let test_cases = trybuild::TestCases::new();
180        test_cases.pass("tests/ui/*_pass.rs");
181    }
182}