Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c339307
Optimize Android emulator initialization by batching reboots
May 15, 2026
8c579ed
Fix reboot_done tracking to ensure device settings are applied
May 16, 2026
af62e75
Revert previous fix and use accumulated state for reboot_done
May 16, 2026
a3c840d
Invert logic to needs_reboot and use bitwise OR operator to fix state…
May 16, 2026
c2fd3e2
Fix needs_reboot logic so ASan setup clears pending reboots
May 16, 2026
09273e2
Fix initialization reboot logic to correctly preserve base requirements
May 16, 2026
d6ab58f
Add unit tests for android device reboot logic
May 16, 2026
4b340a5
Fix formatting issues from linter in device_test.py
May 18, 2026
4d0dfa7
Address PR 5280 comments: Add return types and docstrings
May 18, 2026
9d8cd5c
Rename wait_for_reboot to should_reboot
May 20, 2026
baf5766
Update docstrings for test cases
May 20, 2026
98406b3
Simplify initialize_device logic and refactor tests
May 20, 2026
48f3468
Add descriptive docstring for InitializeEnvironmentTest
May 20, 2026
2c08a43
Add docstring to write_data_to_file
May 20, 2026
336400b
Move helpers import to module level in device_test.py
May 21, 2026
ccffa14
Ensure physical devices always reboot to lock /system
May 21, 2026
829ac70
Merge branch 'master' into optimize-android-reboots
jardondiego May 21, 2026
c347ad6
Fix linter errors in device_test.py
May 21, 2026
baec846
Merge branch 'master' into optimize-android-reboots
jardondiego May 21, 2026
4f7a511
Optimize Android system partition writes with manual read-only remount
May 22, 2026
511b555
Update docstring for write_data_to_file based on PR feedback
May 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions src/clusterfuzz/_internal/platforms/android/adb.py
Original file line number Diff line number Diff line change
Expand Up @@ -880,8 +880,16 @@ def write_command_line_file(command_line, app_path):
write_data_to_file(command_line_file_contents, command_line_path)


def write_data_to_file(contents, file_path):
"""Writes content to file."""
def write_data_to_file(contents, file_path, should_reboot=True):
"""Writes content to file.

Args:
contents: The string content to write.
file_path: The path to the file on the Android device.
should_reboot: Whether to reboot the device after writing the file.
Only applies if `file_path` is under `/system`, since we need to
remount that partition as read-write first.
"""
# If this is a file in /system, we need to remount /system as read-write and
# after file is written, revert it back to read-only.
is_system_file = file_path.startswith('/system')
Expand All @@ -896,5 +904,9 @@ def write_data_to_file(contents, file_path):
run_shell_command('chmod 0644 %s' % file_path, root=True)

if is_system_file:
reboot()
wait_until_fully_booted()
if should_reboot:
reboot()
wait_until_fully_booted()
else:
# Manually revert /system to read-only since we aren't rebooting.
run_shell_command('mount -o ro,remount /system', root=True)
5 changes: 3 additions & 2 deletions src/clusterfuzz/_internal/platforms/android/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,11 +316,12 @@ def initialize_device():
add_test_accounts_if_needed()

# Setup AddressSanitizer if needed.
sanitizer.setup_asan_if_needed()
asan_reboot_done = sanitizer.setup_asan_if_needed()

# Reboot device as above steps would need it and also it brings device in a
# good state.
reboot()
if not asan_reboot_done:
reboot()

# Make sure we are running as root after restart.
adb.run_as_root()
Expand Down
22 changes: 15 additions & 7 deletions src/clusterfuzz/_internal/platforms/android/sanitizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,25 +69,31 @@ def set_options(sanitizer_tool_name, sanitizer_options):
if not sanitizer_options_file_path:
return

adb.write_data_to_file(sanitizer_options, sanitizer_options_file_path)
# Skip reboot as the app will pick up the options file on restart.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to make sure I understand, this means that the next time the app is started, it will read from the options file?

Copy link
Copy Markdown
Collaborator Author

@jardondiego jardondiego May 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. We write the ASan options to the system file. When the target fuzzer application is launched in a new process shortly after this, the new options file are read from disk during its initialization. A full device reboot isn't required for this step, so we can safely skip it to save time. Forcing a ~60s kernel reboot to update a text string that a process is going to read 2 seconds later is redundant.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, thanks. IIUC the logic in write_data_to_file(), that means we'll leave /system in read-write mode. Is that intentional?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I don't think it would be a problem for emulated devices since they would be launched and destroyed in every task bit it might be an issue for non-virtual devices since a fuzzing job could break a system file and we would have to address that issue in a physical device, and that could mean having to physically check such device. We could either skip this optimization for virtual devices or manually trigger a read-only re-mounting. What do you suggest?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I applied Option A for now, but I'm all ears for any suggestions from you. Thanks!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See other comment, I don't think option A was applied in the end.

