-
-
Notifications
You must be signed in to change notification settings - Fork 89
Derive macros are unsound #281
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
I'm not remembering what Derive proc macros are allowed to emit. Can they create code other code besides the trait impl they're for? Otherwise, we might have to do the other option mentioned in the zerocopy thread and bail out on all unknown attributes. BUT that would be extremely uncool. |
Yes, derive macros can emit arbitrary items, essentially. (IIRC this is already used in That said, Is it documented anywhere that it is intentional for derive macros to see the "original" tokens when an item has been modified by an attribute macro? My first instinct would be to call it a Rust bug that the derive macro does not get the correct |
i believe the exact ordering of the attributes matters, unfortunately |
use the_macros::*;
fn main() {
{
#[derive(PrintTokens)]
#[add_fields({y: u32})]
struct Foo {
x: u32,
}
print_tokens()
}
{
#[add_fields({y: u32})]
#[derive(PrintTokens)]
struct Foo {
x: u32,
}
print_tokens()
}
}
Okay, the original bug report made it sound like Rust never applied attribute macros before giving the item to derive macros, but yeah it does if it is before the derive (since it applies macros outside-in) and otherwise the attribute macro is still in the input.
|
Actually after some testing, this won't work in general. Even ignoring all the false positives it would have, there are also false negatives: there's no way in general to differentiate between our own helper attributes for other traits (like false negative exampleuse adversarial_macros::transparent; // inserts a `y: Infallible` field, e.g., or any 1-ZST with a nontrivial safety invariant
use bytemuck::{Zeroable};
#[derive(Clone, Copy, Zeroable, TransparentWrapper)]
#[transparent(u32)] // fine, made inert by `derive(TransparentWrapper)`
#[repr(transparent)]
struct Foo {
x: u32
}
#[derive(Clone, Copy, Zeroable)]
#[transparent(u32)] // unsound false negative, adds a field after `Zeroable` derive macro runs
#[repr(transparent)]
struct Foo {
x: u32
} From inside the Edit: exampleuse adversarial_macros::transparent;
#[derive(Debug, Clone, Copy)]
#[derive(TransparentWrapper)] // comment this out for a different error
#[transparent(u32)]
#[derive(Zeroable)]
#[repr(transparent)]
struct Foo {
x: u32,
}
|
Another wrinkle: For fields, we can check the types using some #[derive(Zeroable)]
#[repr(u32)]
#[adversarial_macro] // changes discriminant so none are 0
enum Foo {
X(i32) = 0,
} Is there even any way to detect this at compile time? Edit: For a fieldless enum you can |
It's sort of the inverse: I don't believe we have fixed the order of macro evaluation for proc macros in a way that is particularly useful or provides any real guarantees to macro authors, in any direction. |
Since we can't rely on being able to see the final tokens, can we just let the compiler do the checks for us? Consider the following examples: #[derive(Zeroable)]
#[random_macro]
#[repr(C)]
struct Example1 {
a : Type1,
b: Type2.
}
#[derive(Zeroable)]
#[random_macro]
#[repr(u8)]
enum Example2 {
Var1 = 0,
Var2
} We can't tell what const _ : () = {
// Check members of Example1
let _ = |derived: Example1| {
fn check_member<T: ::bytemuck::Zeroable>(member: T) {}
match derived {
Example1 { a, b } => {
check_member::<Type1>(a);
check_member::<Type2>(b);
}
}
};
// Check variants of Example2
let _ = |derived: Example2| {
match derived {
Var1 => ()
Var2 => ()
}
};
// Ensure zero variant of Example2 is still zero
assert_eq!(0, Example2::Var1 as u128);
} The exhaustive nature of pattern matching means that we can confirm that we know about all members for The main issue with this method is that there is no way (to my knowledge) to confirm the
There is at least one case where the representation does matter though. For non-fieldless enums with primitive representation #[derive(Zeroable)]
#[random_macro]
#[repr(u8)]
enum Example3a {
First{ a : Type4 } = 0,
Second
}
If we are willing to restrict the user slightly though, we can confirm our representation. First, note that the reference does guarantee that macro attributes evaluate outside in, and that fellow derive macros may not change the item they are deriving. Furthermore, according to my testing, there can only be one #[derive(Zeroable)]
#[repr(u8)]
#[random_macro]
enum Example3b {
First{ a : Type4 } = 0,
Second
} However, struct SwappedOut{}
enum Example3b {
First{ a : Type4 } = 0,
Second
} Which (I believe) would cause the Therefore I think the only safe bet is to only allow #[random_macro]
#[derive(Zeroable)]
#[repr(u8)]
enum Example3c {
First{ a : Type4 } = 0,
Second
} In this case the derive macro would be privy to any changes made by const _ : () = {
let _ = |derived: Example3c| {
fn check_member<T: ::bytemuck::Zeroable>(member: T) {}
match derived {
First { a } => {
check_member::<Type4>(a);
},
Second => ()
}
}
let test_case = Example3c::First { a : ::bytemuck::zeroed() };
let discrim = unsafe { *(&raw test_case).cast::<u8>() };
assert_eq!(discrim, 0);
} We are able to construct a valid value of I haven't figured out how to do this with However, if there was a semver bump, we could remove the #[derive(Clone, Copy, Zeroable)]
#[repr(transparent)]
struct Foo {
#[wrapped]
x: u32
} At which point the Alternatively all of this would be a lot easier if there was some safe way to get the discriminant value of non-fieldless primitive enums as integers. Especially if we could do so without needing to fully construct the associated variants. |
Speaking of a unified way to access enum's discriminant as integers, there is an RFC pull request for that. (rust-lang/rfcs#3607) However, it currently seems to be somewhat stalled out. If this was implemented though, I think that lack of knowledge about the representation would cease to be a problem. |
I came across google/zerocopy#388 (comment) by chance. The same applies to this crate:
Segfaults. Solutions might involve generating a function that asserts (by compiling) that the types seen by the proc macro are the same as the types in the final type and that the final type contains no additional fields.
The text was updated successfully, but these errors were encountered: