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 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 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"]); }
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 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 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] 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}