phink_lib/cli/
ziggy.rs

1use crate::{
2    cli::config::Configuration,
3    fuzzer::parser::MIN_SEED_LEN,
4    EmptyResult,
5    ResultOf,
6};
7use io::BufReader;
8use std::io::BufRead;
9
10use serde_derive::{
11    Deserialize,
12    Serialize,
13};
14use std::{
15    fs,
16    path::PathBuf,
17};
18
19use crate::{
20    cli::{
21        config::{
22            PFiles,
23            PFiles::{
24                AllowlistPath,
25                CoverageTracePath,
26                DictPath,
27            },
28            PhinkFiles,
29        },
30        env::{
31            PhinkEnv,
32            PhinkEnv::{
33                AflDebug,
34                AflForkServerTimeout,
35            },
36        },
37        ui::custom::CustomManager,
38    },
39    fuzzer::environment::AllowListBuilder,
40};
41use anyhow::{
42    bail,
43    Context,
44};
45use std::{
46    cmp::PartialEq,
47    fmt::{
48        Display,
49        Formatter,
50    },
51    io::{
52        self,
53    },
54    process::{
55        Command,
56        Stdio,
57    },
58};
59use PhinkEnv::{
60    AllowList,
61    FromZiggy,
62    FuzzingWithConfig,
63};
64
65pub const AFL_FORKSRV_INIT_TMOUT: &str = "10000000";
66
67#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
68pub enum ZiggyCommand {
69    Run,
70    Cover,
71    Build,
72    Fuzz,
73    Minimize,
74}
75
76impl Display for ZiggyCommand {
77    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
78        let cmd: &str = match &self {
79            ZiggyCommand::Run => "run",
80            ZiggyCommand::Cover => "cover",
81            ZiggyCommand::Build => "build",
82            ZiggyCommand::Fuzz => "fuzz",
83            ZiggyCommand::Minimize => "minimize",
84        };
85        write!(f, "{}", cmd)
86    }
87}
88
89#[derive(Clone, Debug, Serialize, Deserialize, Default)]
90pub struct ZiggyConfig {
91    config: Configuration,
92    contract_path: Option<PathBuf>,
93}
94
95impl Display for ZiggyConfig {
96    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
97        write!(f, "{}", serde_json::to_string(self).unwrap())
98    }
99}
100
101impl ZiggyConfig {
102    pub fn new(config: Configuration) -> ResultOf<Self> {
103        Self::is_valid(&config, None)?;
104
105        Ok(Self {
106            config,
107            contract_path: None,
108        })
109    }
110
111    pub fn new_with_contract(config: Configuration, contract_path: PathBuf) -> ResultOf<Self> {
112        Self::is_valid(&config, Some(&contract_path))?;
113
114        Ok(Self {
115            config,
116            contract_path: Some(contract_path),
117        })
118    }
119
120    pub fn config(&self) -> &Configuration {
121        &self.config
122    }
123
124    /// Returns the contract path of the ink! contract
125    pub fn contract_path(&self) -> ResultOf<PathBuf> {
126        self.contract_path.to_owned().context(
127            "Contract path wasn't passed in the config, it is currently `None`.\
128            Ensure that your `phink.toml` is properly configured",
129        )
130    }
131    fn is_valid(config: &Configuration, contract_path: Option<&PathBuf>) -> EmptyResult {
132        if let Some(path) = contract_path {
133            if !path.exists() {
134                bail!(format!(
135                    "{path:?} doesn't exist; couldn't load this contract"
136                ))
137            }
138        }
139
140        if config.use_honggfuzz {
141            bail!(
142                "Please, set `use_honggfuzz` to `false`, as we do not currently support Honggfuzz
143        due to ALLOW_LIST limitations in Honggfuzz"
144            )
145        }
146
147        Ok(())
148    }
149
150    pub fn fuzz_output(self) -> PathBuf {
151        self.config.fuzz_output.unwrap_or_default()
152    }
153
154    pub fn afl_debug<'a>(&self) -> &'a str {
155        match self.config().verbose {
156            true => "1",
157            false => "0",
158        }
159    }
160
161    pub fn parse(config_str: String) -> ResultOf<Self> {
162        let config: Self =
163            serde_json::from_str(&config_str).context("❌ Failed to parse config")?;
164        if config.config().verbose {
165            println!("🖨️ Using {} = {config_str}\n", FuzzingWithConfig);
166        }
167        Ok(config)
168    }
169
170    /// This function executes 'cargo ziggy `command` `args`'
171    fn build_command(
172        &self,
173        command: ZiggyCommand,
174        args: Option<Vec<String>>,
175        env: Vec<(String, String)>,
176    ) -> EmptyResult {
177        AllowListBuilder::build(self.clone().fuzz_output())
178            .context("Building LLVM allowlist failed")?;
179
180        match command {
181            ZiggyCommand::Cover | ZiggyCommand::Run | ZiggyCommand::Minimize => {
182                self.exist_or_bail()?;
183                self.native_ui(args, env, command)?;
184            }
185            ZiggyCommand::Fuzz => {
186                self.exist_or_bail()?;
187                if self.config.show_ui {
188                    CustomManager::new(args, env, self.to_owned()).start()?;
189                } else {
190                    self.native_ui(args, env, command)?;
191                }
192            }
193            ZiggyCommand::Build => {
194                self.native_ui(args, env, command)?;
195            }
196        }
197
198        Ok(())
199    }
200
201    fn exist_or_bail(&self) -> EmptyResult {
202        let loc = &self.config().instrumented_contract();
203        if !loc.exists() {
204            bail!(format!(
205                "The instrumented contract path `{}` doesn't exist, \
206                ensure that you have properly instrumented your contract to the correct location",
207                loc.to_str().unwrap()
208            ))
209        }
210        Ok(())
211    }
212
213    fn native_ui(
214        &self,
215        maybe_args: Option<Vec<String>>,
216        env: Vec<(String, String)>,
217        ziggy_command: ZiggyCommand,
218    ) -> EmptyResult {
219        let mut binding = Command::new("cargo");
220        let command_builder = binding
221            .arg("ziggy")
222            .arg(ziggy_command.to_string())
223            .env(FromZiggy.to_string(), "1")
224            .env(AflForkServerTimeout.to_string(), AFL_FORKSRV_INIT_TMOUT)
225            .env(AflDebug.to_string(), self.afl_debug())
226            .envs(env)
227            .stdout(Stdio::piped());
228
229        let output = self.to_owned().fuzz_output();
230        let buf = PhinkFiles::new_by_ref(&output).path(PFiles::CorpusPath);
231        let corpus = buf.to_str().unwrap();
232
233        match ziggy_command {
234            ZiggyCommand::Run | ZiggyCommand::Cover => {
235                command_builder.args(vec!["-i", corpus]);
236                command_builder.args(vec!["-z", output.to_str().unwrap()]);
237            }
238            ZiggyCommand::Minimize => {
239                command_builder.args(vec!["-i", corpus]);
240                command_builder.args(vec!["-z", output.to_str().unwrap()]);
241                command_builder.args(vec!["--engine", "afl-plus-plus"]); // don't minimize with
242                                                                         // honggfuzz
243            }
244            _ => {}
245        }
246
247        self.with_allowlist(command_builder)
248            .context("Couldn't use the allowlist")?;
249
250        if let Some(args) = maybe_args {
251            command_builder.args(args.iter());
252        }
253
254        let mut ziggy_child = command_builder
255            .spawn()
256            .context("Spawning Ziggy was unsuccessfull..")?;
257
258        if let Some(stdout) = ziggy_child.stdout.take() {
259            let reader = BufReader::new(stdout);
260            for line in reader.lines() {
261                println!("{}", line?);
262            }
263        }
264
265        let status = ziggy_child.wait().context("Couldn't wait for Ziggy")?;
266        if !status.success() {
267            bail!("`cargo ziggy {ziggy_command}` failed ({status})");
268        }
269
270        Ok(())
271    }
272
273    /// Add the ALLOW_LIST file to a `Command`. This will be done only in not on macOS
274    ///     - see https://github.com/rust-lang/rust/issues/127573
275    ///     - see https://github.com/rust-lang/rust/issues/127577
276    /// # Arguments
277    ///
278    /// * `command_builder`: The prepared command to which we'll add the AFL ALLOWLIST
279    pub fn with_allowlist(&self, command_builder: &mut Command) -> EmptyResult {
280        if cfg!(not(target_os = "macos")) {
281            let allowlist = PhinkFiles::new(self.clone().fuzz_output()).path(AllowlistPath);
282            command_builder.env(
283                AllowList.to_string(),
284                allowlist
285                    .canonicalize()
286                    .context("Couldn't canonicalize the allowlist path")?,
287            );
288        } else if self.config.verbose {
289            println!("This is a macOS machine. We won't use the ALLOW_LIST. Performances will be drastically bad...");
290        }
291        Ok(())
292    }
293
294    pub fn ziggy_fuzz(&self) -> EmptyResult {
295        let fuzzoutput = &self.config.fuzz_output;
296        let dict = PhinkFiles::new(fuzzoutput.to_owned().unwrap_or_default()).path(DictPath);
297
298        let build_args = if !self.config.use_honggfuzz {
299            Some(vec!["--no-honggfuzz".parse()?])
300        } else {
301            None
302        };
303
304        let fuzz_config = vec![(FuzzingWithConfig.to_string(), serde_json::to_string(self)?)];
305        self.build_command(ZiggyCommand::Build, build_args, fuzz_config)?;
306
307        println!("🏗️ Ziggy Build completed");
308
309        let mut fuzzing_args = vec![
310            format!("--jobs={}", self.config.cores.unwrap_or_default()),
311            format!("--dict={}", dict.to_str().unwrap()),
312            format!("--minlength={MIN_SEED_LEN}"),
313        ];
314        if !self.config.use_honggfuzz {
315            fuzzing_args.push("--no-honggfuzz".parse()?)
316        }
317
318        if fuzzoutput.is_some() {
319            fuzzing_args.push(format!(
320                "--ziggy-output={}",
321                fuzzoutput.clone().unwrap().to_str().unwrap()
322            ))
323        }
324
325        let fuzz_config = vec![(FuzzingWithConfig.to_string(), serde_json::to_string(self)?)];
326
327        self.build_command(ZiggyCommand::Fuzz, Some(fuzzing_args), fuzz_config)
328    }
329
330    pub fn ziggy_cover(&self) -> EmptyResult {
331        self.build_command(
332            ZiggyCommand::Cover,
333            None,
334            vec![(FuzzingWithConfig.to_string(), serde_json::to_string(self)?)],
335        )?;
336        Ok(())
337    }
338
339    pub fn ziggy_minimize(&self) -> EmptyResult {
340        self.build_command(
341            ZiggyCommand::Minimize,
342            None,
343            vec![(FuzzingWithConfig.to_string(), serde_json::to_string(self)?)],
344        )?;
345        if self.config.verbose {
346            println!("Minimization finished, your corpus directory should now be smaller");
347        }
348        Ok(())
349    }
350
351    pub fn ziggy_run(&self) -> EmptyResult {
352        let covpath = PhinkFiles::new(self.clone().fuzz_output()).path(CoverageTracePath);
353
354        // We clean up the old one first
355        if fs::remove_file(&covpath).is_ok() {
356            println!("💨 Removed previous coverage file at {covpath:?}")
357        }
358
359        self.build_command(
360            ZiggyCommand::Run,
361            None,
362            vec![(FuzzingWithConfig.to_string(), serde_json::to_string(self)?)],
363        )?;
364        Ok(())
365    }
366}
367#[cfg(test)]
368mod tests {
369    use super::*;
370    use std::env;
371    use tempfile::tempdir;
372
373    fn create_test_config() -> ZiggyConfig {
374        let config = Configuration {
375            verbose: true,
376            cores: Some(4),
377            use_honggfuzz: false,
378            fuzz_output: Some(PathBuf::from("/tmp/fuzz_output")),
379            show_ui: false,
380            ..Default::default()
381        };
382        ZiggyConfig::new_with_contract(config, PathBuf::from("sample/dummy")).unwrap()
383    }
384
385    #[test]
386    fn test_ziggy_config_new() {
387        let config = create_test_config();
388        assert!(config.config().verbose);
389        assert_eq!(config.config().cores, Some(4));
390        assert!(!config.config().use_honggfuzz);
391        assert_eq!(
392            config.config().fuzz_output,
393            Some(PathBuf::from("/tmp/fuzz_output"))
394        );
395        assert_eq!(
396            config.contract_path().unwrap(),
397            PathBuf::from("sample/dummy")
398        );
399    }
400
401    #[test]
402    fn test_ziggy_config_parse() {
403        let config_str = r#"
404                {
405                   "config":{
406                      "cores":2,
407                      "use_honggfuzz":false,
408                      "deployer_address":"5C62Ck4UrFPiBtoCmeSrgF7x9yv9mn38446dhCpsi2mLHiFT",
409                      "max_messages_per_exec":4,
410                      "report_path":"output/phink/contract_coverage",
411                      "fuzz_origin":false,
412                      "default_gas_limit":{
413                         "ref_time":100000000000,
414                         "proof_size":3145728
415                      },
416                      "storage_deposit_limit":"100000000000",
417                      "instantiate_initial_value":"0",
418                      "constructor_payload":"9BAE9D5E5C1100007B000000279C603E9D4B5C6C8C672893AB54D068CECCBFBEC619E56E819A7769EADCBD766D714E7624D4BE6A35BED20D0730277D0F3A13A7B01DCDA7CEDBF67FE3A4E95F0758D2DF54F30DD663424723E09A56B19E1325B830E6CCCCF63C6FF12B78C79A",
419                      "verbose":false,
420                      "catch_trapped_contract": false,
421                      "show_ui":true
422                   },
423                   "contract_path":"/tmp/ink_fuzzed_3h4Wm/"
424                }
425        "#;
426        let config = ZiggyConfig::parse(config_str.to_string()).unwrap();
427        assert!(!config.config.verbose);
428        assert!(config.config.show_ui);
429        assert!(!config.config.use_honggfuzz);
430        assert_eq!(
431            config.config.storage_deposit_limit,
432            Some("100000000000".into())
433        );
434        assert_eq!(config.config.cores, Some(2));
435        assert_eq!(config.config.fuzz_output, Default::default());
436        assert_eq!(
437            config.contract_path().unwrap(),
438            PathBuf::from("/tmp/ink_fuzzed_3h4Wm/")
439        );
440    }
441
442    #[test]
443    #[ignore] // ignored since we often change the allowlist for benchmark purposes
444    fn test_build_llvm_allowlist() -> io::Result<()> {
445        let temp_dir = tempdir()?;
446        let config = Configuration {
447            fuzz_output: Some(temp_dir.path().to_path_buf()),
448            ..Default::default()
449        };
450        let ziggy_config =
451            ZiggyConfig::new_with_contract(config, PathBuf::from("sample/dummy")).unwrap();
452
453        AllowListBuilder::build(ziggy_config.fuzz_output())?;
454
455        let allowlist_path = PhinkFiles::new(temp_dir.path().to_path_buf()).path(AllowlistPath);
456        assert!(allowlist_path.exists());
457
458        let contents = fs::read_to_string(allowlist_path)?;
459        assert!(contents.contains("fun: *redirect_coverage*"));
460        assert!(contents.contains("fun: *try_parse_input*"));
461
462        Ok(())
463    }
464
465    #[test]
466    fn test_with_allowlist() -> EmptyResult {
467        if cfg!(not(target_os = "macos")) {
468            let temp_dir = tempdir()?;
469            let config = Configuration {
470                fuzz_output: Some(temp_dir.path().to_path_buf()),
471                ..Default::default()
472            };
473            let ziggy_config =
474                ZiggyConfig::new_with_contract(config, PathBuf::from("sample/dummy"))?;
475
476            AllowListBuilder::build(ziggy_config.clone().fuzz_output())?;
477
478            let mut command = Command::new("echo");
479            ziggy_config.with_allowlist(&mut command)?;
480
481            let allowlist_path = PhinkFiles::new(temp_dir.path().to_path_buf()).path(AllowlistPath);
482            let env_vars: Vec<(String, String)> = command
483                .get_envs()
484                .map(|(k, v)| {
485                    (
486                        k.to_str().unwrap().to_string(),
487                        v.unwrap().to_str().unwrap().to_string(),
488                    )
489                })
490                .collect();
491
492            assert!(env_vars.contains(&(
493                AllowList.to_string(),
494                allowlist_path.to_str().unwrap().to_string()
495            )));
496        }
497        Ok(())
498    }
499
500    #[test]
501    fn test_start_build_command() -> EmptyResult {
502        let config = create_test_config();
503        let temp_dir = tempdir()?;
504
505        env::set_var("CARGO_MANIFEST_DIR", temp_dir.path());
506
507        let result = config.build_command(
508            ZiggyCommand::Build,
509            Some(vec!["--no-honggfuzz".to_string()]),
510            vec![],
511        );
512
513        let success = result.is_ok();
514        if success {
515            println!("{result:?}",);
516        }
517        assert!(
518            success,
519            "One possibility could be `cargo afl config --build --verbose --force`"
520        );
521        Ok(())
522    }
523}