-
Notifications
You must be signed in to change notification settings - Fork 414
Basic support for FFI callbacks (at least stop segfaulting and report a proper error) #4728
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
base: master
Are you sure you want to change the base?
Conversation
|
Thank you for contributing to Miri! A reviewer will take a look at your PR, typically within a week or two. |
This comment has been minimized.
This comment has been minimized.
e553417 to
b539d51
Compare
This comment has been minimized.
This comment has been minimized.
This commit adds basic support for FFI callbacks by registering a shim function via libffi. This shim function currently only notes if it's actually called and registers an error for these cases. The main motivation for this is to prevent miri segfaulting as described in [4639](rust-lang#4639). In the future miri could try to continue execution in the registered callback, although as far as I understand Ralf that is no easy problem.
b539d51 to
6699145
Compare
| either::Either::Left(mplace) => { | ||
| let ptr_overwrite = match v.layout.ty.kind() { | ||
| ty::Adt(_adt_def, args) => | ||
| if let ty::FnPtr(fn_ptr, _header) = args.type_at(0).kind() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this adt + fndef match detecting? I assumed you'd just want to detect fn ptrs being converted to FFI data and inject your callback there, but that doesn't seem to be what is happening.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are correct that this is likely wrong. The underlying problem there is that this does not only need to handle plain function pointers but also something like Option<unsafe fn()>. Now I do not know much about the involved compiler types, so that is my crude attempt to get the relevant information. If you know a better way I'm certainly open for suggestions.
(Beside of that it seems like I did not actually handle the plain function pointer case, I should definitely add that)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The purpose of this function is to transmit raw bytes to the C side. So matching on the type at all here is almost certainly not the right call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I think I see what you are doing here... you are changing the bytes if they represent a function pointer. That doesn't work, we need the bytes to be exactly the same on both sides. After all, both sides might be casting those pointers to integers and print them, and something would be seriously cursed if the same function pointer then printed as different addresses on the two sides of the FFI call. Also, if you store a function pointer in a static and then pass a pointer to that static to C, this function will never see the function pointer, it will only ever see the pointer to the function pointer.
The code that determines the addresses of allocations (which includes both data allocations for normal static/stack/heap memory, and function allocations for the "memory that a function pointer points to") is here:
miri/src/alloc_addresses/mod.rs
Lines 139 to 178 in 5a47c25
| // In native lib mode, we use the "real" address of the bytes for this allocation. | |
| // This ensures the interpreted program and native code have the same view of memory. | |
| let params = this.machine.get_default_alloc_params(); | |
| let base_ptr = match info.kind { | |
| AllocKind::LiveData => { | |
| if memory_kind == MiriMemoryKind::Global.into() { | |
| // For new global allocations, we always pre-allocate the memory to be able use the machine address directly. | |
| let prepared_bytes = MiriAllocBytes::zeroed(info.size, info.align, params) | |
| .unwrap_or_else(|| { | |
| panic!("Miri ran out of memory: cannot create allocation of {size:?} bytes", size = info.size) | |
| }); | |
| let ptr = prepared_bytes.as_ptr(); | |
| // Store prepared allocation to be picked up for use later. | |
| global_state | |
| .prepared_alloc_bytes | |
| .as_mut() | |
| .unwrap() | |
| .try_insert(alloc_id, prepared_bytes) | |
| .unwrap(); | |
| ptr | |
| } else { | |
| // Non-global allocations are already in memory at this point so | |
| // we can just get a pointer to where their data is stored. | |
| this.get_alloc_bytes_unchecked_raw(alloc_id)? | |
| } | |
| } | |
| AllocKind::Function | AllocKind::VTable => { | |
| // Allocate some dummy memory to get a unique address for this function/vtable. | |
| let alloc_bytes = MiriAllocBytes::from_bytes( | |
| &[0u8; 1], | |
| Align::from_bytes(1).unwrap(), | |
| params, | |
| ); | |
| let ptr = alloc_bytes.as_ptr(); | |
| // Leak the underlying memory to ensure it remains unique. | |
| std::mem::forget(alloc_bytes); | |
| ptr | |
| } | |
| AllocKind::TypeId | AllocKind::Dead => unreachable!(), | |
| }; |
The FFI closure allocation needs to happen in the AllocKind::Function case there, so that we can then make the "virtual" address (i.e., the address in Miri's purely logical interpreter memory) of this function pointer the same as the real address of the closure. This is a key invariant of Miri in native_lib mode: Miri's logical/virtual addresses are the same as the real underlying addresses, and therefore C code can follow pointers stored in Miri memory and everything works out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the pointer. This sounds reasonable, but I fail to see how I would access information about the callback/function/allocation type there. Without this information it's not possible to construct the corresponding libffi type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is get_fn_alloc but it's private (in compiler/rustc_const_eval/src/interpret/memory.rs in the rustc tree). We should probably make it public, and then also rename it to try_get_alloc_fn for better consistency with other, similar methods.
|
Please add a test for this, too. |
I'm more than happy to do that, but as pointed out in the OP I don't know where and how. Is there any documentation for this or can you provide a pointer to the right direction? |
| // Functions with no declared return type (i.e., the default return) | ||
| // have the output_type `Tuple([])`. | ||
| ty::Tuple(t_list) if (*t_list).deref().is_empty() => FfiType::void(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should not be needed -- function pointers are scalar types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function I happened to use for testing returned void. As this function is now also called to construct the return value types for the libffi closure type this was required to make my test case working.
That written: You are correct that this is not needed for a minimal support of callback over fro, it just happens to help with my particular test case and was rather straightforward to add.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this function is now also called to construct the return value types for the libffi closure type
Ah, that makes sense.
Given that I think we should never return from that closure (see my other comments), I think we should not need this either.
The native-lib tests are in |
| ) { | ||
| debug_assert_eq!(cif.nargs as usize, infos.args.len()); | ||
| let mut rust_args = Vec::with_capacity(infos.args.len()); | ||
| // cast away the pointer to pointer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a general style note, please use full sentences in comments, including an upper-case first word and a period at the end.
| // write here the output | ||
| // For now we just try to write some dummy output | ||
| // by using some "reasonable" default values | ||
| // to prevent crashing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should return to the FFI code here. That code clearly wanted this function pointer to do something, and arbitrary nonsense could happen if we just skip whatever that is and return. This could be worse than a segfault. We have to abort execution here.
This commit adds basic support for FFI callbacks by registering a shim function via libffi. This shim function currently only notes if it's actually called and registers an error for these cases. The main motivation for this is to prevent miri segfaulting as described in #4639. Obviously the code is likely highly unsafe, especially if something goes wrong, etc as it's going back and forth over an ffi boundary and just casts pointers as it likes.
In the future miri could try to continue execution in the registered callback, although as far as I understand Ralf that is no easy problem. There are already preparations for this, like actually receiving the arguments and setting up the structure to return something.
This produces the following error for diesel:
I feel that's better than nothing, although a better error would include trace information about the registering side and the call side as well.
There are no tests for this yet, as I'm not familiar with miri's test setup, how to structure them and where to put them. Any pointers for this would be helpful.