Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Rust: read_scalar(_at) is unsound #5825

Closed
eduardosm opened this issue Mar 20, 2020 · 9 comments · Fixed by #6588
Closed

Rust: read_scalar(_at) is unsound #5825

eduardosm opened this issue Mar 20, 2020 · 9 comments · Fixed by #6588
Assignees
Labels

Comments

@eduardosm
Copy link

The read_scalar and read_scalar_at functions are unsound because the allow to do things such as:

flatbuffers::read_scalar::<bool>(&[3]));

or

fn main() {
    #[derive(Copy, Clone, PartialEq, Debug)]
    struct Nz(std::num::NonZeroI32);
    impl flatbuffers::EndianScalar for Nz {
        fn to_little_endian(self) -> Self { self }
        fn from_little_endian(self) -> Self { self }
    }
    println!("{:?}", flatbuffers::read_scalar::<Nz>(&[0, 0, 0, 0]));
}

or even worse:

fn main() {
    #[derive(Copy, Clone, PartialEq, Debug)]
    struct S(&'static str);
    impl flatbuffers::EndianScalar for S {
        fn to_little_endian(self) -> Self { self }
        fn from_little_endian(self) -> Self { self }
    }
    println!("{:?}", flatbuffers::read_scalar::<S>(&[1; std::mem::size_of::<S>()]));
}

(this last one causes a segmentation fault)

read_scalar is behaving similar to transmute, leading to undefined behavior for types where not all bit patterns are valid (such as bool or NonZero*) or allowing to create dangling pointers.

I suggest three breaking solutions:

  • Make read_scalar and read_scalar_at functions unsafe.
  • Make the EndianScalar trait unsafe.
  • Make the EndianScalar trait something like:
pub trait EndianScalar {
    fn emplace_little_endian(self, buf: &mut [u8]);
    fn read_little_endian(buf: &[u8]) -> Self;
}

which, for example, would be implemented for u32 like this:

impl EndianScalar for u32 {
    fn emplace_little_endian(self, buf: &mut [u8]) {
        buf[..4].copy_from_slice(&self.to_le_bytes());
    }
    fn read_little_endian(buf: &[u8]) -> u32 {
        u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]])
    }
}

This last solution, even though it is the most disrupting one, has the advantage of not requiring any unsafe at all.

@rw
Copy link
Collaborator

rw commented May 12, 2020

Thanks for finding this and for recommending solutions!

Do you think that the way we use this in generated code is unsound? It seems that we use it correctly in generated code; as such, adopting your solution of marking these functions as unsafe is the way to go.

I'll note that these functions are part of the public API but we don't expect anyone to use them manually, due to the code generation process flatc uses.

@rw
Copy link
Collaborator

rw commented May 12, 2020

Perhaps we could go all-in on zero-unsafe and interpret these values manually, without unsafe at all, like we do in Go: https://github.com/google/flatbuffers/blob/master/go/encode.go#L55

@eduardosm
Copy link
Author

Looking at rustsec/advisory-db#281, it seems that these functions also allow doing unaligned memory reads.

Looking at emplace_scalar, it is also unsound because it allows doing unaligned memory writes. It also allows reading the padding from structs (which I believe that are uninitialized).

Regarding the generated code, looking at:

impl flatbuffers::EndianScalar for FromInclude {
#[inline]
fn to_little_endian(self) -> Self {
let n = i64::to_le(self as i64);
let p = &n as *const i64 as *const FromInclude;
unsafe { *p }
}
#[inline]
fn from_little_endian(self) -> Self {
let n = i64::from_le(self as i64);
let p = &n as *const i64 as *const FromInclude;
unsafe { *p }
}
}

Besides allowing to read invalid enum values (with read_scalar), it also allows swapping the byte order of enums in big endian machines.

I would go for zero-unsafe code. Reading and writing integers can be done as I suggested, and enums can use match to convert integer to enum and as to convert enum to integer.

I also think that it would great to get rid of unsafe from the generated code because it would allow consumers of flatbuffers to use forbid(unsafe_code).

@rockstar
Copy link

#6393 claims to have fixed this issue. Is that correct? Did it fix this issue?

@eduardosm
Copy link
Author

It didn't. I left some comments there.

@krojew
Copy link
Contributor

krojew commented Feb 2, 2021

@rw @CasperN any info on this? cargo audit can block flatbuffers in CI.

@CasperN
Copy link
Collaborator

CasperN commented Feb 2, 2021

iirc, the consensus was that we should mark it as unsafe due to the lack of bounds checking (which is done in the verifier).

@nico-abram
Copy link

What's the intended (public API) usage of the 2 read_scalar and the emplace_scalar functions? Are they not something that should be private/internal?

@eduardosm Could you explain how emplace_scalar allows unaligned writes? (I only see it writing to the &mut [u8] and afaik that has no alignment requirements). It does have problems for types that can only be represented as T in native endiannes, but that would be fixed by Casper's fn emplace_little_endian(self, buf: &mut [u8]); suggestion if I understand corretly

I see another problem with emplace_scalar: It doesn't seem to be checking the length of the slice to ensure we're not writing out of bounds. Would the check be detrimental to performance? If yes, it should be made unsafe, and probably renamed to something like emplace_scalar_unchecked (And maybe add a safe version that checks the length of the slice called emplace_scalar)

Example:

fn main() {
    flatbuffers::emplace_scalar(&mut [], 0u8);
}

@CasperN
Copy link
Collaborator

CasperN commented Feb 17, 2021

Its meant to be internal. Like most of the flatbuffers crate, its to be used by our generated code.

It doesn't seem to be checking the length of the slice to ensure we're not writing out of bounds. Would the check be detrimental to performance? If yes, it should be made unsafe, and probably renamed to something like emplace_scalar_unchecked (And maybe add a safe version that checks the length of the slice called emplace_scalar)

Yep, I think that's the plan, though no one is working on that atm.

rockstar added a commit to rockstar/advisory-db that referenced this issue May 20, 2021
With the [release of flatbuffers 2.0.0](https://github.com/google/flatbuffers/releases/tag/v2.0.0)
google/flatbuffers#5825 has been fixed and released. This patch updates
the advisory-db to indicate a patched version addressing the issue.
tarcieri pushed a commit to rustsec/advisory-db that referenced this issue May 20, 2021
With the [release of flatbuffers 2.0.0](https://github.com/google/flatbuffers/releases/tag/v2.0.0)
google/flatbuffers#5825 has been fixed and released. This patch updates
the advisory-db to indicate a patched version addressing the issue.
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants