]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
ukify: try to find the uname string in the linux image if not specified
authorZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
Wed, 16 Nov 2022 16:44:28 +0000 (17:44 +0100)
committerZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
Wed, 7 Dec 2022 14:53:45 +0000 (15:53 +0100)
The approach is based on mkinicpio's autodetection.

This is hacky as hell. Some cases are actually fairly nice: ppc64el images have
a note that contains 'uname -r'. (The note is not uniquely labeled at all, and
only contains the release part instead of the full version-hostname-release
string, and we don't actually care about ppc, and it's very hard to read the
note from Python, but in general that'd be the approach I'd like.)

I opted to simply read and decompress the full linux binary in some cases.
Python doesn't make it easy to do streaming decompression with regexp matching,
and it doesn't seem to matter much: the image decompresses in a fraction of a
second.

src/ukify/test/test_ukify.py
src/ukify/ukify.py

index 292540bbde449bd70a40696f6c0e568e673c2fdc..a4afa56ad982222dad45bfd08140c4ed6cd619d8 100755 (executable)
@@ -208,6 +208,14 @@ def unbase64(filename):
     return tmp
 
 
+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 re.match(r'\d+\.\d+\.\d+', uname)
+
+
 def test_efi_signing(kernel_initrd, tmpdir):
     if kernel_initrd is None:
         pytest.skip('linux+initrd not found')
index d4afd88290e7e596589021f52dd1fb60a413d298..d4772d2bae0179c435956d82fdfb7965ed8a4bf3 100755 (executable)
@@ -75,6 +75,125 @@ def round_up(x, blocksize=4096):
     return (x + blocksize - 1) // blocksize * blocksize
 
 
+def maybe_decompress(filename):
+    """Decompress file if compressed. Return contents."""
+    f = open(filename, 'rb')
+    start = f.read(4)
+    f.seek(0)
+
+    if start.startswith(b'\x7fELF'):
+        # not compressed
+        return f.read()
+
+    if start.startswith(b'\x1f\x8b'):
+        import gzip
+        return gzip.open(f).read()
+
+    if start.startswith(b'\x28\xb5\x2f\xfd'):
+        import zstd
+        return zstd.uncompress(f.read())
+
+    if start.startswith(b'\x02\x21\x4c\x18'):
+        import lz4.frame
+        return lz4.frame.decompress(f.read())
+
+    if start.startswith(b'\x04\x22\x4d\x18'):
+        print('Newer lz4 stream format detected! This may not boot!')
+        import lz4.frame
+        return lz4.frame.decompress(f.read())
+
+    if start.startswith(b'\x89LZO'):
+        # python3-lzo is not packaged for Fedora
+        raise NotImplementedError('lzo decompression not implemented')
+
+    if start.startswith(b'BZh'):
+        import bz2
+        return bz2.open(f).read()
+
+    if start.startswith(b'\x5d\x00\x00'):
+        import lzma
+        return lzma.open(f).read()
+
+    raise NotImplementedError(f'unknown file format (starts with {start})')
+
+
+class Uname:
+    # This class is here purely as a namespace for the functions
+
+    VERSION_PATTERN = r'(?P<version>[a-z0-9._-]+) \([^ )]+\) (?:#.*)'
+
+    NOTES_PATTERN = r'^\s+Linux\s+0x[0-9a-f]+\s+OPEN\n\s+description data: (?P<version>[0-9a-f ]+)\s*$'
+
+    # Linux version 6.0.8-300.fc37.ppc64le (mockbuild@buildvm-ppc64le-03.iad2.fedoraproject.org)
+    # (gcc (GCC) 12.2.1 20220819 (Red Hat 12.2.1-2), GNU ld version 2.38-24.fc37)
+    # #1 SMP Fri Nov 11 14:39:11 UTC 2022
+    TEXT_PATTERN = rb'Linux version (?P<version>\d\.\S+) \('
+
+    @classmethod
+    def scrape_x86(cls, filename, opts=None):
+        # Based on https://gitlab.archlinux.org/archlinux/mkinitcpio/mkinitcpio/-/blob/master/functions#L136
+        # and https://www.kernel.org/doc/html/latest/x86/boot.html#the-real-mode-kernel-header
+        with open(filename, 'rb') as f:
+            f.seek(0x202)
+            magic = f.read(4)
+            if magic != b'HdrS':
+                raise ValueError('Real-Mode Kernel Header magic not found')
+            f.seek(0x20E)
+            offset = f.read(1)[0] + f.read(1)[0]*256  # Pointer to kernel version string
+            f.seek(0x200 + offset)
+            text = f.read(128)
+        text = text.split(b'\0', maxsplit=1)[0]
+        text = text.decode()
+
+        if not (m := re.match(cls.VERSION_PATTERN, text)):
+            raise ValueError(f'Cannot parse version-host-release uname string: {text!r}')
+        return m.group('version')
+
+    @classmethod
+    def scrape_elf(cls, filename, opts=None):
+        readelf = find_tool('readelf', opts=opts)
+
+        cmd = [
+            readelf,
+            '--notes',
+            filename,
+        ]
+
+        print('+', shell_join(cmd))
+        notes = subprocess.check_output(cmd, text=True)
+
+        if not (m := re.search(cls.NOTES_PATTERN, notes, re.MULTILINE)):
+            raise ValueError('Cannot find Linux version note')
+
+        text = ''.join(chr(int(c, 16)) for c in m.group('version').split())
+        return text.rstrip('\0')
+
+    @classmethod
+    def scrape_generic(cls, filename, opts=None):
+        # import libarchive
+        # libarchive-c fails with
+        # ArchiveError: Unrecognized archive format (errno=84, retcode=-30, archive_p=94705420454656)
+
+        # Based on https://gitlab.archlinux.org/archlinux/mkinitcpio/mkinitcpio/-/blob/master/functions#L209
+
+        text = maybe_decompress(filename)
+        if not (m := re.search(cls.TEXT_PATTERN, text)):
+            raise ValueError(f'Cannot find {cls.TEXT_PATTERN!r} in {filename}')
+
+        return m.group('version').decode()
+
+    @classmethod
+    def scrape(cls, filename, opts=None):
+        for func in (cls.scrape_x86, cls.scrape_elf, cls.scrape_generic):
+            try:
+                version = func(filename, opts=opts)
+                print(f'Found uname version: {version}')
+                return version
+            except ValueError as e:
+                print(str(e))
+        return None
+
+
 @dataclasses.dataclass
 class Section:
     name: str
@@ -330,6 +449,10 @@ def make_uki(opts):
     else:
         linux = opts.linux
 
+    if opts.uname is None:
+        print('Kernel version not specified, starting autodetection 😖.')
+        opts.uname = Uname.scrape(opts.linux, opts=opts)
+
     uki = UKI(opts.stub)
 
     # TODO: derive public key from from opts.pcr_private_keys?