_ _
| |_ ___| |_ _
| | . | | | |
|_|_|___|_|_ |
|___|
git mirror - github.com/owenewans/holy - branch master
file tests/libc-abi.py
#!/usr/bin/env python3
import hashlib
import json
import os
import pathlib
import shutil
import subprocess
import sys
import tempfile
if len(sys.argv) not in (6, 8):
print("usage: libc-abi.py STATIC-HOLYPKG MUSL32-PACKAGE MUSL32-CC MUSL64-PACKAGE MUSL64-CC [GLIBC32-PACKAGE GLIBC64-PACKAGE]", file=sys.stderr)
sys.exit(2)
binary, package32, cc32, package64, cc64 = map(lambda path: pathlib.Path(path).resolve(), sys.argv[1:6])
glibc = list(map(lambda path: pathlib.Path(path).resolve(), sys.argv[6:]))
commands = {name: shutil.which(name) for name in ("doas", "chroot", "unshare", "mount", "sh", "patchelf", *(("gcc",) if glibc else ()))}
if not all(commands.values()) or not all(path.is_file() for path in (binary, package32, cc32, package64, cc64, *glibc)):
print("missing libc ABI fixture input", file=sys.stderr)
sys.exit(6)
if os.uname().machine != "x86_64":
print("mixed ELF32/ELF64 runtime fixture requires an x86_64 kernel", file=sys.stderr)
sys.exit(6)
if subprocess.run([commands["doas"], "-n", "true"]).returncode:
sys.exit(6)
identity = str(os.getuid()) + ":" + str(os.getgid())
def checked(command, status=0, **kwargs):
result = subprocess.run(list(map(str, command)), capture_output=True, text=True, timeout=60, **kwargs)
assert result.returncode == status, (command, result.returncode, result.stdout, result.stderr)
return result.stdout
with tempfile.TemporaryDirectory(prefix="holy-libc-abi-") as scratch:
tmp = pathlib.Path(scratch)
root = tmp / "root"
for directory in ("usr/bin", "tmp", "proc"):
(root / directory).mkdir(parents=True, exist_ok=True)
(root / "lib").symlink_to("usr/lib")
(root / "lib64").symlink_to("usr/lib64")
shutil.copyfile(binary, root / "usr/bin/holypkg")
(root / "usr/bin/holypkg").chmod(0o755)
def run(*args, status=0):
return checked([binary, *args], status=status)
def guest(*args, status=0):
return checked([commands["doas"], "-n", commands["unshare"], "--mount", "--pid", "--fork",
"--propagation", "private", "--", commands["sh"], "-c",
'"$2" -t proc -o nosuid,nodev,noexec proc "$1/proc" || exit 6; '
'root=$1; chroot=$3; identity=$4; shift 4; '
'exec "$chroot" --userspec="$identity" "$root" /usr/bin/holypkg "$@"',
"holy-libc-abi", root, commands["mount"], commands["chroot"], identity, *args], status=status)
def launch(variant):
return [commands["doas"], "-n", commands["chroot"], "--userspec=" + identity,
str(root), "/usr/bin/libc-probe-" + variant]
assert "runtime nolibc\n" in run("elf", binary)
run("db", "init", "--root", root)
source = tmp / "probe.c"
source.write_text('''#define _POSIX_C_SOURCE 200809L
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
static void *worker(void *unused)
{
char *value = malloc(32);
(void)unused;
if (value) strcpy(value, "thread-ok");
return value;
}
int main(int argc, char **argv)
{
pthread_t thread;
struct timespec now;
void *value;
char input[128];
(void)argv;
if (clock_gettime(CLOCK_MONOTONIC, &now) || pthread_create(&thread, NULL, worker, NULL) ||
pthread_join(thread, &value) || !value) return 1;
if (argc > 1) {
if (!fgets(input, sizeof input, stdin)) return 2;
printf("%s <- %s", HOLY_VARIANT, input);
} else printf("%s %s\\n", HOLY_VARIANT, (char *)value);
free(value);
return 0;
}
''')
runtimes, applications, loaders, links = {}, {}, {}, {}
variants = [
("musl32", 32, "x86", "musl", "i686-linux-musl", "ld-musl-i386.so.1", package32, [cc32]),
("musl64", 64, "x86_64", "musl", "x86_64-linux-musl", "ld-musl-x86_64.so.1", package64, [cc64]),
]
if glibc:
variants += [
("glibc32", 32, "x86", "glibc", "i686-linux-gnu", "ld-linux.so.2", glibc[0], [commands["gcc"], "-m32", "-march=i686"]),
("glibc64", 64, "x86_64", "glibc", "x86_64-linux-gnu", "ld-linux-x86-64.so.2", glibc[1], [commands["gcc"], "-m64", "-march=x86-64"]),
]
for variant, bits, arch, libc, target, loader_name, package, compiler in variants:
tree = tmp / ("tree-" + variant)
(tree / "HOLY").mkdir(parents=True)
(tree / "DATA/usr/bin").mkdir(parents=True)
executable = tree / ("DATA/usr/bin/libc-probe-" + variant)
checked([*compiler, "-O2", "-pthread", '-DHOLY_VARIANT="' + variant + '"', source, "-o", executable])
loader = "/usr/lib/holy/" + target + "/" + loader_name
needed = loader if libc == "musl" else "/usr/lib/holy/" + target + "/libc.so.6"
checked([commands["patchelf"], "--set-interpreter", loader, "--replace-needed",
"libc.so" if libc == "musl" else "libc.so.6", needed, executable])
elf = run("elf", executable)
assert "class ELF" + str(bits) + "\n" in elf
assert "runtime " + libc + "\n" in elf
(tree / "HOLY/meta").write_text("format holy-package-1\nname libc-probe\nversion 1\nrelease 1\nos linux\narch " + arch + "\nlibc " + libc + "\n")
for name in ("deps", "provides", "hooks", "origin", "transform"):
(tree / "HOLY" / name).write_text("")
run("manifest", "generate", tree, "--output", tmp / ("files-" + variant))
shutil.copyfile(tmp / ("files-" + variant), tree / "HOLY/files")
application = tmp / ("probe-" + variant + ".holy")
run("pack", tree, "--output", application)
for artifact in (package, application):
run("cache", "stage", "local:" + str(artifact), "--root", root)
runtimes[variant] = hashlib.sha256(package.read_bytes()).hexdigest()
applications[variant] = hashlib.sha256(application.read_bytes()).hexdigest()
loaders[variant] = [root / loader.lstrip("/")]
if libc == "glibc":
loaders[variant].append(root / needed.lstrip("/"))
links[variant] = root / (("usr/lib64/" if variant == "glibc64" else "usr/lib/") + loader_name)
tree = tmp / "profile"
(tree / "HOLY").mkdir(parents=True)
(tree / "DATA").mkdir()
(tree / "HOLY/meta").write_text("format holy-package-1\nname libc-abi-profile\nversion 1\nrelease 1\nos linux\narch noarch\nlibc nolibc\n")
for name in ("files", "provides", "hooks", "origin", "transform"):
(tree / "HOLY" / name).write_text("")
(tree / "HOLY/deps").write_text("".join(
"require " + variant + " libc-abi-profile package libc-probe " + arch + " " + libc + " any - libc-probe metadata\n"
for variant, bits, arch, libc, *_ in variants))
profile = tmp / "profile.holy"
run("pack", tree, "--output", profile)
run("cache", "stage", "local:" + str(profile), "--root", root)
artifacts = [hashlib.sha256(profile.read_bytes()).hexdigest(), *runtimes.values(), *applications.values()]
answers = [item for variant, bits, *_ in variants if bits == 32
for digest in (runtimes[variant], applications[variant]) for item in ("--accept-arch", digest)]
run("db", "plan-set", *artifacts, "--root", root, status=3)
plan = run("db", "plan-set", *artifacts, *answers, "--root", root).split(" sha256 ")[1].split()[0]
run("db", "apply-set", plan, *artifacts, *answers, "--root", root)
def probes():
for variant in runtimes:
assert checked(launch(variant)) == variant + " thread-ok\n"
for sender in runtimes:
for receiver in runtimes:
if sender == receiver:
continue
with subprocess.Popen(launch(sender), stdout=subprocess.PIPE, stderr=subprocess.PIPE) as producer:
output = checked([*launch(receiver), "read"], stdin=producer.stdout)
producer.stdout.close()
assert producer.wait(timeout=60) == 0, producer.stderr.read()
assert output == receiver + " <- " + sender + " thread-ok\n"
probes()
missing_sets = [(variant,) for variant in runtimes] + [tuple(runtimes)]
if glibc:
missing_sets += [("musl32", "musl64"), ("glibc32", "glibc64")]
for missing in missing_sets:
for variant in missing:
for loader in loaders[variant]:
loader.unlink()
links[variant].unlink()
guest("db", "check", "--all", "--root", "/", status=4)
for variant in missing:
report = guest("db", "check", applications[variant], "--root", "/", "--json", status=4)
assert '"code":"broken-provider"' in report
repair = guest("db", "repair-plan", runtimes[variant], "--root", "/").split(" sha256 ")[1].split()[0]
guest("db", "repair", runtimes[variant], "--plan", repair, "--root", "/")
guest("db", "check", "--all", "--root", "/")
probes()
print(json.dumps({"schema": "holy-libc-abi-test-1", "result": "pass",
"holypkg_sha256": hashlib.sha256(binary.read_bytes()).hexdigest(),
"runtime_sha256": runtimes, "application_sha256": applications,
"probe_source_sha256": hashlib.sha256(source.read_bytes()).hexdigest(),
"kernel": os.uname().release, "coverage": ["ELF32", "ELF64", "threads", "clock", "pipe-both-directions", "local-recovery"],
"not_tested": ["i686-kernel-boot", "hardware", *([] if glibc else ["glibc32", "glibc64"])]}))