Merge pull request #27938 from keszybz/ukify-build-verb

Add 'ukify build' verb, expand tests
This commit is contained in:
Luca Boccassi 2023-06-06 18:22:45 +01:00 committed by GitHub
commit cd8947d0d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 172 additions and 57 deletions

View file

@ -23,9 +23,8 @@
<refsynopsisdiv>
<cmdsynopsis>
<command>/usr/lib/systemd/ukify</command>
<arg choice="opt"><replaceable>LINUX</replaceable></arg>
<arg choice="opt" rep="repeat"><replaceable>INITRD</replaceable></arg>
<arg choice="opt" rep="repeat">OPTIONS</arg>
<arg choice="plain">build</arg>
</cmdsynopsis>
</refsynopsisdiv>
@ -35,13 +34,18 @@
<para>Note: this command is experimental for now. While it is intended to become a regular component of
systemd, it might still change in behaviour and interface.</para>
<para><command>ukify</command> is a tool that combines components (e.g.: a kernel and an initrd with
a UEFI boot stub) to create a
<para><command>ukify</command> is a tool that combines components (usually a kernel, an initrd, and a
UEFI boot stub) to create a
<ulink url="https://uapi-group.org/specifications/specs/unified_kernel_image/">Unified Kernel Image (UKI)</ulink>
— a PE binary that can be executed by the firmware to start the embedded linux kernel.
See <citerefentry><refentrytitle>systemd-stub</refentrytitle><manvolnum>7</manvolnum></citerefentry>
for details about the stub.</para>
<para>The two primary options that should be specified for the <command>build</command> verb are
<varname>Linux=</varname>/<option>--linux=</option>, and
<varname>Initrd=</varname>/<option>--initrd=</option>. <varname>Initrd=</varname> accepts multiple
whitespace-separated paths and <option>--initrd=</option> can be specified multiple times.</para>
<para>Additional sections will be inserted into the UKI, either automatically or only if a specific
option is provided. See the discussions of
<varname>Cmdline=</varname>/<option>--cmdline=</option>,
@ -173,14 +177,14 @@
<variablelist>
<varlistentry>
<term><varname>Linux=<replaceable>LINUX</replaceable></varname></term>
<term>positional argument <replaceable>LINUX</replaceable></term>
<term><option>--linux=<replaceable>LINUX</replaceable></option></term>
<listitem><para>A path to the kernel binary.</para></listitem>
</varlistentry>
<varlistentry>
<term><varname>Initrd=<replaceable>INITRD</replaceable>...</varname></term>
<term>positional argument <replaceable>INITRD</replaceable></term>
<term><option>--initrd=<replaceable>LINUX</replaceable></option></term>
<listitem><para>Zero or more initrd paths. In the configuration file, items are separated by
whitespace. The initrds are combined in the order of specification, with the initrds specified in
@ -399,9 +403,9 @@
<example>
<title>Minimal invocation</title>
<programlisting>$ ukify \
/lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \
/some/path/initramfs-6.0.9-300.fc37.x86_64.img \
<programlisting>$ ukify build \
--linux=/lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \
--initrd=/some/path/initramfs-6.0.9-300.fc37.x86_64.img \
--cmdline='quiet rw'
</programlisting>
@ -411,10 +415,10 @@
<example>
<title>All the bells and whistles</title>
<programlisting># /usr/lib/systemd/ukify \
/lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \
early_cpio \
/some/path/initramfs-6.0.9-300.fc37.x86_64.img \
<programlisting># /usr/lib/systemd/ukify build \
--linux=/lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \
--initrd=early_cpio \
--initrd=/some/path/initramfs-6.0.9-300.fc37.x86_64.img \
--pcr-private-key=pcr-private-initrd-key.pem \
--pcr-public-key=pcr-public-initrd-key.pem \
--phases='enter-initrd' \
@ -468,9 +472,9 @@ Phases=enter-initrd:leave-initrd
enter-initrd:leave-initrd:sysinit
enter-initrd:leave-initrd:sysinit:ready
# /usr/lib/systemd/ukify -c ukify.conf \
/lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \
/some/path/initramfs-6.0.9-300.fc37.x86_64.img
# /usr/lib/systemd/ukify -c ukify.conf build \
--linux=/lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \
--initrd=/some/path/initramfs-6.0.9-300.fc37.x86_64.img
</programlisting>
<para>One "initrd" (<filename index='false'>early_cpio</filename>) is specified in the config file, and
@ -482,7 +486,7 @@ Phases=enter-initrd:leave-initrd
<example>
<title>Kernel command line auxiliary PE</title>
<programlisting>ukify \
<programlisting>ukify build \
--secureboot-private-key=sb.key \
--secureboot-certificate=sb.cert \
--cmdline='debug' \

