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}