Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

build-aux/snap/local: cleanup patch and verification helper scripts #14901

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 77 additions & 51 deletions build-aux/snap/local/patch-dl.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,72 +7,98 @@
import os
import shutil
import subprocess
import sys
import tempfile
import logging

from elftools.elf.elffile import ELFFile


parser = argparse.ArgumentParser()
def parse_arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="patch ELF binaries to request a specific interpreter",
)
craft_prime = os.environ.get("CRAFT_PRIME")
parser.add_argument(
"--prime",
nargs=1,
default=craft_prime,
required=(craft_prime is None),
help="snapcraft part priming location",
)

craft_prime = os.environ.get('CRAFT_PRIME')
parser.add_argument('--prime', nargs=1, default=craft_prime, required=(craft_prime is None))
craft_part_install = os.environ.get("CRAFT_PART_INSTALL")
parser.add_argument(
"--install",
nargs=1,
default=craft_part_install,
required=(craft_part_install is None),
help="snapcraft part install location",
)

craft_part_install = os.environ.get('CRAFT_PART_INSTALL')
parser.add_argument('--install', nargs=1, default=craft_part_install, required=(craft_part_install is None))
parser.add_argument("interp", help="ELF interpreter to use")

parser.add_argument('linker')
return parser.parse_args()

args = parser.parse_args()

def is_shared_exec(path):
with open(path, 'rb') as f:
if f.read(4) != b'\x7fELF':
def is_shared_exec(path: str) -> bool:
with open(path, "rb") as f:
if f.read(4) != b"\x7fELF":
return False
f.seek(0, 0)
elf = ELFFile(f)
for segment in elf.iter_segments():
# TODO: use iter_segments(type='PT_INTERP')
if segment['p_type'] == 'PT_INTERP':
if segment["p_type"] == "PT_INTERP":
return True
return False

owned_executables = []

for dirpath, dirnames, filenames in os.walk(args.install):
for filename in filenames:
path = os.path.join(dirpath, filename)
if os.path.islink(path):
continue
if not is_shared_exec(path):
continue
rel = os.path.relpath(path, args.install)
# Now we need to know if the file in $CRAFT_PRIME is actually
# owned by the current part and see if it is hard-linked to a
# corresponding file in $CRAFT_PART_INSTALL.
#
# Even if we break the hard-links before, subsequent builds will
# re-introduce the hard-links in `crafctl default` call in the
# `override-prime`.
prime_path = os.path.join(args.prime, rel)
install_st = os.lstat(path)
prime_st = os.lstat(path)
if install_st.st_dev != prime_st.st_dev:
continue
if install_st.st_ino != prime_st.st_ino:
continue
owned_executables.append(prime_path)

for path in owned_executables:
# Because files in $CRAFT_PRIME, $CRAFT_STAGE, and $CRAFT_PART_INSTALL are hard-linked,
# we need to copy the file first to avoid writing back to the content of other directories.
with tempfile.NamedTemporaryFile(dir=os.path.dirname(path),
prefix='{}-'.format(os.path.basename(path))) as f:
with open(path, 'rb') as orig:
shutil.copyfileobj(orig, f)
f.flush()
print(f'Running patchelf for "{f.name}"', file=sys.stderr)
subprocess.run(['patchelf', '--set-interpreter', args.linker, f.name], check=True)
shutil.copystat(path, f.name)
os.unlink(path)
os.link(f.name, path)

def main(args) -> None:
owned_executables = []

for dirpath, _, filenames in os.walk(args.install):
for filename in filenames:
path = os.path.join(dirpath, filename)
if os.path.islink(path):
continue
if not is_shared_exec(path):
continue
logging.debug("found owned ELF binary: %s", path)
rel = os.path.relpath(path, args.install)
# Now we need to know if the file in $CRAFT_PRIME is actually
# owned by the current part and see if it is hard-linked to a
# corresponding file in $CRAFT_PART_INSTALL.
#
# Even if we break the hard-links before, subsequent builds will
# re-introduce the hard-links in `crafctl default` call in the
# `override-prime`.
prime_path = os.path.join(args.prime, rel)
install_st = os.lstat(path)
prime_st = os.lstat(path)
if install_st.st_dev != prime_st.st_dev:
continue
if install_st.st_ino != prime_st.st_ino:
continue
owned_executables.append(prime_path)

