Skip to content

[WIP?] QemuSugar Snapshot support (+ OptionalModule) #3341

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

jma-qb
Copy link
Contributor

@jma-qb jma-qb commented Jul 16, 2025

@tokatoka @rmalmain

Description

Trying to add an optional support for snapshot in Qemu Sugar. @domenukk suggested a IfModule like the IfStage to avoid duplicating a lot of code and pleasing Rust's type system.
I went with a simpler version taking a bool rather than a closure as we probably don't want to dynamically change the list of loaded modules.

I had to fix the SnapshotModule because it couldn't retrieve the module wrapped in the OptionalModule but:

  1. it feels ugly
  2. I want to also allow using the AddressFilter, it will require updating it with the same type of logic
  3. I need to document the module and 2) if we want to include more modules in the future
if let Some(h) = emulator_modules.get_mut::<SnapshotModule>() {
    h.access(addr, SIZE);
} else {
    let snap = emulator_modules.get_mut::<OptionalModule<SnapshotModule>>().unwrap();
    let h = snap.get_inner_module_mut();
    h.access(addr, SIZE);
}

I'm open to any suggestion to improve this and how/where I should document it.

Checklist

  • [] I have run ./scripts/precommit.sh and addressed all comments

I ran the precommit script but couldn't address all comments.
In this example clippy wants me to merge the two if because it can't see the arm specific code.

        if let Some(h) = emulator_modules.get_mut::<Self>() {
            if !h.must_instrument(pc) {
                return None;
            }

            #[cfg(cpu_target = "arm")]
            h.cs.set_mode(if pc & 1 == 1 {
                capstone::arch::arm::ArchMode::Thumb.into()
            } else {
                capstone::arch::arm::ArchMode::Arm.into()
            })
            .unwrap();
        }

Also when targeting arm so it doesn't complain about this I got a lot of errors

error[E0432]: unresolved import `crate::SYS_execve`
  --> crates/libafl_qemu/src/modules/usermode/injections.rs:22:5
   |
22 | use crate::SYS_execve;
   |     ^^^^^^^^^^^^^^^^^ no `SYS_execve` in the root

error[E0432]: unresolved import `crate::SYS_fstatat64`
  --> crates/libafl_qemu/src/modules/usermode/snapshot.rs:10:5
   |
10 | use crate::SYS_fstatat64;
   |     ^^^^^^^^^^^^^^^^^^^^ no `SYS_fstatat64` in the root

error[E0432]: unresolved import `crate::SYS_mmap2`
  --> crates/libafl_qemu/src/modules/usermode/snapshot.rs:14:5
   |
14 | use crate::SYS_mmap2;
   |     ^^^^^^^^^^^^^^^^ no `SYS_mmap2` in the root

@jma-qb
Copy link
Contributor Author

jma-qb commented Jul 16, 2025

Forgot to note. It looks like it works when trying from the python bindings on the python_qemu fuzzer's target. The perfs are slightly decreased but I believe this is because the target is so small that the snapshot reset is heavy compared to just reseting a few registers and running again.

Also taking any feedback on how I should test it further.

@tokatoka
Copy link
Member

imo this should be named as IfModule,
and in addition, can you make this OptionalModule to take an closure: CB instead of enabled: bool just like IfStage, then CB returns true or false to say if the module should be enabled ?

@tokatoka
Copy link
Member

@domenukk what do you think?
I think making it consistent with IfStage is better

@domenukk
Copy link
Member

Yes would be good if possible / easy to do

@tokatoka
Copy link
Member

wait..
I start to see the problem.
yes, indeed it is ugly, and i don't this is good.
IfStage works because there's no code that looks up that stage, but for module it's a different story.

I have to understand what this optional is for, is it because you want to make it configurable at runtime to use the snapshot module in libafl_sugar?

@tokatoka
Copy link
Member

tokatoka commented Jul 16, 2025

if that's your goal, how about doing it like

(inside libafl_sugar)
if opt.use_snapshot {
 let module = tuple!(x, y, snapshot);
 // continue other things
}
else {
 let module = tuple!(x, y);
 // continue other things
}

yes, you a lot of duplicate code in this way, but to me it's more reasonable to handle all this in the user's code (libafl_sugar's code)
i don't think complicating the library side code (libafl_qemu) is a good idea.

@jma-qb
Copy link
Contributor Author

jma-qb commented Jul 16, 2025

if that's your goal, how about doing it like

(inside libafl_sugar)
if opt.use_snapshot {
 let module = tuple!(x, y, snapshot);
 // continue other things
}
else {
 let module = tuple!(x, y);
 // continue other things
}

yes, you a lot of duplicate code in this way, but to me it's more reasonable to handle all this in the user's code (libafl_sugar's code) i don't think complicating the library side code (libafl_qemu) is a good idea.

You're right that's the goal.
The thing is that if you stack more options it becomes a huge mess. There's already duplicated code because of the cmplog feature, I'd like to add Snapshot right now and probably AddressFilter. Later we could add ASAN or other things. It will go exponential.

@jma-qb
Copy link
Contributor Author

jma-qb commented Jul 16, 2025

imo this should be named as IfModule, and in addition, can you make this OptionalModule to take an closure: CB instead of enabled: bool just like IfStage, then CB returns true or false to say if the module should be enabled ?

Regarding the name, I chose OptionalModule to differentiate with the IfStage which uses a closure.
I didn't use a closure because:

  1. you probably don't want to change the list of modules while fuzzing
  2. there's probably an overhead to call a closure every time you use the module. Especially with modules like the Asan one.

I can do it but it'll require a few changes. Like dropping the Debug bound for the EmulatorModule Trait (doesn't seem important) and might complicate things a bit but it's doable.

Anyway, if you feel it adds too much complexity to the qemu code I'm happy to drop this.

use crate::modules::{EmulatorModule, EmulatorModuleTuple};

#[derive(Debug)]
pub struct OptionalModule<MD> {
Copy link
Member

Choose a reason for hiding this comment

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

If we don't use a closure, this should probably be an Enum

Copy link
Member

Choose a reason for hiding this comment

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

i agree with that.
the way it's written, it looks like the module could be enabled after it has been initialized, which would definitely not work in most cases.

with an enum like

pub enum OptionalModule<MD> {
    Disabled,
    Enabled(MD),
}

it would make the intention clear.

@domenukk
Copy link
Member

domenukk commented Jul 16, 2025

A closure that checks a single flag should optimize reasonably well, don't you think? Probably emits the same code in the end

@@ -842,8 +845,15 @@ pub fn trace_write_snapshot<ET, I, S, const SIZE: usize>(
I: Unpin,
S: Unpin,
{
let h = emulator_modules.get_mut::<SnapshotModule>().unwrap();
h.access(addr, SIZE);
if let Some(h) = emulator_modules.get_mut::<SnapshotModule>() {
Copy link
Member

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 remove the unwrap here.
if for some reason we end up there and there is no snapshot module, it's actually a bug.
the comment applies to all the other similar changes.

Copy link
Contributor Author

@jma-qb jma-qb Jul 25, 2025

Choose a reason for hiding this comment

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

If the SnapshotModule is wrapped in an OptionalModule this call will fail, so this is done so we have a chance to retrieve the module from inside the OptionalModule.
So the unwrap is kept in the else branch.

Copy link
Member

@rmalmain rmalmain Jul 25, 2025

Choose a reason for hiding this comment

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

yeah i see, i'm not sure the current solution is good.
it forces to do code duplication.
maybe we can replace get_mut with a new method, calling to a method implemented by the trait EmulatorModule, which can return an Option<&mut Self>? it's a bit heavy but at least we can keep the same semantic and avoid duplicating code in callbacks

@@ -81,6 +81,9 @@ pub struct ForkserverBytesCoverageSugar<'a> {
/// Fuzz `iterations` number of times, instead of indefinitely; implies use of `fuzz_loop_for`
#[builder(default = None)]
iterations: Option<u64>,
/// Disable redirection of stdout to /dev/null on unix build targets
Copy link
Member

Choose a reason for hiding this comment

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

why not just use a bool here?

use crate::modules::{EmulatorModule, EmulatorModuleTuple};

#[derive(Debug)]
pub struct OptionalModule<MD> {
Copy link
Member

Choose a reason for hiding this comment

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

i agree with that.
the way it's written, it looks like the module could be enabled after it has been initialized, which would definitely not work in most cases.

with an enum like

pub enum OptionalModule<MD> {
    Disabled,
    Enabled(MD),
}

it would make the intention clear.

@@ -88,6 +91,8 @@ where
/// Disable redirection of stdout to /dev/null on unix build targets
#[builder(default = None)]
Copy link
Member

Choose a reason for hiding this comment

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

i guess this is why you used Option<bool>.
we can just use bool here as well instead, same for next field

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Most of it uses Option<bool> so I mimicked it. Maybe it is done this way because of the python bindings, allowing to have optional parameters. I'm not sure this is the explanation 🤷

Copy link
Member

Choose a reason for hiding this comment

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

we should be able to default to false or smth like that.
i don't think it would break anything, we can try

@rmalmain
Copy link
Member

if that's your goal, how about doing it like

(inside libafl_sugar)
if opt.use_snapshot {
 let module = tuple!(x, y, snapshot);
 // continue other things
}
else {
 let module = tuple!(x, y);
 // continue other things
}

yes, you a lot of duplicate code in this way, but to me it's more reasonable to handle all this in the user's code (libafl_sugar's code) i don't think complicating the library side code (libafl_qemu) is a good idea.

it's fine the way @jma-qb did it, it's adding a file without touching the internal logic.
cf the code of qemu_launcher, we tried to use fully generic code and it's unreadable:

if self.options.rerun_input.is_some() {

in term of performance, the extra check would only be noticeable in pre_exec / post_exec, which should be fine in most cases.

@tokatoka
Copy link
Member

tokatoka commented Jul 25, 2025

in term of performance, the extra check would only be noticeable in pre_exec / post_exec, which should be fine in most cases.

but for example in trace_write_snapshot doesn't this bring you additional hashmap lookup everytime qemu sees a write? you sure it really is ok?

@tokatoka
Copy link
Member

https://github.com/AFLplusplus/LibAFL/blob/poc/crates/libafl_sugar/src/qemu.rs

I have a solution using macros 😜

@rmalmain
Copy link
Member

in term of performance, the extra check would only be noticeable in pre_exec / post_exec, which should be fine in most cases.

but for example in trace_write_snapshot doesn't this bring you additional hashmap lookup everytime qemu sees a write? you sure it really is ok?

which hashmap lookup? if you use snapshot in the same way as before, it will replace let h = ... .unwrap() by if let Some(h) = ..., so it should be equivalent in term of control flow / memory access. in release mode, this should be optimized out since the compiler knows exactly the content of the tuple list, so if the compiler is not too stupid this remove the dead code and just give h without any condition check when snapshot is present.

@rmalmain
Copy link
Member

https://github.com/AFLplusplus/LibAFL/blob/poc/crates/libafl_sugar/src/qemu.rs

I have a solution using macros 😜

yeah i saw, i don't want to maintain this kind of macro, they tend to become unreadable.
this part (https://github.com/AFLplusplus/LibAFL/blob/poc/crates/libafl_sugar/src/qemu.rs#L356) grows exponentially with the number of booleans to check btw. so if you have let's say 4 optional modules in the future, you have to check for 16 couples of booleans in the match

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.

4 participants