Repository: google/clusterfuzz

Issue:
Radamsa and ML RNN are adding testcases over 1 MB to AFL's corpus
This prevents AFL from running.


Discussion:

Resolving patch:
diff --git a/src/python/bot/fuzzers/afl/launcher.py b/src/python/bot/fuzzers/afl/launcher.py
--- a/src/python/bot/fuzzers/afl/launcher.py
+++ b/src/python/bot/fuzzers/afl/launcher.py
@@ -302,17 +302,19 @@ def decide_fast_cal_manual(self, time_spent_fuzzing):
 
     assert self.fast_cal in {strategies.FastCal.MANUAL, strategies.FastCal.OFF}
 
-  def decide_fast_cal_random(self, num_files):
+  def decide_fast_cal_random(self, input_dir):
     """Decides whether to use AFL_FAST_CAL, based on probabilities that are
-    dependant on |num_files| (listed in FAST_CAL_PROBS), sets self.fast_cal
-    to FastCal.RANDOM if we decide yes, or sets it to FastCal.OFF. Does nothing
-    if use_fast_cal was already set. Idempotent.
+    dependant on ther number of files in |input_dir| (listed in FAST_CAL_PROBS),
+    sets self.fast_cal to FastCal.RANDOM if we decide yes, or sets it to
+    FastCal.OFF. Does nothing if use_fast_cal was already set. Idempotent.
     """
     # Don't do anything if we have already made a decision about randomly using
     # fast_cal.
     if self.fast_cal != strategies.FastCal.NOT_SET:
       return
 
+    num_files = len(os.listdir(input_dir))
+
     # Decide if we want to use it.
     for cutoff, prob in self.FAST_CAL_PROBS:
       # Find the correct probability based on the number of input files.
@@ -705,6 +707,30 @@ def set_timeout_arg(cls, afl_args, timeout_value, skip_hangs=False):
     cls.set_arg(afl_args, constants.TIMEOUT_FLAG, timeout_value)
     return afl_args
 
