libafl_qemu_build/
lib.rs

1// #[rustversion::nightly]
2extern crate alloc;
3
4use core::hash::Hasher;
5use std::{
6    collections::hash_map,
7    env,
8    fs::{self, File},
9    io::{Read, Seek, SeekFrom, Write},
10    path::{Path, PathBuf},
11    process::Command,
12    sync::{LazyLock, Mutex},
13};
14
15//#[rustversion::nightly]
16//use regex::Regex;
17//#[rustversion::nightly]
18//use rustc_version::Version;
19use which::which;
20
21mod bindings;
22mod build;
23
24pub use build::build;
25
26#[rustversion::nightly]
27use crate::build::LIBAFL_QEMU_GIT_REV;
28
29const LLVM_VERSION_MAX: i32 = 33;
30
31static CARGO_RPATH: LazyLock<Mutex<Vec<String>>> = LazyLock::new(Mutex::default);
32static CARGO_RPATH_SEPARATOR: &str = "|";
33
34// Add to the list of `rpath`s.
35// Later, print the `cargo::rpath` using [`cargo_propagate_rpath`]
36pub fn cargo_add_rpath(rpath: &str) {
37    CARGO_RPATH.lock().unwrap().push(rpath.to_string());
38}
39
40// Print the `rpath`, set via [`cargo_add_rpath`] as `cargo::rpath`
41pub fn cargo_propagate_rpath() {
42    let cargo_cmds = CARGO_RPATH.lock().unwrap();
43    if !cargo_cmds.is_empty() {
44        let rpath = cargo_cmds.join(CARGO_RPATH_SEPARATOR);
45        println!("cargo:rpath={rpath}");
46    }
47}
48
49/// Must be called from final binary crates
50pub fn build_libafl_qemu() {
51    // Add rpath if there are some
52    if let Some(rpaths) = env::var_os("DEP_QEMU_RPATH") {
53        let rpaths: Vec<&str> = rpaths
54            .to_str()
55            .expect("Cannot convert OsString to str")
56            .split(CARGO_RPATH_SEPARATOR)
57            .collect();
58        for rpath in rpaths {
59            println!("cargo:rustc-link-arg-bins=-Wl,-rpath,{rpath}");
60        }
61    }
62}
63
64pub fn build_with_bindings(
65    cpu_target: &str,
66    is_big_endian: bool,
67    is_usermode: bool,
68    jobs: Option<u32>,
69    bindings_file: &Path,
70) {
71    let build_result = build::build(cpu_target, is_big_endian, is_usermode, jobs);
72
73    let clang_args = qemu_bindgen_clang_args(
74        &build_result.qemu_path,
75        &build_result.build_dir,
76        cpu_target,
77        is_usermode,
78    );
79
80    let bind = bindings::generate(&build_result.build_dir, cpu_target, clang_args)
81        .expect("Failed to generate the bindings");
82
83    // Write the final bindings
84    fs::write(bindings_file, bind.to_string()).expect("Unable to write file");
85
86    cargo_propagate_rpath();
87}
88
89// For bindgen, the llvm version must be >= of the rust llvm version
90fn find_llvm_config() -> Result<String, String> {
91    if let Ok(var) = env::var("LLVM_CONFIG") {
92        return Ok(var);
93    }
94
95    let rustc_llvm_ver = find_rustc_llvm_version().unwrap();
96    for version in (rustc_llvm_ver..=LLVM_VERSION_MAX).rev() {
97        let llvm_config_name: String = format!("llvm-config-{version}");
98        if which(&llvm_config_name).is_ok() {
99            return Ok(llvm_config_name);
100        }
101    }
102
103    if which("llvm-config").is_ok() {
104        if let Some(ver) = find_llvm_version("llvm-config".to_owned()) {
105            if ver < rustc_llvm_ver {
106                println!(
107                    "cargo:warning=Version of llvm-config is {ver} but needs to be at least rustc's version ({rustc_llvm_ver})! We will (try to) continue to build. Continue at your own risk, or rebuild with a set LLVM_CONFIG_PATH env variable, pointing to a newer version."
108                );
109            }
110            return Ok("llvm-config".to_owned());
111        }
112    }
113
114    Err("Could not find llvm-config".to_owned())
115}
116
117fn exec_llvm_config(llvm_config: String, args: &[&str]) -> String {
118    match Command::new(llvm_config).args(args).output() {
119        Ok(output) => String::from_utf8(output.stdout)
120            .expect("Unexpected llvm-config output")
121            .trim()
122            .to_string(),
123        Err(e) => panic!("Could not execute llvm-config: {e}"),
124    }
125}
126
127fn find_llvm_version(llvm_config: String) -> Option<i32> {
128    let output = exec_llvm_config(llvm_config, &["--version"]);
129    if let Some(major) = output.split('.').collect::<Vec<&str>>().first() {
130        if let Ok(res) = major.parse::<i32>() {
131            return Some(res);
132        }
133    }
134    None
135}
136
137fn exec_rustc(args: &[&str]) -> String {
138    let rustc = env::var("RUSTC").unwrap();
139    match Command::new(rustc).args(args).output() {
140        Ok(output) => String::from_utf8(output.stdout)
141            .expect("Unexpected rustc output")
142            .trim()
143            .to_string(),
144        Err(e) => panic!("Could not execute rustc: {e}"),
145    }
146}
147
148fn find_rustc_llvm_version() -> Option<i32> {
149    let output = exec_rustc(&["--verbose", "--version"]);
150    let ver = output.split(':').next_back().unwrap().trim();
151    if let Some(major) = ver.split('.').collect::<Vec<&str>>().first() {
152        if let Ok(res) = major.parse::<i32>() {
153            return Some(res);
154        }
155    }
156    None
157}
158
159//linux-user_main.c.o libqemu-x86_64-linux-user.fa.p
160
161fn qemu_bindgen_clang_args(
162    qemu_dir: &Path,
163    build_dir: &Path,
164    cpu_target: &str,
165    is_usermode: bool,
166) -> Vec<String> {
167    if env::var("LLVM_CONFIG_PATH").is_err() {
168        let found = find_llvm_config().expect("Cannot find a suitable llvm-config, it must be a version equal or greater than the rustc LLVM version. Try specifying LLVM_CONFIG_PATH.");
169        // # Safety
170        // This edits env variables but in a build script there's not much that can go wrong
171        unsafe {
172            env::set_var("LLVM_CONFIG_PATH", found);
173        }
174    }
175
176    // load compile commands
177    let compile_commands_string = &fs::read_to_string(build_dir.join("compile_commands.json"))
178        .expect("failed to read compile commands");
179
180    let compile_commands =
181        json::parse(compile_commands_string).expect("Failed to parse compile commands");
182
183    let (main_file, main_obj) = if is_usermode {
184        (
185            "/linux-user/main.c",
186            format!("libqemu-{cpu_target}-linux-user.fa.p/linux-user_main.c.o"),
187        )
188    } else {
189        (
190            "/system/main.c",
191            format!("libqemu-system-{cpu_target}.so.p/system_main.c.o"),
192        )
193    };
194
195    // find main object
196    let entry = compile_commands
197        .members()
198        .find(|entry| {
199            entry["output"] == main_obj
200                || entry["file"]
201                    .as_str()
202                    .is_some_and(|file| file.ends_with(main_file))
203        })
204        .expect("Didn't find compile command for qemu-system-arm");
205
206    // get main object build command
207    let command = entry["command"].as_str().expect("Command is a string");
208
209    // filter define and include args
210    let mut clang_args = vec![];
211    let mut include_arg = false;
212    for arg in shell_words::split(command)
213        .expect("failed to parse command")
214        .into_iter()
215        .skip(1)
216    {
217        if arg.starts_with("-D") {
218            clang_args.push(arg);
219        } else if let Some(incpath) = arg.strip_prefix("-I") {
220            clang_args.push(format!("-I{}", include_path(build_dir, incpath)));
221        } else if arg == "-iquote" || arg == "-isystem" {
222            include_arg = true;
223            clang_args.push(arg);
224        } else if include_arg {
225            include_arg = false;
226            clang_args.push(include_path(build_dir, &arg));
227        }
228    }
229
230    let target_arch_dir = match cpu_target {
231        "x86_64" => format!("-I{}/target/i386", qemu_dir.display()),
232        "aarch64" => format!("-I{}/target/arm", qemu_dir.display()),
233        "riscv32" | "riscv64" => format!("-I{}/target/riscv", qemu_dir.display()),
234        _ => format!("-I{}/target/{cpu_target}", qemu_dir.display()),
235    };
236
237    // add include dirs
238    clang_args.push(format!("-I{}", qemu_dir.display()));
239    clang_args.push(format!("-I{}/include", qemu_dir.display()));
240    clang_args.push(format!("-I{}/quote", qemu_dir.display()));
241    clang_args.push(target_arch_dir);
242
243    clang_args
244}
245
246fn include_path(build_dir: &Path, path: &str) -> String {
247    let include_path = PathBuf::from(path);
248
249    if include_path.is_absolute() {
250        path.to_string()
251    } else {
252        // make include path absolute
253        build_dir.join(include_path).display().to_string()
254    }
255}
256
257/// If `fresh_content` != `content_file_to_update` (the file is read directly if `content_file_to_update` is None), update the file.
258///
259/// The prefix is not considered for comparison.
260/// If a prefix is given, it will be added as the first line of the file.
261pub fn store_generated_content_if_different(
262    file_to_update: &Path,
263    fresh_content: &[u8],
264    content_file_to_update: Option<Vec<u8>>,
265    first_line_prefixes: Vec<&str>,
266    force_regeneration: bool,
267) {
268    let mut must_rewrite_file = true;
269
270    // Check if equivalent file already exists without relying on filesystem timestamp.
271    let mut file_to_check =
272        if let Ok(mut wrapper_file) = File::options().read(true).write(true).open(file_to_update) {
273            let existing_file_content = content_file_to_update.unwrap_or_else(|| {
274                let mut content = Vec::with_capacity(fresh_content.len());
275                wrapper_file.read_to_end(content.as_mut()).unwrap();
276                content
277            });
278
279            if !force_regeneration {
280                let mut existing_wrapper_hasher = hash_map::DefaultHasher::new();
281                existing_wrapper_hasher.write(existing_file_content.as_ref());
282
283                let mut wrapper_h_hasher = hash_map::DefaultHasher::new();
284                wrapper_h_hasher.write(fresh_content);
285
286                // Check if wrappers are the same
287                if existing_wrapper_hasher.finish() == wrapper_h_hasher.finish() {
288                    must_rewrite_file = false;
289                }
290            }
291
292            // Reset file cursor if it's going to be rewritten
293            if must_rewrite_file {
294                wrapper_file.set_len(0).expect("Could not set file len");
295                wrapper_file
296                    .seek(SeekFrom::Start(0))
297                    .expect("Could not seek file to beginning");
298            }
299
300            wrapper_file
301        } else {
302            File::create(file_to_update)
303                .unwrap_or_else(|_| panic!("Could not create {}", file_to_update.display()))
304        };
305
306    if must_rewrite_file {
307        println!(
308            "cargo::warning={} has been regenerated.",
309            file_to_update.file_name().unwrap().to_str().unwrap()
310        );
311
312        for prefix in first_line_prefixes {
313            writeln!(&file_to_check, "{prefix}").expect("Could not write prefix");
314        }
315
316        file_to_check
317            .write_all(fresh_content)
318            .unwrap_or_else(|_| panic!("Unable to write in {}", file_to_update.display()));
319    }
320}
321
322//#[rustversion::nightly]
323//fn parse_stub(
324//    stub_bindings_file: &Path,
325//    current_rustc_version: &Version,
326//) -> (bool, bool, Option<Vec<u8>>) {
327//    let semver_re = Regex::new(r"/\* (.*) \*/").unwrap();
328//    let qemu_hash_re = Regex::new(r"/\* qemu git hash: (.*) \*/").unwrap();
329//
330//    if let Ok(stub_file) = File::open(stub_bindings_file) {
331//        let mut stub_rdr = BufReader::new(stub_file);
332//
333//        let mut first_line = String::new(); // rustc version
334//        let mut second_line = String::new(); // qemu hash
335//        let mut stub_content = Vec::<u8>::new();
336//
337//        assert!(
338//            stub_rdr
339//                .read_line(&mut first_line)
340//                .expect("Could not read first line")
341//                > 0,
342//            "Error while reading first line."
343//        );
344//
345//        assert!(
346//            stub_rdr
347//                .read_line(&mut second_line)
348//                .expect("Could not read second line")
349//                > 0,
350//            "Error while reading second line."
351//        );
352//
353//        if let Some((_, [version_str])) = semver_re
354//            .captures_iter(&first_line)
355//            .next()
356//            .map(|caps| caps.extract())
357//        {
358//            // The first line matches the regex
359//
360//            if let Some((_, [qemu_hash_str])) = qemu_hash_re
361//                .captures_iter(&second_line)
362//                .next()
363//                .map(|caps| caps.extract())
364//            {
365//                // The second line matches the regex
366//
367//                if let Ok(version) = Version::parse(version_str) {
368//                    // The first line contains a version
369//
370//                    stub_rdr
371//                        .read_to_end(&mut stub_content)
372//                        .expect("could not read stub content");
373//
374//                    return (
375//                        (current_rustc_version > &version) || (qemu_hash_str != QEMU_REVISION),
376//                        false,
377//                        Some(stub_content),
378//                    );
379//                }
380//            }
381//        }
382//
383//        stub_rdr.seek(SeekFrom::Start(0)).unwrap();
384//        stub_rdr
385//            .read_to_end(&mut stub_content)
386//            .expect("could not read stub content");
387//
388//        (true, true, Some(stub_content))
389//    } else {
390//        // No stub file stored
391//        (true, true, None)
392//    }
393//}
394
395#[rustversion::nightly]
396pub fn maybe_generate_stub_bindings(
397    cpu_target: &str,
398    emulation_mode: &str,
399    stub_bindings_file: &Path,
400    bindings_file: &Path,
401) {
402    if env::var("LIBAFL_QEMU_GEN_STUBS").is_ok()
403        && cpu_target == "x86_64"
404        && emulation_mode == "usermode"
405    {
406        let current_rustc_version =
407            rustc_version::version().expect("Could not get current rustc version");
408
409        // We only try to store the stub if the current rustc version is strictly bigger than the one used to generate
410        // the versioned stub or the qemu hash differs.
411        // let (try_generate, force_regeneration, stub_content) =
412        // parse_stub(stub_bindings_file, &current_rustc_version);
413
414        let header = format!("/* {current_rustc_version} */");
415
416        store_generated_content_if_different(
417            stub_bindings_file,
418            fs::read(bindings_file)
419                .expect("Could not read generated bindings file")
420                .as_slice(),
421            None,
422            vec![
423                header.as_str(),
424                format!("/* qemu git hash: {LIBAFL_QEMU_GIT_REV} */").as_str(),
425            ],
426            false,
427        );
428    } else if env::var("CARGO_CFG_DOC").is_ok() {
429        println!(
430            "cargo:warning=Bindings regeneration has been skipped. Please rerun with x86_64 with usermode to trigger the bindings regeneration."
431        );
432    }
433}
434
435#[rustversion::not(nightly)]
436pub fn maybe_generate_stub_bindings(
437    _cpu_target: &str,
438    _emulation_mode: &str,
439    _stub_bindings_file: &Path,
440    _bindings_file: &Path,
441) {
442    // Do nothing
443}