From 3f31ee8ad0ac5018d6fe472cf8cc7d56fce9331e Mon Sep 17 00:00:00 2001 From: Victor Liu Date: Tue, 28 Apr 2026 13:50:50 -0400 Subject: [PATCH 1/3] [internal/LibFuzzer] Add option to set cwd to BUILD_DIR based on environment Add optional cwd argument to LibFuzzerRunner class which passes to parent ProcessRunner class to subprocess.Popen with cwd argument. When a LibFuzzerRunner is requested with get_runner() the cwd can be set to BUILD_DIR using FUZZ_TARGET_CWD_IS_BUILD_DIR boolean env var. --- .../_internal/bot/fuzzers/libfuzzer.py | 18 ++++++++++-- .../_internal/system/new_process.py | 8 +++++- .../bot/fuzzers/libFuzzer/libfuzzer_test.py | 28 +++++++++++++++++++ .../tests/core/system/new_process_test.py | 14 ++++++++++ 4 files changed, 64 insertions(+), 4 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index c8d188c755b..d29110a543b 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -299,14 +299,16 @@ class LibFuzzerRunner(new_process.ModifierProcessRunnerMixin, new_process.UnicodeProcessRunner, LibFuzzerCommon): """libFuzzer runner (when minijail is not used).""" - def __init__(self, executable_path, default_args=None): + def __init__(self, executable_path, default_args=None, cwd=None): """Inits the LibFuzzerRunner. Args: executable_path: Path to the fuzzer executable. default_args: Default arguments to always pass to the fuzzer. + cwd: Optional current working directory for the process. """ - super().__init__(executable_path=executable_path, default_args=default_args) + super().__init__( + executable_path=executable_path, default_args=default_args, cwd=cwd) def fuzz(self, corpus_directories, @@ -1133,6 +1135,10 @@ def get_runner(fuzzer_path, temp_dir=None, use_minijail=None): temp_dir = fuzzer_utils.get_temp_dir() build_dir = environment.get_value('BUILD_DIR') + + cwd = build_dir if environment.get_value( + 'FUZZ_TARGET_CWD_IS_BUILD_DIR') else None + is_android = environment.is_android() is_fuchsia = environment.platform() == 'FUCHSIA' @@ -1141,6 +1147,12 @@ def get_runner(fuzzer_path, temp_dir=None, use_minijail=None): os.chmod(fuzzer_path, 0o755) is_chromeos_system_job = environment.is_chromeos_system_job() + + if cwd and (use_minijail or is_chromeos_system_job or is_fuchsia or + is_android): + logs.warning('FUZZ_TARGET_CWD_IS_BUILD_DIR is only supported for standard ' + 'LibFuzzerRunner and will be ignored.') + if is_chromeos_system_job: minijail_chroot = minijail.ChromeOSChroot(build_dir) elif use_minijail: @@ -1185,7 +1197,7 @@ def get_runner(fuzzer_path, temp_dir=None, use_minijail=None): elif is_android: runner = AndroidLibFuzzerRunner(fuzzer_path, build_dir) else: - runner = LibFuzzerRunner(fuzzer_path) + runner = LibFuzzerRunner(fuzzer_path, cwd=cwd) return runner diff --git a/src/clusterfuzz/_internal/system/new_process.py b/src/clusterfuzz/_internal/system/new_process.py index e1585216d75..2b62bba3106 100644 --- a/src/clusterfuzz/_internal/system/new_process.py +++ b/src/clusterfuzz/_internal/system/new_process.py @@ -246,12 +246,14 @@ class ProcessRunner: executable_path: Path to the executable to be run. default_args: An optional sequence of arguments that are always passed to the executable when run. + cwd: Optional current working directory for the process. """ - def __init__(self, executable_path, default_args=None): + def __init__(self, executable_path, default_args=None, cwd=None): """Inits ProcessRunner.""" self._executable_path = executable_path self._default_args = [] + self.cwd = cwd if default_args: self.default_args.extend(default_args) @@ -332,6 +334,10 @@ def run(self, if extra_env is not None: env.update(extra_env) + if self.cwd and 'cwd' not in popen_args: + logs.info(f'Executing process with custom cwd: {self.cwd}') + popen_args['cwd'] = self.cwd + return ChildProcess( subprocess.Popen( command, diff --git a/src/clusterfuzz/_internal/tests/core/bot/fuzzers/libFuzzer/libfuzzer_test.py b/src/clusterfuzz/_internal/tests/core/bot/fuzzers/libFuzzer/libfuzzer_test.py index 5ae921ca57d..d9548dbbc69 100644 --- a/src/clusterfuzz/_internal/tests/core/bot/fuzzers/libFuzzer/libfuzzer_test.py +++ b/src/clusterfuzz/_internal/tests/core/bot/fuzzers/libFuzzer/libfuzzer_test.py @@ -17,12 +17,14 @@ import os import shutil import unittest +from unittest import mock from clusterfuzz._internal.bot.fuzzers import engine_common from clusterfuzz._internal.bot.fuzzers import libfuzzer from clusterfuzz._internal.bot.fuzzers import strategy_selection from clusterfuzz._internal.bot.fuzzers.libFuzzer import fuzzer from clusterfuzz._internal.fuzzing import strategy +from clusterfuzz._internal.system import environment from clusterfuzz._internal.tests.test_libs import helpers as test_helpers TESTDATA_PATH = os.path.join(os.path.dirname(__file__), 'libfuzzer_test_data') @@ -177,5 +179,31 @@ def do_strategy(self, *args, **kwargs): libfuzzer.should_set_fork_flag(existing_arguments, MockPool())) +class GetRunnerTest(unittest.TestCase): + """Tests for get_runner.""" + + def setUp(self): + test_helpers.patch_environ(self) + + @mock.patch('os.chmod') + def test_cwd_is_build_dir(self, _): + """Test that FUZZ_TARGET_CWD_IS_BUILD_DIR causes cwd to be set to BUILD_DIR""" + environment.set_value('BUILD_DIR', '/build/dir') + environment.set_value('FUZZ_TARGET_CWD_IS_BUILD_DIR', True) + environment.set_value('USE_MINIJAIL', False) + runner = libfuzzer.get_runner('/fake/path') + self.assertIsInstance(runner, libfuzzer.LibFuzzerRunner) + self.assertEqual(runner.cwd, '/build/dir') + + @mock.patch('os.chmod') + def test_no_default_cwd(self, _): + """Test that cwd is None when FUZZ_TARGET_CWD_IS_BUILD_DIR is not set.""" + environment.set_value('BUILD_DIR', '/build/dir') + environment.set_value('USE_MINIJAIL', False) + runner = libfuzzer.get_runner('fake/path') + self.assertIsInstance(runner, libfuzzer.LibFuzzerRunner) + self.assertIsNone(runner.cwd) + + if __name__ == '__main__': unittest.main() diff --git a/src/clusterfuzz/_internal/tests/core/system/new_process_test.py b/src/clusterfuzz/_internal/tests/core/system/new_process_test.py index 09c95a2586a..ce91c931077 100644 --- a/src/clusterfuzz/_internal/tests/core/system/new_process_test.py +++ b/src/clusterfuzz/_internal/tests/core/system/new_process_test.py @@ -142,6 +142,20 @@ def test_results_timeout(self): self.assertLess(abs(results.time_executed - 0.5), self.TIME_ERROR) self.assertTrue(results.timed_out) + def test_cwd(self): + """Test that cwd is passed to Popen.""" + with mock.patch('subprocess.Popen') as mock_popen: + runner = new_process.ProcessRunner( + '/test/path', default_args=['-arg1'], cwd='/working/dir') + runner.run() + mock_popen.assert_called_with( + ['/test/path', '-arg1'], + env=mock.ANY, + stdin=mock.ANY, + stdout=mock.ANY, + stderr=mock.ANY, + cwd='/working/dir') + def test_timeout(self): """Tests timeout signals.""" with mock.patch('subprocess.Popen', mock_popen_factory(1.0, '', From 17fc55cfa1b8b0506390981024e19289401dd03a Mon Sep 17 00:00:00 2001 From: Victor Liu <67168434+notvictorl@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:37:09 -0400 Subject: [PATCH 2/3] Clean up unit tests and warnings --- src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py | 9 ++++----- src/clusterfuzz/_internal/system/new_process.py | 10 +++++++--- .../tests/core/bot/fuzzers/libFuzzer/libfuzzer_test.py | 4 ++++ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index d29110a543b..952b06cbe7d 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1148,11 +1148,6 @@ def get_runner(fuzzer_path, temp_dir=None, use_minijail=None): is_chromeos_system_job = environment.is_chromeos_system_job() - if cwd and (use_minijail or is_chromeos_system_job or is_fuchsia or - is_android): - logs.warning('FUZZ_TARGET_CWD_IS_BUILD_DIR is only supported for standard ' - 'LibFuzzerRunner and will be ignored.') - if is_chromeos_system_job: minijail_chroot = minijail.ChromeOSChroot(build_dir) elif use_minijail: @@ -1199,6 +1194,10 @@ def get_runner(fuzzer_path, temp_dir=None, use_minijail=None): else: runner = LibFuzzerRunner(fuzzer_path, cwd=cwd) + if cwd and not isinstance(runner, LibFuzzerRunner): + logs.warning('FUZZ_TARGET_CWD_IS_BUILD_DIR is only supported for standard ' + 'LibFuzzerRunner and will be ignored.') + return runner diff --git a/src/clusterfuzz/_internal/system/new_process.py b/src/clusterfuzz/_internal/system/new_process.py index 2b62bba3106..8a49dda19bc 100644 --- a/src/clusterfuzz/_internal/system/new_process.py +++ b/src/clusterfuzz/_internal/system/new_process.py @@ -334,9 +334,13 @@ def run(self, if extra_env is not None: env.update(extra_env) - if self.cwd and 'cwd' not in popen_args: - logs.info(f'Executing process with custom cwd: {self.cwd}') - popen_args['cwd'] = self.cwd + if self.cwd: + if 'cwd' in popen_args: + logs.warning(f'Detected two cwd arguments for popen ({self.cwd} and ' + f'{popen_args["cwd"]}). Using {popen_args["cwd"]}') + else: + logs.info(f'Executing process with custom cwd: {self.cwd}') + popen_args['cwd'] = self.cwd return ChildProcess( subprocess.Popen( diff --git a/src/clusterfuzz/_internal/tests/core/bot/fuzzers/libFuzzer/libfuzzer_test.py b/src/clusterfuzz/_internal/tests/core/bot/fuzzers/libFuzzer/libfuzzer_test.py index d9548dbbc69..3c718b55ae1 100644 --- a/src/clusterfuzz/_internal/tests/core/bot/fuzzers/libFuzzer/libfuzzer_test.py +++ b/src/clusterfuzz/_internal/tests/core/bot/fuzzers/libFuzzer/libfuzzer_test.py @@ -191,7 +191,9 @@ def test_cwd_is_build_dir(self, _): environment.set_value('BUILD_DIR', '/build/dir') environment.set_value('FUZZ_TARGET_CWD_IS_BUILD_DIR', True) environment.set_value('USE_MINIJAIL', False) + runner = libfuzzer.get_runner('/fake/path') + self.assertIsInstance(runner, libfuzzer.LibFuzzerRunner) self.assertEqual(runner.cwd, '/build/dir') @@ -200,7 +202,9 @@ def test_no_default_cwd(self, _): """Test that cwd is None when FUZZ_TARGET_CWD_IS_BUILD_DIR is not set.""" environment.set_value('BUILD_DIR', '/build/dir') environment.set_value('USE_MINIJAIL', False) + runner = libfuzzer.get_runner('fake/path') + self.assertIsInstance(runner, libfuzzer.LibFuzzerRunner) self.assertIsNone(runner.cwd) From bbc1eedcdfd9850718808c1d54f89f8519948f69 Mon Sep 17 00:00:00 2001 From: Victor Liu <67168434+notvictorl@users.noreply.github.com> Date: Wed, 29 Apr 2026 16:02:42 -0400 Subject: [PATCH 3/3] Clean up test_cwd unit test style --- src/clusterfuzz/_internal/tests/core/system/new_process_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/clusterfuzz/_internal/tests/core/system/new_process_test.py b/src/clusterfuzz/_internal/tests/core/system/new_process_test.py index ce91c931077..1f247934478 100644 --- a/src/clusterfuzz/_internal/tests/core/system/new_process_test.py +++ b/src/clusterfuzz/_internal/tests/core/system/new_process_test.py @@ -147,7 +147,9 @@ def test_cwd(self): with mock.patch('subprocess.Popen') as mock_popen: runner = new_process.ProcessRunner( '/test/path', default_args=['-arg1'], cwd='/working/dir') + runner.run() + mock_popen.assert_called_with( ['/test/path', '-arg1'], env=mock.ANY,