Skip to content

Commit

Permalink
recovery5: Preliminary support for EEPROM updates on Pi5
Browse files Browse the repository at this point in the history
Create a recovery5 rpiboot mode to support EEPROM updates on
Pi5 / BCM2712.

Secure boot is not supported yet.
  • Loading branch information
timg236 committed Dec 19, 2023
1 parent e92e79b commit 1530249
Show file tree
Hide file tree
Showing 10 changed files with 135 additions and 33 deletions.
7 changes: 5 additions & 2 deletions main.c
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,7 @@ int main(int argc, char *argv[])
// If the boot directory is specified then check that it contains bootcode files.
if (directory)
{
FILE *f, *f4;
FILE *f, *f4, *f5;

if (verbose)
printf("Boot directory '%s'\n", directory);
Expand All @@ -727,7 +727,8 @@ int main(int argc, char *argv[])
{
f = check_file(directory, "bootcode.bin", 0);
f4 = check_file(directory, "bootcode4.bin", 0);
if (!f && !f4)
f5 = check_file(directory, "bootcode5.bin", 0);
if (!f && !f4 && !f5)
{
fprintf(stderr, "No 'bootcode' files found in '%s'\n", directory);
usage(1);
Expand All @@ -736,6 +737,8 @@ int main(int argc, char *argv[])
fclose(f);
if (f4)
fclose(f4);
if (f5)
fclose(f5);
}

if (signed_boot)
Expand Down
14 changes: 14 additions & 0 deletions recovery5/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
To update the SPI EEPROM bootloader on Raspberry Pi 5.

* Modify the EEPROM configuration as desired
* Optionally, replace pieeprom.original.bin with a custom version. The default
version here is the latest stable release recommended for use on Raspberry Pi 5.

N.B The `bootcode5.bin` file in this directory is actually the `recovery.bin`
file used on Raspberry Pi 5 bootloader update cards.

```bash
cd recovery
./update-pieeprom.sh
../rpiboot -d .
```
4 changes: 4 additions & 0 deletions recovery5/boot.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[all]
BOOT_UART=1
POWER_OFF_ON_HALT=0
BOOT_ORDER=0xf461
1 change: 1 addition & 0 deletions recovery5/bootcode5.bin
Binary file added recovery5/pieeprom.bin
Binary file not shown.
Binary file added recovery5/pieeprom.original.bin
Binary file not shown.
2 changes: 2 additions & 0 deletions recovery5/pieeprom.sig
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
675374a8f6709cc1c2440cfe3a3365b82713614f85b60bd73c60a8a34093354c
ts: 1702893546
Binary file added recovery5/recovery.bin
Binary file not shown.
13 changes: 13 additions & 0 deletions recovery5/update-pieeprom.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/sh

# Utility to update the EEPROM image (pieeprom.bin) and signature
# (pieeprom.sig) with a new EEPROM config.
#
# This script is now a thin wrapper for the new version in ../tools
#
# pieeprom.original.bin - The source EEPROM from rpi-eeprom repo
# boot.conf - The bootloader config file to apply.

set -e

../tools/update-pieeprom.sh "$@"
127 changes: 96 additions & 31 deletions tools/rpi-eeprom-config
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,8 @@ import sys
import tempfile
import time

IMAGE_SIZE = 512 * 1024
VALID_IMAGE_SIZES = [512 * 1024, 2 * 1024 * 1024]

# Larger files won't with with "vcgencmd bootloader_config"
MAX_FILE_SIZE = 2024
ALIGN_SIZE = 4096
BOOTCONF_TXT = 'bootconf.txt'
BOOTCONF_SIG = 'bootconf.sig'
PUBKEY_BIN = 'pubkey.bin'
Expand All @@ -39,6 +36,11 @@ FILE_HDR_LEN = 20
FILENAME_LEN = 12
TEMP_DIR = None

# Modifiable files are stored in a single 4K erasable sector.
# The max content 4076 bytes because of the file header.
ERASE_ALIGN_SIZE = 4096
MAX_FILE_SIZE = ERASE_ALIGN_SIZE - FILE_HDR_LEN

DEBUG = False
def debug(s):
if DEBUG:
Expand All @@ -53,6 +55,15 @@ def rpi4():
return True
return False

def rpi5():
compatible_path = "/sys/firmware/devicetree/base/compatible"
if os.path.exists(compatible_path):
with open(compatible_path, "rb") as f:
compatible = f.read().decode('utf-8')
if "bcm2712" in compatible:
return True
return False

def exit_handler():
"""
Delete any temporary files.
Expand Down Expand Up @@ -98,17 +109,22 @@ def exit_error(msg):
sys.stderr.write("ERROR: %s\n" % msg)
sys.exit(1)

def shell_cmd(args):
def shell_cmd(args, timeout=5, echo=False):
"""
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:
bufsize = 0 if echo else -1
result = subprocess.Popen(args, bufsize=bufsize, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

while time.time() - start < timeout:
if echo:
s = result.stdout.read(80).decode('utf-8')
if s != "":
sys.stdout.write(s)
if result.poll() is not None:
break

Expand All @@ -117,8 +133,8 @@ def shell_cmd(args):

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:
(arg_str, result.returncode, result.stdout.read().decode('utf-8'), result.stderr.read().decode('utf-8')))
elif not echo:
return result.stdout.read().decode('utf-8')

def get_latest_eeprom():
Expand Down Expand Up @@ -159,8 +175,10 @@ def apply_update(config, eeprom=None, config_src=None):
# 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)

# If flashrom is used then the command will not return until the EEPROM
# has been updated so use a larger timeout.
shell_cmd(args, timeout=20, echo=True)

def edit_config(eeprom=None):
"""
Expand Down Expand Up @@ -221,7 +239,7 @@ class ImageSection:
self.offset = offset
self.length = length
self.filename = filename
debug("ImageSection %x %x %x %s" % (magic, offset, length, filename))
debug("ImageSection %x offset %d length %d %s" % (magic, offset, length, filename))

class BootloaderImage(object):
def __init__(self, filename, output=None):
Expand All @@ -231,6 +249,7 @@ class BootloaderImage(object):
"""
self._filename = filename
self._sections = []
self._image_size = 0
try:
self._bytes = bytearray(open(filename, 'rb').read())
except IOError as err:
Expand All @@ -239,9 +258,10 @@ class BootloaderImage(object):
if output is not None:
self._out = open(output, 'wb')

if len(self._bytes) != IMAGE_SIZE:
self._image_size = len(self._bytes)
if self._image_size not in VALID_IMAGE_SIZES:
exit_error("%s: Expected size %d bytes actual size %d bytes" %
(filename, IMAGE_SIZE, len(self._bytes)))
(filename, self._image_size, len(self._bytes)))
self.parse()

def parse(self):
Expand All @@ -250,8 +270,7 @@ class BootloaderImage(object):
"""
offset = 0
magic = 0
found = False
while offset < IMAGE_SIZE:
while offset < self._image_size:
magic, length = struct.unpack_from('>LL', self._bytes, offset)
if magic == 0x0 or magic == 0xffffffff:
break # EOF
Expand All @@ -262,6 +281,7 @@ class BootloaderImage(object):
if magic == FILE_MAGIC: # Found a file
# Discard trailing null characters used to pad filename
filename = self._bytes[offset + 8: offset + FILE_HDR_LEN].decode('utf-8').replace('\0', '')
debug("section at %d length %d magic %08x %s" % (offset, length, magic, filename))
self._sections.append(ImageSection(magic, offset, length, filename))

offset += 8 + length # length + type
Expand All @@ -272,26 +292,46 @@ class BootloaderImage(object):
Returns the offset, length and whether this is the last section in the
EEPROM for a modifiable file within the image.
"""
ret = (-1, -1, False)
offset = -1
length = -1
is_last = False

next_offset = self._image_size - ERASE_ALIGN_SIZE # Don't create padding inside the bootloader scratch page
for i in range(0, len(self._sections)):
s = self._sections[i]
if s.magic == FILE_MAGIC and s.filename == filename:
is_last = (i == len(self._sections) - 1)
ret = (s.offset, s.length, is_last)
offset = s.offset
length = s.length
break
debug('%s offset %d length %d last %s' % (filename, ret[0], ret[1], ret[2]))

# Find the start of the next non padding section
i += 1
while i < len(self._sections):
if self._sections[i].magic == PAD_MAGIC:
i += 1
else:
next_offset = self._sections[i].offset
break
ret = (offset, length, is_last, next_offset)
debug('%s offset %d length %d is-last %d next %d' % (filename, ret[0], ret[1], ret[2], ret[3]))
return ret

def update(self, src_bytes, dst_filename):
"""
Replaces a modifiable file with specified byte array.
"""
hdr_offset, length, is_last = self.find_file(dst_filename)
hdr_offset, length, is_last, next_offset = self.find_file(dst_filename)
update_len = len(src_bytes) + FILE_HDR_LEN

if hdr_offset + update_len > self._image_size - ERASE_ALIGN_SIZE:
raise Exception('No space available - image past EOF.')

if hdr_offset < 0:
raise Exception('Update target %s not found' % dst_filename)

if hdr_offset + len(src_bytes) + FILE_HDR_LEN > IMAGE_SIZE:
raise Exception('EEPROM image size exceeded')
if hdr_offset + update_len > next_offset:
raise Exception('Update %d bytes is larger than section size %d' % (update_len, next_offset - hdr_offset))

new_len = len(src_bytes) + FILENAME_LEN + 4
struct.pack_into('>L', self._bytes, hdr_offset + 4, new_len)
Expand All @@ -312,7 +352,7 @@ class BootloaderImage(object):
# by convention bootconf.txt is the last section and there's no need to
# pad to the end of the sector. This also ensures that the loopback
# config read/write tests produce identical binaries.
pad_bytes = ALIGN_SIZE - (pad_start % ALIGN_SIZE)
pad_bytes = next_offset - pad_start
if pad_bytes > 8 and not is_last:
pad_bytes -= 8
struct.pack_into('>i', self._bytes, pad_start, PAD_MAGIC)
Expand Down Expand Up @@ -344,6 +384,15 @@ class BootloaderImage(object):
% (src_filename, len(src_bytes), MAX_FILE_SIZE))
self.update(src_bytes, dst_filename)

def set_timestamp(self, timestamp):
"""
Sets the self-update timestamp in an EEPROM image file. This is useful when
using flashrom to write to SPI flash instead of using the bootloader self-update mode.
"""
ts = int(timestamp)
struct.pack_into('<L', self._bytes, len(self._bytes) - 4, ts)
struct.pack_into('<L', self._bytes, len(self._bytes) - 8, ~ts & 0xffffffff)

def write(self):
"""
Writes the updated EEPROM image to stdout or the specified output file.
Expand All @@ -358,10 +407,17 @@ class BootloaderImage(object):
sys.stdout.write(self._bytes)

def get_file(self, filename):
hdr_offset, length, is_last = self.find_file(filename)
hdr_offset, length, is_last, next_offset = self.find_file(filename)
offset = hdr_offset + 4 + FILE_HDR_LEN
config_bytes = self._bytes[offset:offset+length-FILENAME_LEN-4]
return config_bytes
file_bytes = self._bytes[offset:offset+length-FILENAME_LEN-4]
return file_bytes

def extract_files(self):
for i in range(0, len(self._sections)):
s = self._sections[i]
if s.magic == FILE_MAGIC:
file_bytes = self.get_file(s.filename)
open(s.filename, 'wb').write(file_bytes)

def read(self):
config_bytes = self.get_file('bootconf.txt')
Expand All @@ -377,10 +433,10 @@ class BootloaderImage(object):
def main():
"""
Utility for reading and writing the configuration file in the
Raspberry Pi 4 bootloader EEPROM image.
Raspberry Pi bootloader EEPROM image.
"""
description = """\
Bootloader EEPROM configuration tool for the Raspberry Pi 4.
Bootloader EEPROM configuration tool for the Raspberry Pi 4 and Raspberry Pi 5.
Operating modes:
1. Outputs the current bootloader configuration to STDOUT if no arguments are
Expand Down Expand Up @@ -457,23 +513,30 @@ See 'rpi-eeprom-update -h' for more information about the available EEPROM image
parser.add_argument('-o', '--out', help='Name of output file', required=False)
parser.add_argument('-d', '--digest', help='Signed boot only. The name of the .sig file generated by rpi-eeprom-dgst for config.txt ', required=False)
parser.add_argument('-p', '--pubkey', help='Signed boot only. The name of the RSA public key file to store in the EEPROM', required=False)
parser.add_argument('-x', '--extract', action='store_true', default=False, help='Extract the modifiable files (boot.conf, pubkey, signature)', required=False)
parser.add_argument('-t', '--timestamp', help='Set the timestamp in the EEPROM image file', required=False)
parser.add_argument('eeprom', nargs='?', help='Name of EEPROM file to use as input')
args = parser.parse_args()

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 or args.apply is not None) and not rpi4():
exit_error("--edit/--apply must run on a Raspberry Pi 4")
if (args.edit or args.apply is not None) and not rpi4() and not rpi5():
exit_error("--edit/--apply must run on a Raspberry Pi 4 or Raspberry Pi 5")

if args.edit:
edit_config(args.eeprom)
elif args.eeprom is not None and args.extract:
image = BootloaderImage(args.eeprom, args.out)
image.extract_files()
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.timestamp is not None:
image.set_timestamp(args.timestamp)
if args.config is not None:
if not os.path.exists(args.config):
exit_error("config file '%s' not found" % args.config)
Expand All @@ -483,6 +546,8 @@ See 'rpi-eeprom-update -h' for more information about the available EEPROM image
if args.pubkey is not None:
image.update_key(args.pubkey, PUBKEY_BIN)
image.write()
elif args.config is None and args.timestamp is not None:
image.write()
else:
image.read()
elif args.config is None and args.eeprom is None:
Expand Down

0 comments on commit 1530249

Please sign in to comment.