Add tests to ensure MTE tags are preserved across FFI boundaries

Added run-make tests to verify that, between a Rust-C FFI boundary in both directions,
any MTE tags included in a pointer are preserved for the following pointer types, as
well as any information stored using TBI:
- int
- float
- string
- function
This commit is contained in:
Damian Heaton 2024-06-13 10:55:59 +00:00 committed by dheaton-arm
parent 7e3a971870
commit e8ce9fac85
10 changed files with 342 additions and 0 deletions

View File

@ -0,0 +1,43 @@
#ifndef __BAR_H
#define __BAR_H
#include <sys/mman.h>
#include <sys/auxv.h>
#include <sys/prctl.h>
#include <unistd.h>
#include <stdio.h>
// Set the allocation tag on the destination address using the STG instruction.
#define set_tag(tagged_addr) do { \
asm volatile("stg %0, [%0]" : : "r" (tagged_addr) : "memory"); \
} while (0)
int mte_enabled() {
return (getauxval(AT_HWCAP2)) & HWCAP2_MTE;
}
void *alloc_page() {
// Enable MTE with synchronous checking
if (prctl(PR_SET_TAGGED_ADDR_CTRL,
PR_TAGGED_ADDR_ENABLE | PR_MTE_TCF_SYNC | (0xfffe << PR_MTE_TAG_SHIFT),
0, 0, 0))
{
perror("prctl() failed");
}
// Using `mmap` allows us to ensure that, on systems which support MTE, the allocated
// memory is 16-byte aligned for MTE.
// This also allows us to explicitly specify whether the region should be protected by
// MTE or not.
if (mte_enabled()) {
void *ptr = mmap(NULL, sysconf(_SC_PAGESIZE),
PROT_READ | PROT_WRITE | PROT_MTE, MAP_PRIVATE | MAP_ANONYMOUS,
-1, 0);
} else {
void *ptr = mmap(NULL, sysconf(_SC_PAGESIZE),
PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS,
-1, 0);
}
}
#endif // __BAR_H

View File

@ -0,0 +1,44 @@
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include "bar.h"
extern void foo(char*);
void bar(char *ptr) {
if (((uintptr_t)ptr >> 56) != 0x1f) {
fprintf(stderr, "Top byte corrupted on Rust -> C FFI boundary!\n");
exit(1);
}
}
int main(void)
{
float *ptr = alloc_page();
if (ptr == MAP_FAILED)
{
perror("mmap() failed");
return EXIT_FAILURE;
}
// Store an arbitrary tag in bits 56-59 of the pointer (where an MTE tag may be),
// and a different value in the ignored top 4 bits.
ptr = (float *)((uintptr_t)ptr | 0x1fl << 56);
if (mte_enabled()) {
set_tag(ptr);
}
ptr[0] = 2.0f;
ptr[1] = 1.5f;
foo(ptr); // should change the contents of the page and call `bar`
if (ptr[0] != 0.5f || ptr[1] != 0.2f) {
fprintf(stderr, "invalid data in memory; expected '0.5 0.2', got '%f %f'\n",
ptr[0], ptr[1]);
return EXIT_FAILURE;
}
return 0;
}

View File

@ -0,0 +1,39 @@
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include "bar.h"
typedef void (*fp)(int (*)());
extern void foo(fp);
void bar(int (*ptr)()) {
if (((uintptr_t)ptr >> 56) != 0x2f) {
fprintf(stderr, "Top byte corrupted on Rust -> C FFI boundary!\n");
exit(1);
}
int r = (*ptr)();
if (r != 32) {
fprintf(stderr, "invalid return value; expected 32, got '%d'\n", r);
exit(1);
}
}
int main(void)
{
fp ptr = alloc_page();
if (ptr == MAP_FAILED)
{
perror("mmap() failed");
return EXIT_FAILURE;
}
// Store an arbitrary tag in bits 56-59 of the pointer (where an MTE tag may be),
// and a different value in the ignored top 4 bits.
ptr = (fp)((uintptr_t)&bar | 0x1fl << 56);
foo(ptr);
return 0;
}

View File

@ -0,0 +1,47 @@
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include "bar.h"
extern void foo(unsigned int *);
void bar(char *ptr) {
if (((uintptr_t)ptr >> 56) != 0x1f) {
fprintf(stderr, "Top byte corrupted on Rust -> C FFI boundary!\n");
exit(1);
}
}
int main(void)
{
// Construct a pointer with an arbitrary tag in bits 56-59, simulating an MTE tag.
// It's only necessary that the tag is preserved across FFI bounds for this test.
unsigned int *ptr;
ptr = alloc_page();
if (ptr == MAP_FAILED)
{
perror("mmap() failed");
return EXIT_FAILURE;
}
// Store an arbitrary tag in bits 56-59 of the pointer (where an MTE tag may be),
// and a different value in the ignored top 4 bits.
ptr = (unsigned int *)((uintptr_t)ptr | 0x1fl << 56);
if (mte_enabled()) {
set_tag(ptr);
}
ptr[0] = 61;
ptr[1] = 62;
foo(ptr); // should change the contents of the page to start with 0x63 0x64 and call `bar`
if (ptr[0] != 0x63 || ptr[1] != 0x64) {
fprintf(stderr, "invalid data in memory; expected '63 64', got '%d %d'\n", ptr[0], ptr[1]);
return EXIT_FAILURE;
}
return 0;
}