+  def do_offline_mutations(self):
+    """Mutate the corpus offline using Radamsa or ML RNN if specified."""
+    if not self.strategies.is_mutations_run:
+      return
+
+    target_name = os.path.basename(self.target_path)
+    project_qualified_target_name = (
+        data_types.fuzz_target_project_qualified_name(utils.current_project(),
+                                                      target_name))
+    # Generate new testcase mutations according to candidate generator. If
+    # testcase mutations are properly generated, set generator strategy
+    # accordingly.
+    generator_used = engine_common.generate_new_testcase_mutations(
+        self.afl_input.input_directory, self.afl_input.input_directory,
+        project_qualified_target_name, self.strategies.candidate_generator)
+
+    if generator_used:
+      self.strategies.generator_strategy = self.strategies.candidate_generator
+
+    # Delete large testcases created by generators.
+    for input_path in shell.get_files_list(self.afl_input.input_directory):
+      if os.path.getsize(input_path) >= constants.MAX_FILE_BYTES:
+        remove_path(input_path)
+
   def generate_afl_args(self,
                         afl_input=None,
                         afl_output=None,
@@ -802,28 +828,6 @@ def check_error_and_log(error_regex, log_message_format):
         return erroring_filename
       return None  # else
 
-    if self.strategies.is_mutations_run:
-      target_name = os.path.basename(self.target_path)
-      project_qualified_target_name = (
-          data_types.fuzz_target_project_qualified_name(utils.current_project(),
-                                                        target_name))
-      # Generate new testcase mutations according to candidate generator. If
-      # testcase mutations are properly generated, set generator strategy
-      # accordingly.
-      generator_used = engine_common.generate_new_testcase_mutations(
-          self.afl_input.input_directory, self.afl_input.input_directory,
-          project_qualified_target_name, self.strategies.candidate_generator)
-      if generator_used:
-        self.strategies.generator_strategy = self.strategies.candidate_generator
-
-    # Decide if we want to use fast cal based on the size of the input
-    # directory. This is only done once, but the function can be called
-    # multiple times. This is different than the call to fast_cal_manual where
-    # we deterministically decide to use fast cal based on how long we have
-    # spent fuzzing.
-    self.strategies.decide_fast_cal_random(
-        len(os.listdir(self.afl_input.input_directory)))
-
     num_first_testcase_hangs = 0
     num_retries = 0
     while self.should_try_fuzzing(max_total_time, num_retries):
@@ -1000,6 +1004,15 @@ def fuzz(self):
     if self.afl_input.skip_deterministic:
       self._fuzz_args.insert(0, constants.SKIP_DETERMINISTIC_FLAG)
 
+    self.do_offline_mutations()
+
+    # Decide if we want to use fast cal based on the size of the input
+    # directory. This is only done once, but the function can be called
+    # multiple times. This is different than the call to fast_cal_manual where
+    # we deterministically decide to use fast cal based on how long we have
+    # spent fuzzing.
+    self.strategies.decide_fast_cal_random(self.afl_input.input_directory)
+
     return self.run_afl_fuzz(self._fuzz_args)
 
   def get_file_features(self, input_file_path, showmap_args):


Test patch:
diff --git a/src/python/tests/core/bot/fuzzers/afl/afl_launcher_test.py b/src/python/tests/core/bot/fuzzers/afl/afl_launcher_test.py
--- a/src/python/tests/core/bot/fuzzers/afl/afl_launcher_test.py
+++ b/src/python/tests/core/bot/fuzzers/afl/afl_launcher_test.py
@@ -17,7 +17,6 @@
 from builtins import range
 import mock
 import os
-import unittest
 
 from pyfakefs import fake_filesystem_unittest
 
@@ -74,12 +73,17 @@ def _assert_elements_equal(self, l1, l2):
     self.assertEqual(sorted(l1), sorted(l2))
 
 
-class FuzzingStrategiesTest(unittest.TestCase):
+class FuzzingStrategiesTest(fake_filesystem_unittest.TestCase):
   """Tests for launcher.FuzzingStrategies."""
 
   NUM_FILES = 1000
+  INPUT_DIR = '/input'
 
   def setUp(self):
+    test_utils.set_up_pyfakefs(self)
+    for file_num in range(self.NUM_FILES):
+      self.fs.create_file(os.path.join(self.INPUT_DIR, str(file_num)))
+
     test_helpers.patch(self, ['bot.fuzzers.engine_common.is_lpm_fuzz_target'])
     self.mock.is_lpm_fuzz_target.return_value = True
     self.strategies = launcher.FuzzingStrategies(None)
@@ -88,7 +92,7 @@ def _decide_fast_cal_random(self):
     """Helper method that calls self.strategies.decide_fast_cal_random with a
     default value."""
     self.strategies.fast_cal = strategies.FastCal.NOT_SET
-    return self.strategies.decide_fast_cal_random(self.NUM_FILES)
+    return self.strategies.decide_fast_cal_random(self.INPUT_DIR)
 
   def test_decide_fast_cal_random_finds_correct_prob(self):
     """Tests that FuzzingStrategies.decide_fast_cal finds the correct
@@ -710,8 +714,35 @@ def _initialization_for_run_afl(self):
     self.mock.run_and_wait.side_effect = (
         lambda *args, **kwargs: self._process_result())
 
+    self.runner.strategies.decide_fast_cal_random(
+        self.runner.afl_input.input_directory)
     self._write_bad_input()
 
+  def test_do_offline_mutations_small_testcase(self):
+    """Tests that do_offline_mutations doesn't remove non-oversized testcases
+    from the corpus."""
+    # <1 MB testcase isn't oversized.
+    self.assertTrue(self._do_offline_mutations(2**20 - 1))
+
+  def test_do_offline_mutations_large_testcase(self):
+    """Tests that do_offline_mutations doesn't add oversized testcases to the
+    corpus."""
+    # 1 MB testcase is oversized.
+    self.assertFalse(self._do_offline_mutations(2**20))
+
+  def _do_offline_mutations(self, size):
+    """Creates a file |size| bytes long in the input directory, then calls
+    do_offline_mutations and returns whether the file is in the input directory.
+    """
+    contents = 'A' * size  # 1 MB
+    filename = 'test-file'
+    input_dir = self.runner.afl_input.input_directory
+    filepath = os.path.join(input_dir, filename)
+    self.fs.create_file(filepath, contents=contents)
+    self.runner.strategies.is_mutations_run = True
+    self.runner.do_offline_mutations()
+    return filename in os.listdir(input_dir)
+
   def test_run_afl_fuzz_fuzz_success(self):
     """Test AflRunner.run_afl_fuzz_and_handle_error when fuzzing succeeds."""
     self._initialization_for_run_afl()