diff --git a/rpi-eeprom-config b/rpi-eeprom-config index af0d63b..ad417bd 100755 --- a/rpi-eeprom-config +++ b/rpi-eeprom-config @@ -1,12 +1,17 @@ #!/usr/bin/env python -# rpi-eeprom-config -# Utility for reading and writing the configuration file in the -# Raspberry Pi 4 bootloader EEPROM image. +""" +rpi-eeprom-config +""" import argparse +import atexit +import os +import subprocess import struct import sys +import tempfile +import time IMAGE_SIZE = 512 * 1024 @@ -22,18 +27,160 @@ MAGIC_MASK = 0xfffff00f FILE_MAGIC = 0x55aaf11f # id for modifiable file, currently only bootconf.txt FILE_HDR_LEN = 20 FILENAME_LEN = 12 +TEMP_DIR = None + +def exit_handler(): + """ + Delete any temporary files. + """ + if TEMP_DIR is not None and os.path.exists(TEMP_DIR): + tmp_image = os.path.join(TEMP_DIR, 'pieeprom.upd') + if os.path.exists(tmp_image): + os.remove(tmp_image) + tmp_conf = os.path.join(TEMP_DIR, 'boot.conf') + if os.path.exists(tmp_conf): + os.remove(tmp_conf) + os.rmdir(TEMP_DIR) + +def create_tempdir(): + global TEMP_DIR + if TEMP_DIR is None: + TEMP_DIR = tempfile.mkdtemp() + +def exit_error(msg): + """ + Trapped a fatal arror, output message to stderr and exit with non-zero + return code. + """ + sys.stderr.write("ERROR: %s\n" % msg) + sys.exit(1) + +def shell_cmd(args): + """ + Executes a shell command waits for completion returning STDOUT. If an + error occurs then exit and output the subprocess stdout, stderr messages + for debug. + """ + start = time.time() + arg_str = ' '.join(args) + result = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + while time.time() - start < 5: + if result.poll() is not None: + break + + if result.poll() is None: + exit_error("%s timeout" % arg_str) + + if result.returncode != 0: + exit_error("%s failed: %d\n %s\n %s\n" % + (arg_str, result.returncode, result.stdout.read(), result.stderr.read())) + else: + return result.stdout.read() + +def get_latest_eeprom(): + """ + Returns the path of the latest EEPROM image file if it exists. + """ + latest = shell_cmd(['rpi-eeprom-update', '-l']).rstrip() + if not os.path.exists(latest): + exit_error("EEPROM image '%s' not found" % latest) + return latest + +def apply_update(config, eeprom=None, config_src=None): + """ + Applies the config file to the latest available EEPROM image and spawns + rpi-eeprom-update to schedule the update at the next reboot. + """ + if eeprom is not None: + eeprom_image = eeprom + else: + eeprom_image = get_latest_eeprom() + create_tempdir() + tmp_update = os.path.join(TEMP_DIR, 'pieeprom.upd') + image = BootloaderImage(eeprom_image, tmp_update) + image.write(config) + config_str = open(config).read() + if config_src is None: + config_src = '' + sys.stdout.write("Updating bootloader EEPROM\n image: %s\nconfig_src: %s\nconfig: %s\n%s\n" % + (eeprom_image, config_src, config, config_str)) + + # Ignore APT package checksums so that this doesn't fail when used + # with EEPROMs with configs delivered outside of APT. + # The checksums are really just a safety check for automatic updates. + args = ['rpi-eeprom-update', '-d', '-i', '-f', tmp_update] + resp = shell_cmd(args) + sys.stdout.write(resp) + +def edit_config(eeprom=None): + """ + Implements something like visudo for editing EEPROM configs. + """ + # Default to nano if $EDITOR is not defined. + editor = 'nano' + if 'EDITOR' in os.environ: + editor = os.environ['EDITOR'] + + config_src = '' + if eeprom is None: + # If an EEPROM has not been specified but there is a pending + # update then use that as the current EEPROM image. + bootfs = shell_cmd(['rpi-eeprom-update', '-b']).rstrip() + pending = os.path.join(bootfs, 'pieeprom.upd') + if os.path.exists(pending): + config_src = pending + image = BootloaderImage(pending) + current_config = image.get_config() + else: + config_src = 'vcgencmd bootloader_config' + current_config = read_current_config() + else: + # If an EEPROM image is specified OR there is pending update + # then get the current config from there. + image = BootloaderImage(eeprom) + config_src = eeprom + current_config = image.get_config() + + create_tempdir() + tmp_conf = os.path.join(TEMP_DIR, 'boot.conf') + out = open(tmp_conf, 'w') + out.write(current_config) + out.close() + cmd = "\'%s\' \'%s\'" % (editor, tmp_conf) + result = os.system(cmd) + if result != 0: + exit_error("Aborting update because \'%s\' exited with code %d." % (cmd, result)) + + new_config = open(tmp_conf, 'r').read() + if len(new_config.splitlines()) < 2: + exit_error("Aborting update because \'%s\' appears to be empty." % tmp_conf) + apply_update(tmp_conf, eeprom, config_src) + +def read_current_config(): + """ + Reads the configuration used by the current bootloader. + """ + return shell_cmd(['vcgencmd', 'bootloader_config']) class BootloaderImage(object): - def __init__(self, filename, output): + def __init__(self, filename, output=None): + """ + Instantiates a Bootloader image writer with a source eeprom (filename) + and optionally an output filename. + """ self._filename = filename - self._bytes = bytearray(open(filename, 'rb').read()) + try: + self._bytes = bytearray(open(filename, 'rb').read()) + except IOError as err: + exit_error("Failed to read \'%s\'\n%s\n" % (filename, str(err))) self._out = None if output is not None: self._out = open(output, 'wb') if len(self._bytes) != IMAGE_SIZE: - raise Exception("%s: Expected size %d bytes actual size %d bytes" % - (filename, IMAGE_SIZE, len(self._bytes))) + exit_error("%s: Expected size %d bytes actual size %d bytes" % + (filename, IMAGE_SIZE, len(self._bytes))) def find_config(self): offset = 0 @@ -51,7 +198,7 @@ class BootloaderImage(object): offset += 8 + length # length + type offset = (offset + 7) & ~7 - raise Exception('Bootloader config not found') + raise Exception('EEPROM parse error: Bootloader config not found') def write(self, new_config): hdr_offset, length = self.find_config() @@ -59,12 +206,13 @@ class BootloaderImage(object): new_len = len(new_config_bytes) + FILENAME_LEN + 4 if len(new_config_bytes) > MAX_BOOTCONF_SIZE: raise Exception("Config is too large (%d bytes). The maximum size is %d bytes." - % (len(new_config_bytes), MAX_BOOTCONF_SIZE)) + % (len(new_config_bytes), MAX_BOOTCONF_SIZE)) if hdr_offset + len(new_config_bytes) + FILE_HDR_LEN > IMAGE_SIZE: raise Exception('EEPROM image size exceeded') struct.pack_into('>L', self._bytes, hdr_offset + 4, new_len) - struct.pack_into(("%ds" % len(new_config_bytes)), self._bytes, hdr_offset + 4 + FILE_HDR_LEN, new_config_bytes) + struct.pack_into(("%ds" % len(new_config_bytes)), self._bytes, + hdr_offset + 4 + FILE_HDR_LEN, new_config_bytes) # If the new config is smaller than the old config then set any old # data which is now unused to all ones (erase value) @@ -83,10 +231,14 @@ class BootloaderImage(object): else: sys.stdout.write(self._bytes) - def read(self): + def get_config(self): hdr_offset, length = self.find_config() offset = hdr_offset + 4 + FILE_HDR_LEN config_bytes = self._bytes[offset:offset+length-FILENAME_LEN-4] + return config_bytes + + def read(self): + config_bytes = self.get_config() if self._out is not None: self._out.write(config_bytes) self._out.close() @@ -97,34 +249,99 @@ class BootloaderImage(object): sys.stdout.write(config_bytes) def main(): - parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, \ - description='Bootloader EEPROM configuration tool for the Raspberry Pi 4. \ -\n\nThere are 3 operating modes: \ -\n\n1. Output the bootloader configuration stored in an EEPROM image file to \ -the screen (STDOUT): specify only the name of an EEPROM image file using the \ -\'eeprom\' option. \ -\n\n2. Output the bootloader configuration stored in an EEPROM image file to a \ -file: specify the EEPROM image file using the \'eeprom\' option, and the output \ -file using the \'--out\' option.\ -\n\n3. Insert a new bootloader configuration into an EEPROM image file: specify \ -the source EEPROM image file using the \'eeprom\' option and the bootloader \ -configuration file using the \'--config\' option. A new file which is a \ -combination of the EEPROM image file, together with the new bootloader \ -configuration file will be created - specify its name using the \'--out\' option. \ -The new bootloader configuration will replace any configuration present in the \ -source EEPROM image.\ -\n\nBootloader EEPROM images are contained in the \'rpi-eeprom-images\' package,\ - which installs them to the /lib/firmware/raspberrypi/bootloader directory.') - parser.add_argument('--config', help='Name of bootloader configuration file') - parser.add_argument('--out', help='Name of output file') - parser.add_argument('eeprom', help='Name of EEPROM file to use as input') + """ + Utility for reading and writing the configuration file in the + Raspberry Pi 4 bootloader EEPROM image. + """ + description = """\ +Bootloader EEPROM configuration tool for the Raspberry Pi 4. +Operating modes: + +1. Outputs the current bootloader configuration to STDOUT if no arguments are + specified OR the given output file if --out is specified. + + rpi-eeprom-config [--out boot.conf] + +2. Extracts the configuration file from the given 'eeprom' file and outputs + the result to STDOUT or the output file if --output is specified. + + rpi-eeprom-config pieeprom.bin [--out boot.conf] + +3. Writes a new EEPROM image replacing the configuration file with the contents + of the file specified by --config. + + rpi-eeprom-config --config boot.conf --out newimage.bin pieeprom.bin + + The new image file can be installed via rpi-eeprom-update + rpi-eeprom-update -d -f newimage.bin + +4. Applies a given config file to an EEPROM image and invokes rpi-eeprom-update + to schedule an update of the bootloader when the system is rebooted. + + Since this command launches rpi-eeprom-update to schedule the EERPOM update + it must be run as root. + + sudo rpi-eeprom-config --apply boot.conf [pieeprom.bin] + + If the 'eeprom' argument is not specified then the latest available image + is selected by calling 'rpi-eeprom-update -l'. + +5. The '--edit' parameter behaves the same as '--apply' except that instead of + applying a predefined configuration file a text editor is launched with the + contents of the current EEPROM configuration. + + Since this command launches rpi-eeprom-update to schedule the EERPOM update + it must be run as root. + + The configuration file will be taken from: + * The 'eeprom' file - if specified. + * The current pending update - typically /boot/pieeprom.upd + * The cached bootloader configuration 'vcgencmd bootloader_config' + + sudo -E rpi-eeprom-config --edit [pieeprom.bin] + + The default text editor is nano and may be overriden by setting the 'EDITOR' + environment variable and passing '-E' to 'sudo' to preserve the environment. + +See 'rpi-eeprom-update -h' for more information about the available EEPROM +images. +""" + parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, + description=description) + + parser.add_argument('-a', '--apply', required=False, + help='Updates the bootloader to the given config plus latest available EEPROM release.') + parser.add_argument('-c', '--config', help='Name of bootloader configuration file', required=False) + parser.add_argument('-e', '--edit', action='store_true', default=False, help='Edit the current EEPROM config') + parser.add_argument('-o', '--out', help='Name of output file', required=False) + parser.add_argument('eeprom', nargs='?', help='Name of EEPROM file to use as input') args = parser.parse_args() - image = BootloaderImage(args.eeprom, args.out) - if args.config is not None: - image.write(args.config) - else: - image.read() + + if (args.edit or args.apply is not None) and os.getuid() != 0: + exit_error("--edit/--apply must be run as root") + + if args.edit: + edit_config(args.eeprom) + elif args.apply is not None: + if not os.path.exists(args.apply): + exit_error("config file '%s' not found" % args.apply) + apply_update(args.apply, args.eeprom, args.apply) + elif args.eeprom is not None: + image = BootloaderImage(args.eeprom, args.out) + if args.config is not None: + if not os.path.exists(args.config): + exit_error("config file '%s' not found" % args.config) + image.write(args.config) + else: + image.read() + elif args.config is None and args.eeprom is None: + current_config = read_current_config() + if args.out is not None: + open(args.out, 'w').write(current_config) + else: + sys.stdout.write(current_config) if __name__ == '__main__': + atexit.register(exit_handler) main() diff --git a/rpi-eeprom-update b/rpi-eeprom-update index a5ffecd..b48f287 100755 --- a/rpi-eeprom-update +++ b/rpi-eeprom-update @@ -71,8 +71,12 @@ VL805_UPDATE_VERSION= # The update actions selected by the version check ACTION_UPDATE_BOOTLOADER=0 ACTION_UPDATE_VL805=0 +CHECKSUMS='' cleanup() { + if [ -f "${CHECKSUMS}" ]; then + rm -f "${CHECKSUMS}" + fi if [ -f "${TMP_EEPROM_IMAGE}" ]; then rm -f "${TMP_EEPROM_IMAGE}" fi @@ -143,6 +147,7 @@ applyRecoveryUpdate() [ -n "${BOOTLOADER_UPDATE_IMAGE}" ] || [ -n "${VL805_UPDATE_IMAGE}" ] || die "No update images specified" findBootFS + echo "BOOTFS ${BOOTFS}" # A '.sig' file is created so that recovery.bin can check that the # EEPROM image has not been created (e.g. SD card corruption). @@ -166,16 +171,20 @@ applyRecoveryUpdate() || die "Failed to copy ${TMP_EEPROM_IMAGE} to ${BOOTFS}" # For NFS mounts ensure that the files are readable to the TFTP user - chmod -f go+r "${BOOTFS}/pieeprom.upd" "${BOOTFS}/pieeprom.sig" + chmod -f go+r "${BOOTFS}/pieeprom.upd" "${BOOTFS}/pieeprom.sig" \ + || die "Failed to set permissions on eeprom update files" fi if [ -n "${VL805_UPDATE_IMAGE}" ]; then sha256sum "${VL805_UPDATE_IMAGE}" | awk '{print $1}' > "${BOOTFS}/vl805.sig" \ || die "Failed to create ${BOOTFS}/vl805.sig" - cp -f "${VL805_UPDATE_IMAGE}" "${BOOTFS}/vl805.bin" + + cp -f "${VL805_UPDATE_IMAGE}" "${BOOTFS}/vl805.bin" \ + || die "Failed to copy ${VL805_UPDATE_IMAGE} to ${BOOTFS/vl805.bin}" # For NFS mounts ensure that the files are readable to the TFTP user - chmod -f go+r "${BOOTFS}/vl805.bin" "${BOOTFS}/vl805.sig" + chmod -f go+r "${BOOTFS}/vl805.bin" "${BOOTFS}/vl805.sig" \ + || die "Failed to set permissions on eeprom update files" fi cp -f "${RECOVERY_BIN}" "${BOOTFS}/recovery.bin" \ @@ -183,18 +192,26 @@ applyRecoveryUpdate() } applyUpdate() { - checksums_file="/var/lib/dpkg/info/rpi-eeprom.md5sums" - [ "$(id -u)" = "0" ] || die "* Must be run as root - try 'sudo rpi-eeprom-update'" - if [ "${IGNORE_DPKG_CHECKSUMS}" = 0 ] && [ -f "${checksums_file}" ]; then + if [ "${IGNORE_DPKG_CHECKSUMS}" = 0 ]; then ( + package_info_dir="/var/lib/dpkg/info/" + package_checksums_file="${package_info_dir}/rpi-eeprom.md5sums" + + if ! grep -qE '\.bin$' "${package_info_dir}/rpi-eeprom.md5sums"; then + # Try the old rpi-eeprom-images package + package_checksums_file="${package_info_dir}/rpi-eeprom-images.md5sums" + fi + + CHECKSUMS=$(mktemp) + cat "${package_checksums_file}" | grep -E '\.bin$' > "${CHECKSUMS}" cd / - if ! md5sum -c "${checksums_file}" > /dev/null 2>&1; then - md5sum -c "${checksums_file}" + if ! md5sum -c "${CHECKSUMS}" > /dev/null 2>&1; then + md5sum -c "${CHECKSUMS}" die "rpi-eeprom checksums failed - try reinstalling this package" fi - ) + ) || die "Unable to validate EEPROM image package checksums" fi if [ "${USE_FLASHROM}" = 0 ]; then @@ -268,7 +285,15 @@ getBootloaderUpdateVersion() { } checkDependencies() { - BOARD_INFO="$(od -v -An -t x1 /sys/firmware/devicetree/base/system/linux,revision | tr -d ' \n')" + + if [ -f "/sys/firmware/devicetree/base/system/linux,revision" ]; then + BOARD_INFO="$(od -v -An -t x1 /sys/firmware/devicetree/base/system/linux,revision | tr -d ' \n')" + elif grep -q Revision /proc/cpuinfo; then + BOARD_INFO="$(sed -n '/^Revision/s/^.*: \(.*\)/\1/p' < /proc/cpuinfo)" + else + BOARD_INFO="$(vcgencmd otp_dump | grep '30:' | sed 's/.*://')" + fi + if [ $(((0x$BOARD_INFO >> 23) & 1)) -ne 0 ] && [ $(((0x$BOARD_INFO >> 12) & 15)) -eq 3 ]; then echo "BCM2711 detected" else @@ -370,6 +395,7 @@ retained. Options: -a Automatically install bootloader and USB (VLI) EEPROM updates. -A Specify which type of EEPROM to automatically update (vl805 or bootloader) + -b Outputs the path that pending EEPROM updates will be written to. -d Use the default bootloader config, or if a file is specified using the -f flag use the config in that file. This option only applies when a bootloader EEPROM update is needed; if the bootloader EEPROM is up-to-date @@ -380,6 +406,8 @@ Options: -h Display help text and exit -i Ignore package checksums - for rpi-eeprom developers. -j Write status information using JSON notation + -l Returns the full path to the latest available EEPROM image file according + to the FIRMWARE_RELEASE_STATUS and FIRMWARE_IMAGE_DIR settings. -m Write status information to the given file when run without -a or -f -r Removes temporary EEPROM update files from the boot partition. -u Install the specified VL805 (USB EEPROM) image file. @@ -493,9 +521,7 @@ findBootFS() # If BOOTFS is not a directory or doesn't contain any .elf files then # it's probably not the boot partition. [ -d "${BOOTFS}" ] || die "BOOTFS: \"${BOOTFS}\" is not a directory" - if [ "$(find "${BOOTFS}/" -name "*.elf" | wc -l)" -gt 0 ]; then - echo "BOOTFS ${BOOTFS}" - else + if [ "$(find "${BOOTFS}/" -name "*.elf" | wc -l)" = 0 ]; then echo "WARNING: BOOTFS: \"${BOOTFS}\" contains no .elf files. Please check boot directory" fi } @@ -611,14 +637,16 @@ removePreviousUpdates() if [ "$(id -u)" = "0" ]; then findBootFS - # Remove any stale recovery.bin files or EEPROM images - # N.B. recovery.bin is normally ignored by the ROM if is not a valid - # executable but it's best to not have the file at all. - rm -f "${BOOTFS}/recovery.bin" - rm -f "${BOOTFS}/pieeprom.bin" "${BOOTFS}/pieeprom.upd" "${BOOTFS}/pieeprom.sig" - rm -f "${BOOTFS}/vl805.bin" "${BOOTFS}/vl805.sig" - # Case insensitive for FAT bootfs - find "${BOOTFS}" -maxdepth 1 -type f -follow -iname "recovery.*" -regex '.*\.[0-9][0-9][0-9]$' -exec rm -f {} \; + ( + # Remove any stale recovery.bin files or EEPROM images + # N.B. recovery.bin is normally ignored by the ROM if is not a valid + # executable but it's best to not have the file at all. + rm -f "${BOOTFS}/recovery.bin" + rm -f "${BOOTFS}/pieeprom.bin" "${BOOTFS}/pieeprom.upd" "${BOOTFS}/pieeprom.sig" + rm -f "${BOOTFS}/vl805.bin" "${BOOTFS}/vl805.sig" + # Case insensitive for FAT bootfs + find "${BOOTFS}" -maxdepth 1 -type f -follow -iname "recovery.*" -regex '.*\.[0-9][0-9][0-9]$' -exec rm -f {} \; + ) || die "Failed to remove previous update files" fi } @@ -680,7 +708,7 @@ MACHINE_OUTPUT="" JSON_OUTPUT="no" IGNORE_DPKG_CHECKSUMS=$LOCAL_MODE -while getopts A:adhif:m:ju:r option; do +while getopts A:abdhilf:m:ju:r option; do case "${option}" in A) if [ "${OPTARG}" = "bootloader" ]; then @@ -694,6 +722,11 @@ while getopts A:adhif:m:ju:r option; do a) AUTO_UPDATE_BOOTLOADER=1 AUTO_UPDATE_VL805=1 ;; + b) + findBootFS + echo ${BOOTFS} + exit 0 + ;; d) OVERWRITE_CONFIG=1 ;; f) BOOTLOADER_UPDATE_IMAGE="${OPTARG}" @@ -702,6 +735,11 @@ while getopts A:adhif:m:ju:r option; do ;; j) JSON_OUTPUT="yes" ;; + l) + getBootloaderUpdateVersion + echo "${BOOTLOADER_UPDATE_IMAGE}" + exit 0 + ;; m) MACHINE_OUTPUT="${OPTARG}" ;; h) usage