Skip to content

Commit eb3510e

Browse files
committed
bin/ubuntu-core-initramfs: warn about missing dynamic dependencies
Now systemd loads most of libraries through dlopen. It also defines a .notes.dlopen section to ELF binaries to list those libraries. We use `dlopen-notes` to list those dependencies and warn about missing ones. And eventually fail for required ones.
1 parent ab561c4 commit eb3510e

File tree

2 files changed

+71
-9
lines changed

2 files changed

+71
-9
lines changed

bin/ubuntu-core-initramfs

+69-8
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import sys
1212
import hashlib
1313
from collections import namedtuple
1414
from enum import Enum, auto
15+
import json
1516

1617

1718
class ModTable:
@@ -398,19 +399,13 @@ def install_busybox(dest_dir, sysroot):
398399
os.symlink("busybox", os.path.join(dest_dir, "usr/bin", c))
399400

400401

401-
def install_misc(dest_dir, sysroot):
402+
def install_misc(dest_dir, sysroot, deb_arch):
402403
# dmsetup rules
403404
rules = package_files(["dmsetup"], sysroot)
404405
to_include = re.compile(r".*rules.d/")
405406
rules = [i for i in rules if to_include.match(i)]
406407
install_files(rules, dest_dir, sysroot)
407408

408-
# Other needed stuff
409-
proc_env = os.environ.copy()
410-
proc_env["DPKG_DATADIR"] = sysroot + "/usr/share/dpkg"
411-
out = check_output(["dpkg-architecture", "-q",
412-
"DEB_HOST_MULTIARCH"], env=proc_env).decode("utf-8")
413-
deb_arch = out.splitlines()[0]
414409
files = [
415410
"/usr/bin/kmod",
416411
"/usr/bin/mount",
@@ -616,6 +611,67 @@ def create_initrd_pkg_list(dest_dir, sysroot):
616611
pkgs).decode("utf-8")
617612
pkg_list.write(out)
618613

614+
# verify_missing_dlopen looks at the .notes.dlopen section of ELF
615+
# binaries to find libraries that are not in the dynamic section, and
616+
# that will be loaded with dynamically dlopen when needed.
617+
# See https://systemd.io/ELF_DLOPEN_METADATA/
618+
def verify_missing_dlopen(destdir, libdir):
619+
missing = {}
620+
for dirpath, dirs, files in os.walk(destdir):
621+
for f in files:
622+
path = os.path.join(dirpath, f)
623+
if os.path.islink(path) or not os.path.isfile(path):
624+
continue
625+
with open(path, 'rb') as b:
626+
if b.read(4) != b'\x7fELF':
627+
continue
628+
out = check_output(["dlopen-notes", path])
629+
split = out.splitlines()
630+
json_doc = b'\n'.join([s for s in split if not s[:1] == b'#'])
631+
doc = json.loads(json_doc)
632+
for dep in doc:
633+
sonames = dep["soname"]
634+
priority = dep["priority"]
635+
found_sonames = []
636+
for soname in sonames:
637+
dest = os.path.join(destdir, os.path.relpath(libdir, "/"), soname)
638+
if os.path.exists(os.path.join(destdir, dest)):
639+
found_sonames.append(soname)
640+
if not found_sonames:
641+
# We did not find any library.
642+
# In this case we need to mark all sonames as
643+
# missing. This is required because some features
644+
# may have common subset of sonames and those
645+
# features might have different priorities.
646+
for soname in sonames:
647+
current_priority = missing.get(soname)
648+
if current_priority == "required":
649+
continue
650+
elif current_priority == "recommended" and priority not in ["required"]:
651+
continue
652+
elif current_priority == "suggested" and priority not in ["required", "recommended"]:
653+
continue
654+
else:
655+
missing[soname] = priority
656+
657+
fatal = False
658+
if missing:
659+
print(f"WARNING: These sonames are missing:", file=sys.stderr)
660+
for m, priority in missing.items():
661+
print(f" * {m} ({priority})", file=sys.stderr)
662+
if priority in ["required", "recommended"]:
663+
fatal = True
664+
if fatal:
665+
print(f"WARNING: Some missing sonames are required or recommended. Failing.", file=sys.stderr)
666+
667+
return not fatal
668+
669+
def get_deb_arch(sysroot):
670+
proc_env = os.environ.copy()
671+
proc_env["DPKG_DATADIR"] = sysroot + "/usr/share/dpkg"
672+
out = check_output(["dpkg-architecture", "-q",
673+
"DEB_HOST_MULTIARCH"], env=proc_env).decode("utf-8")
674+
return out.splitlines()[0]
619675

620676
def create_initrd(parser, args):
621677
# TODO generate microcode instead of shipping in debian package
@@ -631,6 +687,8 @@ def create_initrd(parser, args):
631687
if args.kernelver:
632688
args.output = "-".join([args.output, args.kernelver])
633689
with tempfile.TemporaryDirectory(suffix=".ubuntu-core-initramfs") as d:
690+
deb_arch = get_deb_arch(rootfs)
691+
634692
kernel_root = os.path.join(d, "kernel")
635693
modules = os.path.join(kernel_root, "usr", "lib", "modules")
636694
os.makedirs(modules, exist_ok=True)
@@ -650,7 +708,7 @@ def create_initrd(parser, args):
650708
# Copy systemd bits
651709
install_systemd_files(main, rootfs)
652710
# Other miscelanea stuff
653-
install_misc(main, rootfs)
711+
install_misc(main, rootfs, deb_arch)
654712
# Copy snapd bits
655713
snapd_lib = path_join_make_rel_paths(rootfs, "/usr/lib/snapd")
656714
snapd_files = [os.path.join(snapd_lib, "snap-bootstrap"),
@@ -698,6 +756,9 @@ def create_initrd(parser, args):
698756
)
699757
check_call(["depmod", "-a", "-b", main, args.kernelver])
700758

759+
if not verify_missing_dlopen(main, os.path.join("/usr/lib", deb_arch)):
760+
sys.exit(1)
761+
701762
# Create manifest with packages with files included in the initramfs
702763
create_initrd_pkg_list(main, rootfs)
703764

debian/control

+2-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ Depends: ${python3:Depends}, ${misc:Depends}, dracut-core (>= 051-1),
4646
systemd-sysv,
4747
tar,
4848
udev,
49-
util-linux
49+
util-linux,
50+
dh-dlopenlibdeps
5051
Description: standard embedded initrd
5152
Standard embedded initrd implementation to be used with Ubuntu Core
5253
systems. Currently targetting creating BLS Type2 like binaries.

0 commit comments

Comments
 (0)