Skip to content

Commit bcda3b3

Browse files
authored
Merge pull request #4466 from nia-e/native-structs
native-lib: allow passing structs as arguments
2 parents 384beed + eff2b42 commit bcda3b3

13 files changed

+454
-133
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ features = ['unprefixed_malloc_on_supported_platforms']
3939
[target.'cfg(unix)'.dependencies]
4040
libc = "0.2"
4141
# native-lib dependencies
42-
libffi = { version = "4.0.0", optional = true }
42+
libffi = { version = "4.1.1", optional = true }
4343
libloading = { version = "0.8", optional = true }
4444
serde = { version = "1.0.219", features = ["derive"], optional = true }
4545

src/shims/native_lib/ffi.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//! Support code for dealing with libffi.
2+
3+
use libffi::low::CodePtr;
4+
use libffi::middle::{Arg as ArgPtr, Cif, Type as FfiType};
5+
6+
/// Perform the actual FFI call.
7+
///
8+
/// # Safety
9+
///
10+
/// The safety invariants of the foreign function being called must be upheld (if any).
11+
pub unsafe fn call<R: libffi::high::CType>(fun: CodePtr, args: &mut [OwnedArg]) -> R {
12+
let arg_ptrs: Vec<_> = args.iter().map(|arg| arg.ptr()).collect();
13+
let cif = Cif::new(args.iter_mut().map(|arg| arg.ty.take().unwrap()), R::reify().into_middle());
14+
// SAFETY: Caller upholds that the function is safe to call, and since we
15+
// were passed a slice reference we know the `OwnedArg`s won't have been
16+
// dropped by this point.
17+
unsafe { cif.call(fun, &arg_ptrs) }
18+
}
19+
20+
/// An argument for an FFI call.
21+
#[derive(Debug, Clone)]
22+
pub struct OwnedArg {
23+
/// The type descriptor for this argument.
24+
ty: Option<FfiType>,
25+
/// Corresponding bytes for the value.
26+
bytes: Box<[u8]>,
27+
}
28+
29+
impl OwnedArg {
30+
/// Instantiates an argument from a type descriptor and bytes.
31+
pub fn new(ty: FfiType, bytes: Box<[u8]>) -> Self {
32+
Self { ty: Some(ty), bytes }
33+
}
34+
35+
/// Creates a libffi argument pointer pointing to this argument's bytes.
36+
/// NB: Since `libffi::middle::Arg` ignores the lifetime of the reference
37+
/// it's derived from, it is up to the caller to ensure the `OwnedArg` is
38+
/// not dropped before unsafely calling `libffi::middle::Cif::call()`!
39+
fn ptr(&self) -> ArgPtr {
40+
// FIXME: Using `&self.bytes[0]` to reference the whole array is
41+
// definitely unsound under SB, but we're waiting on
42+
// https://github.com/libffi-rs/libffi-rs/commit/112a37b3b6ffb35bd75241fbcc580de40ba74a73
43+
// to land in a release so that we don't need to do this.
44+
ArgPtr::new(&self.bytes[0])
45+
}
46+
}

src/shims/native_lib/mod.rs