for path in owned_executables:
# Because files in $CRAFT_PRIME, $CRAFT_STAGE, and $CRAFT_PART_INSTALL are hard-linked,
# we need to copy the file first to avoid writing back to the content of other directories.
with tempfile.NamedTemporaryFile(
dir=os.path.dirname(path), prefix=f"{os.path.basename(path)}-"
) as f:
with open(path, "rb") as orig:
shutil.copyfileobj(orig, f)
f.flush()
logging.info("patching ELF binary %s", path)
subprocess.run(
["patchelf", "--set-interpreter", args.interp, f.name], check=True
)
shutil.copystat(path, f.name)
os.unlink(path)
os.link(f.name, path)


if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG)
main(parse_arguments())
72 changes: 55 additions & 17 deletions build-aux/snap/local/verify-dl.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,63 @@

import os
import sys
import logging
import argparse

from elftools.elf.elffile import ELFFile

errors = 0

for dirpath, dirnames, filenames in os.walk(sys.argv[1]):
for filename in filenames:
path = os.path.join(dirpath, filename)
if os.path.islink(path):
continue
with open(path, 'rb') as f:
if f.read(4) != b'\x7fELF':
def parse_arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="verify requested interpreter of ELF binaries"
)
craft_prime = os.environ.get("CRAFT_PRIME")
parser.add_argument(
"--prime",
default=craft_prime,
required=(craft_prime is None),
help="snapcraft part priming location (or rootdir of a snap)",
)
parser.add_argument("interp", help="Expected interpreter")

return parser.parse_args()


def main(opts) -> None:
errors: list[str] = []

for dirpath, _, filenames in os.walk(opts.prime):
for filename in filenames:
path = os.path.join(dirpath, filename)
if os.path.islink(path):
continue
f.seek(0, 0)
elf = ELFFile(f)
for segment in elf.iter_segments():
# TODO: use iter_segments(type='PT_INTERP')
if segment['p_type'] == 'PT_INTERP' and segment.get_interp_name() != sys.argv[2]:
print('{}: Expected interpreter to be "{}", got "{}"'.format(path, sys.argv[2], segment.get_interp_name()), file=sys.stderr)
errors +=1

sys.exit(errors)
with open(path, "rb") as f:
if f.read(4) != b"\x7fELF":
continue
f.seek(0, 0)

logging.debug("checking ELF binary: %s", path)

elf = ELFFile(f)
for segment in elf.iter_segments():
# TODO: use iter_segments(type='PT_INTERP')
if (
segment["p_type"] == "PT_INTERP"
and segment.get_interp_name() != opts.interp
):
logging.error(
'%s: expected interpreter to be "%s", got "%s"',
path,
sys.argv[2],
segment.get_interp_name(),
)
errors.append(path)

if errors:
badlist = "\n".join(["- " + n for n in errors])
raise RuntimeError(f"found binaries with incorrect ELF interpreter:\n{badlist}")


if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG)
main(parse_arguments())
6 changes: 4 additions & 2 deletions build-aux/snap/snapcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,8 @@ parts:
- -fips-build-lp
override-prime: |
craftctl default
python3 "${CRAFT_PROJECT_DIR}/build-aux/snap/local/patch-dl.py" "/snap/snapd/current/usr/lib/${CRAFT_ARCH_TRIPLET_BUILD_FOR}/${DYNAMIC_LINKER}"
"${CRAFT_PROJECT_DIR}/build-aux/snap/local/patch-dl.py" \
"/snap/snapd/current/usr/lib/${CRAFT_ARCH_TRIPLET_BUILD_FOR}/${DYNAMIC_LINKER}"

libcrypto-fips:
plugin: nil
Expand Down Expand Up @@ -524,4 +525,5 @@ parts:
<<:
- *dynamic-linker
override-prime: |
python3 "${CRAFT_PROJECT_DIR}/build-aux/snap/local/verify-dl.py" "${CRAFT_PRIME}" "/snap/snapd/current/usr/lib/${CRAFT_ARCH_TRIPLET_BUILD_FOR}/${DYNAMIC_LINKER}" ";"
"${CRAFT_PROJECT_DIR}/build-aux/snap/local/verify-dl.py" \
"/snap/snapd/current/usr/lib/${CRAFT_ARCH_TRIPLET_BUILD_FOR}/${DYNAMIC_LINKER}"
Loading