Skip to content

Conversation

@Avgor46
Copy link

@Avgor46 Avgor46 commented Dec 26, 2025

Hi!

We have implemented custom, user-parametrized syscall generation. Providing arguments to system calls allows them to complete successfully and makes the workload more “real.” We have also added initial support for generating io_uring subsystem events.

@erthalion
Copy link
Collaborator

@Avgor46 wow, thanks. It will take some time for me to review, stay tuned.


impl Drop for ConnectCall {
fn drop(&mut self) {
unsafe { File::from_raw_fd(self.sockfd as i32) };
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Avgor46 can you clarify why it's done this way? I assume it will make the file descriptor go away when leaving the scope, but why not just close it explicitly?

Copy link
Contributor

@Molter73 Molter73 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this PR, it is quite extensive!

I'm leaving a few comments, I've gone through about half the syscalls. Still need to go through the io_uring stuff and the toml files, hopefully I can do that tomorrow morning.

Comment on lines +68 to +79
let s = String::deserialize(deserializer)?;
let mut map = HashMap::new();
for arg in s.split(',').filter(|x| !x.is_empty()) {
let parts = arg.split_once('=');
let Some((key, value)) = parts else {
return Err(serde::de::Error::custom(
"invalid syscall arguments format",
));
};
map.insert(key.to_string(), value.to_string());
}
Ok(map)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might make the code a bit too dense, but how about doing this all in a single functional operation?

Suggested change
let s = String::deserialize(deserializer)?;
let mut map = HashMap::new();
for arg in s.split(',').filter(|x| !x.is_empty()) {
let parts = arg.split_once('=');
let Some((key, value)) = parts else {
return Err(serde::de::Error::custom(
"invalid syscall arguments format",
));
};
map.insert(key.to_string(), value.to_string());
}
Ok(map)
String::deserialize(deserializer)?
.split(',')
.filter(|x| !x.is_empty())
.map(|arg| match arg.split_once('=') {
Some((key, value)) => Ok((key.to_string(), value.to_string())),
None => Err(serde::de::Error::custom(
"invalid syscall arguments format",
)),
})
.collect::<Result<HashMap<_, _>, _>>()

let parts = arg.split_once('=');
let Some((key, value)) = parts else {
return Err(serde::de::Error::custom(
"invalid syscall arguments format",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to consider adding the offending argument to this string to help debug the error.

name = "berserker"
version = "0.1.0"
edition = "2021"
edition = "2024"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a huge deal but do we need to update the edition? Is this a requirement from one of the new dependencies added?

let rng = thread_rng();
let mut rng_iter = rng.sample_iter(exp);

let syscall = Sysno::from(*syscall_nr);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] This same operation is being done in line 87, we can probably move this line above that and re-use syscall, since it seems Sysno is represented as an i32, so it shouldn't be a big deal to copy it.


impl SysCaller for ListenCall {
fn init(&mut self) -> Result<usize, Errno> {
// Create socket directly instead of calling socket_call.call()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Create socket directly instead of calling socket_call.call()
// Create socket directly instead of calling socket_call.call()
// since that would immediately close the fd.

} else {
default
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be a bit of overkill, but do we maybe want to have a wrapper type around HashMap<String, String> and make this the get method for that type? Something like this:

struct SyscallArgs(HashMap<String, String>);

impl SyscallArgs {
    fn get<T: FromStr>(&self, name: &str, default: T) -> T {
        if let Some(arg) = self.0.get(name) {
            arg.parse().unwrap_or(default)
        } else {
            default
        }
    }
}

It would make it clearer at call sites IMO, going from:

    pub fn new(chmod_args: &HashMap<String, String>) -> Self {
        let pathname =
            get_argument(chmod_args, "pathname", CString::new("/tmp").unwrap());
         // ...

To something like:

    pub fn new(chmod_args: &SyscallArgs) -> Self {
        let pathname =
            chmod_args.get("pathname", CString::new("/tmp").unwrap());

}

impl ChmodCall {
pub fn new(chmod_args: &HashMap<String, String>) -> Self {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] I find it a bit redundant to have <syscall>_args on every new method when we are already in the syscall caller implementation block, any reason to not name the argument args directly?

Comment on lines +9 to +24
#[derive(Debug)]
pub struct IoctlCall {
pub fd: usize,
pub op: usize,
pub argp: usize,
}

impl IoctlCall {
pub fn new(_: &HashMap<String, String>) -> Self {
let fd = 0; // Will be initialized in init()
let op = 0; // Default value, can be overridden if needed
let argp = 0; // Default value, can be overridden if needed

Self { fd, op, argp }
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit]

Suggested change
#[derive(Debug)]
pub struct IoctlCall {
pub fd: usize,
pub op: usize,
pub argp: usize,
}
impl IoctlCall {
pub fn new(_: &HashMap<String, String>) -> Self {
let fd = 0; // Will be initialized in init()
let op = 0; // Default value, can be overridden if needed
let argp = 0; // Default value, can be overridden if needed
Self { fd, op, argp }
}
}
#[derive(Debug, Default)]
pub struct IoctlCall {
pub fd: usize,
pub op: usize,
pub argp: usize,
}
impl IoctlCall {
pub fn new(_: &HashMap<String, String>) -> Self {
// Zero initialize all fields, fd will be initialized in `Syscaller::init`.
// All other fields can be overridden as needed
Default::default()
}
}

}

impl SysCaller for MountCall {
fn call(&self) -> Result<usize, Errno> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know mount is probably only used with bogus inputs but, just in case it is not, do we want to umount if we successfully mounted the volume?

),
}

// Otherwise calculate waiting time
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we missing a tight loop check here?

Comment on lines +159 to +169
fn get_argument<T: FromStr>(
args: &HashMap<String, String>,
name: &str,
default: T,
) -> T {
if let Some(arg) = args.get(name) {
arg.parse().unwrap_or(default)
} else {
default
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar comment about the SyscallArgs wrapper type here, maybe a IOUringArgs type? Or maybe an ArgsMap that we can use for both syscalls and io_uring? WDYT?

Comment on lines +42 to +44
ring.submission()
.push(&self.openat)
.expect("submission queue is full");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can be a bit more graceful here, unless we actually want to fully crash, but we probably don't.

Suggested change
ring.submission()
.push(&self.openat)
.expect("submission queue is full");
if ring.submission().push(&self.openat).is_err() {
return Err(Errno::ENOSPC);
}

Comment on lines +46 to +48
ring.submit_and_wait(1).expect("submission failed");

let cqe = ring.completion().next().expect("completion queue is empty");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar comment about not panicking here, though these might be a bit harder.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this same error handling is done on other io_uring operations, we should change those as well of we change them here.

# How often to invoke a syscall. Parameter of exponential distribution.
arrival_rate = 10.0
# Syscall number to invoke.
syscall_nr = 162
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not something to do here but, in the interest of being a bit more user friendly, we might want to allow specifying the syscall by name in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants