From 3facd87c03c517bf8b758683b244ab928994b21e Mon Sep 17 00:00:00 2001 From: Oscar Fernando Flores Garcia Date: Mon, 27 Apr 2026 16:59:22 +0000 Subject: [PATCH 01/59] Added initial apk poc integration --- .../_internal/bot/fuzzers/engine_common.py | 3 +- .../_internal/bot/fuzzers/libfuzzer.py | 70 ++++++++++++++++++- .../_internal/bot/fuzzers/utils.py | 2 +- 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/engine_common.py b/src/clusterfuzz/_internal/bot/fuzzers/engine_common.py index b6881d86e5a..8ede5394bc9 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/engine_common.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/engine_common.py @@ -290,7 +290,8 @@ def find_fuzzer_path(build_directory, fuzzer_name): for root, _, files in shell.walk(build_directory): for filename in files: if (legacy_name_prefix + filename == fuzzer_name or - filename == fuzzer_filename): + filename == fuzzer_filename or + filename == fuzzer_name + '.apk'): return os.path.join(root, filename) # This is an expected case when doing regression testing with old builds diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index c8d188c755b..9b2b20307d3 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1120,6 +1120,71 @@ def cleanse_crash(self, return result +class AndroidApkLibFuzzerRunner(new_process.UnicodeProcessRunner, LibFuzzerCommon): + """Android APK libFuzzer runner.""" + + def __init__(self, executable_path, build_directory, default_args=None): + super().__init__(executable_path=android.adb.get_adb_path(), default_args=[]) + self.apk_path = executable_path + self.package_name = android.app.get_package_name(self.apk_path) + if not self.package_name: + raise LibFuzzerError(f'Failed to get package name for {self.apk_path}') + + android.app.install(self.apk_path) + self.instrumentation_runner = self._get_instrumentation_runner(self.apk_path) + self.launchable_activity = self._get_launchable_activity(self.apk_path) + + def _get_launchable_activity(self, apk_path): + aapt_binary_path = os.path.join( + environment.get_platform_resources_directory(), 'aapt') + aapt_command = '%s dump badging %s' % (aapt_binary_path, apk_path) + output = android.adb.execute_command(aapt_command, timeout=60) + if not output: + return None + match = re.search(r"launchable-activity: name='([^']+)'", output) + if match: + return match.group(1) + return None + + def _get_instrumentation_runner(self, apk_path): + aapt_binary_path = os.path.join( + environment.get_platform_resources_directory(), 'aapt') + aapt_command = '%s dump xmltree %s AndroidManifest.xml' % (aapt_binary_path, apk_path) + output = android.adb.execute_command(aapt_command, timeout=60) + if not output: + return None + + lines = output.splitlines() + found_instrumentation = False + for line in lines: + if 'E: instrumentation' in line: + found_instrumentation = True + continue + if found_instrumentation and 'android:name' in line: + match = re.search(r'="([^"]+)"', line) + if match: + return match.group(1) + return None + + def fuzz(self, corpus_directories, fuzz_timeout, artifact_prefix=None, additional_args=None, extra_env=None): + if self.instrumentation_runner: + args = ['shell', 'am', 'instrument', '-w', f'{self.package_name}/{self.instrumentation_runner}'] + logs.info(f'Starting Instrumentation: {self.package_name}/{self.instrumentation_runner}') + elif self.launchable_activity: + args = ['shell', 'am', 'start', '-n', f'{self.package_name}/{self.launchable_activity}'] + logs.info(f'Starting APK: {self.package_name}/{self.launchable_activity}') + else: + raise LibFuzzerError('No launchable activity or instrumentation found.') + + result = self.run_and_wait( + additional_args=args, + timeout=self.get_total_timeout(fuzz_timeout), + max_stdout_len=MAX_OUTPUT_LEN) + + result.output = f'{result.output}\n\nLogcat:\n{android.logger.log_output()}' + return result + + def get_runner(fuzzer_path, temp_dir=None, use_minijail=None): """Get a libfuzzer runner.""" if use_minijail is None: @@ -1183,7 +1248,10 @@ def get_runner(fuzzer_path, temp_dir=None, use_minijail=None): raise undercoat.UndercoatError('Instance handle not provided.') runner = FuchsiaUndercoatLibFuzzerRunner(fuzzer_path, instance_handle) elif is_android: - runner = AndroidLibFuzzerRunner(fuzzer_path, build_dir) + if fuzzer_path.endswith('.apk'): + runner = AndroidApkLibFuzzerRunner(fuzzer_path, build_dir) + else: + runner = AndroidLibFuzzerRunner(fuzzer_path, build_dir) else: runner = LibFuzzerRunner(fuzzer_path) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/utils.py b/src/clusterfuzz/_internal/bot/fuzzers/utils.py index a7494c0ce86..19f92bec1de 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/utils.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/utils.py @@ -26,7 +26,7 @@ from clusterfuzz._internal.system import environment from clusterfuzz._internal.system import shell -ALLOWED_FUZZ_TARGET_EXTENSIONS = ['', '.exe', '.par'] +ALLOWED_FUZZ_TARGET_EXTENSIONS = ['', '.exe', '.par', '.apk'] FUZZ_TARGET_SEARCH_BYTES = [b'LLVMFuzzerTestOneInput', b'LLVMFuzzerRunDriver'] VALID_TARGET_NAME_REGEX = re.compile(r'^[a-zA-Z0-9@_.-]+$') BLOCKLISTED_TARGET_NAME_REGEX = re.compile(r'^(jazzer_driver.*|jazzerjs)$') From 7403fc136485fd98deb07d246466cd29e7111dc3 Mon Sep 17 00:00:00 2001 From: Oscar Fernando Flores Garcia Date: Thu, 30 Apr 2026 16:14:34 +0000 Subject: [PATCH 02/59] Modified engine to execute apks with different names --- .../_internal/bot/fuzzers/engine_common.py | 3 +- .../_internal/bot/fuzzers/libfuzzer.py | 51 +++++++++++++++++-- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/engine_common.py b/src/clusterfuzz/_internal/bot/fuzzers/engine_common.py index 8ede5394bc9..799e9a044bc 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/engine_common.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/engine_common.py @@ -291,7 +291,8 @@ def find_fuzzer_path(build_directory, fuzzer_name): for filename in files: if (legacy_name_prefix + filename == fuzzer_name or filename == fuzzer_filename or - filename == fuzzer_name + '.apk'): + (filename.endswith('.apk') and fuzzer_name in filename) or + filename.endswith('.apk')): return os.path.join(root, filename) # This is an expected case when doing regression testing with old builds diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 9b2b20307d3..1ed9d8198ef 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1166,13 +1166,54 @@ def _get_instrumentation_runner(self, apk_path): return match.group(1) return None + def _get_device_corpus_paths(self, corpus_directories): + return [self._get_device_path(path) for path in corpus_directories] + + def _get_device_path(self, local_path): + root_directory = environment.get_root_directory() + return os.path.join(android.constants.DEVICE_FUZZING_DIR, + os.path.relpath(local_path, root_directory)) + + def _copy_local_directories_to_device(self, local_directories): + for local_directory in sorted(set(local_directories)): + self.copy_local_directory_to_device(local_directory) + + def copy_local_directory_to_device(self, local_directory): + device_directory = self._get_device_path(local_directory) + android.adb.remove_directory(device_directory, recreate=True) + android.adb.copy_local_directory_to_remote(local_directory, + device_directory) + + def _copy_local_directories_from_device(self, local_directories): + for local_directory in sorted(set(local_directories)): + device_directory = self._get_device_path(local_directory) + shell.remove_directory(local_directory, recreate=True) + android.adb.copy_remote_directory_to_local(device_directory, + local_directory) + def fuzz(self, corpus_directories, fuzz_timeout, artifact_prefix=None, additional_args=None, extra_env=None): + sync_directories = list(corpus_directories) + if artifact_prefix: + sync_directories.append(artifact_prefix) + + self._copy_local_directories_to_device(sync_directories) + device_corpus_dirs = self._get_device_corpus_paths(corpus_directories) + + if artifact_prefix: + artifact_prefix = self._get_device_path(artifact_prefix) + + fuzzer_args = [] + if additional_args: + fuzzer_args.extend(additional_args) + fuzzer_args.extend(device_corpus_dirs) + fuzzer_args_str = ' '.join(fuzzer_args) + if self.instrumentation_runner: - args = ['shell', 'am', 'instrument', '-w', f'{self.package_name}/{self.instrumentation_runner}'] - logs.info(f'Starting Instrumentation: {self.package_name}/{self.instrumentation_runner}') + args = ['shell', 'am', 'instrument', '-w', '-e', 'org.chromium.native_test.NativeTest.CommandLineFlags', f'"{fuzzer_args_str}"', f'{self.package_name}/{self.instrumentation_runner}'] + logs.info(f'Starting Instrumentation: {self.package_name}/{self.instrumentation_runner} with args: {fuzzer_args_str}') elif self.launchable_activity: - args = ['shell', 'am', 'start', '-n', f'{self.package_name}/{self.launchable_activity}'] - logs.info(f'Starting APK: {self.package_name}/{self.launchable_activity}') + args = ['shell', 'am', 'start', '-n', f'{self.package_name}/{self.launchable_activity}', '-e', 'org.chromium.native_test.NativeTest.CommandLineFlags', f'"{fuzzer_args_str}"'] + logs.info(f'Starting APK: {self.package_name}/{self.launchable_activity} with args: {fuzzer_args_str}') else: raise LibFuzzerError('No launchable activity or instrumentation found.') @@ -1182,9 +1223,11 @@ def fuzz(self, corpus_directories, fuzz_timeout, artifact_prefix=None, additiona max_stdout_len=MAX_OUTPUT_LEN) result.output = f'{result.output}\n\nLogcat:\n{android.logger.log_output()}' + self._copy_local_directories_from_device(sync_directories) return result + def get_runner(fuzzer_path, temp_dir=None, use_minijail=None): """Get a libfuzzer runner.""" if use_minijail is None: From b3553525d75c7ca796fb2ceea8d8dbe81f7aff4b Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Fri, 19 Jun 2026 13:48:51 +0000 Subject: [PATCH 03/59] Fix Android engine fuzzers permission denied by running adb root at init, and configure real development GCS buckets --- src/clusterfuzz/_internal/bot/init_scripts/android.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/clusterfuzz/_internal/bot/init_scripts/android.py b/src/clusterfuzz/_internal/bot/init_scripts/android.py index 64293ee76a0..72abf62515a 100644 --- a/src/clusterfuzz/_internal/bot/init_scripts/android.py +++ b/src/clusterfuzz/_internal/bot/init_scripts/android.py @@ -53,5 +53,8 @@ def run(): # Wait until battery charges to a minimum level and temperature threshold. android.battery.wait_until_good_state() + # Ensure adb runs as root for the rest of the session (essential for engine fuzzers). + android.adb.run_as_root() + # Initialize environment settings. android.device.initialize_environment() From e31136989ba8b855066507de60c01133f216afa2 Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Fri, 19 Jun 2026 17:34:31 +0000 Subject: [PATCH 04/59] Skip USB operations and reset on Android emulators to prevent lsusb warnings --- src/clusterfuzz/_internal/platforms/android/adb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/clusterfuzz/_internal/platforms/android/adb.py b/src/clusterfuzz/_internal/platforms/android/adb.py index 7d3c712206b..c0c9ff0feb7 100755 --- a/src/clusterfuzz/_internal/platforms/android/adb.py +++ b/src/clusterfuzz/_internal/platforms/android/adb.py @@ -600,7 +600,7 @@ def _get_device_path_for_usb(): open('/sys/bus/usb/devices/%s/devnum' % device_id).read().strip()) return '/dev/bus/usb/%03d/%03d' % (bus_number, device_number) - if environment.is_android_cuttlefish(): + if environment.is_android_cuttlefish() or environment.is_android_emulator(): return None device_serial = environment.get_value('ANDROID_SERIAL') @@ -612,7 +612,7 @@ def _get_device_path_for_usb(): def reset_usb(): """Reset USB bus for a device serial.""" - if environment.is_android_cuttlefish(): + if environment.is_android_cuttlefish() or environment.is_android_emulator(): # Nothing to do here. return True From 3cb7ef7787419f17c9307b45298ac88fda84a3e2 Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Fri, 19 Jun 2026 18:55:44 +0000 Subject: [PATCH 05/59] Return early from download_trusty_symbols_if_needed if the device is unsupported --- .../_internal/platforms/android/symbols_downloader.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/clusterfuzz/_internal/platforms/android/symbols_downloader.py b/src/clusterfuzz/_internal/platforms/android/symbols_downloader.py index 112d86fc215..fe9c7e43703 100644 --- a/src/clusterfuzz/_internal/platforms/android/symbols_downloader.py +++ b/src/clusterfuzz/_internal/platforms/android/symbols_downloader.py @@ -193,6 +193,7 @@ def download_trusty_symbols_if_needed(symbols_directory, app_name, bid): ab_target = 'slider-fuzz-test-debug' else: logs.error(f'Unsupported device {device}.') + return branch = 'polygon-trusty-whitechapel-master' if not bid: From 07d6bbb9abd98a6f2b8487dc9302b1d0d07f37c1 Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Mon, 22 Jun 2026 18:33:22 +0000 Subject: [PATCH 06/59] Refactor Android APK runner to support standard v2 (base APK + dynamic shared libraries) --- .../_internal/bot/fuzzers/libfuzzer.py | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 1adeecbb152..74e97ae6ad4 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1123,11 +1123,14 @@ def cleanse_crash(self, class AndroidApkLibFuzzerRunner(new_process.UnicodeProcessRunner, LibFuzzerCommon): - """Android APK libFuzzer runner.""" + """Android APK libFuzzer runner (v2 Standard).""" - def __init__(self, executable_path, build_directory, default_args=None): + def __init__(self, apk_path, build_directory, fuzzer_library): super().__init__(executable_path=android.adb.get_adb_path(), default_args=[]) - self.apk_path = executable_path + self.apk_path = apk_path + self.build_directory = build_directory + self.fuzzer_library = fuzzer_library + self.package_name = android.app.get_package_name(self.apk_path) if not self.package_name: raise LibFuzzerError(f'Failed to get package name for {self.apk_path}') @@ -1193,6 +1196,21 @@ def _copy_local_directories_from_device(self, local_directories): android.adb.copy_remote_directory_to_local(device_directory, local_directory) + def _push_libraries_to_device(self): + """Push all shared libraries to the app's private data directory.""" + device_lib_dir = f'/data/data/{self.package_name}/files/' + android.adb.run_as_root() + android.adb.run_shell_command(f'mkdir -p {device_lib_dir}', root=True) + + logs.info(f'Deploying shared libraries to {device_lib_dir}...') + # Find and push all .so files in the build directory + for root, _, files in os.walk(self.build_directory): + for file in files: + if file.endswith('.so'): + local_path = os.path.join(root, file) + device_path = os.path.join(device_lib_dir, file) + android.adb.copy_local_file_to_remote(local_path, device_path) + def fuzz(self, corpus_directories, fuzz_timeout, artifact_prefix=None, additional_args=None, extra_env=None): sync_directories = list(corpus_directories) if artifact_prefix: @@ -1204,10 +1222,17 @@ def fuzz(self, corpus_directories, fuzz_timeout, artifact_prefix=None, additiona if artifact_prefix: artifact_prefix = self._get_device_path(artifact_prefix) + # Deploy the shared libraries to the app's private directory + self._push_libraries_to_device() + fuzzer_args = [] if additional_args: fuzzer_args.extend(additional_args) fuzzer_args.extend(device_corpus_dirs) + + # Inject the library-to-load flag + fuzzer_args.append(f'--library-to-load={self.fuzzer_library}') + fuzzer_args_str = ' '.join(fuzzer_args) if self.instrumentation_runner: @@ -1298,9 +1323,19 @@ def get_runner(fuzzer_path, temp_dir=None, use_minijail=None): raise undercoat.UndercoatError('Instance handle not provided.') runner = FuchsiaUndercoatLibFuzzerRunner(fuzzer_path, instance_handle) elif is_android: - if fuzzer_path.endswith('.apk'): - runner = AndroidApkLibFuzzerRunner(fuzzer_path, build_dir) + base_apk_path = os.path.join(build_dir, 'apks', 'ChromiumFuzzerBase.apk') + if os.path.exists(base_apk_path): + # New standard APK fuzzing v2 + fuzzer_name = os.path.basename(fuzzer_path) + if fuzzer_name.startswith('lib') and fuzzer_name.endswith('__library.so'): + fuzzer_library = fuzzer_name + else: + fuzzer_library = f'lib{fuzzer_name}__library.so' + + logs.info(f'Using standard APK v2 runner for {fuzzer_name}. Base APK: {base_apk_path}, Library: {fuzzer_library}') + runner = AndroidApkLibFuzzerRunner(base_apk_path, build_dir, fuzzer_library) else: + # Non-APK command-line binary fuzzing runner = AndroidLibFuzzerRunner(fuzzer_path, build_dir) else: runner = LibFuzzerRunner(fuzzer_path, cwd=cwd) From 2ee48994e78892b6a7de56463bf1b497bf3063a2 Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Mon, 22 Jun 2026 18:33:22 +0000 Subject: [PATCH 07/59] Refactor Android APK runner to support standard v2 (base APK + dynamic shared libraries) --- .../_internal/bot/fuzzers/libfuzzer.py | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 1adeecbb152..74e97ae6ad4 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1123,11 +1123,14 @@ def cleanse_crash(self, class AndroidApkLibFuzzerRunner(new_process.UnicodeProcessRunner, LibFuzzerCommon): - """Android APK libFuzzer runner.""" + """Android APK libFuzzer runner (v2 Standard).""" - def __init__(self, executable_path, build_directory, default_args=None): + def __init__(self, apk_path, build_directory, fuzzer_library): super().__init__(executable_path=android.adb.get_adb_path(), default_args=[]) - self.apk_path = executable_path + self.apk_path = apk_path + self.build_directory = build_directory + self.fuzzer_library = fuzzer_library + self.package_name = android.app.get_package_name(self.apk_path) if not self.package_name: raise LibFuzzerError(f'Failed to get package name for {self.apk_path}') @@ -1193,6 +1196,21 @@ def _copy_local_directories_from_device(self, local_directories): android.adb.copy_remote_directory_to_local(device_directory, local_directory) + def _push_libraries_to_device(self): + """Push all shared libraries to the app's private data directory.""" + device_lib_dir = f'/data/data/{self.package_name}/files/' + android.adb.run_as_root() + android.adb.run_shell_command(f'mkdir -p {device_lib_dir}', root=True) + + logs.info(f'Deploying shared libraries to {device_lib_dir}...') + # Find and push all .so files in the build directory + for root, _, files in os.walk(self.build_directory): + for file in files: + if file.endswith('.so'): + local_path = os.path.join(root, file) + device_path = os.path.join(device_lib_dir, file) + android.adb.copy_local_file_to_remote(local_path, device_path) + def fuzz(self, corpus_directories, fuzz_timeout, artifact_prefix=None, additional_args=None, extra_env=None): sync_directories = list(corpus_directories) if artifact_prefix: @@ -1204,10 +1222,17 @@ def fuzz(self, corpus_directories, fuzz_timeout, artifact_prefix=None, additiona if artifact_prefix: artifact_prefix = self._get_device_path(artifact_prefix) + # Deploy the shared libraries to the app's private directory + self._push_libraries_to_device() + fuzzer_args = [] if additional_args: fuzzer_args.extend(additional_args) fuzzer_args.extend(device_corpus_dirs) + + # Inject the library-to-load flag + fuzzer_args.append(f'--library-to-load={self.fuzzer_library}') + fuzzer_args_str = ' '.join(fuzzer_args) if self.instrumentation_runner: @@ -1298,9 +1323,19 @@ def get_runner(fuzzer_path, temp_dir=None, use_minijail=None): raise undercoat.UndercoatError('Instance handle not provided.') runner = FuchsiaUndercoatLibFuzzerRunner(fuzzer_path, instance_handle) elif is_android: - if fuzzer_path.endswith('.apk'): - runner = AndroidApkLibFuzzerRunner(fuzzer_path, build_dir) + base_apk_path = os.path.join(build_dir, 'apks', 'ChromiumFuzzerBase.apk') + if os.path.exists(base_apk_path): + # New standard APK fuzzing v2 + fuzzer_name = os.path.basename(fuzzer_path) + if fuzzer_name.startswith('lib') and fuzzer_name.endswith('__library.so'): + fuzzer_library = fuzzer_name + else: + fuzzer_library = f'lib{fuzzer_name}__library.so' + + logs.info(f'Using standard APK v2 runner for {fuzzer_name}. Base APK: {base_apk_path}, Library: {fuzzer_library}') + runner = AndroidApkLibFuzzerRunner(base_apk_path, build_dir, fuzzer_library) else: + # Non-APK command-line binary fuzzing runner = AndroidLibFuzzerRunner(fuzzer_path, build_dir) else: runner = LibFuzzerRunner(fuzzer_path, cwd=cwd) From d8670837a98fb7e7a39f7f3c189f9eacded845dc Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Mon, 22 Jun 2026 18:48:38 +0000 Subject: [PATCH 08/59] Fix base_apk_path lookup in Android runner to search recursively --- src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 74e97ae6ad4..44dba6f1037 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1323,8 +1323,14 @@ def get_runner(fuzzer_path, temp_dir=None, use_minijail=None): raise undercoat.UndercoatError('Instance handle not provided.') runner = FuchsiaUndercoatLibFuzzerRunner(fuzzer_path, instance_handle) elif is_android: - base_apk_path = os.path.join(build_dir, 'apks', 'ChromiumFuzzerBase.apk') - if os.path.exists(base_apk_path): + # Find ChromiumFuzzerBase.apk dynamically under build_dir + base_apk_path = None + for root, _, files in os.walk(build_dir): + if 'ChromiumFuzzerBase.apk' in files: + base_apk_path = os.path.join(root, 'ChromiumFuzzerBase.apk') + break + + if base_apk_path: # New standard APK fuzzing v2 fuzzer_name = os.path.basename(fuzzer_path) if fuzzer_name.startswith('lib') and fuzzer_name.endswith('__library.so'): From e4dbe016a4e58f2ab72b71614ad666128180d8de Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Mon, 22 Jun 2026 18:48:38 +0000 Subject: [PATCH 09/59] Fix base_apk_path lookup in Android runner to search recursively --- src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 74e97ae6ad4..44dba6f1037 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1323,8 +1323,14 @@ def get_runner(fuzzer_path, temp_dir=None, use_minijail=None): raise undercoat.UndercoatError('Instance handle not provided.') runner = FuchsiaUndercoatLibFuzzerRunner(fuzzer_path, instance_handle) elif is_android: - base_apk_path = os.path.join(build_dir, 'apks', 'ChromiumFuzzerBase.apk') - if os.path.exists(base_apk_path): + # Find ChromiumFuzzerBase.apk dynamically under build_dir + base_apk_path = None + for root, _, files in os.walk(build_dir): + if 'ChromiumFuzzerBase.apk' in files: + base_apk_path = os.path.join(root, 'ChromiumFuzzerBase.apk') + break + + if base_apk_path: # New standard APK fuzzing v2 fuzzer_name = os.path.basename(fuzzer_path) if fuzzer_name.startswith('lib') and fuzzer_name.endswith('__library.so'): From 0a35af444770513152beb3ac3a1fa3e3ad344067 Mon Sep 17 00:00:00 2001 From: Ivan Barba Date: Thu, 18 Jun 2026 16:48:27 -0600 Subject: [PATCH 10/59] Adds support for --no-streaming flag in Android ASan jobs (#5332) ## Overview: This PR ensures that Android APKs are installed using the `--no-streaming` flag during ASan fuzzing jobs. Modern Android versions default to streaming installation, which prevents the bundled wrap.sh script (responsible for setting up the ASan library environment) from triggering correctly. This change dynamically detects ASan environments and applies the `--no-streaming` workaround. We still left intact the legacy `asan_device_setup.sh` logic in case any other `haiku` fuzzer job depends on it For more details for the `wrap.sh` script see the [NDK doc](https://developer.android.com/ndk/guides/wrap-script) and the [ASan docs](https://developer.android.com/ndk/guides/asan) ## Changes: - `app.py`: Updated the install method signature to accept and forward additional flags trough `**kwargs`. - `device.py`: Added a private helper to identify ASan jobs or device environments options, and to then pass the `--no-streaming` flag during installation. - Added new unit tests for this --- .../_internal/platforms/android/app.py | 22 +++++++++-- .../_internal/platforms/android/device.py | 25 +++++++++++- .../tests/core/platforms/android/app_test.py | 33 ++++++++++++++++ .../core/platforms/android/device_test.py | 39 +++++++++++++++++++ 4 files changed, 115 insertions(+), 4 deletions(-) diff --git a/src/clusterfuzz/_internal/platforms/android/app.py b/src/clusterfuzz/_internal/platforms/android/app.py index f90a0d0cb95..396782f5e9e 100644 --- a/src/clusterfuzz/_internal/platforms/android/app.py +++ b/src/clusterfuzz/_internal/platforms/android/app.py @@ -93,9 +93,25 @@ def get_package_name(apk_path=None): return match.group(1) -def install(package_apk_path): - """Install a package from an apk path.""" - return adb.run_command(['install', '-r', package_apk_path]) +def install(package_apk_path: str, **kwargs): + """Install a package from an apk path. + + Args: + package_apk_path: Path to the apk file to install. + **kwargs: Additional arguments to pass to the install command. + """ + cmd = ['install', '-r'] + for key, value in kwargs.items(): + if not value: + continue + + flag = '-' + key if len(key) == 1 else '--' + key.replace('_', '-') + cmd.append(flag) + if not isinstance(value, bool): + cmd.append(str(value)) + + cmd.append(package_apk_path) + return adb.run_command(cmd) def is_installed(package_name): diff --git a/src/clusterfuzz/_internal/platforms/android/device.py b/src/clusterfuzz/_internal/platforms/android/device.py index 074aa569b47..34ca44171ff 100755 --- a/src/clusterfuzz/_internal/platforms/android/device.py +++ b/src/clusterfuzz/_internal/platforms/android/device.py @@ -352,6 +352,29 @@ def initialize_environment(): environment.set_value('LOG_TASK_TIMES', True) +def _needs_no_streaming_for_asan() -> bool: + """ + Asan setup requires non-streaming installation because streaming + installation prevents wrap.sh from being invoked on modern Android + versions which is necessary for ASan to function correctly. + + Returns: + bool: True if the job or device requires ASan, False otherwise. + """ + is_asan_job = False + job_name = environment.get_value('JOB_NAME') + if job_name: + memory_tool = environment.get_memory_tool_name(job_name) + if memory_tool == 'ASAN': + is_asan_job = True + + is_asan_device = ( + environment.get_value('ASAN_DEVICE_SETUP') or + settings.get_sanitizer_tool_name() == 'asan') + + return is_asan_job or is_asan_device + + def install_application_if_needed(apk_path, force_update): """Install application package if it does not exist on device or if force_update is set.""" @@ -377,7 +400,7 @@ def install_application_if_needed(apk_path, force_update): # package list or force_update flag has been set. if force_update or not app.is_installed(package_name): app.uninstall(package_name) - app.install(apk_path) + app.install(apk_path, no_streaming=_needs_no_streaming_for_asan()) if not app.is_installed(package_name): logs.error('Package %s was not installed successfully.' % package_name) diff --git a/src/clusterfuzz/_internal/tests/core/platforms/android/app_test.py b/src/clusterfuzz/_internal/tests/core/platforms/android/app_test.py index 9f5e4f54693..7095042c150 100644 --- a/src/clusterfuzz/_internal/tests/core/platforms/android/app_test.py +++ b/src/clusterfuzz/_internal/tests/core/platforms/android/app_test.py @@ -14,10 +14,13 @@ """Tests for app functions.""" import os +from unittest import mock +from unittest import TestCase from clusterfuzz._internal.platforms.android import app from clusterfuzz._internal.system import environment from clusterfuzz._internal.tests.test_libs import android_helpers +from clusterfuzz._internal.tests.test_libs import helpers class IsInstalledTest(android_helpers.AndroidTest): @@ -61,3 +64,33 @@ def test_apk_path_in_arg(self): """Test apk path passed as argument.""" self.assertEqual( app.get_package_name(self.test_apk_path), self.test_apk_pkg_name) + + +class InstallTest(TestCase): + """Tests install.""" + + def setUp(self): + super().setUp() + helpers.patch_environ(self) + self.run_command_patcher = mock.patch( + 'clusterfuzz._internal.platforms.android.adb.run_command') + self.mock_run_command = self.run_command_patcher.start() + self.addCleanup(self.run_command_patcher.stop) + + def test_install_normal(self): + """Test normal installation without any additional flags.""" + app.install('/path/to/app.apk') + self.mock_run_command.assert_called_once_with( + ['install', '-r', '/path/to/app.apk']) + + def test_install_with_additional_flags(self): + """Test installation with additional flags.""" + app.install('/path/to/app.apk', g=True, t=True) + self.mock_run_command.assert_called_once_with( + ['install', '-r', '-g', '-t', '/path/to/app.apk']) + + def test_install_with_valued_flags(self): + """Test installation with flags that take string/numeric values.""" + app.install('/path/to/app.apk', abi='x86', no_streaming=True) + self.mock_run_command.assert_called_once_with( + ['install', '-r', '--abi', 'x86', '--no-streaming', '/path/to/app.apk']) diff --git a/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py b/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py index 9c6eef0b715..b79c3a36be5 100644 --- a/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py +++ b/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py @@ -16,6 +16,7 @@ import unittest from clusterfuzz._internal.platforms.android import device +from clusterfuzz._internal.system import environment from clusterfuzz._internal.tests.test_libs import android_helpers from clusterfuzz._internal.tests.test_libs import helpers @@ -81,3 +82,41 @@ def test_uworker_bypass(self): self.mock.is_uworker.return_value = True device.add_test_accounts_if_needed() self.mock.get_value.assert_not_called() + + +class NeedsNoStreamingForAsanTest(unittest.TestCase): + """Tests _needs_no_streaming_for_asan.""" + + # pylint: disable=protected-access + + def setUp(self): + super().setUp() + helpers.patch_environ(self) + helpers.patch(self, [ + 'clusterfuzz._internal.platforms.android.settings.get_sanitizer_tool_name', + ]) + self.mock.get_sanitizer_tool_name.return_value = None + + def test_normal_job(self): + """Test outcome for a normal job.""" + self.assertFalse(device._needs_no_streaming_for_asan()) + + def test_asan_job(self): + """Test outcome for an ASan job.""" + environment.set_value('JOB_NAME', 'android_asan_job') + self.assertTrue(device._needs_no_streaming_for_asan()) + + def test_hwasan_job(self): + """Test outcome for a non ASAN job like HWASan.""" + environment.set_value('JOB_NAME', 'android_hwasan_job') + self.assertFalse(device._needs_no_streaming_for_asan()) + + def test_asan_device_env(self): + """Test outcome for an ASan device via ASAN_DEVICE_SETUP env.""" + environment.set_value('ASAN_DEVICE_SETUP', True) + self.assertTrue(device._needs_no_streaming_for_asan()) + + def test_asan_device_flavor(self): + """Test outcome for an ASan device via settings build flavor.""" + self.mock.get_sanitizer_tool_name.return_value = 'asan' + self.assertTrue(device._needs_no_streaming_for_asan()) From e8a3902b666f3b32aac70f9ed9b238a46ea8c761 Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Tue, 23 Jun 2026 00:59:07 +0000 Subject: [PATCH 11/59] Optimize Android emulator runs by inverting backwards fastboot checks and quieting false-positive Trusty symbol downloader errors --- src/clusterfuzz/_internal/platforms/android/adb.py | 5 ++++- .../_internal/platforms/android/symbols_downloader.py | 11 ++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/clusterfuzz/_internal/platforms/android/adb.py b/src/clusterfuzz/_internal/platforms/android/adb.py index c0c9ff0feb7..de845270f57 100755 --- a/src/clusterfuzz/_internal/platforms/android/adb.py +++ b/src/clusterfuzz/_internal/platforms/android/adb.py @@ -225,7 +225,10 @@ def get_adb_path(): def get_device_state(): """Return the device status.""" - if environment.is_android_emulator(): + # Emulators do not support the fastboot protocol and cannot enter physical ramdump mode. + # The original check was backwards (checking emulator instead of physical devices), causing a 20-second + # fastboot timeout on every state check on emulators. We correct this to only run fastboot checks on physical devices. + if not environment.is_android_emulator(): fastboot_state = run_fastboot_command( ['getvar', 'is-ramdump-mode'], timeout=GET_DEVICE_STATE_TIMEOUT) if fastboot_state and 'is-ramdump-mode: yes' in fastboot_state: diff --git a/src/clusterfuzz/_internal/platforms/android/symbols_downloader.py b/src/clusterfuzz/_internal/platforms/android/symbols_downloader.py index fe9c7e43703..2d44368dbab 100644 --- a/src/clusterfuzz/_internal/platforms/android/symbols_downloader.py +++ b/src/clusterfuzz/_internal/platforms/android/symbols_downloader.py @@ -192,7 +192,16 @@ def download_trusty_symbols_if_needed(symbols_directory, app_name, bid): elif 'oriole' in device or 'raven' in device or 'bluejay' in device: ab_target = 'slider-fuzz-test-debug' else: - logs.error(f'Unsupported device {device}.') + # Emulators (like sdk_gdesktop) do not run the physical Trusty Secure OS TEE. + # Therefore, no official Trusty symbols exist for them at the moment in GCS. Instead of logging a confusing + # ERROR that can be mistaken for a fuzz task failure, we log a verbose INFO message and return early. + if environment.is_android_emulator() or 'sdk_gdesktop' in device or 'emulator' in device: + logs.info( + f'Skipping Trusty symbols download for emulator device "{device}". ' + 'Virtual/emulator devices do not run a physical Trusty Trusted Execution Environment (TEE), ' + 'so no official Trusty symbol archives are available or needed.') + else: + logs.error(f'Unsupported device {device}.') return branch = 'polygon-trusty-whitechapel-master' From b79ba62db30142bdceeb1de0cb103f92d013a34b Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Tue, 23 Jun 2026 00:59:07 +0000 Subject: [PATCH 12/59] Optimize Android emulator runs by inverting backwards fastboot checks and quieting false-positive Trusty symbol downloader errors --- src/clusterfuzz/_internal/platforms/android/adb.py | 5 ++++- .../_internal/platforms/android/symbols_downloader.py | 11 ++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/clusterfuzz/_internal/platforms/android/adb.py b/src/clusterfuzz/_internal/platforms/android/adb.py index c0c9ff0feb7..de845270f57 100755 --- a/src/clusterfuzz/_internal/platforms/android/adb.py +++ b/src/clusterfuzz/_internal/platforms/android/adb.py @@ -225,7 +225,10 @@ def get_adb_path(): def get_device_state(): """Return the device status.""" - if environment.is_android_emulator(): + # Emulators do not support the fastboot protocol and cannot enter physical ramdump mode. + # The original check was backwards (checking emulator instead of physical devices), causing a 20-second + # fastboot timeout on every state check on emulators. We correct this to only run fastboot checks on physical devices. + if not environment.is_android_emulator(): fastboot_state = run_fastboot_command( ['getvar', 'is-ramdump-mode'], timeout=GET_DEVICE_STATE_TIMEOUT) if fastboot_state and 'is-ramdump-mode: yes' in fastboot_state: diff --git a/src/clusterfuzz/_internal/platforms/android/symbols_downloader.py b/src/clusterfuzz/_internal/platforms/android/symbols_downloader.py index fe9c7e43703..2d44368dbab 100644 --- a/src/clusterfuzz/_internal/platforms/android/symbols_downloader.py +++ b/src/clusterfuzz/_internal/platforms/android/symbols_downloader.py @@ -192,7 +192,16 @@ def download_trusty_symbols_if_needed(symbols_directory, app_name, bid): elif 'oriole' in device or 'raven' in device or 'bluejay' in device: ab_target = 'slider-fuzz-test-debug' else: - logs.error(f'Unsupported device {device}.') + # Emulators (like sdk_gdesktop) do not run the physical Trusty Secure OS TEE. + # Therefore, no official Trusty symbols exist for them at the moment in GCS. Instead of logging a confusing + # ERROR that can be mistaken for a fuzz task failure, we log a verbose INFO message and return early. + if environment.is_android_emulator() or 'sdk_gdesktop' in device or 'emulator' in device: + logs.info( + f'Skipping Trusty symbols download for emulator device "{device}". ' + 'Virtual/emulator devices do not run a physical Trusty Trusted Execution Environment (TEE), ' + 'so no official Trusty symbol archives are available or needed.') + else: + logs.error(f'Unsupported device {device}.') return branch = 'polygon-trusty-whitechapel-master' From fe463067e8a2a47da4f78f57d8a61ab0db0a71a8 Mon Sep 17 00:00:00 2001 From: Javan Lacerda Date: Fri, 19 Jun 2026 10:35:54 -0300 Subject: [PATCH 13/59] Skip Region Load Checking in `is_remote_task` (#5331) This change adds a way to skip external region load queries (`get_region_load`) when we only want to verify if a task is configured to run remotely (e.g. via `is_remote_task` / `is_remote_utask`), preventing unnecessary external calls and speed issues. ## Changes ### 1. Added `should_check_regions` Parameter to `_get_subconfig` * **File:** [service.py](file:///usr/local/google/home/javanlacerda/repos/clusterfuzz/src/clusterfuzz/_internal/batch/service.py) * **Change:** Introduced an optional `should_check_regions` (defaulting to `True`) argument. When it is `False` or no `queue_check_regions` are configured, we skip checking region workloads entirely, log the reason in a single consolidated log statement, and immediately return a weighted subconfig. ### 2. Propagated in `_get_specs_from_config` * **File:** [service.py](file:///usr/local/google/home/javanlacerda/repos/clusterfuzz/src/clusterfuzz/_internal/batch/service.py) * **Change:** Propagated `should_check_regions` to the `_get_subconfig` helper. ### 3. Disabled Region Checks in `is_remote_task` * **File:** [service.py](file:///usr/local/google/home/javanlacerda/repos/clusterfuzz/src/clusterfuzz/_internal/batch/service.py) * **Change:** Passed `should_check_regions=False` when calling `_get_specs_from_config` inside `is_remote_task` since workload size does not affect task remote configuration validation. Signed-off-by: Javan Lacerda --- src/clusterfuzz/_internal/batch/service.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/clusterfuzz/_internal/batch/service.py b/src/clusterfuzz/_internal/batch/service.py index 0721d94e717..db933aa8761 100644 --- a/src/clusterfuzz/_internal/batch/service.py +++ b/src/clusterfuzz/_internal/batch/service.py @@ -256,7 +256,8 @@ def is_remote_task(command: str, job_name: str) -> bool: """ try: _get_specs_from_config( - [remote_task_types.RemoteTask(command, job_name, None)]) + [remote_task_types.RemoteTask(command, job_name, None)], + should_check_regions=False) return True except ValueError: return False @@ -300,15 +301,15 @@ def _get_config_names(batch_tasks: List[remote_task_types.RemoteTask]): return config_map -def _get_subconfig(batch_config, instance_spec): +def _get_subconfig(batch_config, instance_spec, should_check_regions=True): all_subconfigs = batch_config.get('subconfigs', {}) instance_subconfigs = instance_spec['subconfigs'] queue_check_regions = batch_config.get('queue_check_regions') - if not queue_check_regions: - logs.info( - 'Skipping batch load check because queue_check_regions is not configured.' - ) + if not should_check_regions or not queue_check_regions: + logs.info('Skipping batch load check. ' + f'should_check_regions: {should_check_regions}, ' + f'queue_check_regions: {queue_check_regions}') weighted_subconfigs = [ WeightedSubconfig(subconfig['name'], subconfig['weight']) for subconfig in instance_subconfigs @@ -343,8 +344,8 @@ def _get_subconfig(batch_config, instance_spec): return all_subconfigs[chosen_name] -def _get_specs_from_config( - batch_tasks: List[remote_task_types.RemoteTask]) -> Dict: +def _get_specs_from_config(batch_tasks: List[remote_task_types.RemoteTask], + should_check_regions: bool = True) -> Dict: """Gets the configured specifications for a batch workload.""" if not batch_tasks: return {} @@ -382,7 +383,8 @@ def _get_specs_from_config( # This saves us time and reduces fragementation, e.g. every linux fuzz task # run in this call will run in the same zone. if config_name not in subconfig_map: - subconfig = _get_subconfig(batch_config, instance_spec) + subconfig = _get_subconfig(batch_config, instance_spec, + should_check_regions) subconfig_map[config_name] = subconfig should_retry = instance_spec.get('retry', False) From 8e2828fa4852922833439ad1cc763f5db31d3189 Mon Sep 17 00:00:00 2001 From: Matheus Hunsche Date: Tue, 16 Jun 2026 19:30:42 +0000 Subject: [PATCH 14/59] Migrate Android Build API usage to V4 OnePlatform under feature flag The legacy Android Build API V3 (androidbuildinternal.googleapis.com) is being sunsetted. This CL migrates the client calls to the new V4 Private API (androidbuild-pa.googleapis.com) using standard discovery. Changes: - Added a 'use_android_build_api_v4' Datastore Config property to allow runtime feature toggle. - Refactored 'fetch_artifact.py' to support V4 OnePlatform endpoint and resource names dynamically behind the feature flag. - Migrated legacy logs to structured dictionary-based logs with '[AndroidBuildAPI]' tagging for easier Cloud Logging filtering. --- .../_internal/datastore/data_types.py | 3 + .../platforms/android/fetch_artifact.py | 233 +++++++++++++++--- 2 files changed, 207 insertions(+), 29 deletions(-) diff --git a/src/clusterfuzz/_internal/datastore/data_types.py b/src/clusterfuzz/_internal/datastore/data_types.py index e648a58bea3..bb1fe3e7aee 100644 --- a/src/clusterfuzz/_internal/datastore/data_types.py +++ b/src/clusterfuzz/_internal/datastore/data_types.py @@ -883,6 +883,9 @@ class Config(Model): # functional bugs. relax_security_bug_restrictions = ndb.BooleanProperty(default=False) + # Flag to use the V4 Android Build API instead of V3. + use_android_build_api_v4 = ndb.BooleanProperty(default=False) + # Coverage reports bucket. coverage_reports_bucket = ndb.StringProperty(default='') diff --git a/src/clusterfuzz/_internal/platforms/android/fetch_artifact.py b/src/clusterfuzz/_internal/platforms/android/fetch_artifact.py index 4aaf7f40aad..808be3bfda4 100644 --- a/src/clusterfuzz/_internal/platforms/android/fetch_artifact.py +++ b/src/clusterfuzz/_internal/platforms/android/fetch_artifact.py @@ -48,6 +48,23 @@ "gs://android-haiku/target-cuttlefish/stable_build_info.json") +def _use_v4(): + """Return True if we should use V4 Android Build API.""" + try: + use_v4 = db_config.get_value('use_android_build_api_v4') or False + logs.info( + 'AndroidBuildAPI feature flag status read.', + use_android_build_api_v4=use_v4 + ) + return use_v4 + except Exception as e: + logs.error( + 'AndroidBuildAPI error reading feature flag use_android_build_api_v4. Defaulting to False.', + error=str(e) + ) + return False + + def execute_request_with_retries(request): """Executes request and retries on failure.""" result = None @@ -68,24 +85,71 @@ def download_artifact(client, bid, target, attempt_id, name, output_directory, logs.info('artifact to download: %s' % name) logs.info('output_directory: %s' % output_directory) logs.info('output_filename: %s' % output_filename) - artifact_query = client.buildartifact().get( - buildId=bid, target=target, attemptId=attempt_id, resourceId=name) + + version_tag = 'V4' if _use_v4() else 'V3' + logs.info( + 'AndroidBuildAPI download_artifact started.', + api_version=version_tag, + operation='download_artifact', + build_id=bid, + target=target, + attempt_id=attempt_id, + artifact_name=name + ) + + if _use_v4(): + artifact_query = client.buildartifacts().get( + buildId=bid, target=target, attemptId=attempt_id, resourceId=name) + else: + artifact_query = client.buildartifact().get( + buildId=bid, target=target, attemptId=attempt_id, resourceId=name) artifact = execute_request_with_retries(artifact_query) if artifact is None: - logs.error(f'Artifact unreachable with name {name}, target {target} ' - f'and build id {bid}.') + logs.error( + 'AndroidBuildAPI download_artifact failed: artifact metadata unreachable.', + api_version=version_tag, + operation='download_artifact', + build_id=bid, + target=target, + attempt_id=attempt_id, + artifact_name=name, + status='failed' + ) return None # Lucky us, we always have the size. size = int(artifact['size']) + logs.info( + 'AndroidBuildAPI download_artifact metadata retrieved successfully.', + api_version=version_tag, + operation='download_artifact', + build_id=bid, + target=target, + attempt_id=attempt_id, + artifact_name=name, + size=size + ) chunksize = -1 if size >= DEFAULT_CHUNK_SIZE: chunksize = DEFAULT_CHUNK_SIZE # Just like get, except get_media. - dl_request = client.buildartifact().get_media( - buildId=bid, target=target, attemptId=attempt_id, resourceId=name) + logs.info( + 'AndroidBuildAPI download_artifact media download started.', + api_version=version_tag, + operation='download_artifact_media', + build_id=bid, + target=target, + attempt_id=attempt_id, + artifact_name=name + ) + if _use_v4(): + dl_request = client.buildartifacts().get_media( + buildId=bid, target=target, attemptId=attempt_id, resourceId=name) + else: + dl_request = client.buildartifact().get_media( + buildId=bid, target=target, attemptId=attempt_id, resourceId=name) if output_filename: file_name = output_filename @@ -95,7 +159,17 @@ def download_artifact(client, bid, target, attempt_id, name, output_directory, output_path = os.path.join(output_directory, file_name) # If the artifact already exists, then bail out. if os.path.exists(output_path) and os.path.getsize(output_path) == size: - logs.info('Artifact %s already exists, skipping download.' % name) + logs.info( + 'AndroidBuildAPI download_artifact skipped (file already exists).', + api_version=version_tag, + operation='download_artifact', + build_id=bid, + target=target, + attempt_id=attempt_id, + artifact_name=name, + output_path=output_path, + status='skipped_exists' + ) return output_path logs.info('Downloading artifact %s.' % name) @@ -118,6 +192,17 @@ def download_artifact(client, bid, target, attempt_id, name, output_directory, percent_completed = (size_completed * 100.0) / size logs.info('%.1f%% complete.' % percent_completed) + logs.info( + 'AndroidBuildAPI download_artifact completed successfully.', + api_version=version_tag, + operation='download_artifact', + build_id=bid, + target=target, + attempt_id=attempt_id, + artifact_name=name, + output_path=output_path, + status='success' + ) return output_path @@ -127,16 +212,39 @@ def get_artifacts_for_build(client, attempt_id: str = 'latest', regexp: Optional[str] = None) -> List[str]: """Return list of artifacts for a given build.""" - if not regexp: - request = client.buildartifact().list( - buildId=bid, target=target, attemptId=attempt_id) + version_tag = 'V4' if _use_v4() else 'V3' + logs.info( + 'AndroidBuildAPI get_artifacts_for_build started.', + api_version=version_tag, + operation='get_artifacts_for_build', + build_id=bid, + target=target, + attempt_id=attempt_id, + regexp=regexp + ) + + if _use_v4(): + if not regexp: + request = client.buildartifacts().list( + buildId=bid, target=target, attemptId=attempt_id) + else: + request = client.buildartifacts().list( + buildId=bid, + target=target, + attemptId=attempt_id, + nameRegexp=regexp, + maxResults=100) else: - request = client.buildartifact().list( - buildId=bid, - target=target, - attemptId=attempt_id, - nameRegexp=regexp, - maxResults=100) + if not regexp: + request = client.buildartifact().list( + buildId=bid, target=target, attemptId=attempt_id) + else: + request = client.buildartifact().list( + buildId=bid, + target=target, + attemptId=attempt_id, + nameRegexp=regexp, + maxResults=100) request_str = (f'{request.uri}, {request.method}, ' f'{request.body}, {request.methodId}') @@ -151,7 +259,22 @@ def get_artifacts_for_build(client, if result and 'artifacts' in result: for artifact in result['artifacts']: artifacts.append(artifact) - request = client.buildartifact().list_next(request, result) + if _use_v4(): + request = client.buildartifacts().list_next(request, result) + else: + request = client.buildartifact().list_next(request, result) + + logs.info( + 'AndroidBuildAPI get_artifacts_for_build completed.', + api_version=version_tag, + operation='get_artifacts_for_build', + build_id=bid, + target=target, + attempt_id=attempt_id, + regexp=regexp, + artifacts_count=len(artifacts), + status='success' if artifacts else 'empty' + ) if not artifacts: logs.error(f'No artifact found for target {target}, build id {bid}.\n' @@ -174,11 +297,27 @@ def get_client(): credentials = ServiceAccountCredentials.from_json_keyfile_dict( json.loads(build_apiary_service_account_private_key), scopes='https://www.googleapis.com/auth/androidbuild.internal') - client = apiclient.discovery.build( - 'androidbuildinternal', - 'v3', - credentials=credentials, - static_discovery=False) + if _use_v4(): + logs.info( + 'AndroidBuildAPI client initialization started.', + api_version='V4' + ) + client = apiclient.discovery.build( + 'androidbuildinternal', + 'v4', + discoveryServiceUrl='https://androidbuild-pa.googleapis.com/$discovery/rest?version=v4', + credentials=credentials, + static_discovery=False) + else: + logs.info( + 'AndroidBuildAPI client initialization started.', + api_version='V3' + ) + client = apiclient.discovery.build( + 'androidbuildinternal', + 'v3', + credentials=credentials, + static_discovery=False) return client @@ -218,25 +357,61 @@ def get_latest_artifact_info(branch, target, signed=False, stable_build=False): if 'bid' in build_info and build_info['bid'] != '0': return build_info - request = client.build().list( # pylint: disable=no-member - buildType='submitted', + version_tag = 'V4' if _use_v4() else 'V3' + logs.info( + 'AndroidBuildAPI get_latest_artifact_info started.', + api_version=version_tag, + operation='get_latest_artifact_info', branch=branch, target=target, - successful=True, - maxResults=1, - signed=signed) + signed=signed + ) + if _use_v4(): + request = client.builds().list( # pylint: disable=no-member + buildType='submitted', + branch=branch, + target=target, + successful=True, + maxResults=1, + signed=signed) + else: + request = client.build().list( # pylint: disable=no-member + buildType='submitted', + branch=branch, + target=target, + successful=True, + maxResults=1, + signed=signed) request_str = (f'{request.uri}, {request.method}, ' f'{request.body}, {request.methodId}') builds = execute_request_with_retries(request) if not builds: - logs.error(f'No build found for target {target}, branch {branch}, ' - f'request: {request_str}.') + logs.error( + 'AndroidBuildAPI get_latest_artifact_info failed: no builds found.', + api_version=version_tag, + operation='get_latest_artifact_info', + branch=branch, + target=target, + signed=signed, + status='failed' + ) return None build = builds['builds'][0] bid = build['buildId'] target = build['target']['name'] + + logs.info( + 'AndroidBuildAPI get_latest_artifact_info completed.', + api_version=version_tag, + operation='get_latest_artifact_info', + branch=branch, + target=target, + signed=signed, + build_id=bid, + status='success' + ) return {'bid': bid, 'branch': branch, 'target': target} From e6cd75c3dd1d91c7878ac1089492df74d5d9fb7e Mon Sep 17 00:00:00 2001 From: Matheus Hunsche Date: Tue, 16 Jun 2026 20:13:23 +0000 Subject: [PATCH 15/59] refactor: reformat logging calls and add V4 API migration guide --- .../platforms/android/fetch_artifact.py | 58 +++++++------------ 1 file changed, 21 insertions(+), 37 deletions(-) diff --git a/src/clusterfuzz/_internal/platforms/android/fetch_artifact.py b/src/clusterfuzz/_internal/platforms/android/fetch_artifact.py index 808be3bfda4..7c37fef2406 100644 --- a/src/clusterfuzz/_internal/platforms/android/fetch_artifact.py +++ b/src/clusterfuzz/_internal/platforms/android/fetch_artifact.py @@ -54,14 +54,12 @@ def _use_v4(): use_v4 = db_config.get_value('use_android_build_api_v4') or False logs.info( 'AndroidBuildAPI feature flag status read.', - use_android_build_api_v4=use_v4 - ) + use_android_build_api_v4=use_v4) return use_v4 except Exception as e: logs.error( 'AndroidBuildAPI error reading feature flag use_android_build_api_v4. Defaulting to False.', - error=str(e) - ) + error=str(e)) return False @@ -85,7 +83,7 @@ def download_artifact(client, bid, target, attempt_id, name, output_directory, logs.info('artifact to download: %s' % name) logs.info('output_directory: %s' % output_directory) logs.info('output_filename: %s' % output_filename) - + version_tag = 'V4' if _use_v4() else 'V3' logs.info( 'AndroidBuildAPI download_artifact started.', @@ -94,9 +92,8 @@ def download_artifact(client, bid, target, attempt_id, name, output_directory, build_id=bid, target=target, attempt_id=attempt_id, - artifact_name=name - ) - + artifact_name=name) + if _use_v4(): artifact_query = client.buildartifacts().get( buildId=bid, target=target, attemptId=attempt_id, resourceId=name) @@ -113,8 +110,7 @@ def download_artifact(client, bid, target, attempt_id, name, output_directory, target=target, attempt_id=attempt_id, artifact_name=name, - status='failed' - ) + status='failed') return None # Lucky us, we always have the size. @@ -127,8 +123,7 @@ def download_artifact(client, bid, target, attempt_id, name, output_directory, target=target, attempt_id=attempt_id, artifact_name=name, - size=size - ) + size=size) chunksize = -1 if size >= DEFAULT_CHUNK_SIZE: @@ -142,8 +137,7 @@ def download_artifact(client, bid, target, attempt_id, name, output_directory, build_id=bid, target=target, attempt_id=attempt_id, - artifact_name=name - ) + artifact_name=name) if _use_v4(): dl_request = client.buildartifacts().get_media( buildId=bid, target=target, attemptId=attempt_id, resourceId=name) @@ -168,8 +162,7 @@ def download_artifact(client, bid, target, attempt_id, name, output_directory, attempt_id=attempt_id, artifact_name=name, output_path=output_path, - status='skipped_exists' - ) + status='skipped_exists') return output_path logs.info('Downloading artifact %s.' % name) @@ -201,8 +194,7 @@ def download_artifact(client, bid, target, attempt_id, name, output_directory, attempt_id=attempt_id, artifact_name=name, output_path=output_path, - status='success' - ) + status='success') return output_path @@ -220,9 +212,8 @@ def get_artifacts_for_build(client, build_id=bid, target=target, attempt_id=attempt_id, - regexp=regexp - ) - + regexp=regexp) + if _use_v4(): if not regexp: request = client.buildartifacts().list( @@ -273,8 +264,7 @@ def get_artifacts_for_build(client, attempt_id=attempt_id, regexp=regexp, artifacts_count=len(artifacts), - status='success' if artifacts else 'empty' - ) + status='success' if artifacts else 'empty') if not artifacts: logs.error(f'No artifact found for target {target}, build id {bid}.\n' @@ -299,20 +289,17 @@ def get_client(): scopes='https://www.googleapis.com/auth/androidbuild.internal') if _use_v4(): logs.info( - 'AndroidBuildAPI client initialization started.', - api_version='V4' - ) + 'AndroidBuildAPI client initialization started.', api_version='V4') client = apiclient.discovery.build( 'androidbuildinternal', 'v4', - discoveryServiceUrl='https://androidbuild-pa.googleapis.com/$discovery/rest?version=v4', + discoveryServiceUrl= + 'https://androidbuild-pa.googleapis.com/$discovery/rest?version=v4', credentials=credentials, static_discovery=False) else: logs.info( - 'AndroidBuildAPI client initialization started.', - api_version='V3' - ) + 'AndroidBuildAPI client initialization started.', api_version='V3') client = apiclient.discovery.build( 'androidbuildinternal', 'v3', @@ -364,8 +351,7 @@ def get_latest_artifact_info(branch, target, signed=False, stable_build=False): operation='get_latest_artifact_info', branch=branch, target=target, - signed=signed - ) + signed=signed) if _use_v4(): request = client.builds().list( # pylint: disable=no-member buildType='submitted', @@ -394,14 +380,13 @@ def get_latest_artifact_info(branch, target, signed=False, stable_build=False): branch=branch, target=target, signed=signed, - status='failed' - ) + status='failed') return None build = builds['builds'][0] bid = build['buildId'] target = build['target']['name'] - + logs.info( 'AndroidBuildAPI get_latest_artifact_info completed.', api_version=version_tag, @@ -410,8 +395,7 @@ def get_latest_artifact_info(branch, target, signed=False, stable_build=False): target=target, signed=signed, build_id=bid, - status='success' - ) + status='success') return {'bid': bid, 'branch': branch, 'target': target} From d971f33199fa0e48ddb9a7f2bd0a0bc05860903c Mon Sep 17 00:00:00 2001 From: Dylan Jew Date: Tue, 23 Jun 2026 08:47:07 -0400 Subject: [PATCH 16/59] Format, add todo, fix lint --- src/clusterfuzz/_internal/datastore/data_types.py | 1 + .../_internal/platforms/android/fetch_artifact.py | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/clusterfuzz/_internal/datastore/data_types.py b/src/clusterfuzz/_internal/datastore/data_types.py index bb1fe3e7aee..24ad730341e 100644 --- a/src/clusterfuzz/_internal/datastore/data_types.py +++ b/src/clusterfuzz/_internal/datastore/data_types.py @@ -883,6 +883,7 @@ class Config(Model): # functional bugs. relax_security_bug_restrictions = ndb.BooleanProperty(default=False) + # TODO(b/422775458) - Clean up after completing the migration to V4 # Flag to use the V4 Android Build API instead of V3. use_android_build_api_v4 = ndb.BooleanProperty(default=False) diff --git a/src/clusterfuzz/_internal/platforms/android/fetch_artifact.py b/src/clusterfuzz/_internal/platforms/android/fetch_artifact.py index 7c37fef2406..e86cced06c6 100644 --- a/src/clusterfuzz/_internal/platforms/android/fetch_artifact.py +++ b/src/clusterfuzz/_internal/platforms/android/fetch_artifact.py @@ -58,7 +58,8 @@ def _use_v4(): return use_v4 except Exception as e: logs.error( - 'AndroidBuildAPI error reading feature flag use_android_build_api_v4. Defaulting to False.', + 'AndroidBuildAPI error reading feature flag use_android_build_api_v4. ' + 'Defaulting to False.', error=str(e)) return False @@ -103,7 +104,8 @@ def download_artifact(client, bid, target, attempt_id, name, output_directory, artifact = execute_request_with_retries(artifact_query) if artifact is None: logs.error( - 'AndroidBuildAPI download_artifact failed: artifact metadata unreachable.', + 'AndroidBuildAPI download_artifact failed: artifact metadata ' + 'unreachable.', api_version=version_tag, operation='download_artifact', build_id=bid, @@ -241,6 +243,7 @@ def get_artifacts_for_build(client, f'{request.body}, {request.methodId}') artifacts = [] + results = [] while request: result = execute_request_with_retries(request) @@ -380,7 +383,8 @@ def get_latest_artifact_info(branch, target, signed=False, stable_build=False): branch=branch, target=target, signed=signed, - status='failed') + status='failed', + request_str=request_str) return None build = builds['builds'][0] From 34ed1eddf5f8049b5d6b4f40f9c37ab7194873cc Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Fri, 26 Jun 2026 17:00:52 +0000 Subject: [PATCH 17/59] Support dynamic fuzzer-specific APK lookups ({fuzzer_name}-debug.apk) for standard APK per fuzzer builds --- .../_internal/bot/fuzzers/libfuzzer.py | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 44dba6f1037..b8450ec5b63 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1323,13 +1323,29 @@ def get_runner(fuzzer_path, temp_dir=None, use_minijail=None): raise undercoat.UndercoatError('Instance handle not provided.') runner = FuchsiaUndercoatLibFuzzerRunner(fuzzer_path, instance_handle) elif is_android: - # Find ChromiumFuzzerBase.apk dynamically under build_dir + # Find the APK dynamically under build_dir. + # We first look for a fuzzer-specific APK (e.g. {fuzzer_name}-debug.apk or {fuzzer_name}.apk). + # If not found, we fall back to the generic ChromiumFuzzerBase.apk. + fuzzer_name = os.path.basename(fuzzer_path) base_apk_path = None + + specific_apk_names = [f'{fuzzer_name}-debug.apk', f'{fuzzer_name}.apk'] for root, _, files in os.walk(build_dir): - if 'ChromiumFuzzerBase.apk' in files: - base_apk_path = os.path.join(root, 'ChromiumFuzzerBase.apk') + # Check for specific APK first + for apk_name in specific_apk_names: + if apk_name in files: + base_apk_path = os.path.join(root, apk_name) + break + if base_apk_path: break + if not base_apk_path: + # Fallback to generic base APK + for root, _, files in os.walk(build_dir): + if 'ChromiumFuzzerBase.apk' in files: + base_apk_path = os.path.join(root, 'ChromiumFuzzerBase.apk') + break + if base_apk_path: # New standard APK fuzzing v2 fuzzer_name = os.path.basename(fuzzer_path) From 4e6f900821fc82aa8f7a26495a677c4525b1413f Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Fri, 26 Jun 2026 17:02:54 +0000 Subject: [PATCH 18/59] Simplify Android runner selection to use standard AndroidApkLibFuzzerRunner for .apk targets and AndroidLibFuzzerRunner for binaries --- .../_internal/bot/fuzzers/libfuzzer.py | 67 ++----------------- 1 file changed, 5 insertions(+), 62 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index b8450ec5b63..1adeecbb152 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1123,14 +1123,11 @@ def cleanse_crash(self, class AndroidApkLibFuzzerRunner(new_process.UnicodeProcessRunner, LibFuzzerCommon): - """Android APK libFuzzer runner (v2 Standard).""" + """Android APK libFuzzer runner.""" - def __init__(self, apk_path, build_directory, fuzzer_library): + def __init__(self, executable_path, build_directory, default_args=None): super().__init__(executable_path=android.adb.get_adb_path(), default_args=[]) - self.apk_path = apk_path - self.build_directory = build_directory - self.fuzzer_library = fuzzer_library - + self.apk_path = executable_path self.package_name = android.app.get_package_name(self.apk_path) if not self.package_name: raise LibFuzzerError(f'Failed to get package name for {self.apk_path}') @@ -1196,21 +1193,6 @@ def _copy_local_directories_from_device(self, local_directories): android.adb.copy_remote_directory_to_local(device_directory, local_directory) - def _push_libraries_to_device(self): - """Push all shared libraries to the app's private data directory.""" - device_lib_dir = f'/data/data/{self.package_name}/files/' - android.adb.run_as_root() - android.adb.run_shell_command(f'mkdir -p {device_lib_dir}', root=True) - - logs.info(f'Deploying shared libraries to {device_lib_dir}...') - # Find and push all .so files in the build directory - for root, _, files in os.walk(self.build_directory): - for file in files: - if file.endswith('.so'): - local_path = os.path.join(root, file) - device_path = os.path.join(device_lib_dir, file) - android.adb.copy_local_file_to_remote(local_path, device_path) - def fuzz(self, corpus_directories, fuzz_timeout, artifact_prefix=None, additional_args=None, extra_env=None): sync_directories = list(corpus_directories) if artifact_prefix: @@ -1222,17 +1204,10 @@ def fuzz(self, corpus_directories, fuzz_timeout, artifact_prefix=None, additiona if artifact_prefix: artifact_prefix = self._get_device_path(artifact_prefix) - # Deploy the shared libraries to the app's private directory - self._push_libraries_to_device() - fuzzer_args = [] if additional_args: fuzzer_args.extend(additional_args) fuzzer_args.extend(device_corpus_dirs) - - # Inject the library-to-load flag - fuzzer_args.append(f'--library-to-load={self.fuzzer_library}') - fuzzer_args_str = ' '.join(fuzzer_args) if self.instrumentation_runner: @@ -1323,41 +1298,9 @@ def get_runner(fuzzer_path, temp_dir=None, use_minijail=None): raise undercoat.UndercoatError('Instance handle not provided.') runner = FuchsiaUndercoatLibFuzzerRunner(fuzzer_path, instance_handle) elif is_android: - # Find the APK dynamically under build_dir. - # We first look for a fuzzer-specific APK (e.g. {fuzzer_name}-debug.apk or {fuzzer_name}.apk). - # If not found, we fall back to the generic ChromiumFuzzerBase.apk. - fuzzer_name = os.path.basename(fuzzer_path) - base_apk_path = None - - specific_apk_names = [f'{fuzzer_name}-debug.apk', f'{fuzzer_name}.apk'] - for root, _, files in os.walk(build_dir): - # Check for specific APK first - for apk_name in specific_apk_names: - if apk_name in files: - base_apk_path = os.path.join(root, apk_name) - break - if base_apk_path: - break - - if not base_apk_path: - # Fallback to generic base APK - for root, _, files in os.walk(build_dir): - if 'ChromiumFuzzerBase.apk' in files: - base_apk_path = os.path.join(root, 'ChromiumFuzzerBase.apk') - break - - if base_apk_path: - # New standard APK fuzzing v2 - fuzzer_name = os.path.basename(fuzzer_path) - if fuzzer_name.startswith('lib') and fuzzer_name.endswith('__library.so'): - fuzzer_library = fuzzer_name - else: - fuzzer_library = f'lib{fuzzer_name}__library.so' - - logs.info(f'Using standard APK v2 runner for {fuzzer_name}. Base APK: {base_apk_path}, Library: {fuzzer_library}') - runner = AndroidApkLibFuzzerRunner(base_apk_path, build_dir, fuzzer_library) + if fuzzer_path.endswith('.apk'): + runner = AndroidApkLibFuzzerRunner(fuzzer_path, build_dir) else: - # Non-APK command-line binary fuzzing runner = AndroidLibFuzzerRunner(fuzzer_path, build_dir) else: runner = LibFuzzerRunner(fuzzer_path, cwd=cwd) From 4004302860579e0e8fa238ce663c5a05fc235622 Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Fri, 26 Jun 2026 17:22:54 +0000 Subject: [PATCH 19/59] Fix Android APK runner selection by dynamically resolving fuzzer-specific APK paths in get_runner --- .../_internal/bot/fuzzers/libfuzzer.py | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 1adeecbb152..dd22da2012d 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1298,9 +1298,26 @@ def get_runner(fuzzer_path, temp_dir=None, use_minijail=None): raise undercoat.UndercoatError('Instance handle not provided.') runner = FuchsiaUndercoatLibFuzzerRunner(fuzzer_path, instance_handle) elif is_android: - if fuzzer_path.endswith('.apk'): - runner = AndroidApkLibFuzzerRunner(fuzzer_path, build_dir) + # Find the APK dynamically under build_dir. + # We first look for a fuzzer-specific APK (e.g. {fuzzer_name}-debug.apk or {fuzzer_name}.apk). + fuzzer_name = os.path.basename(fuzzer_path) + base_apk_path = None + + specific_apk_names = [f'{fuzzer_name}-debug.apk', f'{fuzzer_name}.apk'] + for root, _, files in os.walk(build_dir): + # Check for specific APK first + for apk_name in specific_apk_names: + if apk_name in files: + base_apk_path = os.path.join(root, apk_name) + break + if base_apk_path: + break + + if base_apk_path: + logs.info(f'Using Android APK runner for {fuzzer_name}. APK path: {base_apk_path}') + runner = AndroidApkLibFuzzerRunner(base_apk_path, build_dir) else: + logs.info(f'Using Android command-line binary runner for {fuzzer_name}. Path: {fuzzer_path}') runner = AndroidLibFuzzerRunner(fuzzer_path, build_dir) else: runner = LibFuzzerRunner(fuzzer_path, cwd=cwd) From 29d31bc5c8ab8b6bd056afcccb059f087ccde4dc Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Fri, 26 Jun 2026 17:49:56 +0000 Subject: [PATCH 20/59] Add FORCE_FUZZ_TARGET environment override support to fuzz_task.py to allow forcing a specific fuzzer target during preprocessing --- src/clusterfuzz/_internal/bot/tasks/utasks/fuzz_task.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/clusterfuzz/_internal/bot/tasks/utasks/fuzz_task.py b/src/clusterfuzz/_internal/bot/tasks/utasks/fuzz_task.py index 1936a8e3ba6..739aff29627 100644 --- a/src/clusterfuzz/_internal/bot/tasks/utasks/fuzz_task.py +++ b/src/clusterfuzz/_internal/bot/tasks/utasks/fuzz_task.py @@ -2253,6 +2253,11 @@ def _get_or_create_fuzz_target(engine_name, fuzz_target_binary, job_type): def _preprocess_get_fuzz_target(fuzzer_name, job_type): + force_target = environment.get_value('FORCE_FUZZ_TARGET') + if force_target: + logs.info(f'Forcing fuzz target selection: {force_target}') + return _get_or_create_fuzz_target(fuzzer_name, force_target, job_type) + fuzz_target_name = _pick_fuzz_target() if fuzz_target_name: return _get_or_create_fuzz_target(fuzzer_name, fuzz_target_name, job_type) From 0cbc78422acae37a59f5fb98f23a37ee08d0d65c Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Fri, 26 Jun 2026 19:03:58 +0000 Subject: [PATCH 21/59] Revert FORCE_FUZZ_TARGET developer override in fuzz_task.py to keep production scheduling clean --- src/clusterfuzz/_internal/bot/tasks/utasks/fuzz_task.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/tasks/utasks/fuzz_task.py b/src/clusterfuzz/_internal/bot/tasks/utasks/fuzz_task.py index 739aff29627..1936a8e3ba6 100644 --- a/src/clusterfuzz/_internal/bot/tasks/utasks/fuzz_task.py +++ b/src/clusterfuzz/_internal/bot/tasks/utasks/fuzz_task.py @@ -2253,11 +2253,6 @@ def _get_or_create_fuzz_target(engine_name, fuzz_target_binary, job_type): def _preprocess_get_fuzz_target(fuzzer_name, job_type): - force_target = environment.get_value('FORCE_FUZZ_TARGET') - if force_target: - logs.info(f'Forcing fuzz target selection: {force_target}') - return _get_or_create_fuzz_target(fuzzer_name, force_target, job_type) - fuzz_target_name = _pick_fuzz_target() if fuzz_target_name: return _get_or_create_fuzz_target(fuzzer_name, fuzz_target_name, job_type) From c6bcf3f89d08cbd6521ee08e58aaf97fd3d2b4df Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Fri, 26 Jun 2026 19:57:13 +0000 Subject: [PATCH 22/59] Log fuzzer run output and logcat in libfuzzer.py to make full execution details visible in Cloud Logging --- src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index dd22da2012d..b2769b6ff14 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1225,6 +1225,7 @@ def fuzz(self, corpus_directories, fuzz_timeout, artifact_prefix=None, additiona max_stdout_len=MAX_OUTPUT_LEN) result.output = f'{result.output}\n\nLogcat:\n{android.logger.log_output()}' + logs.info(f'Fuzzer run output for {self.package_name}:\n{result.output}') self._copy_local_directories_from_device(sync_directories) return result From aaac306120adcdc4aef9f2faf5a99a77b1b4bcb0 Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Fri, 26 Jun 2026 20:39:51 +0000 Subject: [PATCH 23/59] Implement native fuzzer stdout capture using StdoutFile redirection and retrieval on Android APK runner --- .../_internal/bot/fuzzers/libfuzzer.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index b2769b6ff14..4e012db323a 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1211,9 +1211,16 @@ def fuzz(self, corpus_directories, fuzz_timeout, artifact_prefix=None, additiona fuzzer_args_str = ' '.join(fuzzer_args) if self.instrumentation_runner: - args = ['shell', 'am', 'instrument', '-w', '-e', 'org.chromium.native_test.NativeTest.CommandLineFlags', f'"{fuzzer_args_str}"', f'{self.package_name}/{self.instrumentation_runner}'] + device_stdout_file = f'/data/data/{self.package_name}/cache/fuzzer_output.txt' + args = [ + 'shell', 'am', 'instrument', '-w', + '-e', 'org.chromium.native_test.NativeTest.CommandLineFlags', f'"{fuzzer_args_str}"', + '-e', 'org.chromium.native_test.NativeTestInstrumentationTestRunner.StdoutFile', device_stdout_file, + f'{self.package_name}/{self.instrumentation_runner}' + ] logs.info(f'Starting Instrumentation: {self.package_name}/{self.instrumentation_runner} with args: {fuzzer_args_str}') elif self.launchable_activity: + device_stdout_file = None args = ['shell', 'am', 'start', '-n', f'{self.package_name}/{self.launchable_activity}', '-e', 'org.chromium.native_test.NativeTest.CommandLineFlags', f'"{fuzzer_args_str}"'] logs.info(f'Starting APK: {self.package_name}/{self.launchable_activity} with args: {fuzzer_args_str}') else: @@ -1224,6 +1231,12 @@ def fuzz(self, corpus_directories, fuzz_timeout, artifact_prefix=None, additiona timeout=self.get_total_timeout(fuzz_timeout), max_stdout_len=MAX_OUTPUT_LEN) + if device_stdout_file: + fuzzer_output = android.adb.run_shell_command(f'cat {device_stdout_file}') + android.adb.remove_file(device_stdout_file) + if fuzzer_output: + result.output = f'{fuzzer_output}\n\n{result.output}' + result.output = f'{result.output}\n\nLogcat:\n{android.logger.log_output()}' logs.info(f'Fuzzer run output for {self.package_name}:\n{result.output}') self._copy_local_directories_from_device(sync_directories) From a4bd4c7a3fb5d1f7d568878c5b39faada0da6951 Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Fri, 26 Jun 2026 20:40:35 +0000 Subject: [PATCH 24/59] Add dedicated logs.info for fuzzer native C++ stdout in libfuzzer.py --- src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 4e012db323a..8a67f6428fe 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1235,6 +1235,7 @@ def fuzz(self, corpus_directories, fuzz_timeout, artifact_prefix=None, additiona fuzzer_output = android.adb.run_shell_command(f'cat {device_stdout_file}') android.adb.remove_file(device_stdout_file) if fuzzer_output: + logs.info(f'Fuzzer native stdout:\n{fuzzer_output}') result.output = f'{fuzzer_output}\n\n{result.output}' result.output = f'{result.output}\n\nLogcat:\n{android.logger.log_output()}' From a756d6347628d0e42258651f572dc37f60b58023 Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Fri, 26 Jun 2026 22:39:13 +0000 Subject: [PATCH 25/59] Change device stdout redirect file path to the root of the app's private folder to ensure existence --- src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 8a67f6428fe..01573827ad9 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1211,7 +1211,7 @@ def fuzz(self, corpus_directories, fuzz_timeout, artifact_prefix=None, additiona fuzzer_args_str = ' '.join(fuzzer_args) if self.instrumentation_runner: - device_stdout_file = f'/data/data/{self.package_name}/cache/fuzzer_output.txt' + device_stdout_file = f'/data/data/{self.package_name}/fuzzer_output.txt' args = [ 'shell', 'am', 'instrument', '-w', '-e', 'org.chromium.native_test.NativeTest.CommandLineFlags', f'"{fuzzer_args_str}"', From 74feaa11ad5450b15cd0604d4335310e0f7525fa Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Fri, 26 Jun 2026 23:03:11 +0000 Subject: [PATCH 26/59] Pre-create app cache directory and set wide permissions on device before running instrumentation to guarantee native stdout write success --- src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 01573827ad9..8d3114f64a1 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1211,7 +1211,10 @@ def fuzz(self, corpus_directories, fuzz_timeout, artifact_prefix=None, additiona fuzzer_args_str = ' '.join(fuzzer_args) if self.instrumentation_runner: - device_stdout_file = f'/data/data/{self.package_name}/fuzzer_output.txt' + device_cache_dir = f'/data/data/{self.package_name}/cache' + android.adb.run_shell_command(f'mkdir -p {device_cache_dir}', root=True) + android.adb.run_shell_command(f'chmod 777 {device_cache_dir}', root=True) + device_stdout_file = os.path.join(device_cache_dir, 'fuzzer_output.txt') args = [ 'shell', 'am', 'instrument', '-w', '-e', 'org.chromium.native_test.NativeTest.CommandLineFlags', f'"{fuzzer_args_str}"', From e3e5508e548702a8d79228a1c034727c6f81da40 Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Fri, 26 Jun 2026 23:27:54 +0000 Subject: [PATCH 27/59] Remove root=True to avoid su root compatibility errors on emulator, and enable directory creation logging --- src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 8d3114f64a1..c1449fd68c8 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1212,8 +1212,8 @@ def fuzz(self, corpus_directories, fuzz_timeout, artifact_prefix=None, additiona if self.instrumentation_runner: device_cache_dir = f'/data/data/{self.package_name}/cache' - android.adb.run_shell_command(f'mkdir -p {device_cache_dir}', root=True) - android.adb.run_shell_command(f'chmod 777 {device_cache_dir}', root=True) + android.adb.run_shell_command(f'mkdir -p {device_cache_dir}', log_output=True) + android.adb.run_shell_command(f'chmod 777 {device_cache_dir}', log_output=True) device_stdout_file = os.path.join(device_cache_dir, 'fuzzer_output.txt') args = [ 'shell', 'am', 'instrument', '-w', From faffa2b41bf8e1736d063ed41302c1a63d782de8 Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Fri, 26 Jun 2026 23:52:41 +0000 Subject: [PATCH 28/59] Use canonical absolute path /data/user/0/ instead of symlinked /data/data/ to bypass Android sandbox restrictions --- src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index c1449fd68c8..6555b491916 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1211,7 +1211,7 @@ def fuzz(self, corpus_directories, fuzz_timeout, artifact_prefix=None, additiona fuzzer_args_str = ' '.join(fuzzer_args) if self.instrumentation_runner: - device_cache_dir = f'/data/data/{self.package_name}/cache' + device_cache_dir = f'/data/user/0/{self.package_name}/cache' android.adb.run_shell_command(f'mkdir -p {device_cache_dir}', log_output=True) android.adb.run_shell_command(f'chmod 777 {device_cache_dir}', log_output=True) device_stdout_file = os.path.join(device_cache_dir, 'fuzzer_output.txt') From c3a3c7c0b0db5e28fd9966dfd8d23355cfd11bd9 Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sat, 27 Jun 2026 00:27:01 +0000 Subject: [PATCH 29/59] Dynamically construct StdoutFile key using resolved runner name to avoid package mismatches --- src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 6555b491916..009f40b1f2a 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1218,7 +1218,7 @@ def fuzz(self, corpus_directories, fuzz_timeout, artifact_prefix=None, additiona args = [ 'shell', 'am', 'instrument', '-w', '-e', 'org.chromium.native_test.NativeTest.CommandLineFlags', f'"{fuzzer_args_str}"', - '-e', 'org.chromium.native_test.NativeTestInstrumentationTestRunner.StdoutFile', device_stdout_file, + '-e', f'{self.instrumentation_runner}.StdoutFile', device_stdout_file, f'{self.package_name}/{self.instrumentation_runner}' ] logs.info(f'Starting Instrumentation: {self.package_name}/{self.instrumentation_runner} with args: {fuzzer_args_str}') From cacff4ed868fd4d58aafbe491609eca654829fb6 Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sat, 27 Jun 2026 00:36:12 +0000 Subject: [PATCH 30/59] Document Remote Swarming Development and Validation Workflow in AGENTS.md --- AGENTS.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 8204672aaec..5d6978bfb1a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -107,3 +107,55 @@ python butler.py format This will format the changed code in your current branch. It's possible to get into a state where linting and formatting contradict each other. In this case, STOP, the human will fix it. + +## Remote Swarming Development & Validation Workflow + +This section outlines the step-by-step cycle for developing, deploying, launching, and verifying changes on remote Swarming bots. + +### Step 1: Local Development & Formatting +Make your code changes in your feature branch. Always run formatting and linting locally before committing: +```bash +pipenv run python butler.py format +pipenv run python butler.py lint +``` + +### Step 2: Push & Deploy to Remote `dev` +For changes to run on remote Swarming bots, they must be committed, merged, and pushed to the remote **`dev`** branch: +1. Commit and push your feature branch: + ```bash + git add + git commit -m "Your description" + git push origin + ``` +2. Merge into `dev` and push: + ```bash + git checkout dev + git pull origin dev + git merge + git push origin dev + git checkout + ``` +3. **⚠️ Crucial Rebuild Wait Time**: After pushing to `dev`, you **MUST wait 20 to 25 minutes** before triggering any Swarming tasks. This gives the remote Google Cloud Storage (GCS) builder enough time to pull your new commit, compile the binaries, and package them into the deployment ZIP bundle (`linux-3.zip`) fetched by the bots. + +### Step 3: Preprocess & Launch the Swarming Task +Once the deployment package has finished rebuilding on GCS: +1. Trigger the preprocess pipeline and launch a new Swarming task: + ```bash + python3 scratch/preprocess_and_launch.py + ``` +2. Note the generated **Swarming Task ID** (e.g. `791f445b26114a10`) and the task URL printed in the stdout. + +### Step 4: Live Monitoring & Log Retrieval +1. **Live Monitor**: Open `scratch/monitor_swarming_task.py`, update `task_id` with your new Task ID, and run the script to stream the live console output: + ```bash + python3 scratch/monitor_swarming_task.py + ``` +2. **Download High-Resolution Logs**: Once the task terminates: + * Look at the live monitor output to identify the **assigned Bot Name** (e.g. `lin-192-g582`) and the final state (`COMPLETED` or `BOT_DIED`). + * Open `scratch/read_logs.py`, update the `bot_name` and adjust the time filter window (e.g. `timestamp >= "YYYY-MM-DDTHH:MM:SSZ"`), then run: + ```bash + pipenv run python scratch/read_logs.py + ``` + * This downloads all high-resolution Stackdriver bot and logcat streams into `scratch/bot_logs.txt`. +3. **Analyze**: Inspect `scratch/bot_logs.txt` using grep/editors to verify your changes (such as JNI prints or fuzzer loop outputs). + From b2565ccb844cce37731a68afe3f1d4ece41f0bcc Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sat, 27 Jun 2026 00:58:44 +0000 Subject: [PATCH 31/59] Pass both legacy and dynamic StdoutFile intent keys to ensure compatibility across Chromium runner classes --- src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 009f40b1f2a..d85874b77fa 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1218,6 +1218,7 @@ def fuzz(self, corpus_directories, fuzz_timeout, artifact_prefix=None, additiona args = [ 'shell', 'am', 'instrument', '-w', '-e', 'org.chromium.native_test.NativeTest.CommandLineFlags', f'"{fuzzer_args_str}"', + '-e', 'org.chromium.native_test.NativeTestInstrumentationTestRunner.StdoutFile', device_stdout_file, '-e', f'{self.instrumentation_runner}.StdoutFile', device_stdout_file, f'{self.package_name}/{self.instrumentation_runner}' ] From 238b09a88ffb1c2cedf605f24b757bc4483ceac3 Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sat, 27 Jun 2026 01:14:06 +0000 Subject: [PATCH 32/59] Implement full Android C++ dependency resolution and execution flow --- .../_internal/bot/fuzzers/libfuzzer.py | 233 ++++++++++++++++-- 1 file changed, 208 insertions(+), 25 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index d85874b77fa..90afaec91b7 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -20,6 +20,7 @@ import random import re import shutil +import subprocess from clusterfuzz._internal.base import utils from clusterfuzz._internal.bot import testcase_manager @@ -1122,21 +1123,159 @@ def cleanse_crash(self, return result -class AndroidApkLibFuzzerRunner(new_process.UnicodeProcessRunner, LibFuzzerCommon): +class AndroidApkLibFuzzerRunner(new_process.UnicodeProcessRunner, + LibFuzzerCommon): """Android APK libFuzzer runner.""" def __init__(self, executable_path, build_directory, default_args=None): - super().__init__(executable_path=android.adb.get_adb_path(), default_args=[]) + super().__init__( + executable_path=android.adb.get_adb_path(), default_args=[]) self.apk_path = executable_path self.package_name = android.app.get_package_name(self.apk_path) if not self.package_name: raise LibFuzzerError(f'Failed to get package name for {self.apk_path}') android.app.install(self.apk_path) - self.instrumentation_runner = self._get_instrumentation_runner(self.apk_path) + self.instrumentation_runner = self._get_instrumentation_runner( + self.apk_path) self.launchable_activity = self._get_launchable_activity(self.apk_path) + self.fuzzer_name = os.path.basename(self.apk_path).replace( + '-debug.apk', '').replace('.apk', '') + self.library_under_test = None + self.auxiliary_libraries = None + + self._setup_dependencies(build_directory) + + def _setup_dependencies(self, build_directory): + """Resolve, sort, and push C++ dependencies to the device.""" + deps_filename = f"{self.fuzzer_name}__test_runner_script.runtime_deps" + deps_file_path = None + for root, _, files in os.walk(build_directory): + if deps_filename in files: + deps_file_path = os.path.join(root, deps_filename) + break + + if not deps_file_path: + logs.info(f"No runtime_deps file found for {self.fuzzer_name}, " + "skipping dependency setup.") + return + + logs.info(f"Found runtime_deps at: {deps_file_path}") + + # Determine base output dir (parent of gen.runtime) + base_dir = deps_file_path + while True: + parent = os.path.dirname(base_dir) + if parent == base_dir: + break + if os.path.basename(base_dir) == 'gen.runtime': + base_dir = parent + break + base_dir = parent + + # Read .so files from runtime_deps + libs = [] + with open(deps_file_path, "r") as f: + for line in f: + line = line.strip() + if line.endswith(".so"): + abs_path = os.path.abspath(os.path.join(base_dir, line)) + if os.path.exists(abs_path): + libs.append(abs_path) + else: + logs.warning(f"Dependency file {abs_path} does not exist.") + + if not libs: + return + + # Parse dependencies for each library + dependencies_dict = {} + for lib in libs: + lib_name = os.path.basename(lib) + dependencies_dict[lib_name] = self._parse_needed_libs(lib) + + # Sort libraries topologically + sorted_libs = self._topological_sort(libs, dependencies_dict) + + # Prepare device directory + android.adb.run_shell_command('rm -rf /sdcard/chromium_tests_root && ' + 'mkdir -p /sdcard/chromium_tests_root') + + # Push sorted .so files to device + for lib in sorted_libs: + device_path = os.path.join('/sdcard/chromium_tests_root', + os.path.basename(lib)) + android.adb.copy_local_file_to_remote(lib, device_path) + + # Separate main library and auxiliary libraries + main_lib_name = f"lib{self.fuzzer_name}__library.so" + aux_libs = [] + for lib in sorted_libs: + name = os.path.basename(lib) + if name == main_lib_name: + self.library_under_test = name + else: + aux_libs.append(name) + + if aux_libs: + self.auxiliary_libraries = ','.join(aux_libs) + + # Grant MANAGE_EXTERNAL_STORAGE permission + android.adb.run_shell_command( + f'appops set {self.package_name} MANAGE_EXTERNAL_STORAGE allow') + logs.info(f"Dependency setup complete. LibraryUnderTest: " + f"{self.library_under_test}, AuxiliaryLibraries: " + f"{self.auxiliary_libraries}") + + def _parse_needed_libs(self, so_path): + """Parse NEEDED libraries using readelf.""" + try: + res = subprocess.run( + ["readelf", "-d", so_path], capture_output=True, check=True) + output = res.stdout.decode('utf-8') + needed = [] + for line in output.splitlines(): + if "(NEEDED)" in line: + match = re.search(r"Shared library: \[(.*?)\]", line) + if match: + needed.append(match.group(1)) + return needed + except Exception as e: + logs.error(f"Error reading ELF headers for {so_path}: {e}") + return [] + + def _topological_sort(self, libs, dependencies_dict): + """Perform topological sort on libs based on dependencies_dict.""" + visited = {} + result = [] + + def visit(lib): + lib_name = os.path.basename(lib) + if visited.get(lib_name) == "visiting": + return + if visited.get(lib_name) == "visited": + return + + visited[lib_name] = "visiting" + for dep in dependencies_dict.get(lib_name, []): + dep_path = None + for l in libs: + if os.path.basename(l) == dep: + dep_path = l + break + if dep_path: + visit(dep_path) + + visited[lib_name] = "visited" + result.append(lib) + + for lib in libs: + visit(lib) + return result + def _get_launchable_activity(self, apk_path): + """Return the launchable activity name.""" aapt_binary_path = os.path.join( environment.get_platform_resources_directory(), 'aapt') aapt_command = '%s dump badging %s' % (aapt_binary_path, apk_path) @@ -1149,13 +1288,15 @@ def _get_launchable_activity(self, apk_path): return None def _get_instrumentation_runner(self, apk_path): + """Return the instrumentation runner.""" aapt_binary_path = os.path.join( environment.get_platform_resources_directory(), 'aapt') - aapt_command = '%s dump xmltree %s AndroidManifest.xml' % (aapt_binary_path, apk_path) + aapt_command = '%s dump xmltree %s AndroidManifest.xml' % (aapt_binary_path, + apk_path) output = android.adb.execute_command(aapt_command, timeout=60) if not output: return None - + lines = output.splitlines() found_instrumentation = False for line in lines: @@ -1193,7 +1334,12 @@ def _copy_local_directories_from_device(self, local_directories): android.adb.copy_remote_directory_to_local(device_directory, local_directory) - def fuzz(self, corpus_directories, fuzz_timeout, artifact_prefix=None, additional_args=None, extra_env=None): + def fuzz(self, + corpus_directories, + fuzz_timeout, + artifact_prefix=None, + additional_args=None, + extra_env=None): sync_directories = list(corpus_directories) if artifact_prefix: sync_directories.append(artifact_prefix) @@ -1212,43 +1358,76 @@ def fuzz(self, corpus_directories, fuzz_timeout, artifact_prefix=None, additiona if self.instrumentation_runner: device_cache_dir = f'/data/user/0/{self.package_name}/cache' - android.adb.run_shell_command(f'mkdir -p {device_cache_dir}', log_output=True) - android.adb.run_shell_command(f'chmod 777 {device_cache_dir}', log_output=True) + android.adb.run_shell_command( + f'mkdir -p {device_cache_dir}', log_output=True) + android.adb.run_shell_command( + f'chmod 777 {device_cache_dir}', log_output=True) device_stdout_file = os.path.join(device_cache_dir, 'fuzzer_output.txt') args = [ - 'shell', 'am', 'instrument', '-w', - '-e', 'org.chromium.native_test.NativeTest.CommandLineFlags', f'"{fuzzer_args_str}"', - '-e', 'org.chromium.native_test.NativeTestInstrumentationTestRunner.StdoutFile', device_stdout_file, - '-e', f'{self.instrumentation_runner}.StdoutFile', device_stdout_file, - f'{self.package_name}/{self.instrumentation_runner}' + 'shell', + 'am', + 'instrument', + '-w', + '-e', + 'org.chromium.native_test.NativeTest.CommandLineFlags', + f'"{fuzzer_args_str}"', + '-e', + 'org.chromium.native_test.NativeTestInstrumentationTestRunner.' + 'StdoutFile', + device_stdout_file, + '-e', + f'{self.instrumentation_runner}.StdoutFile', + device_stdout_file, ] - logs.info(f'Starting Instrumentation: {self.package_name}/{self.instrumentation_runner} with args: {fuzzer_args_str}') + + if self.library_under_test: + args.extend([ + '-e', + 'org.chromium.native_test.NativeTestInstrumentationTestRunner.' + 'LibraryUnderTest', self.library_under_test + ]) + if self.auxiliary_libraries: + args.extend([ + '-e', + 'org.chromium.native_test.NativeTestInstrumentationTestRunner.' + 'AuxiliaryLibraries', self.auxiliary_libraries + ]) + + args.append(f'{self.package_name}/{self.instrumentation_runner}') + logs.info(f'Starting Instrumentation: {self.package_name}/' + f'{self.instrumentation_runner} with args: {fuzzer_args_str}') elif self.launchable_activity: device_stdout_file = None - args = ['shell', 'am', 'start', '-n', f'{self.package_name}/{self.launchable_activity}', '-e', 'org.chromium.native_test.NativeTest.CommandLineFlags', f'"{fuzzer_args_str}"'] - logs.info(f'Starting APK: {self.package_name}/{self.launchable_activity} with args: {fuzzer_args_str}') + args = [ + 'shell', 'am', 'start', '-n', + f'{self.package_name}/{self.launchable_activity}', '-e', + 'org.chromium.native_test.NativeTest.CommandLineFlags', + f'"{fuzzer_args_str}"' + ] + logs.info(f'Starting APK: {self.package_name}/{self.launchable_activity} ' + f'with args: {fuzzer_args_str}') else: raise LibFuzzerError('No launchable activity or instrumentation found.') - + result = self.run_and_wait( additional_args=args, timeout=self.get_total_timeout(fuzz_timeout), max_stdout_len=MAX_OUTPUT_LEN) - + if device_stdout_file: fuzzer_output = android.adb.run_shell_command(f'cat {device_stdout_file}') android.adb.remove_file(device_stdout_file) if fuzzer_output: logs.info(f'Fuzzer native stdout:\n{fuzzer_output}') result.output = f'{fuzzer_output}\n\n{result.output}' - - result.output = f'{result.output}\n\nLogcat:\n{android.logger.log_output()}' + + result.output = ( + f'{result.output}\n\nLogcat:\n{android.logger.log_output()}') logs.info(f'Fuzzer run output for {self.package_name}:\n{result.output}') self._copy_local_directories_from_device(sync_directories) return result - def get_runner(fuzzer_path, temp_dir=None, use_minijail=None): """Get a libfuzzer runner.""" if use_minijail is None: @@ -1321,7 +1500,7 @@ def get_runner(fuzzer_path, temp_dir=None, use_minijail=None): # We first look for a fuzzer-specific APK (e.g. {fuzzer_name}-debug.apk or {fuzzer_name}.apk). fuzzer_name = os.path.basename(fuzzer_path) base_apk_path = None - + specific_apk_names = [f'{fuzzer_name}-debug.apk', f'{fuzzer_name}.apk'] for root, _, files in os.walk(build_dir): # Check for specific APK first @@ -1331,12 +1510,16 @@ def get_runner(fuzzer_path, temp_dir=None, use_minijail=None): break if base_apk_path: break - + if base_apk_path: - logs.info(f'Using Android APK runner for {fuzzer_name}. APK path: {base_apk_path}') + logs.info( + f'Using Android APK runner for {fuzzer_name}. APK path: {base_apk_path}' + ) runner = AndroidApkLibFuzzerRunner(base_apk_path, build_dir) else: - logs.info(f'Using Android command-line binary runner for {fuzzer_name}. Path: {fuzzer_path}') + logs.info( + f'Using Android command-line binary runner for {fuzzer_name}. Path: {fuzzer_path}' + ) runner = AndroidLibFuzzerRunner(fuzzer_path, build_dir) else: runner = LibFuzzerRunner(fuzzer_path, cwd=cwd) From c441487a4a84d8c90bcb35f63821547cee2e403e Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sat, 27 Jun 2026 01:38:05 +0000 Subject: [PATCH 33/59] Add fallback for stripped libraries in Android dependency resolution --- src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 90afaec91b7..11393eceb78 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1184,7 +1184,14 @@ def _setup_dependencies(self, build_directory): if os.path.exists(abs_path): libs.append(abs_path) else: - logs.warning(f"Dependency file {abs_path} does not exist.") + # Fallback: check if the library exists in the base_dir (stripped) + fallback_path = os.path.join(base_dir, os.path.basename(line)) + if os.path.exists(fallback_path): + libs.append(fallback_path) + else: + logs.warning( + f"Dependency file {abs_path} (and fallback {fallback_path}) " + "does not exist.") if not libs: return From e6fe8c2301175d95a3c89d82e67a77c74efbd10c Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sat, 27 Jun 2026 02:04:38 +0000 Subject: [PATCH 34/59] Add NativeUnitTestActivity extra to am instrument command --- .../_internal/bot/fuzzers/libfuzzer.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 11393eceb78..41390496b37 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1385,6 +1385,9 @@ def fuzz(self, '-e', f'{self.instrumentation_runner}.StdoutFile', device_stdout_file, + '-e', + 'org.chromium.native_test.NativeUnitTestActivity', + 'org.chromium.native_test.NativeUnitTestActivity', ] if self.library_under_test: @@ -1504,7 +1507,8 @@ def get_runner(fuzzer_path, temp_dir=None, use_minijail=None): runner = FuchsiaUndercoatLibFuzzerRunner(fuzzer_path, instance_handle) elif is_android: # Find the APK dynamically under build_dir. - # We first look for a fuzzer-specific APK (e.g. {fuzzer_name}-debug.apk or {fuzzer_name}.apk). + # We first look for a fuzzer-specific APK (e.g. {fuzzer_name}-debug.apk + # or {fuzzer_name}.apk). fuzzer_name = os.path.basename(fuzzer_path) base_apk_path = None @@ -1519,14 +1523,12 @@ def get_runner(fuzzer_path, temp_dir=None, use_minijail=None): break if base_apk_path: - logs.info( - f'Using Android APK runner for {fuzzer_name}. APK path: {base_apk_path}' - ) + logs.info(f'Using Android APK runner for {fuzzer_name}. ' + f'APK path: {base_apk_path}') runner = AndroidApkLibFuzzerRunner(base_apk_path, build_dir) else: - logs.info( - f'Using Android command-line binary runner for {fuzzer_name}. Path: {fuzzer_path}' - ) + logs.info(f'Using Android command-line binary runner for {fuzzer_name}. ' + f'Path: {fuzzer_path}') runner = AndroidLibFuzzerRunner(fuzzer_path, build_dir) else: runner = LibFuzzerRunner(fuzzer_path, cwd=cwd) From ca8d3d9edc4f8e33127e5f9f49c61e04436130ca Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sat, 27 Jun 2026 02:29:29 +0000 Subject: [PATCH 35/59] Reorder am instrument arguments to put CommandLineFlags at the end --- src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 41390496b37..7b8d9b5a02f 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1376,8 +1376,8 @@ def fuzz(self, 'instrument', '-w', '-e', - 'org.chromium.native_test.NativeTest.CommandLineFlags', - f'"{fuzzer_args_str}"', + 'org.chromium.native_test.NativeUnitTestActivity', + 'org.chromium.native_test.NativeUnitTestActivity', '-e', 'org.chromium.native_test.NativeTestInstrumentationTestRunner.' 'StdoutFile', @@ -1385,9 +1385,6 @@ def fuzz(self, '-e', f'{self.instrumentation_runner}.StdoutFile', device_stdout_file, - '-e', - 'org.chromium.native_test.NativeUnitTestActivity', - 'org.chromium.native_test.NativeUnitTestActivity', ] if self.library_under_test: @@ -1403,6 +1400,12 @@ def fuzz(self, 'AuxiliaryLibraries', self.auxiliary_libraries ]) + args.extend([ + '-e', + 'org.chromium.native_test.NativeTest.CommandLineFlags', + f'"{fuzzer_args_str}"', + ]) + args.append(f'{self.package_name}/{self.instrumentation_runner}') logs.info(f'Starting Instrumentation: {self.package_name}/' f'{self.instrumentation_runner} with args: {fuzzer_args_str}') From 6947115d0cd60976f604ebbe3ea3c4a0c9b23c94 Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sat, 27 Jun 2026 02:57:00 +0000 Subject: [PATCH 36/59] Add debug logging for adb command and output --- src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 7b8d9b5a02f..218ecf436d8 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1427,6 +1427,10 @@ def fuzz(self, timeout=self.get_total_timeout(fuzz_timeout), max_stdout_len=MAX_OUTPUT_LEN) + logs.info(f'DEBUG: adb command run: {result.command}') + logs.info(f'DEBUG: adb command return code: {result.return_code}') + logs.info(f'DEBUG: adb command output: {result.output}') + if device_stdout_file: fuzzer_output = android.adb.run_shell_command(f'cat {device_stdout_file}') android.adb.remove_file(device_stdout_file) From be73cd4c343aa0674207854e44b9855ce5fbcdda Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sat, 27 Jun 2026 03:24:01 +0000 Subject: [PATCH 37/59] Add NativeTestActivity extras to am instrument command --- src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 218ecf436d8..abb0650d7c0 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1379,6 +1379,13 @@ def fuzz(self, 'org.chromium.native_test.NativeUnitTestActivity', 'org.chromium.native_test.NativeUnitTestActivity', '-e', + f'{self.instrumentation_runner}.NativeTestActivity', + 'org.chromium.native_test.NativeUnitTestActivity', + '-e', + 'org.chromium.native_test.NativeTestInstrumentationTestRunner.' + 'NativeTestActivity', + 'org.chromium.native_test.NativeUnitTestActivity', + '-e', 'org.chromium.native_test.NativeTestInstrumentationTestRunner.' 'StdoutFile', device_stdout_file, From 422f0febfe8a73f76c982d31a3b8a8afc4f911fc Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sat, 27 Jun 2026 03:50:12 +0000 Subject: [PATCH 38/59] Force ASan wrapper to run via setprop --- .../_internal/bot/fuzzers/libfuzzer.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index abb0650d7c0..0a9fb6fff2e 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1429,10 +1429,19 @@ def fuzz(self, else: raise LibFuzzerError('No launchable activity or instrumentation found.') - result = self.run_and_wait( - additional_args=args, - timeout=self.get_total_timeout(fuzz_timeout), - max_stdout_len=MAX_OUTPUT_LEN) + # Force ASan wrapper to run on userdebug devices. + # We use the extracted wrap.sh in the app's lib directory. + android.adb.run_shell_command( + f'setprop wrap.{self.package_name} /data/data/{self.package_name}/lib/wrap.sh' + ) + try: + result = self.run_and_wait( + additional_args=args, + timeout=self.get_total_timeout(fuzz_timeout), + max_stdout_len=MAX_OUTPUT_LEN) + finally: + # Clear the wrapper property. + android.adb.run_shell_command(f'setprop wrap.{self.package_name} ""') logs.info(f'DEBUG: adb command run: {result.command}') logs.info(f'DEBUG: adb command return code: {result.return_code}') From f16a4ea17f16d2f603667078703274d4f14c8663 Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sat, 27 Jun 2026 04:15:30 +0000 Subject: [PATCH 39/59] Add debug prints for wrap.sh path --- src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 0a9fb6fff2e..a2e56c14bd1 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1429,6 +1429,20 @@ def fuzz(self, else: raise LibFuzzerError('No launchable activity or instrumentation found.') + # Debug paths to diagnose wrap.sh execution failure + logs.info(f"DEBUG: package_name: {self.package_name}") + pm_path = android.adb.run_shell_command(f'pm path {self.package_name}') + logs.info(f"DEBUG: pm path: {pm_path}") + ls_lib = android.adb.run_shell_command( + f'ls -l /data/data/{self.package_name}/lib') + logs.info(f"DEBUG: ls -l /data/data/.../lib: {ls_lib}") + ls_wrap_data = android.adb.run_shell_command( + f'ls -l /data/data/{self.package_name}/lib/wrap.sh') + logs.info(f"DEBUG: ls -l /data/data/.../lib/wrap.sh: {ls_wrap_data}") + ls_wrap_user = android.adb.run_shell_command( + f'ls -l /data/user/0/{self.package_name}/lib/wrap.sh') + logs.info(f"DEBUG: ls -l /data/user/0/.../lib/wrap.sh: {ls_wrap_user}") + # Force ASan wrapper to run on userdebug devices. # We use the extracted wrap.sh in the app's lib directory. android.adb.run_shell_command( From d6420d02863dbba34d9b2a61f9fcac9136c1185c Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sat, 27 Jun 2026 04:40:57 +0000 Subject: [PATCH 40/59] Upgrade wrap.sh path debug prints to deep inspection --- .../_internal/bot/fuzzers/libfuzzer.py | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index a2e56c14bd1..0227da6fbac 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1429,19 +1429,28 @@ def fuzz(self, else: raise LibFuzzerError('No launchable activity or instrumentation found.') - # Debug paths to diagnose wrap.sh execution failure - logs.info(f"DEBUG: package_name: {self.package_name}") - pm_path = android.adb.run_shell_command(f'pm path {self.package_name}') - logs.info(f"DEBUG: pm path: {pm_path}") - ls_lib = android.adb.run_shell_command( - f'ls -l /data/data/{self.package_name}/lib') - logs.info(f"DEBUG: ls -l /data/data/.../lib: {ls_lib}") - ls_wrap_data = android.adb.run_shell_command( - f'ls -l /data/data/{self.package_name}/lib/wrap.sh') - logs.info(f"DEBUG: ls -l /data/data/.../lib/wrap.sh: {ls_wrap_data}") - ls_wrap_user = android.adb.run_shell_command( - f'ls -l /data/user/0/{self.package_name}/lib/wrap.sh') - logs.info(f"DEBUG: ls -l /data/user/0/.../lib/wrap.sh: {ls_wrap_user}") + # Deep inspection of paths to diagnose wrap.sh execution failure + try: + logs.info(f"DEBUG: package_name: {self.package_name}") + pm_path_output = android.adb.run_shell_command( + f'pm path {self.package_name}') + logs.info(f"DEBUG: pm path output: {pm_path_output}") + if pm_path_output and pm_path_output.startswith('package:'): + apk_path = pm_path_output.split(':')[1].strip() + install_dir = os.path.dirname(apk_path) + logs.info(f"DEBUG: install_dir: {install_dir}") + ls_install = android.adb.run_shell_command(f'ls -R {install_dir}') + logs.info(f"DEBUG: ls -R install_dir:\n{ls_install}") + + ls_data_dir = android.adb.run_shell_command( + f'ls -d /data/data/{self.package_name}') + logs.info(f"DEBUG: ls -d /data/data/...: {ls_data_dir}") + + ls_data_R = android.adb.run_shell_command( + f'ls -R /data/data/{self.package_name}') + logs.info(f"DEBUG: ls -R /data/data/...:\n{ls_data_R}") + except Exception as e: + logs.error(f"DEBUG: Failed during deep inspection: {e}") # Force ASan wrapper to run on userdebug devices. # We use the extracted wrap.sh in the app's lib directory. From ea796efe177a619b0cee76584a1a9fb63f1fa5c7 Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sat, 27 Jun 2026 05:04:51 +0000 Subject: [PATCH 41/59] Add wrap.sh manual extraction workaround --- .../_internal/bot/fuzzers/libfuzzer.py | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 0227da6fbac..effa41396c4 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1429,7 +1429,28 @@ def fuzz(self, else: raise LibFuzzerError('No launchable activity or instrumentation found.') - # Deep inspection of paths to diagnose wrap.sh execution failure + # Workaround for wrap.sh not being extracted by the package manager. + try: + logs.info("Attempting to manually extract wrap.sh...") + pm_path_output = android.adb.run_shell_command( + f'pm path {self.package_name}') + if pm_path_output and pm_path_output.startswith('package:'): + apk_path = pm_path_output.split(':')[1].strip() + install_dir = os.path.dirname(apk_path) + abi = "x86_64" + target_dir = f"{install_dir}/lib/{abi}" + + android.adb.run_shell_command(f'mkdir -p {target_dir}') + unzip_cmd = f'unzip -o -j {apk_path} lib/{abi}/wrap.sh -d {target_dir}' + unzip_result = android.adb.run_shell_command(unzip_cmd) + logs.info(f"DEBUG: unzip result: {unzip_result}") + android.adb.run_shell_command(f'chmod 755 {target_dir}/wrap.sh') + else: + logs.error(f"DEBUG: Could not get APK path for {self.package_name}") + except Exception as e: + logs.error(f"DEBUG: Failed to manually extract wrap.sh: {e}") + + # Deep inspection of paths to verify try: logs.info(f"DEBUG: package_name: {self.package_name}") pm_path_output = android.adb.run_shell_command( @@ -1448,7 +1469,7 @@ def fuzz(self, ls_data_R = android.adb.run_shell_command( f'ls -R /data/data/{self.package_name}') - logs.info(f"DEBUG: ls -R /data/data/...:\n{ls_data_R}") + logs.info(f"DEBUG: ls -R /data/data/...: {ls_data_R}") except Exception as e: logs.error(f"DEBUG: Failed during deep inspection: {e}") From c1b98dd2d5b12f7a9355a15f4fb3040c4c3c9508 Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sat, 27 Jun 2026 05:32:31 +0000 Subject: [PATCH 42/59] Run wrap.sh manual extraction and setprop as root (su 0) --- src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index effa41396c4..89aa92e4537 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1430,8 +1430,9 @@ def fuzz(self, raise LibFuzzerError('No launchable activity or instrumentation found.') # Workaround for wrap.sh not being extracted by the package manager. + # We run these commands as root (su 0) to bypass permission restrictions. try: - logs.info("Attempting to manually extract wrap.sh...") + logs.info("Attempting to manually extract wrap.sh as root...") pm_path_output = android.adb.run_shell_command( f'pm path {self.package_name}') if pm_path_output and pm_path_output.startswith('package:'): @@ -1440,11 +1441,11 @@ def fuzz(self, abi = "x86_64" target_dir = f"{install_dir}/lib/{abi}" - android.adb.run_shell_command(f'mkdir -p {target_dir}') - unzip_cmd = f'unzip -o -j {apk_path} lib/{abi}/wrap.sh -d {target_dir}' + android.adb.run_shell_command(f'su 0 mkdir -p {target_dir}') + unzip_cmd = f'su 0 unzip -o -j {apk_path} lib/{abi}/wrap.sh -d {target_dir}' unzip_result = android.adb.run_shell_command(unzip_cmd) logs.info(f"DEBUG: unzip result: {unzip_result}") - android.adb.run_shell_command(f'chmod 755 {target_dir}/wrap.sh') + android.adb.run_shell_command(f'su 0 chmod 755 {target_dir}/wrap.sh') else: logs.error(f"DEBUG: Could not get APK path for {self.package_name}") except Exception as e: @@ -1475,8 +1476,9 @@ def fuzz(self, # Force ASan wrapper to run on userdebug devices. # We use the extracted wrap.sh in the app's lib directory. + # We run setprop as root (su 0) to ensure we have permission. android.adb.run_shell_command( - f'setprop wrap.{self.package_name} /data/data/{self.package_name}/lib/wrap.sh' + f'su 0 setprop wrap.{self.package_name} /data/data/{self.package_name}/lib/wrap.sh' ) try: result = self.run_and_wait( @@ -1485,7 +1487,7 @@ def fuzz(self, max_stdout_len=MAX_OUTPUT_LEN) finally: # Clear the wrapper property. - android.adb.run_shell_command(f'setprop wrap.{self.package_name} ""') + android.adb.run_shell_command(f'su 0 setprop wrap.{self.package_name} ""') logs.info(f'DEBUG: adb command run: {result.command}') logs.info(f'DEBUG: adb command return code: {result.return_code}') From f2e1e2190e3e3d5bb41a5b55478bc9406a82c135 Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sat, 27 Jun 2026 05:57:53 +0000 Subject: [PATCH 43/59] Use su 0 -c syntax for root commands --- .../_internal/bot/fuzzers/libfuzzer.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 89aa92e4537..4dd9ccf05db 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1432,6 +1432,12 @@ def fuzz(self, # Workaround for wrap.sh not being extracted by the package manager. # We run these commands as root (su 0) to bypass permission restrictions. try: + logs.info("Testing su 0...") + id_result = android.adb.run_shell_command('su 0 id') + logs.info(f"DEBUG: su 0 id result: {id_result}") + id_c_result = android.adb.run_shell_command('su 0 -c id') + logs.info(f"DEBUG: su 0 -c id result: {id_c_result}") + logs.info("Attempting to manually extract wrap.sh as root...") pm_path_output = android.adb.run_shell_command( f'pm path {self.package_name}') @@ -1441,11 +1447,12 @@ def fuzz(self, abi = "x86_64" target_dir = f"{install_dir}/lib/{abi}" - android.adb.run_shell_command(f'su 0 mkdir -p {target_dir}') - unzip_cmd = f'su 0 unzip -o -j {apk_path} lib/{abi}/wrap.sh -d {target_dir}' + android.adb.run_shell_command(f'su 0 -c "mkdir -p {target_dir}"') + unzip_cmd = f'su 0 -c "unzip -o -j {apk_path} lib/{abi}/wrap.sh -d {target_dir}"' unzip_result = android.adb.run_shell_command(unzip_cmd) logs.info(f"DEBUG: unzip result: {unzip_result}") - android.adb.run_shell_command(f'su 0 chmod 755 {target_dir}/wrap.sh') + android.adb.run_shell_command( + f'su 0 -c "chmod 755 {target_dir}/wrap.sh"') else: logs.error(f"DEBUG: Could not get APK path for {self.package_name}") except Exception as e: @@ -1478,7 +1485,7 @@ def fuzz(self, # We use the extracted wrap.sh in the app's lib directory. # We run setprop as root (su 0) to ensure we have permission. android.adb.run_shell_command( - f'su 0 setprop wrap.{self.package_name} /data/data/{self.package_name}/lib/wrap.sh' + f'su 0 -c "setprop wrap.{self.package_name} /data/data/{self.package_name}/lib/wrap.sh"' ) try: result = self.run_and_wait( @@ -1487,7 +1494,8 @@ def fuzz(self, max_stdout_len=MAX_OUTPUT_LEN) finally: # Clear the wrapper property. - android.adb.run_shell_command(f'su 0 setprop wrap.{self.package_name} ""') + android.adb.run_shell_command( + f'su 0 -c "setprop wrap.{self.package_name} \\"\\""') logs.info(f'DEBUG: adb command run: {result.command}') logs.info(f'DEBUG: adb command return code: {result.return_code}') From 9c619ea9a7d96e3ee51b77886cfe30e36f3756f1 Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sat, 27 Jun 2026 06:27:44 +0000 Subject: [PATCH 44/59] Implement two-step wrap.sh extraction and correct setprop path --- .../_internal/bot/fuzzers/libfuzzer.py | 53 +++++++++++++------ 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 4dd9ccf05db..b4fb92908dd 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1430,15 +1430,14 @@ def fuzz(self, raise LibFuzzerError('No launchable activity or instrumentation found.') # Workaround for wrap.sh not being extracted by the package manager. - # We run these commands as root (su 0) to bypass permission restrictions. + # We extract to /data/local/tmp first, then move to the install dir as root. + wrap_sh_path = None try: logs.info("Testing su 0...") id_result = android.adb.run_shell_command('su 0 id') logs.info(f"DEBUG: su 0 id result: {id_result}") - id_c_result = android.adb.run_shell_command('su 0 -c id') - logs.info(f"DEBUG: su 0 -c id result: {id_c_result}") - logs.info("Attempting to manually extract wrap.sh as root...") + logs.info("Attempting to manually extract wrap.sh...") pm_path_output = android.adb.run_shell_command( f'pm path {self.package_name}') if pm_path_output and pm_path_output.startswith('package:'): @@ -1446,13 +1445,28 @@ def fuzz(self, install_dir = os.path.dirname(apk_path) abi = "x86_64" target_dir = f"{install_dir}/lib/{abi}" + wrap_sh_path = f"{target_dir}/wrap.sh" - android.adb.run_shell_command(f'su 0 -c "mkdir -p {target_dir}"') - unzip_cmd = f'su 0 -c "unzip -o -j {apk_path} lib/{abi}/wrap.sh -d {target_dir}"' + # 1. Create target directory as root (su 0) + mkdir_result = android.adb.run_shell_command( + f'su 0 mkdir -p {target_dir}') + logs.info(f"DEBUG: mkdir result: {mkdir_result}") + + # 2. Unzip to /data/local/tmp as shell (no su needed) + tmp_dir = "/data/local/tmp" + unzip_cmd = f'unzip -o -j {apk_path} lib/{abi}/wrap.sh -d {tmp_dir}' unzip_result = android.adb.run_shell_command(unzip_cmd) - logs.info(f"DEBUG: unzip result: {unzip_result}") - android.adb.run_shell_command( - f'su 0 -c "chmod 755 {target_dir}/wrap.sh"') + logs.info(f"DEBUG: unzip to tmp result: {unzip_result}") + + # 3. Move to target directory as root (su 0) + mv_cmd = f'su 0 mv {tmp_dir}/wrap.sh {wrap_sh_path}' + mv_result = android.adb.run_shell_command(mv_cmd) + logs.info(f"DEBUG: mv result: {mv_result}") + + # 4. Chmod as root (su 0) + chmod_result = android.adb.run_shell_command( + f'su 0 chmod 755 {wrap_sh_path}') + logs.info(f"DEBUG: chmod result: {chmod_result}") else: logs.error(f"DEBUG: Could not get APK path for {self.package_name}") except Exception as e: @@ -1482,11 +1496,19 @@ def fuzz(self, logs.error(f"DEBUG: Failed during deep inspection: {e}") # Force ASan wrapper to run on userdebug devices. - # We use the extracted wrap.sh in the app's lib directory. - # We run setprop as root (su 0) to ensure we have permission. - android.adb.run_shell_command( - f'su 0 -c "setprop wrap.{self.package_name} /data/data/{self.package_name}/lib/wrap.sh"' - ) + # We use the actual path of the extracted wrap.sh if available. + if wrap_sh_path: + logs.info(f"DEBUG: Setting wrap property to {wrap_sh_path}") + android.adb.run_shell_command( + f'su 0 setprop wrap.{self.package_name} {wrap_sh_path}') + else: + logs.warning( + "DEBUG: wrap_sh_path not found, falling back to default /data/data/.../lib/wrap.sh" + ) + android.adb.run_shell_command( + f'su 0 setprop wrap.{self.package_name} /data/data/{self.package_name}/lib/wrap.sh' + ) + try: result = self.run_and_wait( additional_args=args, @@ -1494,8 +1516,7 @@ def fuzz(self, max_stdout_len=MAX_OUTPUT_LEN) finally: # Clear the wrapper property. - android.adb.run_shell_command( - f'su 0 -c "setprop wrap.{self.package_name} \\"\\""') + android.adb.run_shell_command(f'su 0 setprop wrap.{self.package_name} ""') logs.info(f'DEBUG: adb command run: {result.command}') logs.info(f'DEBUG: adb command return code: {result.return_code}') From 906c8cb413c9fe61929f42656b77e79ebf6b307b Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sat, 27 Jun 2026 06:54:50 +0000 Subject: [PATCH 45/59] Temporarily disable SELinux during wrap.sh setup --- .../_internal/bot/fuzzers/libfuzzer.py | 61 +++++++++++++------ 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index b4fb92908dd..b17bfada048 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1431,6 +1431,7 @@ def fuzz(self, # Workaround for wrap.sh not being extracted by the package manager. # We extract to /data/local/tmp first, then move to the install dir as root. + # We temporarily disable SELinux (setenforce 0) to bypass permission restrictions. wrap_sh_path = None try: logs.info("Testing su 0...") @@ -1447,26 +1448,46 @@ def fuzz(self, target_dir = f"{install_dir}/lib/{abi}" wrap_sh_path = f"{target_dir}/wrap.sh" - # 1. Create target directory as root (su 0) - mkdir_result = android.adb.run_shell_command( - f'su 0 mkdir -p {target_dir}') - logs.info(f"DEBUG: mkdir result: {mkdir_result}") - - # 2. Unzip to /data/local/tmp as shell (no su needed) - tmp_dir = "/data/local/tmp" - unzip_cmd = f'unzip -o -j {apk_path} lib/{abi}/wrap.sh -d {tmp_dir}' - unzip_result = android.adb.run_shell_command(unzip_cmd) - logs.info(f"DEBUG: unzip to tmp result: {unzip_result}") - - # 3. Move to target directory as root (su 0) - mv_cmd = f'su 0 mv {tmp_dir}/wrap.sh {wrap_sh_path}' - mv_result = android.adb.run_shell_command(mv_cmd) - logs.info(f"DEBUG: mv result: {mv_result}") - - # 4. Chmod as root (su 0) - chmod_result = android.adb.run_shell_command( - f'su 0 chmod 755 {wrap_sh_path}') - logs.info(f"DEBUG: chmod result: {chmod_result}") + # Temporarily disable SELinux + selinux_status = android.adb.run_shell_command('su 0 getenforce') + logs.info(f"DEBUG: Initial SELinux status: {selinux_status.strip()}") + android.adb.run_shell_command('su 0 setenforce 0') + logs.info( + f"DEBUG: SELinux status after disabling: {android.adb.run_shell_command('su 0 getenforce').strip()}" + ) + + try: + # 1. Create target directory as root (su 0) + mkdir_result = android.adb.run_shell_command( + f'su 0 mkdir -p {target_dir}') + logs.info(f"DEBUG: mkdir result: {mkdir_result}") + + # 2. Unzip to /data/local/tmp as shell (no su needed) + tmp_dir = "/data/local/tmp" + unzip_cmd = f'unzip -o -j {apk_path} lib/{abi}/wrap.sh -d {tmp_dir}' + unzip_result = android.adb.run_shell_command(unzip_cmd) + logs.info(f"DEBUG: unzip to tmp result: {unzip_result}") + + # 3. Move to target directory as root (su 0) + mv_cmd = f'su 0 mv {tmp_dir}/wrap.sh {wrap_sh_path}' + mv_result = android.adb.run_shell_command(mv_cmd) + logs.info(f"DEBUG: mv result: {mv_result}") + + # 4. Chmod as root (su 0) + chmod_result = android.adb.run_shell_command( + f'su 0 chmod 755 {wrap_sh_path}') + logs.info(f"DEBUG: chmod result: {chmod_result}") + + # 5. Restore SELinux context + restorecon_result = android.adb.run_shell_command( + f'su 0 restorecon {wrap_sh_path}') + logs.info(f"DEBUG: restorecon result: {restorecon_result}") + finally: + # Re-enable SELinux + android.adb.run_shell_command('su 0 setenforce 1') + logs.info( + f"DEBUG: SELinux status after re-enabling: {android.adb.run_shell_command('su 0 getenforce').strip()}" + ) else: logs.error(f"DEBUG: Could not get APK path for {self.package_name}") except Exception as e: From fb64f244f3d6ea7f83217e870f87bc2945a5c37b Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sat, 27 Jun 2026 07:20:27 +0000 Subject: [PATCH 46/59] Add diagnostics and dynamic owner-elevation fallback to wrap.sh setup --- .../_internal/bot/fuzzers/libfuzzer.py | 49 +++++++++++++++---- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index b17bfada048..730a82707d5 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1430,13 +1430,21 @@ def fuzz(self, raise LibFuzzerError('No launchable activity or instrumentation found.') # Workaround for wrap.sh not being extracted by the package manager. - # We extract to /data/local/tmp first, then move to the install dir as root. - # We temporarily disable SELinux (setenforce 0) to bypass permission restrictions. + # We extract to /data/local/tmp first, then move to the install dir. + # We temporarily disable SELinux (setenforce 0) to bypass MAC restrictions. + # If 'su 0' fails with Permission denied (likely due to missing CAP_DAC_OVERRIDE), + # we dynamically detect the owner of the target directory and run as them. wrap_sh_path = None try: - logs.info("Testing su 0...") - id_result = android.adb.run_shell_command('su 0 id') - logs.info(f"DEBUG: su 0 id result: {id_result}") + logs.info("Running deep diagnostics...") + logs.info( + f"DEBUG: su 0 id: {android.adb.run_shell_command('su 0 id').strip()}") + logs.info( + f"DEBUG: su 0 capabilities:\n{android.adb.run_shell_command('su 0 cat /proc/self/status')}" + ) + logs.info( + f"DEBUG: ls -ld /data/app: {android.adb.run_shell_command('ls -ld /data/app').strip()}" + ) logs.info("Attempting to manually extract wrap.sh...") pm_path_output = android.adb.run_shell_command( @@ -1468,15 +1476,36 @@ def fuzz(self, unzip_result = android.adb.run_shell_command(unzip_cmd) logs.info(f"DEBUG: unzip to tmp result: {unzip_result}") - # 3. Move to target directory as root (su 0) + # 3. Move to target directory mv_cmd = f'su 0 mv {tmp_dir}/wrap.sh {wrap_sh_path}' mv_result = android.adb.run_shell_command(mv_cmd) logs.info(f"DEBUG: mv result: {mv_result}") - # 4. Chmod as root (su 0) - chmod_result = android.adb.run_shell_command( - f'su 0 chmod 755 {wrap_sh_path}') - logs.info(f"DEBUG: chmod result: {chmod_result}") + if "Permission denied" in mv_result: + logs.warning( + "DEBUG: mv as root failed with Permission denied. Trying as directory owner..." + ) + ls_output = android.adb.run_shell_command(f'ls -ld {target_dir}') + logs.info(f"DEBUG: ls -ld target_dir: {ls_output.strip()}") + parts = ls_output.split() + if len(parts) >= 3: + owner = parts[2] + logs.info( + f"DEBUG: Target directory owner is {owner}. Trying mv as {owner}..." + ) + mv_owner_cmd = f'su {owner} mv {tmp_dir}/wrap.sh {wrap_sh_path}' + mv_owner_result = android.adb.run_shell_command(mv_owner_cmd) + logs.info(f"DEBUG: mv as owner result: {mv_owner_result}") + + chmod_owner_cmd = f'su {owner} chmod 755 {wrap_sh_path}' + chmod_owner_result = android.adb.run_shell_command( + chmod_owner_cmd) + logs.info(f"DEBUG: chmod as owner result: {chmod_owner_result}") + else: + # If mv succeeded as root, chmod as root + chmod_result = android.adb.run_shell_command( + f'su 0 chmod 755 {wrap_sh_path}') + logs.info(f"DEBUG: chmod result: {chmod_result}") # 5. Restore SELinux context restorecon_result = android.adb.run_shell_command( From 6ce07aa71e15e77489d79168881a9e030c9029b3 Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sat, 27 Jun 2026 07:47:37 +0000 Subject: [PATCH 47/59] Use cp with world-writable source and add mount/write diagnostics --- .../_internal/bot/fuzzers/libfuzzer.py | 81 +++++++++++-------- 1 file changed, 49 insertions(+), 32 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 730a82707d5..972fd774451 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1430,10 +1430,10 @@ def fuzz(self, raise LibFuzzerError('No launchable activity or instrumentation found.') # Workaround for wrap.sh not being extracted by the package manager. - # We extract to /data/local/tmp first, then move to the install dir. + # We extract to /data/local/tmp first, then copy to the install dir. # We temporarily disable SELinux (setenforce 0) to bypass MAC restrictions. - # If 'su 0' fails with Permission denied (likely due to missing CAP_DAC_OVERRIDE), - # we dynamically detect the owner of the target directory and run as them. + # We use 'cp' instead of 'mv' to handle potential cross-mount boundaries, + # and ensure the source file is world-writable before copying. wrap_sh_path = None try: logs.info("Running deep diagnostics...") @@ -1465,6 +1465,26 @@ def fuzz(self, ) try: + # Log mounts + mount_output = android.adb.run_shell_command('su 0 mount') + data_mounts = [ + line for line in mount_output.split('\n') if '/data' in line + ] + logs.info(f"DEBUG: /data mounts:\n" + '\n'.join(data_mounts)) + + # Test writing dummy file + test_file_path = f"{target_dir}/test_antigravity.txt" + logs.info("DEBUG: Testing dummy write as root (su 0 sh -c)...") + write_root_result = android.adb.run_shell_command( + f"su 0 sh -c 'echo test > {test_file_path}'") + logs.info(f"DEBUG: write_root_result: {write_root_result.strip()}") + + logs.info("DEBUG: Testing dummy write as system (su system sh -c)...") + write_system_result = android.adb.run_shell_command( + f"su system sh -c 'echo test > {test_file_path}'") + logs.info( + f"DEBUG: write_system_result: {write_system_result.strip()}") + # 1. Create target directory as root (su 0) mkdir_result = android.adb.run_shell_command( f'su 0 mkdir -p {target_dir}') @@ -1476,41 +1496,38 @@ def fuzz(self, unzip_result = android.adb.run_shell_command(unzip_cmd) logs.info(f"DEBUG: unzip to tmp result: {unzip_result}") - # 3. Move to target directory - mv_cmd = f'su 0 mv {tmp_dir}/wrap.sh {wrap_sh_path}' - mv_result = android.adb.run_shell_command(mv_cmd) - logs.info(f"DEBUG: mv result: {mv_result}") - - if "Permission denied" in mv_result: - logs.warning( - "DEBUG: mv as root failed with Permission denied. Trying as directory owner..." - ) - ls_output = android.adb.run_shell_command(f'ls -ld {target_dir}') - logs.info(f"DEBUG: ls -ld target_dir: {ls_output.strip()}") - parts = ls_output.split() - if len(parts) >= 3: - owner = parts[2] - logs.info( - f"DEBUG: Target directory owner is {owner}. Trying mv as {owner}..." - ) - mv_owner_cmd = f'su {owner} mv {tmp_dir}/wrap.sh {wrap_sh_path}' - mv_owner_result = android.adb.run_shell_command(mv_owner_cmd) - logs.info(f"DEBUG: mv as owner result: {mv_owner_result}") - - chmod_owner_cmd = f'su {owner} chmod 755 {wrap_sh_path}' - chmod_owner_result = android.adb.run_shell_command( - chmod_owner_cmd) - logs.info(f"DEBUG: chmod as owner result: {chmod_owner_result}") + # Make tmp file world-readable/writable + android.adb.run_shell_command(f'chmod 666 {tmp_dir}/wrap.sh') + + # 3. Try CP instead of MV + logs.info("DEBUG: Trying cp as root...") + cp_cmd = f'su 0 cp {tmp_dir}/wrap.sh {wrap_sh_path}' + cp_result = android.adb.run_shell_command(cp_cmd) + logs.info(f"DEBUG: cp result: {cp_result}") + + if "Permission denied" in cp_result: + logs.warning("DEBUG: cp as root failed. Trying cp as system...") + cp_system_cmd = f'su system cp {tmp_dir}/wrap.sh {wrap_sh_path}' + cp_system_result = android.adb.run_shell_command(cp_system_cmd) + logs.info(f"DEBUG: cp as system result: {cp_system_result}") + + if "Permission denied" in cp_system_result: + logs.error("DEBUG: Both cp as root and cp as system failed.") + else: + # cp as system succeeded, chmod as system + android.adb.run_shell_command( + f'su system chmod 755 {wrap_sh_path}') else: - # If mv succeeded as root, chmod as root - chmod_result = android.adb.run_shell_command( - f'su 0 chmod 755 {wrap_sh_path}') - logs.info(f"DEBUG: chmod result: {chmod_result}") + # cp as root succeeded, chmod as root + android.adb.run_shell_command(f'su 0 chmod 755 {wrap_sh_path}') # 5. Restore SELinux context restorecon_result = android.adb.run_shell_command( f'su 0 restorecon {wrap_sh_path}') logs.info(f"DEBUG: restorecon result: {restorecon_result}") + + # Clean up dummy file if created + android.adb.run_shell_command(f'su 0 rm -f {test_file_path}') finally: # Re-enable SELinux android.adb.run_shell_command('su 0 setenforce 1') From ee9d641e2216e841955c7fa2007d299ed93a8aaf Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sat, 27 Jun 2026 08:17:35 +0000 Subject: [PATCH 48/59] Fix Datastore PermissionDenied on uworker by injecting custom binary details during preprocess --- AGENTS.md | 2 +- .../_internal/bot/fuzzers/libfuzzer.py | 35 ++++++++----------- .../_internal/bot/tasks/utasks/fuzz_task.py | 8 +++++ .../build_management/build_manager.py | 19 +++++++++- 4 files changed, 42 insertions(+), 22 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5d6978bfb1a..caeafc6aa6e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -135,7 +135,7 @@ For changes to run on remote Swarming bots, they must be committed, merged, and git push origin dev git checkout ``` -3. **⚠️ Crucial Rebuild Wait Time**: After pushing to `dev`, you **MUST wait 20 to 25 minutes** before triggering any Swarming tasks. This gives the remote Google Cloud Storage (GCS) builder enough time to pull your new commit, compile the binaries, and package them into the deployment ZIP bundle (`linux-3.zip`) fetched by the bots. +3. **⚠️ Crucial Rebuild Wait Time**: After pushing to `dev`, you **MUST wait 25 to 30 minutes** before triggering any Swarming tasks. This gives the remote Google Cloud Storage (GCS) builder enough time to pull your new commit, compile the binaries, and package them into the deployment ZIP bundle (`linux-3.zip`) fetched by the bots. ### Step 3: Preprocess & Launch the Swarming Task Once the deployment package has finished rebuilding on GCS: diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 972fd774451..5a223a2920a 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1439,12 +1439,10 @@ def fuzz(self, logs.info("Running deep diagnostics...") logs.info( f"DEBUG: su 0 id: {android.adb.run_shell_command('su 0 id').strip()}") - logs.info( - f"DEBUG: su 0 capabilities:\n{android.adb.run_shell_command('su 0 cat /proc/self/status')}" - ) - logs.info( - f"DEBUG: ls -ld /data/app: {android.adb.run_shell_command('ls -ld /data/app').strip()}" - ) + su_status = android.adb.run_shell_command('su 0 cat /proc/self/status') + logs.info(f"DEBUG: su 0 capabilities:\n{su_status}") + ls_ld_data_app = android.adb.run_shell_command('ls -ld /data/app').strip() + logs.info(f"DEBUG: ls -ld /data/app: {ls_ld_data_app}") logs.info("Attempting to manually extract wrap.sh...") pm_path_output = android.adb.run_shell_command( @@ -1460,9 +1458,8 @@ def fuzz(self, selinux_status = android.adb.run_shell_command('su 0 getenforce') logs.info(f"DEBUG: Initial SELinux status: {selinux_status.strip()}") android.adb.run_shell_command('su 0 setenforce 0') - logs.info( - f"DEBUG: SELinux status after disabling: {android.adb.run_shell_command('su 0 getenforce').strip()}" - ) + selinux_dis = android.adb.run_shell_command('su 0 getenforce').strip() + logs.info(f"DEBUG: SELinux status after disabling: {selinux_dis}") try: # Log mounts @@ -1470,7 +1467,7 @@ def fuzz(self, data_mounts = [ line for line in mount_output.split('\n') if '/data' in line ] - logs.info(f"DEBUG: /data mounts:\n" + '\n'.join(data_mounts)) + logs.info("DEBUG: /data mounts:\n" + '\n'.join(data_mounts)) # Test writing dummy file test_file_path = f"{target_dir}/test_antigravity.txt" @@ -1531,9 +1528,8 @@ def fuzz(self, finally: # Re-enable SELinux android.adb.run_shell_command('su 0 setenforce 1') - logs.info( - f"DEBUG: SELinux status after re-enabling: {android.adb.run_shell_command('su 0 getenforce').strip()}" - ) + selinux_en = android.adb.run_shell_command('su 0 getenforce').strip() + logs.info(f"DEBUG: SELinux status after re-enabling: {selinux_en}") else: logs.error(f"DEBUG: Could not get APK path for {self.package_name}") except Exception as e: @@ -1556,9 +1552,9 @@ def fuzz(self, f'ls -d /data/data/{self.package_name}') logs.info(f"DEBUG: ls -d /data/data/...: {ls_data_dir}") - ls_data_R = android.adb.run_shell_command( + ls_data_recursive = android.adb.run_shell_command( f'ls -R /data/data/{self.package_name}') - logs.info(f"DEBUG: ls -R /data/data/...: {ls_data_R}") + logs.info(f"DEBUG: ls -R /data/data/...: {ls_data_recursive}") except Exception as e: logs.error(f"DEBUG: Failed during deep inspection: {e}") @@ -1569,12 +1565,11 @@ def fuzz(self, android.adb.run_shell_command( f'su 0 setprop wrap.{self.package_name} {wrap_sh_path}') else: - logs.warning( - "DEBUG: wrap_sh_path not found, falling back to default /data/data/.../lib/wrap.sh" - ) + logs.warning("DEBUG: wrap_sh_path not found, " + "falling back to default /data/data/.../lib/wrap.sh") + fallback_wrap = f'/data/data/{self.package_name}/lib/wrap.sh' android.adb.run_shell_command( - f'su 0 setprop wrap.{self.package_name} /data/data/{self.package_name}/lib/wrap.sh' - ) + f'su 0 setprop wrap.{self.package_name} {fallback_wrap}') try: result = self.run_and_wait( diff --git a/src/clusterfuzz/_internal/bot/tasks/utasks/fuzz_task.py b/src/clusterfuzz/_internal/bot/tasks/utasks/fuzz_task.py index 1936a8e3ba6..a1f9fa8c337 100644 --- a/src/clusterfuzz/_internal/bot/tasks/utasks/fuzz_task.py +++ b/src/clusterfuzz/_internal/bot/tasks/utasks/fuzz_task.py @@ -2299,6 +2299,14 @@ def _utask_preprocess(fuzzer_name, job_type, uworker_env): fuzz_task_input.global_blacklisted_functions.extend( leak_blacklist.get_global_blacklisted_functions()) + # Inject custom binary details if this is a custom binary job. + if uworker_env.get('CUSTOM_BINARY'): + job = data_types.Job.query(data_types.Job.name == job_type).get() + if job and job.custom_binary_key: + uworker_env['CUSTOM_BINARY_KEY'] = job.custom_binary_key + uworker_env['CUSTOM_BINARY_FILENAME'] = job.custom_binary_filename + uworker_env['CUSTOM_BINARY_REVISION'] = str(job.custom_binary_revision) + return uworker_msg_pb2.Input( # pylint: disable=no-member fuzz_task_input=fuzz_task_input, job_type=job_type, diff --git a/src/clusterfuzz/_internal/build_management/build_manager.py b/src/clusterfuzz/_internal/build_management/build_manager.py index 02b84e4d18f..5fddf06149e 100644 --- a/src/clusterfuzz/_internal/build_management/build_manager.py +++ b/src/clusterfuzz/_internal/build_management/build_manager.py @@ -1416,7 +1416,24 @@ def setup_symbolized_builds(revision): def setup_custom_binary(): """Set up the custom binary for a particular job.""" job_name = environment.get_value('JOB_NAME') - # Verify that this is really a custom binary job. + + # Try to get from environment first (to avoid Datastore query on uworker) + custom_binary_key = environment.get_value('CUSTOM_BINARY_KEY') + custom_binary_filename = environment.get_value('CUSTOM_BINARY_FILENAME') + custom_binary_revision = environment.get_value('CUSTOM_BINARY_REVISION') + + if custom_binary_key and custom_binary_filename: + logs.info('Using custom binary details from environment.') + base_build_dir = _base_build_dir('') + build = CustomBuild(base_build_dir, + custom_binary_key, custom_binary_filename, + int(custom_binary_revision or 0)) + if build.setup(): + return build + return None + + # Fallback to Datastore query + logs.info('Custom binary details not in environment, querying Datastore...') job = data_types.Job.query(data_types.Job.name == job_name).get() if not job or not job.custom_binary_key or not job.custom_binary_filename: logs.error( From 46396b7065ea587289e05f6a51d20552d83a0dd6 Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sat, 27 Jun 2026 11:30:28 +0000 Subject: [PATCH 49/59] Implement signed URL download for custom binaries to avoid GCS 403 on uworker --- .../_internal/bot/tasks/utasks/fuzz_task.py | 14 +++++ .../build_management/build_manager.py | 53 +++++++++++++------ 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/tasks/utasks/fuzz_task.py b/src/clusterfuzz/_internal/bot/tasks/utasks/fuzz_task.py index a1f9fa8c337..e7893afc6e6 100644 --- a/src/clusterfuzz/_internal/bot/tasks/utasks/fuzz_task.py +++ b/src/clusterfuzz/_internal/bot/tasks/utasks/fuzz_task.py @@ -2307,6 +2307,20 @@ def _utask_preprocess(fuzzer_name, job_type, uworker_env): uworker_env['CUSTOM_BINARY_FILENAME'] = job.custom_binary_filename uworker_env['CUSTOM_BINARY_REVISION'] = str(job.custom_binary_revision) + # Generate signed URL for the custom binary to avoid 403 on uworker + try: + custom_builds_bucket = local_config.ProjectConfig().get( + 'custom_builds.bucket') + if custom_builds_bucket: + gcs_path = f'/{custom_builds_bucket}/{job.custom_binary_key}' + else: + gcs_path = f'/{storage.blobs_bucket()}/{job.custom_binary_key}' + + uworker_env['CUSTOM_BINARY_SIGNED_URL'] = ( + storage.get_signed_download_url(gcs_path)) + except Exception as e: + logs.error(f'Failed to generate signed URL for custom binary: {e}') + return uworker_msg_pb2.Input( # pylint: disable=no-member fuzz_task_input=fuzz_task_input, job_type=job_type, diff --git a/src/clusterfuzz/_internal/build_management/build_manager.py b/src/clusterfuzz/_internal/build_management/build_manager.py index 5fddf06149e..1d25c024be6 100644 --- a/src/clusterfuzz/_internal/build_management/build_manager.py +++ b/src/clusterfuzz/_internal/build_management/build_manager.py @@ -893,11 +893,16 @@ def setup(self): class CustomBuild(Build): """Custom binary.""" - def __init__(self, base_build_dir, custom_binary_key, custom_binary_filename, - custom_binary_revision): + def __init__(self, + base_build_dir, + custom_binary_key, + custom_binary_filename, + custom_binary_revision, + custom_binary_signed_url=None): super().__init__(base_build_dir, custom_binary_revision) self.custom_binary_key = custom_binary_key self.custom_binary_filename = custom_binary_filename + self.custom_binary_signed_url = custom_binary_signed_url self._build_dir = os.path.join(self.base_build_dir, 'custom') @property @@ -913,20 +918,35 @@ def _unpack_custom_build(self): build_local_archive = os.path.join(self.build_dir, self.custom_binary_filename) - custom_builds_bucket = local_config.ProjectConfig().get( - 'custom_builds.bucket') download_start_time = time.time() - if custom_builds_bucket: - directory = os.path.dirname(build_local_archive) - if not os.path.exists(directory): - os.makedirs(directory) - gcs_path = f'/{custom_builds_bucket}/{self.custom_binary_key}' - storage.copy_file_from(gcs_path, build_local_archive) - elif not blobs.read_blob_to_disk(self.custom_binary_key, - build_local_archive): - return False + # Try to download using signed URL if available (uworker friendly) + if self.custom_binary_signed_url: + logs.info('Downloading custom binary using signed URL.') + try: + storage.download_signed_url_to_file(self.custom_binary_signed_url, + build_local_archive) + if not os.path.exists(build_local_archive) or os.path.getsize( + build_local_archive) == 0: + logs.error('Downloaded custom binary is empty or missing.') + return False + except Exception as e: + logs.error(f'Failed to download custom binary using signed URL: {e}') + return False + else: + custom_builds_bucket = local_config.ProjectConfig().get( + 'custom_builds.bucket') + + if custom_builds_bucket: + directory = os.path.dirname(build_local_archive) + if not os.path.exists(directory): + os.makedirs(directory) + gcs_path = f'/{custom_builds_bucket}/{self.custom_binary_key}' + storage.copy_file_from(gcs_path, build_local_archive) + elif not blobs.read_blob_to_disk(self.custom_binary_key, + build_local_archive): + return False _emit_job_build_retrieval_metric(download_start_time, 'download', self._build_type) @@ -1421,13 +1441,14 @@ def setup_custom_binary(): custom_binary_key = environment.get_value('CUSTOM_BINARY_KEY') custom_binary_filename = environment.get_value('CUSTOM_BINARY_FILENAME') custom_binary_revision = environment.get_value('CUSTOM_BINARY_REVISION') + custom_binary_signed_url = environment.get_value('CUSTOM_BINARY_SIGNED_URL') if custom_binary_key and custom_binary_filename: logs.info('Using custom binary details from environment.') base_build_dir = _base_build_dir('') - build = CustomBuild(base_build_dir, - custom_binary_key, custom_binary_filename, - int(custom_binary_revision or 0)) + build = CustomBuild( + base_build_dir, custom_binary_key, custom_binary_filename, + int(custom_binary_revision or 0), custom_binary_signed_url) if build.setup(): return build return None From fd93bd83835adf50cee38ff620170055e3fc77d2 Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sat, 27 Jun 2026 12:06:55 +0000 Subject: [PATCH 50/59] Bypass incremental-fs write restriction by extracting wrap.sh to /data/local/tmp and running with SELinux Permissive --- .../_internal/bot/fuzzers/libfuzzer.py | 179 +++++------------- 1 file changed, 46 insertions(+), 133 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 5a223a2920a..66a4dfdc779 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1429,147 +1429,53 @@ def fuzz(self, else: raise LibFuzzerError('No launchable activity or instrumentation found.') - # Workaround for wrap.sh not being extracted by the package manager. - # We extract to /data/local/tmp first, then copy to the install dir. - # We temporarily disable SELinux (setenforce 0) to bypass MAC restrictions. - # We use 'cp' instead of 'mv' to handle potential cross-mount boundaries, - # and ensure the source file is world-writable before copying. - wrap_sh_path = None + # Workaround for wrap.sh execution on Android. + # We extract wrap.sh to /data/local/tmp/wrap.sh, make it executable, + # and run with SELinux permissive to allow execution from /data/local/tmp. + wrap_sh_path = "/data/local/tmp/wrap.sh" + selinux_revert = False + try: - logs.info("Running deep diagnostics...") - logs.info( - f"DEBUG: su 0 id: {android.adb.run_shell_command('su 0 id').strip()}") - su_status = android.adb.run_shell_command('su 0 cat /proc/self/status') - logs.info(f"DEBUG: su 0 capabilities:\n{su_status}") - ls_ld_data_app = android.adb.run_shell_command('ls -ld /data/app').strip() - logs.info(f"DEBUG: ls -ld /data/app: {ls_ld_data_app}") - - logs.info("Attempting to manually extract wrap.sh...") pm_path_output = android.adb.run_shell_command( f'pm path {self.package_name}') if pm_path_output and pm_path_output.startswith('package:'): apk_path = pm_path_output.split(':')[1].strip() - install_dir = os.path.dirname(apk_path) - abi = "x86_64" - target_dir = f"{install_dir}/lib/{abi}" - wrap_sh_path = f"{target_dir}/wrap.sh" - - # Temporarily disable SELinux - selinux_status = android.adb.run_shell_command('su 0 getenforce') - logs.info(f"DEBUG: Initial SELinux status: {selinux_status.strip()}") - android.adb.run_shell_command('su 0 setenforce 0') - selinux_dis = android.adb.run_shell_command('su 0 getenforce').strip() - logs.info(f"DEBUG: SELinux status after disabling: {selinux_dis}") - - try: - # Log mounts - mount_output = android.adb.run_shell_command('su 0 mount') - data_mounts = [ - line for line in mount_output.split('\n') if '/data' in line - ] - logs.info("DEBUG: /data mounts:\n" + '\n'.join(data_mounts)) - - # Test writing dummy file - test_file_path = f"{target_dir}/test_antigravity.txt" - logs.info("DEBUG: Testing dummy write as root (su 0 sh -c)...") - write_root_result = android.adb.run_shell_command( - f"su 0 sh -c 'echo test > {test_file_path}'") - logs.info(f"DEBUG: write_root_result: {write_root_result.strip()}") - - logs.info("DEBUG: Testing dummy write as system (su system sh -c)...") - write_system_result = android.adb.run_shell_command( - f"su system sh -c 'echo test > {test_file_path}'") - logs.info( - f"DEBUG: write_system_result: {write_system_result.strip()}") - - # 1. Create target directory as root (su 0) - mkdir_result = android.adb.run_shell_command( - f'su 0 mkdir -p {target_dir}') - logs.info(f"DEBUG: mkdir result: {mkdir_result}") - - # 2. Unzip to /data/local/tmp as shell (no su needed) - tmp_dir = "/data/local/tmp" - unzip_cmd = f'unzip -o -j {apk_path} lib/{abi}/wrap.sh -d {tmp_dir}' - unzip_result = android.adb.run_shell_command(unzip_cmd) - logs.info(f"DEBUG: unzip to tmp result: {unzip_result}") - - # Make tmp file world-readable/writable - android.adb.run_shell_command(f'chmod 666 {tmp_dir}/wrap.sh') - - # 3. Try CP instead of MV - logs.info("DEBUG: Trying cp as root...") - cp_cmd = f'su 0 cp {tmp_dir}/wrap.sh {wrap_sh_path}' - cp_result = android.adb.run_shell_command(cp_cmd) - logs.info(f"DEBUG: cp result: {cp_result}") - - if "Permission denied" in cp_result: - logs.warning("DEBUG: cp as root failed. Trying cp as system...") - cp_system_cmd = f'su system cp {tmp_dir}/wrap.sh {wrap_sh_path}' - cp_system_result = android.adb.run_shell_command(cp_system_cmd) - logs.info(f"DEBUG: cp as system result: {cp_system_result}") - - if "Permission denied" in cp_system_result: - logs.error("DEBUG: Both cp as root and cp as system failed.") - else: - # cp as system succeeded, chmod as system - android.adb.run_shell_command( - f'su system chmod 755 {wrap_sh_path}') - else: - # cp as root succeeded, chmod as root - android.adb.run_shell_command(f'su 0 chmod 755 {wrap_sh_path}') - - # 5. Restore SELinux context - restorecon_result = android.adb.run_shell_command( - f'su 0 restorecon {wrap_sh_path}') - logs.info(f"DEBUG: restorecon result: {restorecon_result}") - - # Clean up dummy file if created - android.adb.run_shell_command(f'su 0 rm -f {test_file_path}') - finally: - # Re-enable SELinux - android.adb.run_shell_command('su 0 setenforce 1') - selinux_en = android.adb.run_shell_command('su 0 getenforce').strip() - logs.info(f"DEBUG: SELinux status after re-enabling: {selinux_en}") + + # Clean up old file if any + android.adb.run_shell_command(f'su 0 rm -f {wrap_sh_path}') + + # Extract using unzip + unzip_cmd = (f'su 0 unzip -o -j {apk_path} ' + 'lib/x86_64/wrap.sh -d /data/local/tmp') + unzip_result = android.adb.run_shell_command(unzip_cmd) + logs.info(f"DEBUG: unzip result: {unzip_result.strip()}") + + if android.adb.file_exists(wrap_sh_path): + android.adb.run_shell_command(f'su 0 chmod 777 {wrap_sh_path}') + + selinux_status = android.adb.run_shell_command('su 0 getenforce') + logs.info(f"DEBUG: Initial SELinux status: {selinux_status.strip()}") + if selinux_status.strip() == 'Enforcing': + android.adb.run_shell_command('su 0 setenforce 0') + selinux_revert = True + logs.info('DEBUG: Disabled SELinux (set to Permissive) ' + 'for wrap.sh execution.') + + logs.info(f"DEBUG: Setting wrap property to {wrap_sh_path}") + setprop_result = android.adb.run_shell_command( + f'su 0 setprop wrap.{self.package_name} {wrap_sh_path}') + if setprop_result: + logs.warning( + f"DEBUG: setprop output/error: {setprop_result.strip()}") + else: + logs.error("DEBUG: Failed to extract wrap.sh to /data/local/tmp") + wrap_sh_path = None else: logs.error(f"DEBUG: Could not get APK path for {self.package_name}") + wrap_sh_path = None except Exception as e: - logs.error(f"DEBUG: Failed to manually extract wrap.sh: {e}") - - # Deep inspection of paths to verify - try: - logs.info(f"DEBUG: package_name: {self.package_name}") - pm_path_output = android.adb.run_shell_command( - f'pm path {self.package_name}') - logs.info(f"DEBUG: pm path output: {pm_path_output}") - if pm_path_output and pm_path_output.startswith('package:'): - apk_path = pm_path_output.split(':')[1].strip() - install_dir = os.path.dirname(apk_path) - logs.info(f"DEBUG: install_dir: {install_dir}") - ls_install = android.adb.run_shell_command(f'ls -R {install_dir}') - logs.info(f"DEBUG: ls -R install_dir:\n{ls_install}") - - ls_data_dir = android.adb.run_shell_command( - f'ls -d /data/data/{self.package_name}') - logs.info(f"DEBUG: ls -d /data/data/...: {ls_data_dir}") - - ls_data_recursive = android.adb.run_shell_command( - f'ls -R /data/data/{self.package_name}') - logs.info(f"DEBUG: ls -R /data/data/...: {ls_data_recursive}") - except Exception as e: - logs.error(f"DEBUG: Failed during deep inspection: {e}") - - # Force ASan wrapper to run on userdebug devices. - # We use the actual path of the extracted wrap.sh if available. - if wrap_sh_path: - logs.info(f"DEBUG: Setting wrap property to {wrap_sh_path}") - android.adb.run_shell_command( - f'su 0 setprop wrap.{self.package_name} {wrap_sh_path}') - else: - logs.warning("DEBUG: wrap_sh_path not found, " - "falling back to default /data/data/.../lib/wrap.sh") - fallback_wrap = f'/data/data/{self.package_name}/lib/wrap.sh' - android.adb.run_shell_command( - f'su 0 setprop wrap.{self.package_name} {fallback_wrap}') + logs.error(f"DEBUG: Error setting up wrap.sh workaround: {e}") + wrap_sh_path = None try: result = self.run_and_wait( @@ -1579,6 +1485,13 @@ def fuzz(self, finally: # Clear the wrapper property. android.adb.run_shell_command(f'su 0 setprop wrap.{self.package_name} ""') + # Restore SELinux if we changed it + if selinux_revert: + android.adb.run_shell_command('su 0 setenforce 1') + logs.info("DEBUG: Restored SELinux to Enforcing.") + # Clean up wrap.sh + if wrap_sh_path: + android.adb.run_shell_command(f'su 0 rm -f {wrap_sh_path}') logs.info(f'DEBUG: adb command run: {result.command}') logs.info(f'DEBUG: adb command return code: {result.return_code}') From 8798020ab9b3af32361b80dab5c4639c3126619b Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sat, 27 Jun 2026 12:36:53 +0000 Subject: [PATCH 51/59] Extract ASan runtime to /data/local/tmp along with wrap.sh to fix failed to attach error --- .../_internal/bot/fuzzers/libfuzzer.py | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 66a4dfdc779..b965b7de927 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1441,17 +1441,28 @@ def fuzz(self, if pm_path_output and pm_path_output.startswith('package:'): apk_path = pm_path_output.split(':')[1].strip() - # Clean up old file if any + # Clean up old files if any android.adb.run_shell_command(f'su 0 rm -f {wrap_sh_path}') - - # Extract using unzip - unzip_cmd = (f'su 0 unzip -o -j {apk_path} ' - 'lib/x86_64/wrap.sh -d /data/local/tmp') - unzip_result = android.adb.run_shell_command(unzip_cmd) - logs.info(f"DEBUG: unzip result: {unzip_result.strip()}") + android.adb.run_shell_command( + 'su 0 rm -f /data/local/tmp/libclang_rt.asan-*.so') + + # Extract wrap.sh using unzip + unzip_wrap_cmd = (f'su 0 unzip -o -j {apk_path} ' + 'lib/x86_64/wrap.sh -d /data/local/tmp') + unzip_wrap_result = android.adb.run_shell_command(unzip_wrap_cmd) + logs.info(f"DEBUG: unzip wrap.sh result: {unzip_wrap_result.strip()}") + + # Extract ASan runtime using unzip + unzip_asan_cmd = ( + f'su 0 unzip -o -j {apk_path} ' + '"lib/x86_64/libclang_rt.asan-*.so" -d /data/local/tmp') + unzip_asan_result = android.adb.run_shell_command(unzip_asan_cmd) + logs.info(f"DEBUG: unzip ASan rt result: {unzip_asan_result.strip()}") if android.adb.file_exists(wrap_sh_path): android.adb.run_shell_command(f'su 0 chmod 777 {wrap_sh_path}') + android.adb.run_shell_command( + 'su 0 chmod 777 /data/local/tmp/libclang_rt.asan-*.so') selinux_status = android.adb.run_shell_command('su 0 getenforce') logs.info(f"DEBUG: Initial SELinux status: {selinux_status.strip()}") @@ -1489,9 +1500,11 @@ def fuzz(self, if selinux_revert: android.adb.run_shell_command('su 0 setenforce 1') logs.info("DEBUG: Restored SELinux to Enforcing.") - # Clean up wrap.sh + # Clean up wrap.sh and ASan runtime if wrap_sh_path: android.adb.run_shell_command(f'su 0 rm -f {wrap_sh_path}') + android.adb.run_shell_command( + 'su 0 rm -f /data/local/tmp/libclang_rt.asan-*.so') logs.info(f'DEBUG: adb command run: {result.command}') logs.info(f'DEBUG: adb command return code: {result.return_code}') From 3116ef591a9f1290504fecefe2974ea1fcaaef10 Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sat, 27 Jun 2026 13:08:39 +0000 Subject: [PATCH 52/59] Bypass system symbols download on uworker to avoid 403 Datastore errors --- .../platforms/android/symbols_downloader.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/clusterfuzz/_internal/platforms/android/symbols_downloader.py b/src/clusterfuzz/_internal/platforms/android/symbols_downloader.py index 2d44368dbab..0ba650c8b73 100644 --- a/src/clusterfuzz/_internal/platforms/android/symbols_downloader.py +++ b/src/clusterfuzz/_internal/platforms/android/symbols_downloader.py @@ -35,6 +35,11 @@ def get_repo_prop_archive_filename(build_id, target): def should_download_symbols(): """Return True if we should continue to download symbols.""" + # Uworkers do not have Datastore access to retrieve the API key needed for + # downloading symbols. + if environment.is_uworker(): + return False + # For local testing, we do not have access to the cloud storage bucket with # the symbols. In this case, just bail out. We have archived symbols for # google builds only. @@ -192,13 +197,16 @@ def download_trusty_symbols_if_needed(symbols_directory, app_name, bid): elif 'oriole' in device or 'raven' in device or 'bluejay' in device: ab_target = 'slider-fuzz-test-debug' else: - # Emulators (like sdk_gdesktop) do not run the physical Trusty Secure OS TEE. - # Therefore, no official Trusty symbols exist for them at the moment in GCS. Instead of logging a confusing - # ERROR that can be mistaken for a fuzz task failure, we log a verbose INFO message and return early. - if environment.is_android_emulator() or 'sdk_gdesktop' in device or 'emulator' in device: + # Emulators (like sdk_gdesktop) do not run the physical Trusty Secure OS + # TEE. Therefore, no official Trusty symbols exist for them at the moment + # in GCS. Instead of logging a confusing ERROR that can be mistaken for a + # fuzz task failure, we log a verbose INFO message and return early. + if environment.is_android_emulator( + ) or 'sdk_gdesktop' in device or 'emulator' in device: logs.info( f'Skipping Trusty symbols download for emulator device "{device}". ' - 'Virtual/emulator devices do not run a physical Trusty Trusted Execution Environment (TEE), ' + 'Virtual/emulator devices do not run a physical Trusty ' + 'Trusted Execution Environment (TEE), ' 'so no official Trusty symbol archives are available or needed.') else: logs.error(f'Unsupported device {device}.') From d8bbce5dc42e643d00949179cb646f50811fdfd9 Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sat, 27 Jun 2026 15:18:07 +0000 Subject: [PATCH 53/59] Fix ASan wrap.sh linker restriction by using app_native_libs and fix lints --- .../_internal/bot/fuzzers/libfuzzer.py | 36 ++++++++++++------- .../_internal/bot/init_scripts/android.py | 3 +- .../_internal/platforms/android/adb.py | 8 +++-- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index b965b7de927..0dc4d68b92b 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1430,9 +1430,11 @@ def fuzz(self, raise LibFuzzerError('No launchable activity or instrumentation found.') # Workaround for wrap.sh execution on Android. - # We extract wrap.sh to /data/local/tmp/wrap.sh, make it executable, - # and run with SELinux permissive to allow execution from /data/local/tmp. - wrap_sh_path = "/data/local/tmp/wrap.sh" + # We extract wrap.sh and ASan RT to the app's secure app_native_libs + # directory to avoid linker restrictions on /data/local/tmp, and run with + # SELinux permissive. + target_dir = f"/data/data/{self.package_name}/app_native_libs" + wrap_sh_path = f"{target_dir}/wrap.sh" selinux_revert = False try: @@ -1441,28 +1443,36 @@ def fuzz(self, if pm_path_output and pm_path_output.startswith('package:'): apk_path = pm_path_output.split(':')[1].strip() + # Ensure target directory exists and is writable + android.adb.run_shell_command(f'su 0 mkdir -p {target_dir}') + android.adb.run_shell_command(f'su 0 chmod 777 {target_dir}') + # Clean up old files if any android.adb.run_shell_command(f'su 0 rm -f {wrap_sh_path}') android.adb.run_shell_command( - 'su 0 rm -f /data/local/tmp/libclang_rt.asan-*.so') + f'su 0 rm -f {target_dir}/libclang_rt.asan-*.so') - # Extract wrap.sh using unzip + # Extract wrap.sh using unzip directly to target_dir unzip_wrap_cmd = (f'su 0 unzip -o -j {apk_path} ' - 'lib/x86_64/wrap.sh -d /data/local/tmp') + f'lib/x86_64/wrap.sh -d {target_dir}') unzip_wrap_result = android.adb.run_shell_command(unzip_wrap_cmd) logs.info(f"DEBUG: unzip wrap.sh result: {unzip_wrap_result.strip()}") - # Extract ASan runtime using unzip - unzip_asan_cmd = ( - f'su 0 unzip -o -j {apk_path} ' - '"lib/x86_64/libclang_rt.asan-*.so" -d /data/local/tmp') + # Extract ASan runtime using unzip directly to target_dir + unzip_asan_cmd = (f'su 0 unzip -o -j {apk_path} ' + f'"lib/x86_64/libclang_rt.asan-*.so" -d {target_dir}') unzip_asan_result = android.adb.run_shell_command(unzip_asan_cmd) logs.info(f"DEBUG: unzip ASan rt result: {unzip_asan_result.strip()}") if android.adb.file_exists(wrap_sh_path): android.adb.run_shell_command(f'su 0 chmod 777 {wrap_sh_path}') android.adb.run_shell_command( - 'su 0 chmod 777 /data/local/tmp/libclang_rt.asan-*.so') + f'su 0 chmod 777 {target_dir}/libclang_rt.asan-*.so') + + # Print wrap.sh content for diagnostics + wrap_content = android.adb.run_shell_command( + f'su 0 cat {wrap_sh_path}') + logs.info(f"DEBUG: wrap.sh content:\n{wrap_content}") selinux_status = android.adb.run_shell_command('su 0 getenforce') logs.info(f"DEBUG: Initial SELinux status: {selinux_status.strip()}") @@ -1479,7 +1489,7 @@ def fuzz(self, logs.warning( f"DEBUG: setprop output/error: {setprop_result.strip()}") else: - logs.error("DEBUG: Failed to extract wrap.sh to /data/local/tmp") + logs.error(f"DEBUG: Failed to extract wrap.sh to {target_dir}") wrap_sh_path = None else: logs.error(f"DEBUG: Could not get APK path for {self.package_name}") @@ -1504,7 +1514,7 @@ def fuzz(self, if wrap_sh_path: android.adb.run_shell_command(f'su 0 rm -f {wrap_sh_path}') android.adb.run_shell_command( - 'su 0 rm -f /data/local/tmp/libclang_rt.asan-*.so') + f'su 0 rm -f {target_dir}/libclang_rt.asan-*.so') logs.info(f'DEBUG: adb command run: {result.command}') logs.info(f'DEBUG: adb command return code: {result.return_code}') diff --git a/src/clusterfuzz/_internal/bot/init_scripts/android.py b/src/clusterfuzz/_internal/bot/init_scripts/android.py index 72abf62515a..acfc8c0ca43 100644 --- a/src/clusterfuzz/_internal/bot/init_scripts/android.py +++ b/src/clusterfuzz/_internal/bot/init_scripts/android.py @@ -53,7 +53,8 @@ def run(): # Wait until battery charges to a minimum level and temperature threshold. android.battery.wait_until_good_state() - # Ensure adb runs as root for the rest of the session (essential for engine fuzzers). + # Ensure adb runs as root for the rest of the session (essential for + # engine fuzzers). android.adb.run_as_root() # Initialize environment settings. diff --git a/src/clusterfuzz/_internal/platforms/android/adb.py b/src/clusterfuzz/_internal/platforms/android/adb.py index de845270f57..e5f62dee6f2 100755 --- a/src/clusterfuzz/_internal/platforms/android/adb.py +++ b/src/clusterfuzz/_internal/platforms/android/adb.py @@ -225,9 +225,11 @@ def get_adb_path(): def get_device_state(): """Return the device status.""" - # Emulators do not support the fastboot protocol and cannot enter physical ramdump mode. - # The original check was backwards (checking emulator instead of physical devices), causing a 20-second - # fastboot timeout on every state check on emulators. We correct this to only run fastboot checks on physical devices. + # Emulators do not support the fastboot protocol and cannot enter physical + # ramdump mode. The original check was backwards (checking emulator instead + # of physical devices), causing a 20-second fastboot timeout on every state + # check on emulators. We correct this to only run fastboot checks on physical + # devices. if not environment.is_android_emulator(): fastboot_state = run_fastboot_command( ['getvar', 'is-ramdump-mode'], timeout=GET_DEVICE_STATE_TIMEOUT) From 54864f8101e1f8cd290a07213bd14934a0c68208 Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sat, 27 Jun 2026 15:53:18 +0000 Subject: [PATCH 54/59] Robust wrap.sh generation and diagnostic marker --- .../_internal/bot/fuzzers/libfuzzer.py | 62 ++++++++++++++----- 1 file changed, 48 insertions(+), 14 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 0dc4d68b92b..84e3b2f4b18 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1430,8 +1430,8 @@ def fuzz(self, raise LibFuzzerError('No launchable activity or instrumentation found.') # Workaround for wrap.sh execution on Android. - # We extract wrap.sh and ASan RT to the app's secure app_native_libs - # directory to avoid linker restrictions on /data/local/tmp, and run with + # We write a custom wrap.sh and extract ASan RT to the app's secure + # app_native_libs directory to avoid linker restrictions, and run with # SELinux permissive. target_dir = f"/data/data/{self.package_name}/app_native_libs" wrap_sh_path = f"{target_dir}/wrap.sh" @@ -1452,28 +1452,49 @@ def fuzz(self, android.adb.run_shell_command( f'su 0 rm -f {target_dir}/libclang_rt.asan-*.so') - # Extract wrap.sh using unzip directly to target_dir - unzip_wrap_cmd = (f'su 0 unzip -o -j {apk_path} ' - f'lib/x86_64/wrap.sh -d {target_dir}') - unzip_wrap_result = android.adb.run_shell_command(unzip_wrap_cmd) - logs.info(f"DEBUG: unzip wrap.sh result: {unzip_wrap_result.strip()}") - # Extract ASan runtime using unzip directly to target_dir unzip_asan_cmd = (f'su 0 unzip -o -j {apk_path} ' f'"lib/x86_64/libclang_rt.asan-*.so" -d {target_dir}') unzip_asan_result = android.adb.run_shell_command(unzip_asan_cmd) logs.info(f"DEBUG: unzip ASan rt result: {unzip_asan_result.strip()}") + # Generate custom wrap.sh content (ensuring LF line endings and marker) + wrap_sh_content = ( + f"#!/system/bin/sh\n" + f"HERE=\"{target_dir}\"\n" + f"_ASAN_OPTIONS=\"log_to_syslog=false," + f"allow_user_segv_handler=1\"\n" + f"_ASAN_OPTIONS=\"$_ASAN_OPTIONS," + f"strict_memcmp=0,use_sigaltstack=1\"\n" + f"_LD_PRELOAD=\"$HERE/libclang_rt.asan-x86_64-android.so\"\n" + f"echo wrap_sh_ran > /data/local/tmp/wrap_marker.txt\n" + f"log -t cr_wrap.sh -- \"Launching with ASAN enabled.\"\n" + f"log -t cr_wrap.sh -- \"Command: $0 $@\"\n" + f"log -t cr_wrap.sh -- \"LD_PRELOAD=$_LD_PRELOAD\"\n" + f"log -t cr_wrap.sh -- \"ASAN_OPTIONS=$_ASAN_OPTIONS\"\n" + f"export LD_PRELOAD=\"$_LD_PRELOAD\"\n" + f"export ASAN_OPTIONS=\"$_ASAN_OPTIONS\"\n" + f"exec \"$@\"\n") + + # Write wrap.sh locally and push to avoid permission issues + import tempfile + with tempfile.NamedTemporaryFile( + mode='w', suffix='.sh', delete=False) as temp_wrap: + temp_wrap.write(wrap_sh_content) + temp_wrap_path = temp_wrap.name + + tmp_wrap_path = "/data/local/tmp/temp_wrap.sh" + android.adb.copy_local_file_to_remote(temp_wrap_path, tmp_wrap_path) + os.remove(temp_wrap_path) + + android.adb.run_shell_command(f"su 0 cp {tmp_wrap_path} {wrap_sh_path}") + android.adb.run_shell_command(f"su 0 rm -f {tmp_wrap_path}") + if android.adb.file_exists(wrap_sh_path): android.adb.run_shell_command(f'su 0 chmod 777 {wrap_sh_path}') android.adb.run_shell_command( f'su 0 chmod 777 {target_dir}/libclang_rt.asan-*.so') - # Print wrap.sh content for diagnostics - wrap_content = android.adb.run_shell_command( - f'su 0 cat {wrap_sh_path}') - logs.info(f"DEBUG: wrap.sh content:\n{wrap_content}") - selinux_status = android.adb.run_shell_command('su 0 getenforce') logs.info(f"DEBUG: Initial SELinux status: {selinux_status.strip()}") if selinux_status.strip() == 'Enforcing': @@ -1489,7 +1510,7 @@ def fuzz(self, logs.warning( f"DEBUG: setprop output/error: {setprop_result.strip()}") else: - logs.error(f"DEBUG: Failed to extract wrap.sh to {target_dir}") + logs.error(f"DEBUG: Failed to create wrap.sh at {wrap_sh_path}") wrap_sh_path = None else: logs.error(f"DEBUG: Could not get APK path for {self.package_name}") @@ -1510,6 +1531,19 @@ def fuzz(self, if selinux_revert: android.adb.run_shell_command('su 0 setenforce 1') logs.info("DEBUG: Restored SELinux to Enforcing.") + + # Check for marker + marker_path = "/data/local/tmp/wrap_marker.txt" + if android.adb.file_exists(marker_path): + logs.info("DEBUG: SUCCESS! wrap.sh executed (marker found).") + marker_content = android.adb.run_shell_command( + f"su 0 cat {marker_path}") + logs.info(f"DEBUG: marker content: {marker_content.strip()}") + android.adb.run_shell_command(f"su 0 rm -f {marker_path}") + else: + logs.error( + "DEBUG: FAILURE! wrap.sh did NOT execute (marker not found).") + # Clean up wrap.sh and ASan runtime if wrap_sh_path: android.adb.run_shell_command(f'su 0 rm -f {wrap_sh_path}') From c1ee5c3f11f4150dd01957f4fe9cecbc85707399 Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sat, 27 Jun 2026 16:25:48 +0000 Subject: [PATCH 55/59] Support wrapping sub-processes and restore wrap.sh print --- .../_internal/bot/fuzzers/libfuzzer.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 84e3b2f4b18..2ef76e1925e 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1495,6 +1495,11 @@ def fuzz(self, android.adb.run_shell_command( f'su 0 chmod 777 {target_dir}/libclang_rt.asan-*.so') + # Print wrap.sh content for diagnostics + wrap_content = android.adb.run_shell_command( + f'su 0 cat {wrap_sh_path}') + logs.info(f"DEBUG: wrap.sh content:\n{wrap_content}") + selinux_status = android.adb.run_shell_command('su 0 getenforce') logs.info(f"DEBUG: Initial SELinux status: {selinux_status.strip()}") if selinux_status.strip() == 'Enforcing': @@ -1509,6 +1514,14 @@ def fuzz(self, if setprop_result: logs.warning( f"DEBUG: setprop output/error: {setprop_result.strip()}") + + # Also set for :test_process suffix (Chromium sub-process) + setprop_result_sub = android.adb.run_shell_command( + f'su 0 setprop wrap.{self.package_name}:test_process ' + f'{wrap_sh_path}') + if setprop_result_sub: + logs.warning(f"DEBUG: setprop sub-process output/error: " + f"{setprop_result_sub.strip()}") else: logs.error(f"DEBUG: Failed to create wrap.sh at {wrap_sh_path}") wrap_sh_path = None @@ -1527,6 +1540,8 @@ def fuzz(self, finally: # Clear the wrapper property. android.adb.run_shell_command(f'su 0 setprop wrap.{self.package_name} ""') + android.adb.run_shell_command( + f'su 0 setprop wrap.{self.package_name}:test_process ""') # Restore SELinux if we changed it if selinux_revert: android.adb.run_shell_command('su 0 setenforce 1') From ad56143c7ec0ecb86f7f1d90b376210741bd126c Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sat, 27 Jun 2026 17:19:15 +0000 Subject: [PATCH 56/59] Refactor SECCOMP bypass into helper method and resolve lint --- .../_internal/bot/fuzzers/libfuzzer.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 2ef76e1925e..078dc2c9a56 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1341,6 +1341,38 @@ def _copy_local_directories_from_device(self, local_directories): android.adb.copy_remote_directory_to_local(device_directory, local_directory) + def _disable_seccomp(self): + """Disables SECCOMP by restarting the runtime in Permissive mode.""" + # Check if we already disabled seccomp in a previous round + seccomp_disabled = android.adb.run_shell_command( + 'getprop temp.debug.seccomp_disabled') + if seccomp_disabled.strip() == '1': + logs.info("DEBUG: SECCOMP already disabled in a previous round.") + return + + logs.info("DEBUG: Disabling SECCOMP via runtime restart...") + import time + android.adb.run_shell_command('su 0 stop') + android.adb.run_shell_command('su 0 setprop sys.boot_completed 0') + time.sleep(2) + android.adb.run_shell_command('su 0 start') + + # Wait for boot completion (max 60 seconds) + boot_timeout = 60 + start_time = time.time() + while time.time() - start_time < boot_timeout: + time.sleep(2) + boot_completed = android.adb.run_shell_command( + 'getprop sys.boot_completed') + if boot_completed.strip() == '1': + logs.info("DEBUG: Runtime restarted successfully.") + android.adb.run_shell_command( + 'su 0 setprop temp.debug.seccomp_disabled 1') + return + + logs.error("DEBUG: Timed out waiting for runtime restart. " + "Fuzzer might fail.") + def fuzz(self, corpus_directories, fuzz_timeout, @@ -1522,6 +1554,8 @@ def fuzz(self, if setprop_result_sub: logs.warning(f"DEBUG: setprop sub-process output/error: " f"{setprop_result_sub.strip()}") + + self._disable_seccomp() else: logs.error(f"DEBUG: Failed to create wrap.sh at {wrap_sh_path}") wrap_sh_path = None From 608821d1166290c078da377472a7683b61cff388 Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sat, 27 Jun 2026 22:09:54 +0000 Subject: [PATCH 57/59] Grant legacy storage permissions to fix copyLibrary failure --- src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 078dc2c9a56..4ad7d74dd8e 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1231,6 +1231,11 @@ def _setup_dependencies(self, build_directory): # Grant MANAGE_EXTERNAL_STORAGE permission android.adb.run_shell_command( f'appops set {self.package_name} MANAGE_EXTERNAL_STORAGE allow') + # Also grant legacy storage permissions just in case the APK uses them + android.adb.run_shell_command(f'su 0 pm grant {self.package_name} ' + f'android.permission.READ_EXTERNAL_STORAGE') + android.adb.run_shell_command(f'su 0 pm grant {self.package_name} ' + f'android.permission.WRITE_EXTERNAL_STORAGE') logs.info(f"Dependency setup complete. LibraryUnderTest: " f"{self.library_under_test}, AuxiliaryLibraries: " f"{self.auxiliary_libraries}") From fae87ad30a56fe481dafa26b595c1f3978413273 Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sun, 28 Jun 2026 16:35:09 +0000 Subject: [PATCH 58/59] Add diagnostic logging and robust permission granting for storage --- .../_internal/bot/fuzzers/libfuzzer.py | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 4ad7d74dd8e..0fdeb62cef7 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1229,13 +1229,28 @@ def _setup_dependencies(self, build_directory): self.auxiliary_libraries = ','.join(aux_libs) # Grant MANAGE_EXTERNAL_STORAGE permission - android.adb.run_shell_command( - f'appops set {self.package_name} MANAGE_EXTERNAL_STORAGE allow') + logs.info("Granting MANAGE_EXTERNAL_STORAGE...") + op_names = ['MANAGE_EXTERNAL_STORAGE', 'android:manage_external_storage'] + for op in op_names: + cmd = f'su 0 appops set {self.package_name} {op} allow' + out = android.adb.run_shell_command(cmd) + logs.info(f"Command: {cmd}, Output: {out}") + + # Verify + verify_cmd = f'su 0 appops get {self.package_name} {op}' + verify_out = android.adb.run_shell_command(verify_cmd) + logs.info(f"Verification: {verify_cmd}, Output: {verify_out}") + # Also grant legacy storage permissions just in case the APK uses them - android.adb.run_shell_command(f'su 0 pm grant {self.package_name} ' - f'android.permission.READ_EXTERNAL_STORAGE') - android.adb.run_shell_command(f'su 0 pm grant {self.package_name} ' - f'android.permission.WRITE_EXTERNAL_STORAGE') + logs.info("Granting legacy storage permissions...") + permissions = [ + 'android.permission.READ_EXTERNAL_STORAGE', + 'android.permission.WRITE_EXTERNAL_STORAGE' + ] + for perm in permissions: + cmd = f'su 0 pm grant {self.package_name} {perm}' + out = android.adb.run_shell_command(cmd) + logs.info(f"Command: {cmd}, Output: {out}") logs.info(f"Dependency setup complete. LibraryUnderTest: " f"{self.library_under_test}, AuxiliaryLibraries: " f"{self.auxiliary_libraries}") From f77f2230dbb34953d12abd65e95d8dfb3effa1ec Mon Sep 17 00:00:00 2001 From: Fernando Flores Date: Sun, 28 Jun 2026 17:19:03 +0000 Subject: [PATCH 59/59] Fix library name typo for LibraryUnderTest in libfuzzer.py --- src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py index 0fdeb62cef7..f20773d8106 100644 --- a/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py +++ b/src/clusterfuzz/_internal/bot/fuzzers/libfuzzer.py @@ -1221,7 +1221,13 @@ def _setup_dependencies(self, build_directory): for lib in sorted_libs: name = os.path.basename(lib) if name == main_lib_name: - self.library_under_test = name + # Strip 'lib' prefix and '.so' suffix for System.loadLibrary + stripped_name = name + if stripped_name.startswith('lib'): + stripped_name = stripped_name[3:] + if stripped_name.endswith('.so'): + stripped_name = stripped_name[:-3] + self.library_under_test = stripped_name else: aux_libs.append(name)