-
Notifications
You must be signed in to change notification settings - Fork 3.3k
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
[rust] Generated enums can cause undefined behavior #5467
Comments
I'll take a look at the code generator tomorrow if I find the time. |
@jean-airoldie Great! You think this is better than using the non-exhaustive attribute you mentioned in #5405 ? |
I think the two should be used together. The problem with only using the Using non-exhaustive is meant to "future-proof" enums so that they can add variants without it being a breaking change (because this is basically enforced by the compiler). So still I think this usage fits flatbuffers. |
Thanks for thinking this through. I'm interested in your |
Sure I'll do that. |
This is the simplest solution I can think of.
// This is the generated code
#[allow(non_camel_case_types)]
#[repr(u8)]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
enum MsgType {
Notification = 0,
Data = 1,
}
const ENUM_MIN_MSG_TYPE: u8 = 0;
const ENUM_MAX_MSG_TYPE: u8 = 1;
impl flatbuffers::EndianScalar for MsgType {
#[inline]
fn to_little_endian(self) -> Self {
let n = u8::to_le(self as u8);
let p = &n as *const u8 as *const MsgType;
unsafe { *p }
}
#[inline]
fn try_from_little_endian(self) -> Result<Self, u8> {
let n = u8::from_le(self as u8);
if n > ENUM_MAX_MSG_TYPE || n < ENUM_MIN_MSG_TYPE {
Err(n)
} else {
let p = &n as *const u8 as *const MsgType;
Ok(unsafe { *p })
}
}
} |
This comment has been minimized.
This comment has been minimized.
Is anyone working on this? I already ran into this issue after adding a variant. |
I'm not, so my guess is no one is. If you are interested a PR is welcomed, otherwise I'll implement it when I get the time (at last 1 month from now). You can step around this by always adding a |
I think the ideal solution would be to use the |
Is any progress with this issue?
@jean-airoldie |
I'm not working on it atm.
I'm not sure I'm following, are you talking about the generated code or a rust c enum? What kind of error is produced by the "Red Blue" bitfield in your example in rust? |
For a bitflag in rust you would want to use an integer as opposed to an enum. But ideally you would want something more convenient like the bitflag crate. |
I'm talking about the generated code. Rust code generator fails with:
At generation time "Red Blue" is equal to ((1u << 0) | (1u << 3)) which isn't part of flatbuffers/src/idl_gen_rust.cpp Lines 736 to 741 in 7f1af7c
The bitflag crate is a good idea but the
What value should the program pass to |
The problem is that a bitflag really isn't an enum, but rather a combination of enums. So you would need a special case where you check that every individual flag is contained within the enum.
I think the enum default value should be To solve this issue we need to change the enum generated code. I can help but I'm really busy atm so I can't commit too much time. This is what the current generated code would look like: #[allow(non_camel_case_types)]
#[repr(u8)]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub enum WithoutZero {
Two = 2,
}
const ENUM_MIN_WITHOUT_ZERO: u8 = 2;
const ENUM_MAX_WITHOUT_ZERO: u8 = 2;
impl<'a> flatbuffers::Follow<'a> for WithoutZero {
type Inner = Self;
#[inline]
fn follow(buf: &'a [u8], loc: usize) -> Self::Inner {
// this would cause UB if scalar == 0 && 0 is not a valid enum value
flatbuffers::read_scalar_at::<Self>(buf, loc)
}
}
impl flatbuffers::EndianScalar for WithoutZero {
#[inline]
fn to_little_endian(self) -> Self {
let n = u8::to_le(self as u8);
let p = &n as *const u8 as *const WithoutZero;
unsafe { *p } // this is also potentially UB
}
#[inline]
fn from_little_endian(self) -> Self {
let n = u8::from_le(self as u8);
let p = &n as *const u8 as *const WithoutZero;
unsafe { *p } // so is this
}
}
impl flatbuffers::Push for WithoutZero {
type Output = WithoutZero;
#[inline]
fn push(&self, dst: &mut [u8], _rest: &[u8]) {
flatbuffers::emplace_scalar::<WithoutZero>(dst, *self);
}
} And this is what I think it should look like: #[allow(non_camel_case_types)]
#[repr(u8)]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, FromPrimitive, ToPrimitive)]
pub enum WithoutZero {
Two = 2,
}
const ENUM_MIN_WITHOUT_ZERO: u8 = 2;
const ENUM_MAX_WITHOUT_ZERO: u8 = 2;
impl<'a> flatbuffers::Follow<'a> for WithoutZero {
// We make the de-serialization fallible
type Inner = Option<Self>;
#[inline]
fn follow(buf: &'a [u8], loc: usize) -> Self::Inner {
// We read as a scalar then do the faillible FromPrimitive conversion
let scalar = flatbuffers::read_scalar_at::<u8>(buf, loc);
Self::from_u8(scalar)
}
}
impl flatbuffers::Push for WithoutZero {
type Output = u8;
#[inline]
fn push(&self, dst: &mut [u8], _rest: &[u8]) {
let scalar = self.to_u8().unwrap(); // We use the ToPrimitive conversion.
flatbuffers::emplace_scalar::<u8>(dst, scalar);
}
} |
As for bitflags, here is what I think the generated code should look like:
bitflags! {
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct Color: u8 {
pub const RED = 1 << 0;
pub const GREEN = 1 << 1;
pub const BLUE = 1 << 2;
}
}
impl<'a> flatbuffers::Follow<'a> for Color {
type Inner = Option<Self>;
#[inline]
fn follow(buf: &'a [u8], loc: usize) -> Self::Inner {
let scalar = flatbuffers::read_scalar_at::<u8>(buf, loc);
Self::from_bits(scalar)
}
}
impl flatbuffers::Push for Color {
type Output = u8;
#[inline]
fn push(&self, dst: &mut [u8], _rest: &[u8]) {
let scalar = self.bits();
flatbuffers::emplace_scalar::<u8>(dst, scalar);
}
} |
@jean-airoldie I get you. |
@jean-airoldie do you know if this also an issue for the enums generated to discriminate flatbuffer unions? A brief glance I took today suggested yes. |
Good question, I'm not sure unions are handled internally since I don't use them. I'll take a look. |
Yep the same problem occurs in unions unfortunately. flatbuffers/tests/monster_test_generated.rs Lines 469 to 471 in 26f238c
|
@jean-airoldie flatbuffers/src/lib.rs pub extern crate num_derive;
pub extern crate num_traits; flatbuffers/cargo.toml
monster_test_generated.rs use self::flatbuffers::num_derive::{FromPrimitive, ToPrimitive}; // resolve #[derive(FromPrimitive)]
use self::flatbuffers::num_traits::*; // doesn't work I get the error: "error[E0463]: can't find crate for |
// flatbuffers/src/lib.rs
pub extern crate num_derive;
pub extern crate num_traits; Is the old style way (pre-edition 2018) of saying that you depend on this crate. This does not reexport the crate. To reexport the crate: // flatbuffers/src/lib.rs
pub use num_derive::{FromPrimitive, ToPrimitive}; I haven't use this old style syntax in a while so I'm not sure but its possible that you need to also add // flatbuffers/src/lib.rs
#[macro_use]
pub extern crate num_derive;
pub extern crate num_traits; |
I'm happy to review PRs to fix what you've been discussing in the thread, @jean-airoldie! If you think you have a solution. |
Yeah eventually, I'll find the time. |
@jean-airoldie, @rw This is my first experience with Rust. |
This issue is stale because it has been open 6 months with no activity. Please comment or this will be closed in 14 days. |
Still would be nice to see this fixed. Any updates on the status of this / and or PR #5614 ? |
I think #6098 fixed this |
Just remembered that a discriminant absent from an C-like enum is considered undefined behaviour in rust. This means that all currently generated enums could potentially cause undefined behavior if someone adds a new enum field and sends that buffer to someone using a older version of the schema.
I think the ideal solution would be to use a standard enums and add the appropriate
TryFrom
implementation (e.g.TryFrom<u8>
). Maybe we should returned the unmatched integer as the error type.The text was updated successfully, but these errors were encountered: