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

Generated tests use UB in their calculations #1651

Closed
Lokathor opened this issue Oct 21, 2019 · 77 comments · Fixed by #2203
Closed

Generated tests use UB in their calculations #1651

Lokathor opened this issue Oct 21, 2019 · 77 comments · Fixed by #2203

Comments

@Lokathor
Copy link
Contributor

Test code for struct offsets run checks like this:

    assert_eq!(
        unsafe { &(*(::core::ptr::null::<timeval>())).tv_sec as *const _ as usize },
        0usize,
        concat!(
            "Offset of field: ",
            stringify!(timeval),
            "::",
            stringify!(tv_sec)
        )
    );

But it's UB to dereference a null pointer, and it's also UB to create a &T with numeric value 0.

There is actually trivial to fix. Instead of using a null pointer, just create a value on the stack (using core::mem::zeroed(), which is safe since all C structs can be created as zeroed-memory), and then use a reference to that stack value, the fields of that stack value, and so on.

@emilio
Copy link
Contributor

emilio commented Oct 22, 2019

This is known, but thanks for filing. I've always hoped that rust would have an offsetof macro, but... ;)

Creating structs on the stack with zeroed memory is ok for C, but not so sure for C++, where what bindgen may generate is not POD, or may have destructors or other side effects.

@Lokathor
Copy link
Contributor Author

Lokathor commented Oct 22, 2019

Can go by if the type is Copy or not. Also can use ManuallyDrop if the Rust target is new enough.

I'm not sure what most of the point of those tests is anyway, most of them can't ever fail so it seems kinda silly.

@emilio
Copy link
Contributor

emilio commented Oct 23, 2019

How not? Those tests are important, if bindgen generates something that has the wrong size or alignment or anything, those tests catch it.

@Lokathor
Copy link
Contributor Author

Replied in a separate issue because it's not really related to this "offset calculation is UB" issue.

@gnzlbg
Copy link
Contributor

gnzlbg commented Nov 4, 2019

@Lokathor whatever you come up with will be UB anyways, because rust-bindgen needs to compute the offset of #[repr(packed)] struct fields, and there is no way to do that in Rust without UB.

@RalfJung
Copy link
Member

RalfJung commented Nov 4, 2019

@gnzlbg that is true, but it has nothing to do with packed. You can't do it for non-packed struct either, and once we have fixed that (using &raw) the fix will work for packed structs, too.

https://github.com/Gilnaa/memoffset/ implements the currently best way we have to do this. The most important part is to avoid NULL references; you could use NonNull::dangling instead.

@RalfJung
Copy link
Member

RalfJung commented Nov 4, 2019

I've always hoped that rust would have an offsetof macro, but... ;)

Given that library solutions cannot (it seems) soundly support nested fields and arrays, I think indeed this should become a built-in macro. But I don't have a thick enough skin for the syntax bikeshed that will undoubtedly trigger. ;)

Until then, we have that macro as a library.

@gnzlbg
Copy link
Contributor

gnzlbg commented Nov 4, 2019

You can't do it for non-packed struct either, and once we have fixed that (using &raw) the fix will work for packed structs, too.

Notice that rust-bindgen derives Default for its types, so at least when it does that it could actually construct a value of a type by using Default::default and then compute the field offsets on that value without any null dereferences. That computation still requires creating a reference to a field, and since those fields can be unaligned for #[repr(packed)] structs, that would still be UB.

@Lokathor
Copy link
Contributor Author

Lokathor commented Nov 4, 2019

@gnzlbg why did i get pinged?

@gnzlbg
Copy link
Contributor

gnzlbg commented Nov 4, 2019

Because you proposed a solution:

There is actually trivial to fix. Instead of using a null pointer, just create a value on the stack (using core::mem::zeroed(), which is safe since all C structs can be created as zeroed-memory), and then use a reference to that stack value, the fields of that stack value, and so on.

so I thought you might work on it, but that solution doesn't work, because of repr(packed).

I think the mem::zeroed part happens to work because rust-bindgen never generates types (at least by default, or at least it shouldn't) for which it doesn't work, but that's not the only source of UB.

@Lokathor
Copy link
Contributor Author

Lokathor commented Nov 4, 2019

Oh, well (1) I'm not going to work on this myself I'm just reporting the problem, (2) bindgen knows when it will emit repr(packed) so just don't do the simplistic version in that case (3) right the main problem is dereferencing a null pointer.

@gnzlbg
Copy link
Contributor

gnzlbg commented Nov 4, 2019

(2) bindgen knows when it will emit repr(packed) so just don't do the simplistic version in that case

What should it do in that case then?

(3) right the main problem is dereferencing a null pointer.

The main problem is undefined behavior, but there are no solutions without undefined behavior.


There is nothing for rust-bindgen to do here: whatever it does, it will be UB, and someone will open an issue to report that, and maybe change that, into something else that also has UB.

This should either be "blocked on" some rust-lang/rust tracking issue, or closed as "non-actionable", but right now there is no way to fix on rust-bindgen's side.

@Lokathor
Copy link
Contributor Author

Lokathor commented Nov 4, 2019

Structs that aren't repr(packed) can be fixed.

Further, structs that are repr(packed) can still at least be tested against a zeroed value on the stack instead of on a null pointer and at least eliminate one of the problems, even if it's not every problem being solved.

It's okay to solve part of a problem at a time.

@gnzlbg
Copy link
Contributor

gnzlbg commented Nov 4, 2019

I don't see the point yet.

If this is a problem because it is UB, there are currently not solutions to this problem. But if "UB is ok as long as it works", then the current code, which works, is as good as anything else. Time would be better spent into trying to provide a way to do this without any UB, but that cannot happen here.

@Lokathor
Copy link
Contributor Author

Lokathor commented Nov 4, 2019

Well, most structs aren't repr(packed), so actually it can happen here.

@gnzlbg
Copy link
Contributor

gnzlbg commented Nov 4, 2019

Well, most structs aren't repr(packed), so actually it can happen here.

Even for non repr(packed) structs, this would still be UB if the type has zero as a niche, which I don't think happens by default (except maybe for C++), but happens often when using the opaque-type feature, for example.

@Lokathor
Copy link
Contributor Author

Lokathor commented Nov 4, 2019

...okay but you get my point that this can be fixed for the vast majority of C struct bindings I hope.

@gnzlbg
Copy link
Contributor

gnzlbg commented Nov 4, 2019

I disagree with vast majority (all C bindings I work on use repr(packed)), but yes, for some subset of C bindings, some of rust-bindgen tests could become UB free.

@RalfJung
Copy link
Member

RalfJung commented Nov 4, 2019

@gnzlbg there's still a big difference between "theoretical UB" (that works because it's spec-only UB but we currently -- as of the latest nightly rustc -- never exploit it) and UB that rustc/LLVM might actually exploit, but they happen to not do that in the cases you checked (like NULL references).

I don't understand why you are fighting back so hard against what is clearly a strict improvement in language compliance. Why this black-and-white attitude?

@gnzlbg
Copy link
Contributor

gnzlbg commented Nov 4, 2019 via email

@RalfJung
Copy link
Member

RalfJung commented Nov 4, 2019

So, let me rephrase: "if it can't be perfect (doesn't really fix the issue) then we don't care how awful it is".

many more crates use bindgen than offsetof

offsetof is used in crossbeam, so if you really want to compare numbers...
memoffset recent downloads: 1,592,597
bindgen recent downloads: 310,642

But also, this is clearly not leading anywhere, so I'll just stop here.

@gnzlbg
Copy link
Contributor

gnzlbg commented Nov 4, 2019

"if it can't be perfect (doesn't really fix the issue) then we don't care how awful it is".

That's not what I said.

@Lokathor
Copy link
Contributor Author

Lokathor commented Mar 7, 2020

Update: there is a fully sound offset_of! macro available in the bytemuck crate:
https://docs.rs/bytemuck/1.2.0/bytemuck/macro.offset_of.html

The "catch" is that you have to pass in an instance of the type to the macro. So either the type needs to have default or makeable with all 0s or something like that.

@anp
Copy link
Member

anp commented Apr 15, 2021

Would the addition of addr_of make this easier to implement without UB?

FYI, the recent addition of a new lint for deref'ing a null ptr surfaces this:

error: dereferencing a null pointer
  --> ../../src/lib/usb_bulk/rust/src/usb.rs:59:19
   |
59 |         unsafe { &(*(::std::ptr::null::<usb_ifc_info>())).dev_vendor as *const _ as usize },
   |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this code causes undefined behavior when executed
   |
   = note: `-D deref-nullptr` implied by `-D warnings`

@emilio
Copy link
Contributor

emilio commented Apr 16, 2021

Perhaps? But for that we'd also need to create an instance of the type, which we might not know how to create, and which may have other side effects... Can we get an address into a MaybeUninit::<NonTrivial>::uninit() without it being UB somehow?

I don't see how off-hand but that doesn't necessarily mean it doesn't exist... In any case I think we'd still be working around the lack of a standard offset_of! macro...

@Lokathor
Copy link
Contributor Author

any C type can be constructed of zeroed memory at least.

@emilio
Copy link
Contributor

emilio commented Apr 16, 2021

Not if you implement Drop on the rust side or what not, which might be a reasonable thing to do.

@simlay
Copy link
Contributor

simlay commented Apr 19, 2022

It looks like rust-lang/rust#95372 caused these warnings are now errors in nightly which might means it'll error on rust 1.62 (in ~10 weeks).

For the next person, it looks like quick work around (but definitely not the correct long term solution) is to use layout_tests(false) similar as seen with the iOS cross compilaion of boring.

@koutheir
Copy link