In any case, I don't think it's a big deal to lose 1m per fuzzing session - they last 3 hours anyway. I would rather go for option B and avoid the risk of weird issues where /system files get corrupted. I agree with you that for emulated devices that should be safe, but I'm not 100% confident :)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think something weird happened in my workspace, I was moving around several branches and got confused. Agreed! I will adjust as suggested!

adb.write_data_to_file(
sanitizer_options, sanitizer_options_file_path, should_reboot=False)
Comment thread
jardondiego marked this conversation as resolved.


def setup_asan_if_needed():
"""Set up asan on device."""
def setup_asan_if_needed() -> bool:
"""Set up asan on device.

Returns:
True if the device was rebooted or restarted during setup, False otherwise.
"""
if not environment.get_value('ASAN_DEVICE_SETUP'):
# Only do this step if explicitly enabled in the job type. This cannot be
# determined from libraries in application directory since they can go
# missing in a bad build, so we want to catch that.
return
return False

if settings.get_sanitizer_tool_name():
# If this is a sanitizer build, no need to setup ASAN (incompatible).
return
return False

app_directory = environment.get_value('APP_DIR')
if not app_directory:
# No app directory -> No ASAN runtime library. No work to do, bail out.
return
return False

# Initialize variables.
android_directory = environment.get_platform_resources_directory()
Expand All @@ -108,7 +114,7 @@ def setup_asan_if_needed():
result = process.run_and_wait()
if result.return_code:
logs.error('Failed to setup ASan on device.', output=result.output)
return
return False

logs.info(
'ASan device setup script successfully finished, waiting for boot.',
Expand All @@ -117,3 +123,5 @@ def setup_asan_if_needed():
# Wait until fully booted as otherwise shell restart followed by a quick
# reboot can trigger data corruption in /data/data.
adb.wait_until_fully_booted()

return True
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,60 @@

from clusterfuzz._internal.platforms.android import device
from clusterfuzz._internal.tests.test_libs import android_helpers
from clusterfuzz._internal.tests.test_libs import helpers


class InitializeEnvironmentTest(android_helpers.AndroidTest):
"""Tests for """
"""Tests for the device environment initialization process (`initialize_environment`)."""

def test(self):
"""Ensure that initialize_environment throws no exceptions."""
device.initialize_environment()


class InitializeDeviceRebootLogicTest(unittest.TestCase):
"""Tests the reboot batching logic in initialize_device."""

def setUp(self):
helpers.patch(self, [
'clusterfuzz._internal.system.environment.is_engine_fuzzer_job',
'clusterfuzz._internal.platforms.android.adb.setup_adb',
'clusterfuzz._internal.platforms.android.adb.run_as_root',
'clusterfuzz._internal.platforms.android.device.configure_system_build_properties',
'clusterfuzz._internal.platforms.android.device.configure_device_settings',
'clusterfuzz._internal.platforms.android.device.add_test_accounts_if_needed',
'clusterfuzz._internal.platforms.android.sanitizer.setup_asan_if_needed',
'clusterfuzz._internal.platforms.android.device.reboot',
'clusterfuzz._internal.platforms.android.wifi.configure',
'clusterfuzz._internal.platforms.android.device.setup_host_and_device_forwarder_if_needed',
'clusterfuzz._internal.platforms.android.settings.change_se_linux_to_permissive_mode',
'clusterfuzz._internal.platforms.android.app.wait_until_optimization_complete',
'clusterfuzz._internal.platforms.android.ui.clear_notifications',
'clusterfuzz._internal.platforms.android.ui.unlock_screen',
])
self.mock.is_engine_fuzzer_job.return_value = False

def test_reboot_if_asan_did_not_run(self):
"""Test that `initialize_device()` calls `reboot()` if the ASan setup
script did not."""
self.mock.setup_asan_if_needed.return_value = False

device.initialize_device()
self.mock.reboot.assert_called_once()

def test_no_reboot_if_asan_ran(self):
"""Test that `initialize_device()` skips calling `reboot()` if the ASan
setup script did."""
self.mock.setup_asan_if_needed.return_value = True

device.initialize_device()
self.mock.reboot.assert_not_called()


class AddTestAccountsIfNeededTest(unittest.TestCase):
"""Tests for add_test_accounts_if_needed."""

def setUp(self):
from clusterfuzz._internal.tests.test_libs import helpers
helpers.patch(self, [
'clusterfuzz._internal.system.environment.is_uworker',
'clusterfuzz._internal.base.persistent_cache.get_value',
Expand Down
Loading