Lines changed: 175 additions & 132 deletions
Large diffs are not rendered by default.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#include <stdint.h>
2+
3+
// See comments in build_native_lib()
4+
#define EXPORT __attribute__((visibility("default")))
5+
6+
/* Test: fail/pass_struct_expose_only_range */
7+
8+
typedef struct HasPointer {
9+
uint8_t *ptr;
10+
} HasPointer;
11+
12+
EXPORT uint8_t access_struct_ptr(const HasPointer s) {
13+
return *s.ptr;
14+
}
15+
16+
/* Test: test_pass_struct */
17+
18+
typedef struct PassMe {
19+
int32_t value;
20+
int64_t other_value;
21+
} PassMe;
22+
23+
EXPORT int64_t pass_struct(const PassMe pass_me) {
24+
return pass_me.value + pass_me.other_value;
25+
}
26+
27+
/* Test: test_pass_struct_complex */
28+
29+
typedef struct Part1 {
30+
uint16_t high;
31+
uint16_t low;
32+
} Part1;
33+
34+
typedef struct Part2 {
35+
uint32_t bits;
36+
} Part2;
37+
38+
typedef struct ComplexStruct {
39+
Part1 part_1;
40+
Part2 part_2;
41+
uint32_t part_3;
42+
} ComplexStruct;
43+
44+
EXPORT int32_t pass_struct_complex(const ComplexStruct complex, uint16_t high, uint16_t low, uint32_t bits) {
45+
if (complex.part_1.high == high && complex.part_1.low == low
46+
&& complex.part_2.bits == bits
47+
&& complex.part_3 == bits)
48+
return 0;
49+
else {
50+
return 1;
51+
}
52+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//@compile-flags: -Zmiri-permissive-provenance
2+
3+
#[repr(C)]
4+
#[derive(Copy, Clone)]
5+
struct HasPointer {
6+
ptr: *const u8,
7+
}
8+
9+
extern "C" {
10+
fn access_struct_ptr(s: HasPointer) -> u8;
11+
}
12+
13+
fn main() {
14+
let structs = vec![HasPointer { ptr: &0 }, HasPointer { ptr: &1 }];
15+
unsafe {
16+
let r = access_struct_ptr(structs[1]);
17+
assert_eq!(r, 1);
18+
// There are two pointers stored in the allocation backing `structs`; ensure
19+
// we only exposed the one that was actually passed to C.
20+
let _val = *std::ptr::with_exposed_provenance::<u8>(structs[1].ptr.addr()); // fine, ptr got sent to C
21+
let _val = *std::ptr::with_exposed_provenance::<u8>(structs[0].ptr.addr()); //~ ERROR: memory access failed
22+
};
23+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
warning: sharing memory with a native function called via FFI
2+
--> tests/native-lib/fail/pass_struct_expose_only_range.rs:LL:CC
3+
|
4+
LL | let r = access_struct_ptr(structs[1]);
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ sharing memory with a native function
6+
|
7+
= help: when memory is shared with a native function call, Miri stops tracking initialization and provenance for that memory
8+
= help: in particular, Miri assumes that the native call initializes all memory it has access to
9+
= help: Miri also assumes that any part of this memory may be a pointer that is permitted to point to arbitrary exposed memory
10+
= help: what this means is that Miri will easily miss Undefined Behavior related to incorrect usage of this shared memory, so you should not take a clean Miri run as a signal that your FFI code is UB-free
11+
= note: BACKTRACE:
12+
= note: inside `main` at tests/native-lib/fail/pass_struct_expose_only_range.rs:LL:CC
13+
14+
error: Undefined Behavior: memory access failed: attempting to access 1 byte, but got $HEX[noalloc] which is a dangling pointer (it has no provenance)
15+
--> tests/native-lib/fail/pass_struct_expose_only_range.rs:LL:CC
16+
|
17+
LL | let _val = *std::ptr::with_exposed_provenance::<u8>(structs[0].ptr.addr());
18+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
19+
|
20+
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
21+
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
22+
= note: BACKTRACE:
23+
= note: inside `main` at tests/native-lib/fail/pass_struct_expose_only_range.rs:LL:CC
24+
25+
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
26+
27+
error: aborting due to 1 previous error; 1 warning emitted
28+
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Only works on Unix targets
2+
//@ignore-target: windows wasm
3+
//@only-on-host
4+
5+
#![allow(improper_ctypes)]
6+
7+
pub struct PassMe {
8+
pub value: i32,
9+
pub other_value: i64,
10+
}
11+
12+
extern "C" {
13+
fn pass_struct(s: PassMe) -> i64;
14+
}
15+
16+
fn main() {
17+
let pass_me = PassMe { value: 42, other_value: 1337 };
18+
unsafe { pass_struct(pass_me) }; //~ ERROR: unsupported operation: passing a non-#[repr(C)] struct over FFI
19+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
error: unsupported operation: passing a non-#[repr(C)] struct over FFI: PassMe
2+
--> tests/native-lib/fail/struct_not_extern_c.rs:LL:CC
3+
|
4+
LL | unsafe { pass_struct(pass_me) };
5+
| ^^^^^^^^^^^^^^^^^^^^ unsupported operation occurred here
6+
|
7+
= help: this is likely not a bug in the program; it indicates that the program performed an operation that Miri does not support
8+
= note: BACKTRACE:
9+
= note: inside `main` at tests/native-lib/fail/struct_not_extern_c.rs:LL:CC
10+
11+
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
12+
13+
error: aborting due to 1 previous error
14+
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#[repr(C)]
2+
#[derive(Copy, Clone)]
3+
struct ComplexStruct {
4+
part_1: Part1,
5+
part_2: Part2,
6+
part_3: u32,
7+
}
8+
#[repr(C)]
9+
#[derive(Copy, Clone)]
10+
struct Part1 {
11+
high: u16,
12+
low: u16,
13+
}
14+
#[repr(C)]
15+
#[derive(Copy, Clone)]
16+
struct Part2 {
17+
bits: u32,
18+
}
19+
20+
extern "C" {
21+
fn pass_struct_complex(s: ComplexStruct, high: u16, low: u16, bits: u32) -> i32;
22+
}
23+
24+
fn main() {
25+
let arg = std::mem::MaybeUninit::<ComplexStruct>::uninit();
26+
unsafe { pass_struct_complex(*arg.as_ptr(), 0, 0, 0) }; //~ ERROR: Undefined Behavior: constructing invalid value
27+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
error: Undefined Behavior: constructing invalid value at .part_1.high: encountered uninitialized memory, but expected an integer
2+
--> tests/native-lib/fail/uninit_struct.rs:LL:CC
3+
|
4+
LL | unsafe { pass_struct_complex(*arg.as_ptr(), 0, 0, 0) };
5+
| ^^^^^^^^^^^^^ Undefined Behavior occurred here
6+
|
7+
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
8+
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
9+
= note: BACKTRACE:
10+
= note: inside `main` at tests/native-lib/fail/uninit_struct.rs:LL:CC
11+
12+
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
13+
14+
error: aborting due to 1 previous error
15+

0 commit comments

Comments
 (0)