#!/usr/bin/env python3 # Copyright (C) 2020 Denis 'GNUtoo' Carikli # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import os import re import sh import time from lib.common import get_output from lib.common import trace from lib.heimdall import Heimdall # TODO: Derive that information from the PIT in the data repository instead class BootloaderRuntime(object): def __init__(self, config, host, device, quiet=False): self.config = config self.device = device self.host = host self.quiet = quiet self.heimdall = Heimdall(self.host, quiet=quiet) @trace def get_cache_partition(self): if not self.quiet: print("BootloaderRuntime.get_cache_partition()") if str(self.device) in ['espresso3g', 'espressowifi', 'i9100', 'i9300', 'i9305', 'n5100', 'n5110', 'n7000', 'n7100']: return 'CACHE' elif str(self.device) in ['maguro']: return 'cache' else: assert(False) @trace def get_recovery_partition(self): if str(self.device) in ['espresso3g', 'espressowifi', 'i9100', 'i9300', 'i9305', 'n5100', 'n5110', 'n7000', 'n7100']: return 'RECOVERY' elif str(self.device) in ['maguro']: return 'recovery' else: assert(False) @trace def get_boot_img_partition(self): if str(self.device) in ['espresso3g', 'espressowifi', 'i9100', 'n7000']: return 'KERNEL' elif str(self.device) in ['i9300', 'i9305', 'n5100', 'n5110', 'n7100']: return 'BOOT' elif str(self.device) in ['maguro']: return 'boot' else: assert(False) @trace def is_ready(self): return self.heimdall.is_ready() @trace def wait_for_ready(self): while True: if self.is_ready(): return True else: time.sleep(1) @trace def boot_to_bootloader(self): device_offline = False while True: if self.is_ready(): return True adb_state = None try: adb_state = get_output(self.host.run(['adb', 'get-state'])) except sh.ErrorReturnCode_1: if not device_offline and not self.quiet: print('\tNo devices detected. Possible causes:') print('\t- ADB is not enabled? ' '(device booted in Replicant with adb off)') print('\t- No devices are connected (bad cable?)') print('\t- The device is off ' '(no powered on device detected)') print('\t- The device is booting ' 'and adbd is not yet started') device_offline = True time.sleep(1) if adb_state in ['device', 'recovery']: self.host.run(['adb', 'reboot', 'bootloader']) return self.wait_for_ready() @trace def install_recovery(self, path, retries=100): if not self.quiet: print('Starting to install the Recovery on {}'.format(self.device)) self.boot_to_bootloader() partitions = { self.get_boot_img_partition() : path, self.get_recovery_partition() : path, } return self.heimdall.flash(partitions, retries) @trace def install_boot_img(self, path, retries=100): if not self.quiet: print('Starting to install the {} boot image on {}'.format( path, self.device)) self.boot_to_bootloader() partitions = { self.get_boot_img_partition() : path, } return self.heimdall.flash(partitions) @trace def wipe_cache(self): # It currently fails with the following error with a file of 8M on the # Galaxy SIII (GT-I9300): # Uploading CACHE # 100%Ending session... # R... (30 more, please see e.stdout) # # STDERR: # # ERROR: Failed to confirm end of file transfer sequence! # ERROR: CACHE upload failed! # # ERROR: Failed to send end session packet! assert(False) # The code responsible for that failure has been kept below for # reference and to enable to fix it. # The protocol that heimdall uses is called Thor # On all Replicant compatible devices with an Exynos, the nonfree # bootloader (s-boot 4.0) has a very buggy implementation of Thor. # Practically speaking: # - If your computer is under heavy I/O and/or CPU load, it increases # the probability of the transfer failling. # - The probability of the transfer failing also increases with the # increase of the image size. # So we create a small file. On devices with other SOCs, like the Galaxy # Nexus (GT-I9250) or the Galaxy Tab 2 (GT-P3100, GT-P3110, GT-P5100, # GT-P5510), the stock bootloader seem to have implemented that protocol # correctly. Upstream U-boot also has a free software implementation of # that protocol. cache_file_size = 8 * 1024 *1024 cache_partition_name = self.get_cache_partition() tmpdir = self.host.mktemp() part_img_path = '{}{}{}.img'.format(tmpdir, os.sep, cache_partition_name) self.host.run(['dd', 'if=/dev/zero', 'of={}'.format(part_img_path), 'count={}'.format(int(cache_file_size / 512))]) self.boot_to_bootloader() partitions = { cache_partition_name : part_img_path, } return self.heimdall.flash(partitions) @trace def run(self, commands): # Remove that assert if you really want to run commands inside the # bootloader. # For now we support various nonfree bootloader that can run commands # through the UART port and code through an exploit through USB. # While it would probably not be easy to run bootloaders commands # through USB, it might be possible to manipulate the PARAM partition # to implement a subset of the functionalities. assert(False) @trace def boot(self, environment, retries=100): if environment == 'bootloader': return self.heimdall.reboot()