diff --git a/.github/workflows/build-lite.yml b/.github/workflows/build-lite.yml index 9414c09..345899f 100644 --- a/.github/workflows/build-lite.yml +++ b/.github/workflows/build-lite.yml @@ -54,6 +54,10 @@ jobs: cp wrapper-lite dist-native/ cp wrapper-lite-rootless dist-native/ cp -R rootfs dist-native/ + # The package keeps only the libraries wrapper-lite loads. rootfs/ + # stays whole: lite links against it, and the QEMU assets and the + # Docker image copy it. + python3 tools/slim-rootfs.py dist-native/rootfs cp README.md dist-native/ cp LICENSE dist-native/ diff --git a/README.md b/README.md index ed0a289..41571ec 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,11 @@ Build outputs: - Interactive prompt when a TTY is available. - With `--code-from-file`, the code is read from `data/2fa.txt`. +The native package (the `wrapper-lite-linux-x86_64` artifact) carries 22 of the +98 libraries in `rootfs/system/lib64`: the ones wrapper-lite loads. +`tools/slim-rootfs.py` removes the rest when the package is assembled, so the +repository's `rootfs/` stays whole. Its `KEEP` list says why each library stays. + ## Run with QEMU (recommended) `wrapper-lite-qemu` is the recommended launcher. It boots the prebuilt guest diff --git a/tools/slim-rootfs.py b/tools/slim-rootfs.py new file mode 100755 index 0000000..a9be5bf --- /dev/null +++ b/tools/slim-rootfs.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +"""Slim a built wrapper-lite rootfs to the libraries wrapper-lite needs. + +rootfs/system/lib64 holds 98 libraries from the Apple Music Android app and +AOSP. wrapper-lite uses a small part of them; the rest is pulled in only by +DT_NEEDED entries of the Apple libraries (the UI, graphics, audio and media +frameworks), and none of that code runs in a headless service. + +This script keeps KEEP and removes every other library. A kept library that +lists a removed one in DT_NEEDED would no longer load, so each such entry is +pointed at the string of another entry of the same library (libc.so, which +every library lists); the loader sees one name twice and loads it once. No +kept library imports a symbol that only a removed library defines (checked +with readelf --dyn-syms on the full 98), so no call resolves into a removed +library. `--report` prints each entry it rewrites. + +Usage: slim-rootfs.py ROOTFS [--report] +ROOTFS is edited in place; run it on a copy. +""" +import os +import struct +import sys + +# Libraries wrapper-lite loads: the closure of the lite binary's DT_NEEDED +# entries (main() checks that every name here is in it). libCoreFP.so and +# libCoreLSKD.so hold the FairPlay code (lite/playback.cpp hooks into +# libCoreLSKD.so by address). libCoreADI.so is the one a DT_NEEDED walk +# would not justify: nothing imports from it, but libCoreFP.so and +# libstoreservicescore.so look up its exports (cvu8io98wun, vdfut768ig) by +# dlsym. Without it every request fails to build its anisette headers +# (ADISetProvisioningPath -45075) and /m3u8 and the token refresh fail. +KEEP = frozenset(""" +libandroidappmusic.so libstoreservicescore.so libmediaplatform.so +libc++_shared.so libcurl.so liblog.so libc.so libm.so libstdc++.so +libCoreFoundation.so libCoreFP.so libCoreLSKD.so libCoreADI.so libdaapkit.so +libmedialibrarycore.so libdispatch.so libBlocksRuntime.so +libicudata_sv_apple.so libicui18n_sv_apple.so libicuuc_sv_apple.so +libxml2.so libz.so +""".split()) + +DT_NULL, DT_NEEDED = 0, 1 +PT_LOAD, PT_DYNAMIC = 1, 2 + + +class Elf: + def __init__(self, path): + self.path = path + self.data = bytearray(open(path, "rb").read()) + d = self.data + if d[:4] != b"\x7fELF" or d[4] != 2 or d[5] != 1: + raise SystemExit(f"{path}: not a little-endian ELF64") + phoff = struct.unpack_from(" {n}") + if changed: + elf.save() + + # Everything a kept library lists must now be kept or provided by the + # loader, and every kept library must be reachable from lite. + reached, todo = set(), [targets[0]] + while todo: + for _, n in Elf(todo.pop()).needed(): + if n in present and n not in KEEP: + raise SystemExit(f"{n} is still listed after the cuts") + if n in KEEP and n not in reached: + reached.add(n) + todo.append(os.path.join(lib, n)) + if reached != KEEP: + raise SystemExit(f"KEEP names libraries lite does not reach: {sorted(KEEP - reached)}") + + freed = 0 + for n in removed: + p = os.path.join(lib, n) + freed += os.path.getsize(p) + os.unlink(p) + print(f"kept {len(KEEP)} libraries, removed {len(removed)} ({freed / 1048576:.1f} MiB), " + f"cut {cut} dependency entries") + + +if __name__ == "__main__": + main()