zephyr/scripts/west_commands/blobs.py
Christophe Dufaza 2b2a0e04b2 west: blobs: verify fetched blobs after downloading
Running 'west blobs fetch' does not verify the digest of downloaded files:
1. if the checksum of the previously downloaded file does match
   that in the blob metadata (status BLOB_PRESENT), do nothing
2. if the checksum of the previously downloaded file does not match
   that in the blob metadata (status BLOB_OUTDATED),
   download the "up to date" file
3. if the blob has not yet been downloaded (status BLOB_NOT_PRESENT),
   download it

None of the 2) and 3) code paths will verify that the checksum of the file
just downloaded actually matches the digest in the blob's metadata.

In the event that the metadata of a module is incorrect, then the user
will not notice anything, and may rely on an unexpected binary,
e.g. a static library for a different architecture.

According to the Binary Blobs documentation [1], the expected
behavior is to check the blob digest after downloading.

[1] Fetching blobs, Zephyr 3.6.0 (still applies to Zephyr 3.7.0rc3)
docs.zephyrproject.org/3.6.0/contribute/bin_blobs.html#fetching-blobs

Signed-off-by: Christophe Dufaza <chris@openmarl.org>
2024-07-30 18:29:39 +01:00

172 lines
6 KiB
Python

# Copyright (c) 2022 Nordic Semiconductor ASA
#
# SPDX-License-Identifier: Apache-2.0
import argparse
import os
from pathlib import Path
import sys
import textwrap
from urllib.parse import urlparse
from west import log
from west.commands import WestCommand
from zephyr_ext_common import ZEPHYR_BASE
sys.path.append(os.fspath(Path(__file__).parent.parent))
import zephyr_module
class Blobs(WestCommand):
DEFAULT_LIST_FMT = '{module} {status} {path} {type} {abspath}'
def __init__(self):
super().__init__(
'blobs',
# Keep this in sync with the string in west-commands.yml.
'work with binary blobs',
'Work with binary blobs',
accepts_unknown_args=False)
def do_add_parser(self, parser_adder):
parser = parser_adder.add_parser(
self.name,
help=self.help,
formatter_class=argparse.RawDescriptionHelpFormatter,
description=self.description,
epilog=textwrap.dedent(f'''\
FORMAT STRINGS
--------------
Blobs are listed using a Python 3 format string. Arguments
to the format string are accessed by name.
The default format string is:
"{self.DEFAULT_LIST_FMT}"
The following arguments are available:
- module: name of the module that contains this blob
- abspath: blob absolute path
- status: short status (A: present, M: hash failure, D: not present)
- path: blob local path from <module>/zephyr/blobs/
- sha256: blob SHA256 hash in hex
- type: type of blob
- version: version string
- license_path: path to the license file for the blob
- uri: URI to the remote location of the blob
- description: blob text description
- doc-url: URL to the documentation for this blob
'''))
# Remember to update west-completion.bash if you add or remove
# flags
parser.add_argument('subcmd', nargs=1,
choices=['list', 'fetch', 'clean'],
help='sub-command to execute')
parser.add_argument('modules', metavar='MODULE', nargs='*',
help='''zephyr modules to operate on;
all modules will be used if not given''')
group = parser.add_argument_group('west blob list options')
group.add_argument('-f', '--format',
help='''format string to use to list each blob;
see FORMAT STRINGS below''')
return parser
def get_blobs(self, args):
blobs = []
modules = args.modules
all_modules = zephyr_module.parse_modules(ZEPHYR_BASE, self.manifest)
all_names = [m.meta.get('name', None) for m in all_modules]
unknown = set(modules) - set(all_names)
if len(unknown):
log.die(f'Unknown module(s): {unknown}')
for module in all_modules:
# Filter by module
module_name = module.meta.get('name', None)
if len(modules) and module_name not in modules:
continue
blobs += zephyr_module.process_blobs(module.project, module.meta)
return blobs
def list(self, args):
blobs = self.get_blobs(args)
fmt = args.format or self.DEFAULT_LIST_FMT
for blob in blobs:
log.inf(fmt.format(**blob))
def ensure_folder(self, path):
path.parent.mkdir(parents=True, exist_ok=True)
def fetch_blob(self, url, path):
scheme = urlparse(url).scheme
log.dbg(f'Fetching {path} with {scheme}')
import fetchers
fetcher = fetchers.get_fetcher_cls(scheme)
log.dbg(f'Found fetcher: {fetcher}')
inst = fetcher()
self.ensure_folder(path)
inst.fetch(url, path)
# Compare the checksum of a file we've just downloaded
# to the digest in blob metadata, warn user if they differ.
def verify_blob(self, blob):
log.dbg('Verifying blob {module}: {abspath}'.format(**blob))
status = zephyr_module.get_blob_status(blob['abspath'], blob['sha256'])
if status == zephyr_module.BLOB_OUTDATED:
log.err(textwrap.dedent(
f'''\
The checksum of the downloaded file does not match that
in the blob metadata:
- if it is not certain that the download was successful,
try running 'west blobs fetch {blob['module']}'
to re-download the file
- if the error persists, please consider contacting
the maintainers of the module so that they can check
the corresponding blob metadata
Module: {blob['module']}
Blob: {blob['path']}
URL: {blob['url']}
Info: {blob['description']}'''))
def fetch(self, args):
blobs = self.get_blobs(args)
for blob in blobs:
if blob['status'] == zephyr_module.BLOB_PRESENT:
log.dbg('Blob {module}: {abspath} is up to date'.format(**blob))
continue
log.inf('Fetching blob {module}: {abspath}'.format(**blob))
self.fetch_blob(blob['url'], blob['abspath'])
self.verify_blob(blob)
def clean(self, args):
blobs = self.get_blobs(args)
for blob in blobs:
if blob['status'] == zephyr_module.BLOB_NOT_PRESENT:
log.dbg('Blob {module}: {abspath} not in filesystem'.format(**blob))
continue
log.inf('Deleting blob {module}: {status} {abspath}'.format(**blob))
blob['abspath'].unlink()
def do_run(self, args, _):
log.dbg(f'subcmd: \'{args.subcmd[0]}\' modules: {args.modules}')
subcmd = getattr(self, args.subcmd[0])
if args.subcmd[0] != 'list' and args.format is not None:
log.die(f'unexpected --format argument; this is a "west blobs list" option')
subcmd(args)