Disabling layout tests is only useful to remove the warning and undefined behavior, at the expense of more risks with regard to ABI changes. That's a compromise one needs to think about. The layout_tests(false) workaround simply hides the issue instead of solving it.

@Lokathor
Copy link
Contributor Author

The layout tests can't detect that to begin with so there's no difference if you turn them off.

@koutheir
Copy link

If the layout tests can't detect ABI changes, then what are they supposed to validate?

@Lokathor
Copy link
Contributor Author

Lokathor commented Apr 19, 2022

They don't validate much because they don't call into C. It's just checking rust code against other rust code. All they actually end up doing is a clumsy check that your C type aliases are correct. Which you can do once per crate, instead of once per type, and get just as much benefit.

EDIT: even then, checking the type aliases once per crate is excessive probably. Really they should be checked once when defined (eg: in cty or std or wherever else) and then no downstream crate needs to double check after that.

@koutheir
Copy link

Does this mean that one way of actually solving this issue would be to remove the generation of these checks entirely?

@Lokathor
Copy link
Contributor Author

that's my suggestion, yes.

@emilio
Copy link
Contributor

emilio commented Apr 19, 2022

This is not right. The layout tests are useful, because they test the actual generated rust code with the offsets that we get from libclang.

@emilio
Copy link
Contributor

emilio commented Apr 19, 2022

So even though it's not the same as calling into C and back, it's pretty close, and they've caught ABI issues in Firefox in the past, for example.

@Lokathor
Copy link
Contributor Author

the tests could certainly detect some problems, but if all C type aliases are correct then that's about the extent of what they can check. For example, if a conditional compilation adds or changes a field in a C struct in a new C lib version, or on a different target, the rust code has no way to notice, and these tests won't help you. They are extremely un-portable tests.

PierreZ added a commit to PierreZ/foundationdb-rs that referenced this issue Apr 23, 2022
@kulp kulp linked a pull request May 12, 2022 that will close this issue
xmas7 pushed a commit to RubyOnWorld/rust_mysql_common that referenced this issue Sep 6, 2022
petreeftime pushed a commit to petreeftime/aws-nitro-enclaves-cli that referenced this issue Nov 21, 2022
Use latest version of bindgen (0.62) and add Default to implementations.
This fixes `clippy::derivable_impls` warning and rust-lang/rust-bindgen#1651.

Signed-off-by: Petre Eftime <[email protected]>
petreeftime pushed a commit to aws/aws-nitro-enclaves-cli that referenced this issue Nov 21, 2022
Use latest version of bindgen (0.62) and add Default to implementations.
This fixes `clippy::derivable_impls` warning and rust-lang/rust-bindgen#1651.

Signed-off-by: Petre Eftime <[email protected]>
KarimHamidou added a commit to KarimHamidou/godot-rust that referenced this issue Feb 6, 2023
Since the update to rustc 1.53, bindgen using UB causes warnings.
See rust-lang/rust-bindgen#1651 for detail.

Warnings all in the style of:
---------------------------------------------------------------------
warning: dereferencing a null pointer
  --> ../godot-rust/target/<toolchain>/debug/build/gdnative-sys-7023d0a45a684438/out/bindings.rs:98:19
   |
98 |         unsafe { &(*(::std::ptr::null::<godot_string>()))._dont_touch_that as *const _ as usize },
   |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this code causes undefined behavior when executed
   |
   = note: `#[warn(deref_nullptr)]` on by default
---------------------------------------------------------------------
KarimHamidou added a commit to KarimHamidou/godot-rust that referenced this issue Feb 6, 2023
776: Silence UB warnings caused by bindgen r=Bromeon a=Bromeon

Since the update to rustc 1.53, bindgen using UB causes warnings.
See rust-lang/rust-bindgen#1651 for detail.

Warnings all in the style of:
```
warning: dereferencing a null pointer
  --> ../godot-rust/target/<toolchain>/debug/build/gdnative-sys-7023d0a45a684438/out/bindings.rs:98:19
   |
98 |         unsafe { &(*(::std::ptr::null::<godot_string>()))._dont_touch_that as *const _ as usize },
   |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this code causes undefined behavior when executed
   |
   = note: `#[warn(deref_nullptr)]` on by default
```

Co-authored-by: Jan Haller <[email protected]>
likebreath added a commit to likebreath/kvm-bindings that referenced this issue Jan 3, 2024
The clippy override `allow(deref_nullptr)` is no longer needed since the
issue has been fixed from rust-bindgen [1].

[1] rust-lang/rust-bindgen#1651

Signed-off-by: Bo Chen <[email protected]>
roypat pushed a commit to rust-vmm/kvm-bindings that referenced this issue Jan 4, 2024
The clippy override `allow(deref_nullptr)` is no longer needed since the
issue has been fixed from rust-bindgen [1].

[1] rust-lang/rust-bindgen#1651

Signed-off-by: Bo Chen <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet