Skip to content
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

[derive] Fix AsBytes for #[repr(C, packed(N))] #672

Merged
merged 2 commits into from Dec 5, 2023
Merged

Conversation

maurer
Copy link
Contributor

@maurer maurer commented Dec 4, 2023

packed(N) does not gaurantee no padding, but it doesn't prevent it either. This was previously supported.

This regressed in #643 (left out of the commit message because of the requirement not to link to review sites).

`packed(N)` does not gaurantee no padding, but it doesn't prevent it
either. This was previously supported.
@joshlf
Copy link
Member

joshlf commented Dec 4, 2023

Woah, great catch, and thanks for such a thorough PR! We'll review this soon. Out of curiosity, how did you find this issue?

@maurer
Copy link
Contributor Author

maurer commented Dec 4, 2023

crosvm uses #[repr(C, packed(4))] in a correct but not load bearing way (compared to #[repr(C, packed)]) in one of their tests. This hit us when updating zerocopy.

@joshlf
Copy link
Member

joshlf commented Dec 4, 2023

Okay cool, thanks!

@jswrenn jswrenn added the bug Something isn't working label Dec 4, 2023
@jswrenn
Copy link
Collaborator

jswrenn commented Dec 5, 2023

What happened here is subtle, so I'm attempting to figure it out and put it in writing:

Parsing before #643

In 7b98c7f (the commit before #643), the regression test provided by @maurer is accepted by zerocopy:

#[derive(AsBytes)]
#[repr(C, packed(2))]
// The same caveats as for CPacked apply - we're assuming u64 is at least
// 4-byte aligned by default. Without packed(2), this should fail, as there
// would be padding between a/b assuming u64 is 4+ byte aligned.
struct CPacked2 {
    a: u16,
    b: u64,
}

static_assertions::assert_impl_all!(CPacked2: AsBytes);

This was accepted possibly accidentally. To see why, let's trace the expansion of derive(AsBytes). We start at derive_as_bytes:

#[proc_macro_derive(AsBytes)]
pub fn derive_as_bytes(ts: proc_macro::TokenStream) -> proc_macro::TokenStream {
let ast = syn::parse_macro_input!(ts as DeriveInput);
match &ast.data {
Data::Struct(strct) => derive_as_bytes_struct(&ast, strct),

Then enter derive_as_bytes_struct:

fn derive_as_bytes_struct(ast: &DeriveInput, strct: &DataStruct) -> proc_macro2::TokenStream {
let reprs = try_or_print!(STRUCT_UNION_AS_BYTES_CFG.validate_reprs(ast));
let is_transparent = reprs.contains(&StructRepr::Transparent);
let is_packed = reprs.contains(&StructRepr::Packed);

The definition of STRUCT_UNION_ALLOWED_REPR_COMBINATIONS in STRUCT_UNION_AS_BYTES_CFG would suggest that packed(N) isn't permitted:

const STRUCT_UNION_ALLOWED_REPR_COMBINATIONS: &[&[StructRepr]] = &[
&[StructRepr::C],
&[StructRepr::Transparent],
&[StructRepr::Packed],
&[StructRepr::C, StructRepr::Packed],
];

So what happens when we call valid_reprs? The first thing is that we call reprs to parse the metas:

pub fn validate_reprs(&self, input: &DeriveInput) -> Result<Vec<R>, Vec<Error>> {
let mut metas_reprs = reprs(&input.attrs)?;

...which calls StructRepr::parse:

fn parse(meta: &Meta) -> syn::Result<$repr_name> {
match Repr::from_meta(meta)? {
$(Repr::$repr_variant => Ok($repr_name::$repr_variant),)*
Repr::Align(u) => Ok($repr_name::Align(u)),
_ => Err(Error::new_spanned(meta, concat!("unsupported representation for deriving FromBytes, AsBytes, or Unaligned on ", $type_name)))
}

...which calls Repr::from_meta:

fn from_meta(meta: &Meta) -> Result<Repr, Error> {
match meta {
Meta::Path(path) => {
let ident = path
.get_ident()
.ok_or_else(|| Error::new_spanned(meta, "unrecognized representation hint"))?;
match format!("{}", ident).as_str() {
"u8" => return Ok(Repr::U8),
"u16" => return Ok(Repr::U16),
"u32" => return Ok(Repr::U32),
"u64" => return Ok(Repr::U64),
"usize" => return Ok(Repr::Usize),
"i8" => return Ok(Repr::I8),
"i16" => return Ok(Repr::I16),
"i32" => return Ok(Repr::I32),
"i64" => return Ok(Repr::I64),
"isize" => return Ok(Repr::Isize),
"C" => return Ok(Repr::C),
"transparent" => return Ok(Repr::Transparent),
"packed" => return Ok(Repr::Packed),
_ => {}
}
}
Meta::List(list) => {
return Ok(Repr::Align(list.parse_args::<LitInt>()?.base10_parse::<u64>()?))
}
_ => {}
}
Err(Error::new_spanned(meta, "unrecognized representation hint"))
}

Here, we find the bug. If the meta has arguments, it is unconditionally parsed as Repr::Align(..):

Meta::List(list) => {
return Ok(Repr::Align(list.parse_args::<LitInt>()?.base10_parse::<u64>()?))
}

validate_reprs then filters this out:

metas_reprs.into_iter().filter(|(_, repr)| !repr.is_align()).for_each(|(meta, repr)| {
metas.push(meta);
reprs.push(repr)
});

...so the apparent repr of CPacked2 becomes just repr(C). Since this is in the allowed_combinations of STRUCT_UNION_ALLOWED_REPR_COMBINATIONS, the repr is accepted.

Fortunately, this is not a soundness bug. If the example is modified to use repr(C, packed(4)) instead, an error is produced. This is because the expansion of AsBytes includes a const-eval padding check for padding:

let padding_check_bound = padding_check.and_then(|check| (!field_types.is_empty()).then_some(check)).map(|check| {
let fields = field_types.iter();
let validator_macro = check.validator_macro_ident();
parse_quote!(
::zerocopy::macro_util::HasPadding<#type_ident, {::zerocopy::#validator_macro!(#type_ident, #(#fields),*)}>:
::zerocopy::macro_util::ShouldBe<false>
)
});

Parsing after #643

In #643, from_repr is modified to parse packed(n) as Repr::PackedN(n), rather than Repr::Align(n). Since PackedN is not in the allowed combinations of STRUCT_UNION_ALLOWED_REPR_COMBINATIONS, the repr is rejected by validate_reprs.

jswrenn
jswrenn previously requested changes Dec 5, 2023
Copy link
Collaborator

@jswrenn jswrenn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR partially repairs a longstanding oddity in our repr parsing: the special treatment of Align. align does not appear in our list of allowed combinations:

const STRUCT_UNION_ALLOWED_REPR_COMBINATIONS: &[&[StructRepr]] = &[
&[StructRepr::C],
&[StructRepr::Transparent],
&[StructRepr::Packed],
&[StructRepr::C, StructRepr::Packed],
];

...nor the invocation of define_kind_specific_repr:
define_kind_specific_repr!("a struct", StructRepr, C, Transparent, Packed);

Rather, it is defined implicitly during macro expansion:

pub enum $repr_name {
$($repr_variant,)*
Align(u64),
}

This is because, unlike the other modifiers, align always takes an argument. This PR makes define_kind_specific_repr a little less magical: It extends the macro with the ability to generate provided argument-taking modifiers.

However, the way it fixes the issue is still a little magical. Recall that validate_reprs filters out the Align repr (because it would be a pain to enumerate these in allowed_combinations):

metas_reprs.into_iter().filter(|(_, repr)| !repr.is_align()).for_each(|(meta, repr)| {
metas.push(meta);
reprs.push(repr)
});

This PR leverages that behavior by having PackedN(n).is_align() return true:

fn is_align(&self) -> bool {
match self {
$($repr_name::$repr_variant_aligned(_) => true,)*
_ => false,
}
}

I'm not entirely comfortable with this. Not only is the name misleading now, but what happens when a new argument-consuming is added (particularly one that doesn't modify alignment)?

@joshlf, I think we need to reconsider our repr parsing framework, and reduce the the special handling of Align and PackedN. My understanding is we need this special handling because we can't write out argument-bearing modifiers in allowed_combinations. Perhaps we can abstract the argument component so we can specify either concrete or pattern arguments; e.g.:

// ditto for `StructRepr`, etc.
enum ReprKind<A: ReprArgument> {
    C,
    Transparent,
    Packed,
    PackedN(A::PackedN),
    Align(A::Align),
}

trait ReprArgument {
    type PackedN;
    type Align,
}

struct Pattern;
struct Concrete;

impl ReprArgument for Pattern {
    type PackedN = u64;
    type Align = u64;
}

impl ReprArgument for Concrete {
    type PackedN = RangeFull;
    type Align = RangeFull;
}

const STRUCT_UNION_ALLOWED_REPR_COMBINATIONS: &[&[StructRepr<Pattern>]] = &[
    &[StructRepr::C],
    &[StructRepr::Transparent],
    &[StructRepr::Packed],
    &[StructRepr::PackedN(..)],
    &[StructRepr::Align],
    &[StructRepr::C, StructRepr::Packed],
    &[StructRepr::C, StructRepr::Packed(..)],
    &[StructRepr::C, StructRepr::Align],
];

Copy link
Member

@joshlf joshlf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jswrenn and I discussed this offline. We're going to follow up with the changes he suggested, but in the meantime we'll merge this and publish a new version. Thanks again for catching this and for submitting a fix!

@joshlf joshlf enabled auto-merge December 5, 2023 18:04
@joshlf joshlf dismissed jswrenn’s stale review December 5, 2023 18:06

Discussed offline

@joshlf joshlf added this pull request to the merge queue Dec 5, 2023
Merged via the queue into google:main with commit 941c83f Dec 5, 2023
126 checks passed
@joshlf
Copy link
Member

joshlf commented Dec 5, 2023

Published as 0.7.29.

@joshlf
Copy link
Member

joshlf commented Dec 5, 2023

Yanked 0.7.27 and 0.7.28, both of which contain the regression.

emilengler pushed a commit to emilengler/arti that referenced this pull request Dec 6, 2023
The version in our lockfile was yanked because of this bug
  google/zerocopy#672

Our use of zerocopy is only via ahash and AFAICT it doesn't provide it
with any affected types.

It's not clear to me if the bug is an unsoundness bug.  There isn't a
RustSec advisory atm.
@jswrenn jswrenn mentioned this pull request Dec 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants