Skip to content

Commit c606ac1

Browse files
authored
Add Lua mutator, a mutator to write mutations in Lua (#3220)
* Add Lua mutator, a mutator using Lua * lua? * fix name * move lints about * Testing more fix * More fix? * macros? * macros * more fmt * fix doc?
1 parent f901c20 commit c606ac1

File tree

3 files changed

+367
-4
lines changed

3 files changed

+367
-4
lines changed

libafl/Cargo.toml

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ features = ["document-features"]
2626
all-features = true
2727
rustc-args = ["--cfg", "docsrs"]
2828

29+
[lints]
30+
workspace = true
31+
2932
[features]
3033
default = [
3134
"std",
@@ -187,11 +190,15 @@ llmp_small_maps = [
187190
"libafl_bolts/llmp_small_maps",
188191
] # reduces initial map size for llmp
189192

190-
## Grammar mutator. Requires nightly.
193+
## Grammar mutator.
191194
nautilus = ["std", "serde_json/std", "rand_trait", "regex-syntax", "regex"]
192195

196+
## Python grammar support for nautilus
193197
nautilus_py = ["nautilus", "dep:pyo3"]
194198

199+
## Lua Mutator support (mutators implemented in Lua)
200+
lua_mutator = ["mlua"]
201+
195202
## Use the best SIMD implementation by our benchmark
196203
simd = ["libafl_bolts/simd"]
197204

@@ -298,9 +305,13 @@ document-features = { workspace = true, optional = true }
298305
clap = { workspace = true, optional = true }
299306
num_enum = { workspace = true, optional = true }
300307
fastbloom = { workspace = true, optional = true }
301-
302-
[lints]
303-
workspace = true
308+
# For Lua Mutators
309+
# TODO: macros is not needed/ a temporary fix for docsrs, see <https://github.com/mlua-rs/mlua/issues/579>
310+
mlua = { version = "0.10.3", features = [
311+
"lua54",
312+
"vendored",
313+
"macros",
314+
], optional = true }
304315

305316
[target.'cfg(unix)'.dependencies]
306317
libc = { workspace = true } # For (*nix) libc

libafl/src/mutators/lua.rs

Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
//! This module implements the [`LuaMutator`], where each mutation drops into a Lua VM to mutate bytes in a target-specific way.
2+
#[cfg(feature = "std")]
3+
use alloc::boxed::Box;
4+
use alloc::{
5+
borrow::Cow,
6+
rc::Rc,
7+
string::{String, ToString},
8+
vec::Vec,
9+
};
10+
use core::cell::Cell;
11+
#[cfg(feature = "std")]
12+
use std::{fs, path::Path};
13+
14+
use libafl_bolts::{
15+
Error, Named,
16+
rands::{Rand, StdRand},
17+
};
18+
use mlua::{Function, HookTriggers, Lua, VmState, prelude::LuaError};
19+
20+
use super::MutationResult;
21+
use crate::{
22+
HasMetadata,
23+
corpus::CorpusId,
24+
inputs::{HasMutatorBytes, ResizableMutator},
25+
mutators::Mutator,
26+
state::{HasMaxSize, HasRand},
27+
};
28+
29+
// Note: Loops including, and above, ~400 instructions never trigger in LuaJIT due to jitting.
30+
/// How many steps to take before timeout-ing from a mutator
31+
const DEFAULT_TIMEOUT_STEPS: u32 = 1_000_000;
32+
33+
/// Converts a [`LuaError`] to a libafl-native [`Error`]
34+
#[allow(clippy::needless_pass_by_value)] // We need this signature for `.map_error`
35+
fn convert_error(err: LuaError) -> Error {
36+
Error::illegal_argument(format!("Lua execution returned error: {err:?}"))
37+
}
38+
39+
/// Create an initial Rng with a fixed state..
40+
struct RandState(StdRand);
41+
impl HasRand for RandState {
42+
type Rand = StdRand;
43+
fn rand(&self) -> &Self::Rand {
44+
&self.0
45+
}
46+
fn rand_mut(&mut self) -> &mut Self::Rand {
47+
&mut self.0
48+
}
49+
}
50+
51+
/// Load the list of lua mutators from a given folder
52+
#[cfg(all(feature = "lua_mutator", feature = "std"))]
53+
pub fn load_lua_mutations<
54+
I: HasMutatorBytes + ResizableMutator<u8>,
55+
S: HasMetadata + HasRand + HasMaxSize,
56+
>(
57+
lua_path: &Path,
58+
) -> Result<Vec<Box<dyn Mutator<I, S>>>, Error> {
59+
let mut mutations: Vec<Box<dyn Mutator<I, S>>> = vec![];
60+
let mut rand_state = RandState(StdRand::with_seed(1337));
61+
62+
let lua_dir = fs::read_dir(lua_path).unwrap();
63+
64+
for mutation in lua_dir {
65+
let mutation = mutation?;
66+
log::info!("Loading lua_mutator from {mutation:?}");
67+
let mutator =
68+
LuaMutator::eat_errors(&mut rand_state, &fs::read_to_string(mutation.path())?);
69+
if let Ok(mutator) = mutator {
70+
mutations.push(Box::new(mutator));
71+
} else {
72+
log::warn!("Mutator {mutation:?} did not run: {mutator:?}");
73+
}
74+
}
75+
Ok(mutations)
76+
}
77+
78+
/// Creates a new [`Lua`] VM, sets the seed using the provided rng state,
79+
/// creates a function from the provided string, and (optionally) executes it once.
80+
/// Return the function and a timeout tracker bool that you should set to `false` before running a function
81+
/// Since the VM keeps counting, this bool is needed to know that we started a new execution.
82+
/// So, in practice, the timeout / instruction counter has to trigger twice to exit execution.
83+
/// The `timeout_steps_min` are the minimum amount of steps until execution quits.
84+
/// In practice, the amount of steps might be up to `2x` that value.
85+
fn create_lua_fn<S: HasRand>(
86+
lua: &Lua,
87+
state: &mut S,
88+
mutator_lua_fn: &str,
89+
timeout_steps_min: Option<u32>,
90+
test: bool,
91+
) -> Result<(Function, Rc<Cell<bool>>), Error> {
92+
#[allow(clippy::cast_possible_truncation)] // we specifically want a u32
93+
let lua_seed = state.rand_mut().next() as u32;
94+
95+
let timeouted_once = Rc::new(Cell::new(true));
96+
let timeouted_once_cb = timeouted_once.clone();
97+
98+
// Seed
99+
lua.load(format!("math.randomseed({lua_seed})"))
100+
.exec()
101+
.map_err(convert_error)?;
102+
103+
// Set hook for timeout steps
104+
if let Some(timeout_steps_min) = timeout_steps_min {
105+
let hook_triggers = HookTriggers::new().every_nth_instruction(timeout_steps_min);
106+
107+
lua.set_hook(hook_triggers, move |_lua, _debug| {
108+
log::trace!("I'm here. Timeouted_once: {timeouted_once_cb:?}");
109+
if timeouted_once_cb.get() {
110+
Err(mlua::Error::RuntimeError(
111+
"Instruction limit reached!".to_string(),
112+
))
113+
} else {
114+
timeouted_once_cb.set(false);
115+
Ok(VmState::Continue)
116+
}
117+
});
118+
}
119+
120+
let func = mutator_lua_fn.to_string();
121+
let chunk = lua.load(&func);
122+
123+
let mutator: Function = chunk.eval().map_err(convert_error)?;
124+
125+
// Simple test that the mutator works
126+
if test {
127+
let bytes = vec![1_u8, 2, 3, 4, 5, 6, 7, 8, 9];
128+
drop(mutator.call::<Vec<u8>>((bytes,)).map_err(convert_error)?);
129+
}
130+
Ok((mutator, timeouted_once))
131+
}
132+
133+
/// Inserts a random token at a random position in the `Input`.
134+
pub struct LuaMutator {
135+
/// The Lua VM
136+
#[allow(dead_code)] // We need to keep a handle around.
137+
lua: Lua,
138+
/// The function string we loaded
139+
func: String,
140+
/// The actual lua function we can call
141+
mutator: Function,
142+
/// If we should get rid of errors
143+
eat_errors: bool,
144+
/// If this had an error
145+
errored: bool,
146+
/// If the timeout handler has been called at least once
147+
timeout_handler_called_once: Rc<Cell<bool>>,
148+
}
149+
150+
impl core::fmt::Debug for LuaMutator {
151+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
152+
f.debug_struct("LuaMutator")
153+
.field("func", &self.func)
154+
.field("mutator", &self.mutator)
155+
.field(
156+
"timeout_handler_called_once",
157+
&self.timeout_handler_called_once,
158+
)
159+
.field("eat_errors", &self.eat_errors)
160+
.field("errored", &self.errored)
161+
.finish_non_exhaustive()
162+
}
163+
}
164+
165+
impl LuaMutator {
166+
/// Creates a new lua mutator, will call the mutator with a random bytes sequence to make sure it's not crashing.
167+
/// Will block if the mutator is an endless loop!
168+
#[allow(unused)]
169+
pub fn new<S: HasRand>(state: &mut S, mutator_lua_fn: &str) -> Result<Self, Error> {
170+
let lua = Lua::new();
171+
let func = mutator_lua_fn.to_string();
172+
let (mutator, timeouted_once) = create_lua_fn(
173+
&lua,
174+
state,
175+
mutator_lua_fn,
176+
Some(DEFAULT_TIMEOUT_STEPS),
177+
true,
178+
)?;
179+
Ok(Self {
180+
lua,
181+
func,
182+
mutator,
183+
timeout_handler_called_once: timeouted_once,
184+
eat_errors: false,
185+
errored: false,
186+
})
187+
}
188+
189+
/// Creates a new lua mutator, will call the mutator with a random bytes sequence to make sure it's not crashing.
190+
/// Will block if the mutator is an endless loop!
191+
pub fn eat_errors<S: HasRand>(state: &mut S, mutator_lua_fn: &str) -> Result<Self, Error> {
192+
let lua = Lua::new();
193+
let func = mutator_lua_fn.to_string();
194+
let (mutator, timeouted_once) = create_lua_fn(
195+
&lua,
196+
state,
197+
mutator_lua_fn,
198+
Some(DEFAULT_TIMEOUT_STEPS),
199+
true,
200+
)?;
201+
Ok(Self {
202+
lua,
203+
func,
204+
mutator,
205+
timeout_handler_called_once: timeouted_once,
206+
eat_errors: true,
207+
errored: false,
208+
})
209+
}
210+
}
211+
212+
impl<I, S> Mutator<I, S> for LuaMutator
213+
where
214+
S: HasMetadata + HasRand + HasMaxSize,
215+
I: HasMutatorBytes + ResizableMutator<u8>,
216+
{
217+
fn mutate(&mut self, _state: &mut S, input: &mut I) -> Result<MutationResult, Error> {
218+
self.timeout_handler_called_once.set(false);
219+
let bytes = input.mutator_bytes().to_vec();
220+
let result = match self.mutator.call::<Vec<u8>>(bytes) {
221+
Err(err) => Err(Error::illegal_state(format!("Lua mutation failed: {err}"))),
222+
Ok(mutated) => {
223+
if mutated.eq(input.mutator_bytes()) {
224+
Ok(MutationResult::Skipped)
225+
} else {
226+
input.resize(mutated.len(), 0);
227+
input.mutator_bytes_mut().clone_from_slice(&mutated);
228+
Ok(MutationResult::Mutated)
229+
}
230+
}
231+
};
232+
if self.eat_errors {
233+
log::debug!("Mutation Errored: {}", &self.func);
234+
self.errored = true;
235+
if result.is_err() {
236+
Ok(MutationResult::Skipped)
237+
} else {
238+
result
239+
}
240+
} else {
241+
result
242+
}
243+
}
244+
245+
#[inline]
246+
fn post_exec(&mut self, _state: &mut S, _new_corpus_id: Option<CorpusId>) -> Result<(), Error> {
247+
Ok(())
248+
}
249+
}
250+
251+
impl Named for LuaMutator {
252+
fn name(&self) -> &Cow<'static, str> {
253+
&Cow::Borrowed("LuaMutator")
254+
}
255+
}
256+
257+
#[cfg(test)]
258+
mod tests {
259+
#[cfg(feature = "std")]
260+
use std::println;
261+
262+
use libafl_bolts::{Error, rands::StdRand, serdeany::SerdeAnyMap};
263+
264+
use crate::{
265+
HasMetadata,
266+
inputs::BytesInput,
267+
mutators::{MutationResult, Mutator, lua::LuaMutator},
268+
state::{HasMaxSize, HasRand},
269+
};
270+
271+
struct NopState(StdRand);
272+
impl HasRand for NopState {
273+
type Rand = StdRand;
274+
275+
fn rand(&self) -> &Self::Rand {
276+
&self.0
277+
}
278+
279+
fn rand_mut(&mut self) -> &mut Self::Rand {
280+
&mut self.0
281+
}
282+
}
283+
impl HasMaxSize for NopState {
284+
fn max_size(&self) -> usize {
285+
1337
286+
}
287+
288+
fn set_max_size(&mut self, _max_size: usize) {
289+
unimplemented!()
290+
}
291+
}
292+
impl HasMetadata for NopState {
293+
fn metadata_map(&self) -> &SerdeAnyMap {
294+
unimplemented!()
295+
}
296+
297+
fn metadata_map_mut(&mut self) -> &mut SerdeAnyMap {
298+
unimplemented!()
299+
}
300+
}
301+
302+
#[test]
303+
fn simple_test() {
304+
let mut state = NopState(StdRand::with_seed(1337));
305+
306+
let mut lua_mutator = LuaMutator::new(
307+
&mut state,
308+
r"function (bytes)
309+
for i, byte in ipairs(bytes) do
310+
if math.random() < 0.5 then
311+
bytes[i] = math.random(0, 255)
312+
end
313+
end
314+
return bytes
315+
end
316+
",
317+
)
318+
.unwrap();
319+
320+
let bytes = vec![0, 1, 2, 3, 4];
321+
let mut bytesinput = BytesInput::new(bytes);
322+
let mutation_result = lua_mutator.mutate(&mut state, &mut bytesinput).unwrap();
323+
assert!(matches!(mutation_result, MutationResult::Mutated));
324+
325+
#[cfg(feature = "std")]
326+
println!("MutationResult: {mutation_result:?}");
327+
}
328+
329+
#[test]
330+
fn test_timeout() {
331+
let mut state = NopState(StdRand::with_seed(1337));
332+
333+
assert!(
334+
matches!(
335+
LuaMutator::new(
336+
&mut state,
337+
r"function (bytes)
338+
while true do
339+
i = i + 1
340+
end
341+
end
342+
"
343+
),
344+
Err(Error::IllegalArgument(_, _))
345+
),
346+
"Expected endless loop to raise an 'IllegalArgument' error!"
347+
);
348+
}
349+
}

0 commit comments

Comments
 (0)