View file

@ -1,6 +1,7 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
efi_config_h_dir = meson.current_build_dir()
efi_addon = ''
if efi_arch != ''
libefitest = static_library(
@ -376,6 +377,11 @@ foreach efi_elf_binary : efi_elf_binaries
if name.startswith('linux')
boot_stubs += exe
endif
# This is supposed to match exactly one time
if name == 'addon@0@.efi.stub'.format(efi_arch)
efi_addon = exe.full_path()
endif
endforeach
alias_target('systemd-boot', boot_targets)

View file

@ -183,11 +183,10 @@ def call_ukify(opts):
# The solution with runpy gives a dictionary, which isn't great, but will do.
ukify = runpy.run_path(UKIFY, run_name='ukify')
# Create "empty" namespace. We want to override just a few settings,
# so it doesn't make sense to duplicate all the fields. We use a hack
# to pre-populate the namespace like argparse would, all defaults.
# We need to specify the two mandatory arguments to not get an error.
opts2 = ukify['create_parser']().parse_args(('A','B'))
# Create "empty" namespace. We want to override just a few settings, so it
# doesn't make sense to configure everything. We pretend to parse an empty
# argument set to prepopulate the namespace with the defaults.
opts2 = ukify['create_parser']().parse_args(())
opts2.config = config_file_location()
opts2.uname = opts.kernel_version

View file

@ -15,6 +15,10 @@ test_env.set('SYSTEMD_LANGUAGE_FALLBACK_MAP', language_fallback_map)
test_env.set('PATH', project_build_root + ':' + path)
test_env.set('PROJECT_BUILD_ROOT', project_build_root)
if efi_addon != ''
test_env.set('EFI_ADDON', efi_addon)
endif
############################################################
generate_sym_test_py = find_program('generate-sym-test.py')

View file

@ -50,9 +50,9 @@ def test_round_up():
assert ukify.round_up(4097) == 8192
def test_namespace_creation():
ns = ukify.create_parser().parse_args(('A','B'))
assert ns.linux == pathlib.Path('A')
assert ns.initrd == [pathlib.Path('B')]
ns = ukify.create_parser().parse_args(())
assert ns.linux is None
assert ns.initrd is None
def test_config_example():
ex = ukify.config_example()
@ -87,7 +87,7 @@ def test_apply_config(tmp_path):
Phases = {':'.join(ukify.KNOWN_PHASES)}
'''))
ns = ukify.create_parser().parse_args(('A','B'))
ns = ukify.create_parser().parse_args(())
ns.linux = None
ns.initrd = []
ukify.apply_config(ns, config)
@ -143,7 +143,7 @@ def test_parse_args_minimal():
assert opts.os_release in (pathlib.Path('/etc/os-release'),
pathlib.Path('/usr/lib/os-release'))
def test_parse_args_many():
def test_parse_args_many_deprecated():
opts = ukify.parse_args(
['/ARG1', '///ARG2', '/ARG3 WITH SPACE',
'--cmdline=a b c',
@ -186,9 +186,57 @@ def test_parse_args_many():
assert opts.output == pathlib.Path('OUTPUT')
assert opts.measure is False
def test_parse_args_many():
opts = ukify.parse_args(
['build',
'--linux=/ARG1',
'--initrd=///ARG2',
'--initrd=/ARG3 WITH SPACE',
'--cmdline=a b c',
'--os-release=K1=V1\nK2=V2',
'--devicetree=DDDDTTTT',
'--splash=splash',
'--pcrpkey=PATH',
'--uname=1.2.3',
'--stub=STUBPATH',
'--pcr-private-key=PKEY1',
'--pcr-public-key=PKEY2',
'--pcr-banks=SHA1,SHA256',
'--signing-engine=ENGINE',
'--secureboot-private-key=SBKEY',
'--secureboot-certificate=SBCERT',
'--sign-kernel',
'--no-sign-kernel',
'--tools=TOOLZ///',
'--output=OUTPUT',
'--measure',
'--no-measure',
])
assert opts.linux == pathlib.Path('/ARG1')
assert opts.initrd == [pathlib.Path('/ARG2'), pathlib.Path('/ARG3 WITH SPACE')]
assert opts.cmdline == 'a b c'
assert opts.os_release == 'K1=V1\nK2=V2'
assert opts.devicetree == pathlib.Path('DDDDTTTT')
assert opts.splash == pathlib.Path('splash')
assert opts.pcrpkey == pathlib.Path('PATH')
assert opts.uname == '1.2.3'
assert opts.stub == pathlib.Path('STUBPATH')
assert opts.pcr_private_keys == [pathlib.Path('PKEY1')]
assert opts.pcr_public_keys == [pathlib.Path('PKEY2')]
assert opts.pcr_banks == ['SHA1', 'SHA256']
assert opts.signing_engine == 'ENGINE'
assert opts.sb_key == 'SBKEY'
assert opts.sb_cert == 'SBCERT'
assert opts.sign_kernel is False
assert opts.tools == [pathlib.Path('TOOLZ/')]
assert opts.output == pathlib.Path('OUTPUT')
assert opts.measure is False
def test_parse_sections():
opts = ukify.parse_args(
['/ARG1', '/ARG2',
['build',
'--linux=/ARG1',
'--initrd=/ARG2',
'--section=test:TESTTESTTEST',
'--section=test2:@FILE',
])
@ -239,7 +287,10 @@ def test_config_priority(tmp_path):
'''))
opts = ukify.parse_args(
['/ARG1', '///ARG2', '/ARG3 WITH SPACE',
['build',
'--linux=/ARG1',
'--initrd=///ARG2',
'--initrd=/ARG3 WITH SPACE',
'--cmdline= a b c ',
'--os-release=K1=V1\nK2=V2',
'--devicetree=DDDDTTTT',
@ -302,7 +353,7 @@ def test_help(capsys):
assert '--section' in out.out
assert not out.err
def test_help_error(capsys):
def test_help_error_deprecated(capsys):
with pytest.raises(SystemExit):
ukify.parse_args(['a', 'b', '--no-such-option'])
out = capsys.readouterr()
@ -310,6 +361,14 @@ def test_help_error(capsys):
assert '--no-such-option' in out.err
assert len(out.err.splitlines()) == 1
def test_help_error(capsys):
with pytest.raises(SystemExit):
ukify.parse_args(['build', '--no-such-option'])
out = capsys.readouterr()
assert not out.out
assert '--no-such-option' in out.err
assert len(out.err.splitlines()) == 1
@pytest.fixture(scope='session')
def kernel_initrd():
try:
@ -326,7 +385,7 @@ def kernel_initrd():
initrd = f"{item['root']}{item['initrd'][0].split(' ')[0]}"
except (KeyError, IndexError):
continue
return [linux, initrd]
return ['--linux', linux, '--initrd', initrd]
else:
return None
@ -345,7 +404,11 @@ def test_basic_operation(kernel_initrd, tmpdir):
pytest.skip('linux+initrd not found')
output = f'{tmpdir}/basic.efi'
opts = ukify.parse_args(kernel_initrd + [f'--output={output}'])
opts = ukify.parse_args([
'build',
*kernel_initrd,
f'--output={output}',
])
try:
ukify.check_inputs(opts)
except OSError as e:
@ -362,6 +425,7 @@ def test_sections(kernel_initrd, tmpdir):
output = f'{tmpdir}/basic.efi'
opts = ukify.parse_args([
'build',
*kernel_initrd,
f'--output={output}',
'--uname=1.2.3',
@ -385,15 +449,22 @@ def test_sections(kernel_initrd, tmpdir):
def test_addon(kernel_initrd, tmpdir):
output = f'{tmpdir}/addon.efi'
opts = ukify.parse_args([
args = [
'build',
f'--output={output}',
'--cmdline=ARG1 ARG2 ARG3',
'--section=.test:CONTENTZ',
])
]
if stub := os.getenv('EFI_ADDON'):
args += [f'--stub={stub}']
expected_exceptions = ()
else:
expected_exceptions = FileNotFoundError,
opts = ukify.parse_args(args)
try:
ukify.check_inputs(opts)
except OSError as e:
except expected_exceptions as e:
pytest.skip(str(e))
ukify.make_uki(opts)
@ -416,7 +487,8 @@ def test_uname_scraping(kernel_initrd):
if kernel_initrd is None:
pytest.skip('linux+initrd not found')
uname = ukify.Uname.scrape(kernel_initrd[0])
assert kernel_initrd[0] == '--linux'
uname = ukify.Uname.scrape(kernel_initrd[1])
assert re.match(r'\d+\.\d+\.\d+', uname)
def test_efi_signing_sbsign(kernel_initrd, tmpdir):
@ -431,6 +503,7 @@ def test_efi_signing_sbsign(kernel_initrd, tmpdir):
output = f'{tmpdir}/signed.efi'
opts = ukify.parse_args([
'build',
*kernel_initrd,
f'--output={output}',
'--uname=1.2.3',
@ -474,6 +547,7 @@ def test_efi_signing_pesign(kernel_initrd, tmpdir):
output = f'{tmpdir}/signed.efi'
opts = ukify.parse_args([
'build',
*kernel_initrd,
f'--output={output}',
'--uname=1.2.3',
@ -501,10 +575,6 @@ def test_efi_signing_pesign(kernel_initrd, tmpdir):
def test_pcr_signing(kernel_initrd, tmpdir):
if kernel_initrd is None:
pytest.skip('linux+initrd not found')
if os.getuid() != 0:
pytest.skip('must be root to access tpm2')
if subprocess.call(['systemd-creds', 'has-tpm2', '-q']) != 0:
pytest.skip('tpm2 is not available')
ourdir = pathlib.Path(__file__).parent
pub = unbase64(ourdir / 'example.tpm2-pcr-public.pem.base64')
@ -512,6 +582,7 @@ def test_pcr_signing(kernel_initrd, tmpdir):
output = f'{tmpdir}/signed.efi'
opts = ukify.parse_args([
'build',
*kernel_initrd,
f'--output={output}',
'--uname=1.2.3',
@ -562,10 +633,6 @@ def test_pcr_signing(kernel_initrd, tmpdir):
def test_pcr_signing2(kernel_initrd, tmpdir):
if kernel_initrd is None:
pytest.skip('linux+initrd not found')
if os.getuid() != 0:
pytest.skip('must be root to access tpm2')
if subprocess.call(['systemd-creds', 'has-tpm2', '-q']) != 0:
pytest.skip('tpm2 is not available')
ourdir = pathlib.Path(__file__).parent
pub = unbase64(ourdir / 'example.tpm2-pcr-public.pem.base64')
@ -578,8 +645,12 @@ def test_pcr_signing2(kernel_initrd, tmpdir):
microcode.write(b'1234567890')
output = f'{tmpdir}/signed.efi'
assert kernel_initrd[0] == '--linux'
opts = ukify.parse_args([
kernel_initrd[0], microcode.name, kernel_initrd[1],
'build',
*kernel_initrd[:2],
f'--initrd={microcode.name}',
*kernel_initrd[2:],
f'--output={output}',
'--uname=1.2.3',
'--cmdline=ARG1 ARG2 ARG3',

View file

@ -19,7 +19,9 @@
# pylint: disable=missing-docstring,invalid-name,import-outside-toplevel
# pylint: disable=consider-using-with,unspecified-encoding,line-too-long
# pylint: disable=too-many-locals,too-many-statements,too-many-return-statements
# pylint: disable=too-many-branches,fixme
# pylint: disable=too-many-branches,too-many-lines,too-many-instance-attributes
# pylint: disable=too-many-arguments,unnecessary-lambda-assignment,fixme
# pylint: disable=unused-argument
import argparse
import configparser
@ -436,9 +438,9 @@ def call_systemd_measure(uki, linux, opts):
def join_initrds(initrds):
if len(initrds) == 0:
if not initrds:
return None
elif len(initrds) == 1:
if len(initrds) == 1:
return initrds[0]
seq = []
@ -478,6 +480,9 @@ def pe_add_sections(uki: UKI, output: str):
pe.FILE_HEADER.IMAGE_FILE_LOCAL_SYMS_STRIPPED = True
# Old stubs might have been stripped, leading to unaligned raw data values, so let's fix them up here.
# pylint thinks that Structure doesn't have various members that it has…
# pylint: disable=no-member
for i, section in enumerate(pe.sections):
oldp = section.PointerToRawData
oldsz = section.SizeOfRawData
@ -745,6 +750,7 @@ class ConfigItem:
) -> None:
"Set namespace.<dest>[idx] to value, with idx derived from group"
# pylint: disable=protected-access
if group not in namespace._groups:
namespace._groups += [group]
idx = namespace._groups.index(group)
@ -814,7 +820,10 @@ class ConfigItem:
else:
conv = lambda s:s
if self.nargs == '*':
# This is a bit ugly, but --initrd is the only option which is specified
# with multiple args on the command line and a space-separated list in the
# config file.
if self.name == '--initrd':
value = [conv(v) for v in value.split()]
else:
value = conv(value)
@ -834,7 +843,16 @@ class ConfigItem:
return (section_name, key, value)
VERBS = ('build',)
CONFIG_ITEMS = [
ConfigItem(
'positional',
metavar = 'VERB',
nargs = '*',
help = f"operation to perform ({','.join(VERBS)})",
),
ConfigItem(
'--version',
action = 'version',
@ -848,20 +866,18 @@ CONFIG_ITEMS = [
),
ConfigItem(
'linux',
metavar = 'LINUX',
'--linux',
type = pathlib.Path,
nargs = '?',
help = 'vmlinuz file [.linux section]',
config_key = 'UKI/Linux',
),
ConfigItem(
'initrd',
metavar = 'INITRD',
'--initrd',
metavar = 'INITRD',
type = pathlib.Path,
nargs = '*',
help = 'initrd files [.initrd section]',
action = 'append',
help = 'initrd file [part of .initrd section]',
config_key = 'UKI/Initrd',
config_push = ConfigItem.config_list_prepend,
),
@ -1068,7 +1084,7 @@ def apply_config(namespace, filename=None):
# Fill in ._groups based on --pcr-public-key=, --pcr-private-key=, and --phases=.
assert '_groups' not in namespace
n_pcr_priv = len(namespace.pcr_private_keys or ())
namespace._groups = list(range(n_pcr_priv))
namespace._groups = list(range(n_pcr_priv)) # pylint: disable=protected-access
cp = configparser.ConfigParser(
comment_prefixes='#',
@ -1193,6 +1209,20 @@ def parse_args(args=None):
p = create_parser()
opts = p.parse_args(args)
# Figure out which syntax is being used, one of:
# ukify verb --arg --arg --arg
# ukify linux initrd…
if len(opts.positional) == 1 and opts.positional[0] in VERBS:
opts.verb = opts.positional[0]
elif opts.linux or opts.initrd:
raise ValueError('--linux/--initrd options cannot be used with positional arguments')
else:
print("Assuming obsolete commandline syntax with no verb. Please use 'build'.")
if opts.positional:
opts.linux = pathlib.Path(opts.positional[0])
opts.initrd = [pathlib.Path(arg) for arg in opts.positional[1:]]
opts.verb = 'build'
# Check that --pcr-public-key=, --pcr-private-key=, and --phases=
# have either the same number of arguments are are not specified at all.
n_pcr_pub = None if opts.pcr_public_keys is None else len(opts.pcr_public_keys)
@ -1213,6 +1243,7 @@ def parse_args(args=None):
def main():
opts = parse_args()
check_inputs(opts)
assert opts.verb == 'build'
make_uki(opts)