View File

@ -0,0 +1,48 @@
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include "bar.h"
extern void foo(char*);
void bar(char *ptr) {
if (((uintptr_t)ptr >> 56) != 0x2f) {
fprintf(stderr, "Top byte corrupted on Rust -> C FFI boundary!\n");
exit(1);
}
if (strcmp(ptr, "cd")) {
fprintf(stderr, "invalid data in memory; expected 'cd', got '%s'\n", ptr);
exit(1);
}
}
int main(void)
{
// Construct a pointer with an arbitrary tag in bits 56-59, simulating an MTE tag.
// It's only necessary that the tag is preserved across FFI bounds for this test.
char *ptr;
ptr = alloc_page();
if (ptr == MAP_FAILED)
{
perror("mmap() failed");
return EXIT_FAILURE;
}
// Store an arbitrary tag in bits 56-59 of the pointer (where an MTE tag may be),
// and a different value in the ignored top 4 bits.
ptr = (unsigned int *)((uintptr_t)ptr | 0x1fl << 56);
if (mte_enabled()) {
set_tag(ptr);
}
ptr[0] = 'a';
ptr[1] = 'b';
ptr[2] = '\0';
foo(ptr);
return 0;
}

View File

@ -0,0 +1,19 @@
#![crate_type = "cdylib"]
#![crate_name = "foo"]
use std::os::raw::c_float;
extern "C" {
fn bar(ptr: *const c_float);
}
#[no_mangle]
pub extern "C" fn foo(ptr: *mut c_float) {
assert_eq!((ptr as usize) >> 56, 0x1f);
unsafe {
*ptr = 0.5;
*ptr.wrapping_add(1) = 0.2;
bar(ptr);
}
}

View File

@ -0,0 +1,17 @@
#![crate_type = "cdylib"]
#![crate_name = "foo"]
extern "C" fn ret32() -> i32 {
32
}
#[no_mangle]
pub extern "C" fn foo(ptr: extern "C" fn(extern "C" fn() -> i32)) {
assert_eq!((ptr as usize) >> 56, 0x1f);
// Store an arbitrary tag in the tag bits, and convert back to the correct pointer type.
let p = ((ret32 as usize) | (0x2f << 56)) as *const ();
let p: extern "C" fn() -> i32 = unsafe { std::mem::transmute(p) };
unsafe { ptr(p) }
}

View File

@ -0,0 +1,19 @@
#![crate_type = "cdylib"]
#![crate_name = "foo"]
use std::os::raw::c_uint;
extern "C" {
fn bar(ptr: *const c_uint);
}
#[no_mangle]
pub extern "C" fn foo(ptr: *mut c_uint) {
assert_eq!((ptr as usize) >> 56, 0x1f);
unsafe {
*ptr = 0x63;
*ptr.wrapping_add(1) = 0x64;
bar(ptr);
}
}

View File

@ -0,0 +1,27 @@
#![crate_type = "cdylib"]
#![crate_name = "foo"]
use std::arch::asm;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
extern "C" {
fn bar(ptr: *const c_char);
}
#[no_mangle]
pub extern "C" fn foo(ptr: *const c_char) {
assert_eq!((ptr as usize) >> 56, 0x1f);
let s = unsafe { CStr::from_ptr(ptr) };
assert_eq!(s.to_str().unwrap(), "ab");
let s = CString::from_vec_with_nul("cd\0".into()).unwrap();
let mut p = ((s.as_ptr() as usize) | (0x2f << 56)) as *const c_char;
unsafe {
#[cfg(target_feature = "mte")]
asm!("stg {p}, [{p}]", p = inout(reg) p);
bar(p);
}
}

View File

@ -0,0 +1,39 @@
// Tests that MTE tags and values stored in the top byte of a pointer (TBI) are
// preserved across FFI boundaries (C <-> Rust).
// This test does not require MTE: whilst the test will use MTE if available, if it is not,
// arbitrary tag bits are set using TBI.
//@ only-aarch64
//@ only-linux
//@ only-gnu
//@ run-pass
use run_make_support::{cc, dynamic_lib_name, extra_c_flags, run, rustc, target};
fn main() {
run_test("int");
run_test("float");
run_test("string");
run_test("function");
}
fn run_test(variant: &str) {
let flags = {
let mut flags = extra_c_flags();
flags.push("-march=armv8.5-a+memtag");
flags
};
print!("{variant} test...");
rustc()
.input(format!("foo_{variant}.rs"))
.target(target())
.linker("aarch64-linux-gnu-gcc")
.run();
cc().input(format!("bar_{variant}.c"))
.input(dynamic_lib_name("foo"))
.out_exe("test")
.args(&flags)
.run();
run("test");
println!("\tpassed");
}