Skip to content

Commit

Permalink
build-aux/snap/local: cleanup patch and verification helper scripts (#…
Browse files Browse the repository at this point in the history
…14901)

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

Improve the patching and verification scripts used during snap build.

Signed-off-by: Maciej Borzecki <[email protected]>

* snapcraft: update call sites of ELF patching/verification scripts

Signed-off-by: Maciej Borzecki <[email protected]>

---------

Signed-off-by: Maciej Borzecki <[email protected]>
  • Loading branch information
bboozzoo authored Jan 10, 2025
1 parent be6b631 commit f9578f5
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 70 deletions.
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}"

0 comments on commit f9578f5

Please sign in to comment.