Introduce new infrastructure for ioctls

1. Introduce the new infrastructure for ioctl support
2. Refactor the old ioctls to use the new infrastructure
3. Implement builtin ioctls (e.g., TIOCGWINSZ and TIOCSWINSZ for stdout)
4. Implement non-builtin, driver-specific ioctls (e.g., ioctls for /dev/sgx)
This commit is contained in:
Tate, Hongliang Tian 2019-11-14 15:23:08 +00:00
parent 1024360b8c
commit 9c4391b32d
13 changed files with 598 additions and 32 deletions

@ -0,0 +1,34 @@
use super::*;
#[derive(Debug)]
pub struct DevSgx;
const SGX_MAGIC_CHAR: u8 = 's' as u8;
/// Ioctl to check if EDMM (Enclave Dynamic Memory Management) is supported
const SGX_CMD_NUM_IS_EDMM_SUPPORTED: u32 =
StructuredIoctlNum::new::<i32>(0, SGX_MAGIC_CHAR, StructuredIoctlArgType::Output).as_u32();
impl File for DevSgx {
fn ioctl(&self, cmd: &mut IoctlCmd) -> Result<()> {
let nonbuiltin_cmd = match cmd {
IoctlCmd::NonBuiltin(nonbuiltin_cmd) => nonbuiltin_cmd,
_ => return_errno!(EINVAL, "unknown ioctl cmd for /dev/sgx"),
};
let cmd_num = nonbuiltin_cmd.cmd_num().as_u32();
match cmd_num {
SGX_CMD_NUM_IS_EDMM_SUPPORTED => {
let arg = nonbuiltin_cmd.arg_mut::<i32>()?;
*arg = 0; // no support for now
}
_ => {
return_errno!(EINVAL, "unknown ioctl cmd for /dev/sgx");
}
}
Ok(())
}
fn as_any(&self) -> &Any {
self
}
}

@ -67,6 +67,10 @@ pub trait File: Debug + Sync + Send + Any {
Ok(())
}
fn ioctl(&self, cmd: &mut IoctlCmd) -> Result<()> {
return_op_unsupported_error!("ioctl")
}
fn as_any(&self) -> &Any;
}
@ -387,6 +391,32 @@ impl File for StdoutFile {
Ok(())
}
fn ioctl(&self, cmd: &mut IoctlCmd) -> Result<()> {
let can_delegate_to_host = match cmd {
IoctlCmd::TIOCGWINSZ(_) => true,
IoctlCmd::TIOCSWINSZ(_) => true,
_ => false,
};
if !can_delegate_to_host {
return_errno!(EINVAL, "unknown ioctl cmd for stdout");
}
let cmd_bits = cmd.cmd_num() as c_int;
let cmd_arg_ptr = cmd.arg_ptr() as *const c_int;
let host_stdout_fd = {
use std::os::unix::io::AsRawFd;
self.inner.as_raw_fd() as i32
};
try_libc!(libc::ocall::ioctl_arg1(
host_stdout_fd,
cmd_bits,
cmd_arg_ptr
));
cmd.validate_arg_val()?;
Ok(())
}
fn as_any(&self) -> &Any {
self
}

@ -0,0 +1,12 @@
//! Built-in ioctls.
use super::*;
#[derive(Debug)]
#[repr(C)]
pub struct WinSize {
pub ws_row: u16,
pub ws_col: u16,
pub ws_xpixel: u16,
pub ws_ypixel: u16,
}

@ -0,0 +1,167 @@
//! Macros to implement `BuiltinIoctlNum` and `IoctlCmd` given a list of ioctl
//! names, numbers, and argument types.
/// Implement `BuiltinIoctlNum` and `IoctlCmd`.
#[macro_export]
macro_rules! impl_ioctl_nums_and_cmds {
($( $ioctl_name: ident => ( $ioctl_num: expr, $($ioctl_type_tt: tt)* ) ),+,) => {
// Implement BuiltinIoctlNum given ioctl names and their numbers
impl_builtin_ioctl_nums! {
$(
$ioctl_name => ( $ioctl_num, has_arg!($($ioctl_type_tt)*) ),
)*
}
// Implement IoctlCmd given ioctl names and their argument types
impl_ioctl_cmds! {
$(
$ioctl_name => ( $($ioctl_type_tt)*),
)*
}
}
}
////////////////////////////////////////////////////////////////////////////////
// BuiltinIoctlNum
////////////////////////////////////////////////////////////////////////////////
macro_rules! impl_builtin_ioctl_nums {
($($ioctl_name: ident => ($ioctl_num: expr, $ioctl_has_arg: expr)),+,) => {
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum BuiltinIoctlNum {
$(
$ioctl_name = $ioctl_num,
)*
}
impl BuiltinIoctlNum {
pub fn from_u32(raw_cmd_num: u32) -> Option<BuiltinIoctlNum> {
let cmd_num = match raw_cmd_num {
$(
$ioctl_num => BuiltinIoctlNum::$ioctl_name,
)*
_ => return None,
};
Some(cmd_num)
}
pub fn require_arg(&self) -> bool {
match self {
$(
BuiltinIoctlNum::$ioctl_name => $ioctl_has_arg,
)*
}
}
}
}
}
////////////////////////////////////////////////////////////////////////////////
// IoctlCmd
////////////////////////////////////////////////////////////////////////////////
macro_rules! impl_ioctl_cmds {
($( $ioctl_name: ident => ( $($ioctl_type_tt: tt)* ) ),+,) => {
#[derive(Debug)]
pub enum IoctlCmd<'a> {
$(
$ioctl_name( get_arg_type!($($ioctl_type_tt)*) ),
)*
NonBuiltin(NonBuiltinIoctlCmd<'a>),
}
impl<'a> IoctlCmd<'a> {
pub unsafe fn new(cmd_num: u32, arg_ptr: *mut u8) -> Result<IoctlCmd<'a>> {
if let Some(builtin_cmd_num) = BuiltinIoctlNum::from_u32(cmd_num) {
unsafe { Self::new_builtin_cmd(builtin_cmd_num, arg_ptr) }
} else {
unsafe { Self::new_nonbuiltin_cmd(cmd_num, arg_ptr) }
}
}
unsafe fn new_builtin_cmd(cmd_num: BuiltinIoctlNum, arg_ptr: *mut u8) -> Result<IoctlCmd<'a>> {
if cmd_num.require_arg() && arg_ptr.is_null() {
return_errno!(EINVAL, "arg_ptr cannot be null");
}
// Note that we do allow the caller to give an non-enull arg even
// when the ioctl cmd does not take an arguement
let cmd = match cmd_num {
$(
BuiltinIoctlNum::$ioctl_name => {
let arg = get_arg!($($ioctl_type_tt)*, arg_ptr);
IoctlCmd::$ioctl_name(arg)
}
)*
};
Ok(cmd)
}
unsafe fn new_nonbuiltin_cmd(cmd_num: u32, arg_ptr: *mut u8) -> Result<IoctlCmd<'a>> {
let structured_cmd_num = StructuredIoctlNum::from_u32(cmd_num)?;
let inner_cmd = unsafe { NonBuiltinIoctlCmd::new(structured_cmd_num, arg_ptr)? };
Ok(IoctlCmd::NonBuiltin(inner_cmd))
}
pub fn arg_ptr(&self) -> *const u8 {
match self {
$(
IoctlCmd::$ioctl_name(arg_ref) => get_arg_ptr!($($ioctl_type_tt)*, arg_ref),
)*
IoctlCmd::NonBuiltin(inner) => inner.arg_ptr(),
}
}
pub fn cmd_num(&self) -> u32 {
match self {
$(
IoctlCmd::$ioctl_name(_) => BuiltinIoctlNum::$ioctl_name as u32,
)*
IoctlCmd::NonBuiltin(inner) => inner.cmd_num().as_u32(),
}
}
}
}
}
macro_rules! has_arg {
(()) => {
false
};
($($ioctl_type_tt: tt)*) => {
true
};
}
macro_rules! get_arg_type {
(()) => {
()
};
($($ioctl_type_tt: tt)*) => {
&'a $($ioctl_type_tt)*
};
}
macro_rules! get_arg {
((), $arg_ptr: ident) => {
()
};
(mut $type: ty, $arg_ptr: ident) => {
unsafe { &mut *($arg_ptr as *mut $type) }
};
($type: ty, $arg_ptr: ident) => {
unsafe { &*($arg_ptr as *const $type) }
};
}
macro_rules! get_arg_ptr {
((), $arg_ref: ident) => {
std::ptr::null() as *const u8
};
(mut $type: ty, $arg_ref: ident) => {
(*$arg_ref as *const $type) as *const u8
};
($type: ty, $arg_ref: ident) => {
(*$arg_ref as *const $type) as *const u8
};
}

@ -0,0 +1,65 @@
//! Define builtin ioctls and provide utilities for non-builtin ioctls.
//!
//! A builtin ioctl is defined as part of the OS kernel and is used by various
//! OS sub-system. In contrast, an non-builtin ioctl is specific to a device or
//! driver.
use super::*;
pub use self::builtin::WinSize;
pub use self::non_builtin::{NonBuiltinIoctlCmd, StructuredIoctlArgType, StructuredIoctlNum};
#[macro_use]
mod macros;
mod builtin;
mod non_builtin;
/// This is the centralized place to define built-in ioctls.
///
/// By giving the names, numbers, and argument types of built-in ioctls,
/// the macro below generates the corresponding code of `BuiltinIoctlNum` and
/// `IoctlCmd`.
///
/// To add a new built-in ioctl, just follow the convention as shown
/// by existing built-in ioctls.
impl_ioctl_nums_and_cmds! {
// Format:
// ioctl_name => (ioctl_num, ioctl_type_arg)
// Get window size
TIOCGWINSZ => (0x5413, mut WinSize),
// Set window size
TIOCSWINSZ => (0x5414, WinSize),
// If the given terminal was the controlling terminal of the calling process, give up this
// controlling terminal. If the process was session leader, then send SIGHUP and SIGCONT to
// the foreground process group and all processes in the current session lose their controlling
// terminal
TIOCNOTTY => (0x5422, ()),
// Get the number of bytes in the input buffer
FIONREAD => (0x541B, mut i32),
}
/// This is the centralized place to add sanity checks for the argument values
/// of built-in ioctls.
///
/// Sanity checks are mostly useful when the argument values are returned from
/// the untrusted host OS.
impl<'a> IoctlCmd<'a> {
pub fn validate_arg_val(&self) -> Result<()> {
match self {
IoctlCmd::TIOCGWINSZ(winsize_ref) => {
// ws_row and ws_col are not supposed to be zeros
if winsize_ref.ws_row == 0 || winsize_ref.ws_col == 0 {
return_errno!(EINVAL, "invalid data from host");
}
}
IoctlCmd::FIONREAD(nread_ref) => {
if (**nread_ref < 0) {
return_errno!(EINVAL, "invalid data from host");
}
}
_ => {}
}
Ok(())
}
}

@ -0,0 +1,176 @@
//! Non-builtin ioctls.
use super::*;
#[derive(Debug)]
pub struct NonBuiltinIoctlCmd<'a> {
cmd_num: StructuredIoctlNum,
arg_buf: Option<&'a mut [u8]>,
}
impl<'a> NonBuiltinIoctlCmd<'a> {
pub unsafe fn new(
cmd_num: StructuredIoctlNum,
arg_ptr: *mut u8,
) -> Result<NonBuiltinIoctlCmd<'a>> {
let arg_buf = if cmd_num.require_arg() {
if arg_ptr.is_null() {
return_errno!(EINVAL, "arg_ptr must be provided for the ioctl");
}
let arg_size = cmd_num.arg_size();
let arg_slice = unsafe { std::slice::from_raw_parts_mut::<'a>(arg_ptr, arg_size) };
Some(arg_slice)
} else {
None
};
Ok(NonBuiltinIoctlCmd { cmd_num, arg_buf })
}
pub fn cmd_num(&self) -> &StructuredIoctlNum {
&self.cmd_num
}
pub fn arg<T>(&self) -> Result<&T> {
if self.cmd_num.arg_type().can_be_input() == false {
return_errno!(EINVAL, "cannot get a constant argument");
}
if std::mem::size_of::<T>() != self.cmd_num.arg_size() {
return_errno!(
EINVAL,
"the size of target type does not match the given buf size"
);
}
let arg_ref = unsafe { &*(self.arg_buf.as_ref().unwrap().as_ptr() as *const T) };
Ok(arg_ref)
}
pub fn arg_mut<T>(&mut self) -> Result<&mut T> {
if self.cmd_num.arg_type().can_be_output() == false {
return_errno!(EINVAL, "cannot get a mutable argument");
}
if std::mem::size_of::<T>() != self.cmd_num.arg_size() {
return_errno!(
EINVAL,
"the size of target type does not match the given buf size"
);
}
let arg_mut = unsafe { &mut *(self.arg_buf.as_mut().unwrap().as_mut_ptr() as *mut T) };
Ok(arg_mut)
}
pub fn arg_ptr(&self) -> *const u8 {
self.arg_buf
.as_ref()
.map_or(std::ptr::null(), |arg_slice| arg_slice.as_ptr())
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct StructuredIoctlNum {
cmd_id: u8,
magic_char: u8,
arg_size: u16,
arg_type: StructuredIoctlArgType,
}
impl StructuredIoctlNum {
pub const fn new<T>(
cmd_id: u8,
magic_char: u8,
arg_type: StructuredIoctlArgType,
) -> StructuredIoctlNum {
// TODO: make sure the size of T is not too big
// assert!(std::mem::size_of::<T>() <= (std::u16::MAX as usize));
let arg_size = std::mem::size_of::<T>() as u16;
StructuredIoctlNum {
cmd_id,
magic_char,
arg_size,
arg_type,
}
}
pub fn from_u32(raw_cmd_num: u32) -> Result<StructuredIoctlNum> {
// bits: [0, 8)
let cmd_id = (raw_cmd_num >> 0) as u8;
// bits: [8, 16)
let magic_char = (raw_cmd_num >> 8) as u8;
// bits: [16, 30)
let arg_size = ((raw_cmd_num >> 16) as u16) & 0x3FFF_u16;
// bits: [30, 32)
let arg_type = {
let type_bits = ((raw_cmd_num) >> 30) as u8;
match type_bits {
0 => StructuredIoctlArgType::Void,
1 => StructuredIoctlArgType::Input,
2 => StructuredIoctlArgType::Output,
3 => StructuredIoctlArgType::InputOutput,
_ => unreachable!(),
}
};
if arg_type == StructuredIoctlArgType::Void {
if arg_size != 0 {
return_errno!(EINVAL, "invalid combination between type and size");
}
} else {
if arg_size == 0 {
return_errno!(EINVAL, "invalid combination between type and size");
}
}
Ok(StructuredIoctlNum {
cmd_id,
magic_char,
arg_size,
arg_type,
})
}
pub const fn as_u32(&self) -> u32 {
(self.cmd_id as u32)
| (self.magic_char as u32) << 8
| (self.arg_size as u32) << 16
| (self.arg_type as u32) << 30
}
pub fn require_arg(&self) -> bool {
self.arg_type != StructuredIoctlArgType::Void
}
pub fn cmd_id(&self) -> u8 {
self.cmd_id
}
pub fn magic_char(&self) -> u8 {
self.magic_char
}
pub fn arg_size(&self) -> usize {
self.arg_size as usize
}
pub fn arg_type(&self) -> StructuredIoctlArgType {
self.arg_type
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum StructuredIoctlArgType {
Void = 0,
Output = 1,
Input = 2,
InputOutput = 3,
}
impl StructuredIoctlArgType {
pub fn can_be_input(&self) -> bool {
*self == StructuredIoctlArgType::Input || *self == StructuredIoctlArgType::InputOutput
}
pub fn can_be_output(&self) -> bool {
*self == StructuredIoctlArgType::Output || *self == StructuredIoctlArgType::InputOutput
}
}

@ -9,12 +9,14 @@ use {process, std};
pub use self::access::{do_access, do_faccessat, AccessFlags, AccessModes, AT_FDCWD};
use self::dev_null::DevNull;
use self::dev_random::DevRandom;
use self::dev_sgx::DevSgx;
use self::dev_zero::DevZero;
pub use self::file::{File, FileRef, SgxFile, StdinFile, StdoutFile};
pub use self::file_table::{FileDesc, FileTable};
use self::inode_file::OpenOptions;
pub use self::inode_file::{INodeExt, INodeFile};
pub use self::io_multiplexing::*;
pub use self::ioctl::*;
pub use self::pipe::Pipe;
pub use self::root_inode::ROOT_INODE;
pub use self::socket_file::{AsSocket, SocketFile};
@ -26,12 +28,14 @@ use std::mem::uninitialized;
mod access;
mod dev_null;
mod dev_random;
mod dev_sgx;
mod dev_zero;
mod file;
mod file_table;
mod hostfs;
mod inode_file;
mod io_multiplexing;
mod ioctl;
mod pipe;
mod root_inode;
mod sgx_impl;
@ -221,6 +225,14 @@ pub fn do_close(fd: FileDesc) -> Result<()> {
Ok(())
}
pub fn do_ioctl(fd: FileDesc, cmd: &mut IoctlCmd) -> Result<()> {
info!("ioctl: fd: {}, cmd: {:?}", fd, cmd);
let current_ref = process::get_current();
let current_process = current_ref.lock().unwrap();
let file_ref = current_process.get_files().lock().unwrap().get(fd)?;
file_ref.ioctl(cmd)
}
pub fn do_pipe2(flags: u32) -> Result<[FileDesc; 2]> {
info!("pipe2: flags: {:#x}", flags);
let flags = OpenFlags::from_bits_truncate(flags);
@ -434,6 +446,9 @@ impl Process {
if path == "/dev/random" || path == "/dev/urandom" || path == "/dev/arandom" {
return Ok(Box::new(DevRandom));
}
if path == "/dev/sgx" {
return Ok(Box::new(DevSgx));
}
let inode = if flags.contains(OpenFlags::CREATE) {
let (dir_path, file_name) = split_path(&path);
let dir_inode = self.lookup_inode(dir_path)?;

@ -119,6 +119,15 @@ impl File for SocketFile {
})
}
fn ioctl(&self, cmd: &mut IoctlCmd) -> Result<()> {
let cmd_num = cmd.cmd_num() as c_int;
let cmd_arg_ptr = cmd.arg_ptr() as *const c_int;
try_libc!(libc::ocall::ioctl_arg1(self.fd(), cmd_num, cmd_arg_ptr));
// FIXME: add sanity checks for results returned for socket-related ioctls
cmd.validate_arg_val()?;
Ok(())
}
fn as_any(&self) -> &Any {
self
}

@ -78,6 +78,11 @@ impl File for UnixSocketFile {
})
}
fn ioctl(&self, cmd: &mut IoctlCmd) -> Result<()> {
let mut inner = self.inner.lock().unwrap();
inner.ioctl(cmd)
}
fn as_any(&self) -> &Any {
self
}
@ -118,11 +123,6 @@ impl UnixSocketFile {
let mut inner = self.inner.lock().unwrap();
inner.poll()
}
pub fn ioctl(&self, cmd: c_int, argp: *mut c_int) -> Result<()> {
let mut inner = self.inner.lock().unwrap();
inner.ioctl(cmd, argp)
}
}
impl Debug for UnixSocketFile {
@ -231,18 +231,19 @@ impl UnixSocket {
Ok((r, w, false))
}
pub fn ioctl(&self, cmd: c_int, argp: *mut c_int) -> Result<()> {
const FIONREAD: c_int = 0x541B; // Get the number of bytes to read
if cmd == FIONREAD {
let bytes_to_read = self.channel()?.reader.bytes_to_read();
unsafe {
argp.write(bytes_to_read as c_int);
pub fn ioctl(&self, cmd: &mut IoctlCmd) -> Result<()> {
match cmd {
IoctlCmd::FIONREAD(arg) => {
let bytes_to_read = self
.channel()?
.reader
.bytes_to_read()
.min(std::i32::MAX as usize) as i32;
**arg = bytes_to_read;
}
_ => return_errno!(EINVAL, "unknown ioctl cmd for unix socket"),
}
Ok(())
} else {
warn!("ioctl for unix socket is unimplemented");
return_errno!(ENOSYS, "ioctl for unix socket is unimplemented")
}
}
fn channel(&self) -> Result<&Channel> {

@ -106,7 +106,7 @@ pub extern "C" fn dispatch_syscall(
arg3 as usize,
),
SYS_FCNTL => do_fcntl(arg0 as FileDesc, arg1 as u32, arg2 as u64),
SYS_IOCTL => do_ioctl(arg0 as FileDesc, arg1 as c_int, arg2 as *mut c_int),
SYS_IOCTL => do_ioctl(arg0 as FileDesc, arg1 as u32, arg2 as *mut u8),
// IO multiplexing
SYS_SELECT => do_select(
@ -962,22 +962,16 @@ fn do_fcntl(fd: FileDesc, cmd: u32, arg: u64) -> Result<isize> {
fs::do_fcntl(fd, &cmd)
}
fn do_ioctl(fd: FileDesc, cmd: c_int, argp: *mut c_int) -> Result<isize> {
fn do_ioctl(fd: FileDesc, cmd: u32, argp: *mut u8) -> Result<isize> {
info!("ioctl: fd: {}, cmd: {}, argp: {:?}", fd, cmd, argp);
let current_ref = process::get_current();
let mut proc = current_ref.lock().unwrap();
let file_ref = proc.get_files().lock().unwrap().get(fd as FileDesc)?;
if let Ok(socket) = file_ref.as_socket() {
let ret = try_libc!(libc::ocall::ioctl_arg1(socket.fd(), cmd, argp));
Ok(ret as isize)
} else if let Ok(unix_socket) = file_ref.as_unix_socket() {
// TODO: check argp
unix_socket.ioctl(cmd, argp)?;
Ok(0)
} else {
warn!("ioctl is unimplemented");
return_errno!(ENOSYS, "ioctl is unimplemented")
let mut ioctl_cmd = unsafe {
if argp.is_null() == false {
check_mut_ptr(argp)?;
}
IoctlCmd::new(cmd, argp)?
};
fs::do_ioctl(fd, &mut ioctl_cmd)?;
Ok(0)
}
fn do_arch_prctl(code: u32, addr: *mut usize) -> Result<isize> {

@ -7,7 +7,8 @@ TEST_DEPS := dev_null client
# Tests: need to be compiled and run by test-% target
TESTS := empty env hello_world malloc mmap file fs_perms getpid spawn sched pipe time \
truncate readdir mkdir link tls pthread uname rlimit server \
server_epoll unix_socket cout hostfs cpuid rdtsc device sleep exit_group
server_epoll unix_socket cout hostfs cpuid rdtsc device sleep exit_group \
ioctl
# Benchmarks: need to be compiled and run by bench-% target
BENCHES := spawn_and_exit_latency pipe_throughput unix_socket_throughput

5
test/ioctl/Makefile Normal file

@ -0,0 +1,5 @@
include ../test_common.mk
EXTRA_C_FLAGS :=
EXTRA_LINK_FLAGS :=
BIN_ARGS :=

57
test/ioctl/main.c Normal file

@ -0,0 +1,57 @@
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <termios.h>
#include <unistd.h>
#include "test.h"
// ============================================================================
// Test cases for TTY ioctl
// ============================================================================
int test_tty_ioctl_TIOCGWINSZ(void) {
struct winsize winsize;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &winsize) < 0) {
throw_error("failed to ioctl TIOCGWINSZ");
}
return 0;
}
// ============================================================================
// Test cases for SGX ioctl
// ============================================================================
#define SGXIOC_IS_EDDM_SUPPORTED _IOR('s', 0, int)
int test_sgx_ioctl_SGXIOC_IS_EDDM_SUPPORTED(void) {
int sgx_fd;
if ((sgx_fd = open("/dev/sgx", O_RDONLY)) < 0) {
throw_error("failed to open /dev/sgx ");
}
int is_edmm_supported = 0;
if (ioctl(sgx_fd, SGXIOC_IS_EDDM_SUPPORTED, &is_edmm_supported) < 0) {
throw_error("failed to ioctl /dev/sgx");
}
if (is_edmm_supported != 0) {
throw_error("SGX EDMM supported are not expected to be enabled");
}
close(sgx_fd);
return 0;
}
// ============================================================================
// Test suite
// ============================================================================
static test_case_t test_cases[] = {
TEST_CASE(test_tty_ioctl_TIOCGWINSZ),
TEST_CASE(test_sgx_ioctl_SGXIOC_IS_EDDM_SUPPORTED)
};
int main() {
return test_suite_run(test_cases, ARRAY_SIZE(test_cases));
}