File size: 6,222 Bytes
5f25733 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | """Prepare verified GNU Linux audio libraries with package-relative loader paths."""
import argparse
import os
from pathlib import Path
import re
import struct
import sys
# Import only this script's siblings, including under PYTHONSAFEPATH.
sys.path.insert(0, str(Path(__file__).resolve().parent))
from runtime import Binary, PLUGINS, RuntimeFormat, prepare, required_library_paths
SYSTEM_IMPORTS = frozenset(
{
"libc.so.6",
"libm.so.6",
"libdl.so.2",
"libpthread.so.0",
"librt.so.1",
"libresolv.so.2",
"ld-linux-x86-64.so.2",
"ld-linux-aarch64.so.1",
}
)
def inspect(path, target):
machine = {"x86_64-unknown-linux-gnu": 62, "aarch64-unknown-linux-gnu": 183}[target]
with path.open("rb") as source:
data = source.read(64 * 1024 * 1024 + 1)
if not 64 <= len(data) <= 64 * 1024 * 1024:
raise ValueError("invalid ELF file size")
header = struct.unpack_from("<16sHHIQQQIHHHHHH", data)
if header[0][:7] != b"\x7fELF\x02\x01\x01" or header[0][7] not in (0, 3):
raise ValueError("expected a little-endian ELF64 library")
if header[1:4] != (3, machine, 1) or header[8:10] != (64, 56):
raise ValueError(f"expected a {target} shared library")
phoff, count = header[5], header[10]
if not 1 <= count <= 128 or phoff < 64 or phoff + count * 56 > len(data):
raise ValueError("invalid ELF program headers")
loads, dynamic = [], []
for index in range(count):
kind, _, offset, address, _, size, memory_size, _ = struct.unpack_from(
"<IIQQQQQQ", data, phoff + index * 56
)
if size > memory_size or offset + size > len(data):
raise ValueError("invalid ELF segment bounds")
if kind == 1:
loads.append((address, offset, size))
elif kind == 2:
dynamic.append((offset, size, address))
elif kind == 3:
raise ValueError("an ELF runtime library must not name an interpreter")
if len(dynamic) != 1 or not 16 <= dynamic[0][1] <= 65536 or dynamic[0][1] % 16:
raise ValueError("invalid ELF dynamic table")
tags = {}
offset, size, address = dynamic[0]
mappings = [
file_offset + address - start
for start, file_offset, length in loads
if start <= address and address + size <= start + length
]
if mappings != [offset]:
raise ValueError("ELF dynamic table must have one matching file-backed mapping")
for cursor in range(offset, offset + size, 16):
tag, value = struct.unpack_from("<qQ", data, cursor)
if tag == 0:
break
# Auxiliary/filter libraries and audit modules are additional loader inputs.
if tag in (0x7FFFFFFD, 0x7FFFFFFF, 0x6FFFFEFB, 0x6FFFFEFC):
raise ValueError("unsupported ELF loader dependency")
tags.setdefault(tag, []).append(value)
else:
raise ValueError("unterminated ELF dynamic table")
if any(len(tags.get(tag, [])) != 1 for tag in (5, 10)):
raise ValueError("invalid ELF dynamic string table")
address, size = tags[5][0], tags[10][0]
offsets = [
offset + address - start
for start, offset, length in loads
if start <= address and address + size <= start + length
]
if len(offsets) != 1 or not 1 <= size <= 1024 * 1024:
raise ValueError("invalid ELF string table bounds")
strings = data[offsets[0] : offsets[0] + size]
values = {}
for tag in (1, 14, 15, 29):
values[tag] = []
if tag != 1 and len(tags.get(tag, [])) > 1:
raise ValueError("duplicate ELF loader metadata")
for offset in tags.get(tag, []):
end = strings.find(b"\0", offset)
if not 0 <= offset < len(strings) or end < 0:
raise ValueError("invalid ELF loader string")
value = (
os.fsdecode(strings[offset:end])
if tag in (15, 29)
else strings[offset:end].decode("ascii")
)
if tag in (1, 14) and not re.fullmatch(
r"[A-Za-z0-9_+.-]+\.so(?:\.[0-9]+)*", value
):
raise ValueError("ELF dependencies must be plain library names")
values[tag].append(value)
identity = values[14][0] if values[14] else path.name
return Binary(
identity,
tuple(values[1]),
tuple(f"{tag}={value}" for tag in (15, 29) for value in values[tag]),
)
def finalize_copy(destination, metadata, dependency_paths):
allowed = {"29=$ORIGIN", "29=$ORIGIN:$ORIGIN/.."}
needs_parent = destination.parent.name == "gstreamer-1.0"
required = "29=$ORIGIN:$ORIGIN/.." if needs_parent else "29=$ORIGIN"
if any(path not in allowed for path in metadata.rpaths) or (
any(name not in SYSTEM_IMPORTS for name in metadata.imports)
and required not in metadata.rpaths
and "29=$ORIGIN:$ORIGIN/.." not in metadata.rpaths
):
raise ValueError("rebuild native libraries with package-relative runtime paths")
return metadata
def project(prefix, receipts, target, output):
if sys.platform != "linux" or target not in (
"x86_64-unknown-linux-gnu",
"aarch64-unknown-linux-gnu",
):
raise ValueError(
"runtime preparation requires GNU Linux and an explicit GNU target"
)
format = RuntimeFormat(
tuple(Path(f"lib/gstreamer-1.0/libgst{name}.so") for name in sorted(PLUGINS)),
SYSTEM_IMPORTS,
inspect,
finalize_copy,
plugin_dir="lib/gstreamer-1.0",
required_libraries=tuple(
Path(path).name for path in required_library_paths(target)
),
)
prepare(prefix, receipts, target, output, format)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--prefix", type=Path, required=True)
parser.add_argument("--receipts", type=Path, required=True)
parser.add_argument("--target", required=True)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
project(args.prefix, args.receipts, args.target, args.output)
|