diff --git a/initramfs/build.py b/initramfs/build.py index 730fd88..3a379f1 100755 --- a/initramfs/build.py +++ b/initramfs/build.py @@ -1,5 +1,10 @@ #!/usr/bin/env python3 -"""Pack a static ARM64 BusyBox and init.sh into a gzip newc initramfs.""" +"""Pack a static ARM64 BusyBox and init.sh into a gzip newc initramfs. + +With --os-dir the archive also carries everything needed to assemble a custom +Android from pinned images on userdata: the extent maps, the partition table and +the qkx-* helper scripts. +""" import argparse import gzip import stat @@ -38,8 +43,18 @@ def main(): ap.add_argument("--init", type=Path, default=Path(__file__).with_name("init.sh")) ap.add_argument("--extra", action="append", default=[], metavar="SRC:DEST", help="add a static ARM64 binary, e.g. modeset:bin/modeset") + ap.add_argument("--os-dir", type=Path, + help="directory from os-install.sh holding maps/ and partitions") + ap.add_argument("--os-scripts", type=Path, + default=Path(__file__).with_name("os"), + help="directory of qkx-* target scripts") + ap.add_argument("--qkx-dm", type=Path, + help="static qkx_dm binary, required with --os-dir") + ap.add_argument("--max-size", type=lambda s: int(s, 0), default=0x200000) a = ap.parse_args() - dirs = ("bin", "sbin", "dev", "dev/pts", "proc", "sys", "config", "etc", "root", "tmp") + dirs = ["bin", "sbin", "dev", "dev/pts", "proc", "sys", "config", "etc", "root", "tmp"] + if a.os_dir: + dirs += ["etc/qkx", "etc/qkx/maps", "android"] entries = [(d, stat.S_IFDIR | 0o755, b"", 0, 0) for d in dirs] entries += [ ("bin/busybox", stat.S_IFREG | 0o755, check_static_arm64(a.busybox), 0, 0), @@ -50,13 +65,40 @@ def main(): ("etc/passwd", stat.S_IFREG | 0o644, b"root::0:0:root:/root:/bin/sh\n", 0, 0), ("etc/group", stat.S_IFREG | 0o644, b"root:x:0:\n", 0, 0), ] + # The kernel-log watcher is useful even without the OS payload. + for script in sorted(a.os_scripts.glob("qkx-*")): + if not a.os_dir and script.name != "qkx-klogwatch": + continue + entries.append((f"bin/{script.name}", stat.S_IFREG | 0o755, + script.read_bytes(), 0, 0)) + if a.os_dir: + if not a.qkx_dm: + raise SystemExit("--os-dir requires --qkx-dm") + entries.append(("bin/qkx_dm", stat.S_IFREG | 0o755, + check_static_arm64(a.qkx_dm), 0, 0)) + partitions = a.os_dir / "partitions" + if not partitions.is_file(): + raise SystemExit(f"{partitions}: missing; rerun tools/os-install.sh") + entries.append(("etc/qkx/partitions", stat.S_IFREG | 0o644, + partitions.read_bytes(), 0, 0)) + maps = sorted((a.os_dir / "maps").glob("*.map")) + if not maps: + raise SystemExit(f"{a.os_dir}/maps: no extent maps found") + for m in maps: + if not m.read_text().strip(): + raise SystemExit(f"{m}: empty extent map") + entries.append((f"etc/qkx/maps/{m.name}", stat.S_IFREG | 0o644, + m.read_bytes(), 0, 0)) + print(f"OS payload: {len(maps)} maps " + f"({', '.join(m.stem for m in maps)})") for spec in a.extra: src, dest = spec.split(":", 1) entries.append((dest, stat.S_IFREG | 0o755, check_static_arm64(Path(src)), 0, 0)) entries.append(("TRAILER!!!", 0, b"", 0, 0)) data = gzip.compress(archive(entries), mtime=0) - if len(data) > 0x200000: - raise SystemExit(f"initramfs is {len(data)} bytes; the staging layout allows 2 MiB") + if len(data) > a.max_size: + raise SystemExit(f"initramfs is {len(data)} bytes; the staging layout " + f"allows {a.max_size}") a.output.parent.mkdir(parents=True, exist_ok=True) a.output.write_bytes(data) print(f"{a.output}: {len(data)} bytes") diff --git a/initramfs/init.sh b/initramfs/init.sh index 8471c22..fb557ff 100755 --- a/initramfs/init.sh +++ b/initramfs/init.sh @@ -1,7 +1,10 @@ #!/bin/busybox sh -# RAM-only init: USB Ethernet (ECM) gadget with a BusyBox telnet shell. -# The headset is 10.42.0.2; udhcpd hands the host 10.42.0.1. -# Nothing here mounts a block device. +# Init for the kexec target: USB Ethernet (ECM) gadget with a BusyBox telnet +# shell. The headset is 10.42.0.2; udhcpd hands the host 10.42.0.1. +# +# If /etc/qkx/maps is present this also assembles a custom Android under +# /android, but never starts it: it leaves a shell so a broken boot stays +# debuggable. Run qkx-launch-android when ready. export PATH=/bin:/sbin export HOME=/root export TERM=vt100 @@ -68,8 +71,31 @@ option lease 86400 EOC touch /tmp/udhcpd.leases udhcpd /tmp/udhcpd.conf -telnetd -l /bin/sh -p 23 +# Respawn telnetd: the new OS shares our process table and has killed it before. +(while :; do telnetd -F -l /bin/sh -p 23; log "telnetd exited ($?); restarting"; sleep 1; done) & +echo $! > /tmp/telnet.pid bootdone d + +# Keep a full kernel log on hand: the shell arrives long after early boot, and +# BusyBox dmesg cannot follow the log by itself. +qkx-klogwatch -f /tmp/kernel.log >/dev/null 2>&1 & + +if [ -d /etc/qkx/maps ]; then + log 'assembling custom OS' + # Keep the transcript: the shell only arrives after this has run. Avoid a + # pipeline so the exit status is qkx-mount-os's own. + qkx-mount-os >/tmp/mount-os.log 2>&1 + status=$? + cat /tmp/mount-os.log + if [ $status -eq 0 ]; then + log 'custom OS mounted at /android; run qkx-launch-android to start it' + else + log "qkx-mount-os failed (status $status); see /tmp/mount-os.log" + fi +fi + log "ready: telnet 10.42.0.2" -while :; do sleep 3600; done +# Only PID 1 may switch_root, so qkx-launch-android asks this loop to do it. +while [ ! -e /tmp/qkx-launch ]; do sleep 1; done +exec qkx-switch diff --git a/initramfs/os/qkx-apply-overrides b/initramfs/os/qkx-apply-overrides new file mode 100755 index 0000000..14ae9ec --- /dev/null +++ b/initramfs/os/qkx-apply-overrides @@ -0,0 +1,28 @@ +#!/bin/busybox sh +# Bind-mount edited copies of files from the read-only OS images. +# +# Meta's init scripts live on images we mount read-only, so edits go into +# /data/qkx/overrides mirroring the absolute path, e.g. +# /data/qkx/overrides/vendor/etc/init/hw/init.seacliff.rc +# replaces /vendor/etc/init/hw/init.seacliff.rc for the next launch. +# +# Use qkx-edit to seed a file from the original. +set -e + +ROOT=${1:-/android} +OVR="$ROOT/data/qkx/overrides" + +log() { echo "qkx-override: $*"; } + +[ -d "$OVR" ] || exit 0 + +find "$OVR" -type f | while read -r src; do + rel=${src#"$OVR"} + dst="$ROOT$rel" + if [ ! -e "$dst" ]; then + log "skipping $rel (no such file in the OS)" + continue + fi + mount --bind "$src" "$dst" + log "applied $rel" +done diff --git a/initramfs/os/qkx-edit b/initramfs/os/qkx-edit new file mode 100755 index 0000000..dbac671 --- /dev/null +++ b/initramfs/os/qkx-edit @@ -0,0 +1,30 @@ +#!/bin/busybox sh +# Seed an override from the OS image so it can be edited, e.g. +# qkx-edit /vendor/etc/init/hw/init.seacliff.rc +# vi /android/data/qkx/overrides/vendor/etc/init/hw/init.seacliff.rc +# +# Overrides live on the writable data image, so they survive reboots and are +# applied by qkx-apply-overrides on the next mount. +set -e + +ROOT=/android +REL=${1:?usage: qkx-edit } +case "$REL" in +/*) ;; +*) echo "qkx-edit: path must be absolute"; exit 1 ;; +esac + +SRC="$ROOT$REL" +DST="$ROOT/data/qkx/overrides$REL" + +[ -f "$SRC" ] || { echo "qkx-edit: $REL is not a file in the mounted OS"; exit 1; } + +mkdir -p "${DST%/*}" +if [ -e "$DST" ]; then + echo "qkx-edit: $DST already exists; leaving it alone" +else + cp "$SRC" "$DST" + echo "qkx-edit: copied $REL" +fi +echo "edit: $DST" +echo "then: qkx-apply-overrides (or reboot the target) before qkx-launch-android" diff --git a/initramfs/os/qkx-klogwatch b/initramfs/os/qkx-klogwatch new file mode 100755 index 0000000..c08e67a --- /dev/null +++ b/initramfs/os/qkx-klogwatch @@ -0,0 +1,48 @@ +#!/bin/busybox sh +# Follow the kernel log, which BusyBox's dmesg cannot do (it has no -w). +# +# qkx-klogwatch follow, showing the buffered log first +# qkx-klogwatch -n follow only messages newer than this moment +# qkx-klogwatch -g PATTERN keep only matching lines +# qkx-klogwatch -f FILE also append everything to FILE +# +# /dev/kmsg yields one record per read as ",,,;" +# and keeps blocking for new records, which is what makes following possible. +CUTOFF=-1 +PATTERN= +OUTFILE= + +while [ $# -gt 0 ]; do + case $1 in + -n) + # Drop whatever is already buffered by taking the newest buffered + # record's timestamp as the cutoff. + CUTOFF=$(dmesg | sed -n 's/^\[ *\([0-9][0-9]*\.[0-9]*\)\].*/\1/p' | + tail -1) + [ -n "$CUTOFF" ] || CUTOFF=-1 + ;; + -g) PATTERN=$2; shift ;; + -f) OUTFILE=$2; shift ;; + -h|--help) sed -n '3,8p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + *) echo "qkx-klogwatch: unknown option $1" >&2; exit 1 ;; + esac + shift +done + +# cat blocks on /dev/kmsg once the buffer is drained, so this follows the log. +cat /dev/kmsg | awk -v cutoff="$CUTOFF" -v pat="$PATTERN" ' + { + sep = index($0, ";") + if (sep) { + split(substr($0, 1, sep - 1), f, ",") + stamp = f[3] / 1000000 + text = substr($0, sep + 1) + } else { + stamp = 0 + text = $0 + } + if (stamp <= cutoff) next + if (pat != "" && text !~ pat) next + printf "[%12.6f] %s\n", stamp, text + fflush() + }' | { [ -n "$OUTFILE" ] && tee -a "$OUTFILE" || cat; } diff --git a/initramfs/os/qkx-launch-android b/initramfs/os/qkx-launch-android new file mode 100755 index 0000000..b5eabe9 --- /dev/null +++ b/initramfs/os/qkx-launch-android @@ -0,0 +1,72 @@ +#!/bin/busybox sh +# Start the mounted Android in one of two ways. +# +# qkx-launch-android switch_root (default): the system image becomes +# / and Android init runs as PID 1, as on a normal +# boot. PID 1 does the switch (see qkx-switch); +# telnet and the log watcher are restarted inside +# the new root, so reconnect after a few seconds. +# +# qkx-launch-android chroot init in a chroot beside this shell. Kept for +# experiments; second stage expects to be PID 1 +# on the real root and does not get far. +# +# Android's own first stage is skipped because it would insist on AVB, logical +# partitions and Keymaster-backed encryption. Entering at selinux_setup keeps the +# rest of the normal sequence: it loads sepolicy and then execs second_stage. +set -e + +ROOT=/android +MODE=${1:-switch} + +log() { echo "qkx-launch: $*"; } + +[ -x "$ROOT/system/bin/init" ] || { log 'nothing mounted; run qkx-mount-os first'; exit 1; } + +case "$MODE" in +chroot) + # This kernel has no PID namespaces (CONFIG_PID_NS off, as on stock + # Android), so init runs as an ordinary process beside our shell. + log 'starting init in a chroot; output in /tmp/android-init.log' + setsid chroot "$ROOT" /system/bin/init selinux_setup \ + /tmp/android-init.log 2>&1 & + log "init pid $!" + ;; +switch) + # The loader cleanly shuts down stock ADSP before kexec. Acquire it again + # through the normal subsystem/PAS API before Android class-main starts; + # otherwise adsprpcd starts with no RPMSG transport and never triggers PIL. + if [ -f /bin/qkx-adsp-hold.ko ] && ! grep -q '^adsp_hold ' /proc/modules; then + # Android init normally mounts modem_a at vendor/firmware_mnt, but + # that is too late for this pre-init request. Use the pinned, verified + # copy qkx-mount-os already reconstructed at /dev/qkx/modemfw instead + # of the real partition -- modem_a itself is never opened here. + if [ -b /dev/qkx/modemfw ]; then + mkdir -p /tmp/qkx-fw + mount -o ro -t vfat /dev/qkx/modemfw /tmp/qkx-fw + echo -n /tmp/qkx-fw/image > /sys/module/firmware_class/parameters/path + log 'starting ADSP through subsystem_get' + insmod /bin/qkx-adsp-hold.ko + echo -n '' > /sys/module/firmware_class/parameters/path + umount /tmp/qkx-fw + else + log 'no pinned modemfw image (run tools/os-install-modemfw.sh); skipping early ADSP boot' + fi + fi + # Stand in for the bootloader's continuous splash: bind the panel + # pipeline and drop DRM master before Android's composer opens the card, + # so the composer becomes master and Oculus's runtime finds the panel + # bound, as on a normal boot. + if [ -x /bin/qkx-splash ] && [ -e /dev/dri/card0 ]; then + setsid /bin/qkx-splash 180 /dev/kmsg 2>&1 & + sleep 2 + fi + log 'asking PID 1 to switch root; reconnect telnet in a few seconds' + sync + touch /tmp/qkx-launch + ;; +*) + echo "usage: qkx-launch-android [chroot|switch]" + exit 1 + ;; +esac diff --git a/initramfs/os/qkx-mount-os b/initramfs/os/qkx-mount-os new file mode 100755 index 0000000..649f4af --- /dev/null +++ b/initramfs/os/qkx-mount-os @@ -0,0 +1,129 @@ +#!/bin/busybox sh +# Assemble the custom Android OS under /android, without touching the stock OS. +# +# The OS images live in pinned files on userdata, whose raw blocks we stitch back +# together with dm-linear. Device-specific partitions (persist, modem firmware, +# spunvm) are left to Android's own fstab so they come from the real hardware. +# +# /metadata is deliberately a tmpfs: the real one holds the stock OS's +# encryption keys, and the new OS must never write there. +set -e + +ROOT=/android +CFG=/etc/qkx +IMAGES="system system_ext vendor odm product data" +# modemfw is a pinned, verified copy of modem_a (holds the ADSP/CDSP firmware +# images); reconstructed here like the others but left unmounted until +# qkx-launch-android needs it, so the real modem_a partition is never touched. +[ -s "$CFG/maps/modemfw.map" ] && IMAGES="$IMAGES modemfw" + +log() { echo "qkx-mount: $*"; } + +byname() { + awk -v n="$1" '$1 == n { print $2; found = 1 } END { exit !found }' \ + "$CFG/partitions" +} + +# The partition map records Android's paths, but here only devtmpfs exists, which +# uses /dev/ and has no /dev/block hierarchy or by-name symlinks. Fall back +# to creating the node from sysfs so the name in the map is all we rely on. +resolve_dev() { + base=${1##*/} + for p in "/dev/$base" "/dev/block/$base"; do + [ -b "$p" ] && { echo "$p"; return 0; } + done + if [ -r "/sys/class/block/$base/dev" ]; then + mm=$(cat "/sys/class/block/$base/dev") + mkdir -p /dev/block + if mknod "/dev/block/$base" b "${mm%:*}" "${mm#*:}"; then + echo "/dev/block/$base" + return 0 + fi + fi + return 1 +} + +USERDATA=$(byname userdata) || { log 'no userdata in partition map'; exit 1; } +USERDATA=$(resolve_dev "$USERDATA") || { log 'cannot find the userdata device'; exit 1; } + +if [ -e "$ROOT/system/bin/init" ]; then + log 'already mounted' + exit 0 +fi + +# device-mapper's control node is not created by devtmpfs. +if [ ! -e /dev/mapper/control ]; then + mkdir -p /dev/mapper + mknod /dev/mapper/control c 10 \ + "$(awk '$2 == "device-mapper" { print $1 }' /proc/misc)" +fi + +log "userdata is $USERDATA" +mkdir -p /dev/qkx "$ROOT" + +for name in $IMAGES; do + [ -s "$CFG/maps/$name.map" ] || { log "no map for $name"; exit 1; } + if [ -e "/dev/qkx/$name" ]; then + continue + fi + set -- $(qkx_dm create "qkx-$name" "$USERDATA" "$CFG/maps/$name.map") + mknod "/dev/qkx/$name" b "$1" "$2" + log "$name: $(( $3 / 1024 / 1024 )) MiB via dm-linear" +done + +# system.img is the root filesystem; everything else mounts inside it. +log 'mounting root (system)' +mount -o ro -t ext4 /dev/qkx/system "$ROOT" +[ -e "$ROOT/system/bin/init" ] || { log 'system image has no /system/bin/init'; exit 1; } + +for name in system_ext vendor odm product; do + mkdir -p "$ROOT/$name" 2>/dev/null || true + log "mounting $name" + mount -o ro -t ext4 "/dev/qkx/$name" "$ROOT/$name" +done + +log 'mounting data' +mount -o rw,noatime -t ext4 /dev/qkx/data "$ROOT/data" +mkdir -p "$ROOT/data/qkx/overrides" + +# Pseudo filesystems that Android's first stage would normally have set up. +log 'mounting pseudo filesystems' +mount -t tmpfs -o mode=0755,nosuid tmpfs "$ROOT/dev" +mkdir -p "$ROOT/dev/pts" "$ROOT/dev/socket" +mount -t devpts devpts "$ROOT/dev/pts" +mount -t proc proc "$ROOT/proc" +mount -t sysfs sysfs "$ROOT/sys" +[ -d "$ROOT/sys/fs/selinux" ] && mount -t selinuxfs selinuxfs "$ROOT/sys/fs/selinux" +mount -t tmpfs -o mode=0755 tmpfs "$ROOT/metadata" +# First-stage init normally puts these writable tmpfs mounts over the read-only +# system root. We skip first stage, and second stage aborts in +# SetupMountNamespaces without them. +for d in linkerconfig apex debug_ramdisk second_stage_resources; do + [ -d "$ROOT/$d" ] && mount -t tmpfs -o mode=0755,nodev,nosuid tmpfs "$ROOT/$d" +done +mount -t tmpfs -o mode=0755,uid=0,gid=1000,nodev,noexec,nosuid tmpfs "$ROOT/mnt" +mkdir -p "$ROOT/mnt/vendor" "$ROOT/mnt/product" + +# ueventd repopulates /dev, but init and the linker need these immediately. +mknod "$ROOT/dev/null" c 1 3 && chmod 666 "$ROOT/dev/null" +mknod "$ROOT/dev/zero" c 1 5 && chmod 666 "$ROOT/dev/zero" +mknod "$ROOT/dev/random" c 1 8 && chmod 666 "$ROOT/dev/random" +mknod "$ROOT/dev/urandom" c 1 9 && chmod 666 "$ROOT/dev/urandom" +mknod "$ROOT/dev/kmsg" c 1 11 +mknod "$ROOT/dev/console" c 5 1 +mknod "$ROOT/dev/ptmx" c 5 2 && chmod 666 "$ROOT/dev/ptmx" +# Android's /dev is this tmpfs, not the initramfs devtmpfs: give it the +# device-mapper node (ueventd and apexd race for it during coldboot) and the +# image nodes the patched fstab points at. +mknod "$ROOT/dev/device-mapper" c 10 236 && chmod 600 "$ROOT/dev/device-mapper" +mkdir -p "$ROOT/dev/qkx" +for n in /dev/qkx/*; do + set -- $(stat -c '%t %T' "$n") + mknod "$ROOT$n" b $((0x$1)) $((0x$2)) +done + +qkx-patch-fstab "$ROOT" +qkx-patch-usb "$ROOT" +qkx-apply-overrides "$ROOT" + +log 'ready; run qkx-launch-android to start init' diff --git a/initramfs/os/qkx-patch-fstab b/initramfs/os/qkx-patch-fstab new file mode 100755 index 0000000..901418e --- /dev/null +++ b/initramfs/os/qkx-patch-fstab @@ -0,0 +1,42 @@ +#!/bin/busybox sh +# Rewrite the vendor fstab for a pre-mounted, unencrypted boot. +# +# The stock fstab expects logical partitions verified by AVB and an encrypted +# /data unlocked through Keymaster. We already mounted those from images, and we +# cannot reproduce the encryption, so those entries are dropped or relaxed. +# +# Device-specific entries (persist, modem firmware, spunvm, usbotg) are kept +# untouched so the new OS mounts them from the real hardware. +# +# The rewritten file is bind-mounted over the read-only original. +set -e + +ROOT=${1:-/android} +HW=$(sed -n 's/.*androidboot.hardware=\([^ ]*\).*/\1/p' /proc/cmdline) +[ -n "$HW" ] || HW=seacliff +SRC="$ROOT/vendor/etc/fstab.$HW" +NEW="/tmp/fstab.$HW" + +log() { echo "qkx-fstab: $*"; } + +[ -f "$SRC" ] || { log "no $SRC"; exit 1; } + +# Drop the entries we pre-mount, and /metadata so the new OS keeps to its tmpfs +# instead of writing to the stock OS's key storage. +awk ' + $2 == "/system" || $2 == "/system_ext" || $2 == "/vendor" || + $2 == "/odm" || $2 == "/product" || $2 == "/metadata" { + print "# qkx: pre-mounted or withheld -- " $0 + next + } + $2 == "/data" { + print "# qkx: replaced -- " $0 + print "/dev/qkx/data /data ext4 noatime,nosuid,nodev wait,nofail" + next + } + { print } +' "$SRC" > "$NEW" + +mount --bind "$NEW" "$SRC" +log "patched $SRC" +grep -E '^(/dev|[a-z])' "$SRC" | awk '{ printf " %-22s %s\n", $2, $1 }' diff --git a/initramfs/os/qkx-patch-usb b/initramfs/os/qkx-patch-usb new file mode 100755 index 0000000..d2ffc08 --- /dev/null +++ b/initramfs/os/qkx-patch-usb @@ -0,0 +1,66 @@ +#!/bin/busybox sh +# Keep the new OS away from the real USB gadget. +# +# Android configures USB through configfs from its rc files, adbd property +# triggers and the USB HAL binary; binding its gadget replaced our ECM gadget and +# cut off the debug shell. Rather than chase every writer, the new OS gets a +# plain tmpfs at /config: its gadget writes succeed there but reach no hardware, +# and our qkx gadget in the real configfs stays bound. +# +# Rewritten rc files are bind-mounted over the read-only originals. +set -e + +ROOT=${1:-/android} +log() { echo "qkx-usb: $*"; } + +n=0 +for rc in "$ROOT"/system/etc/init/hw/*.rc "$ROOT"/vendor/etc/init/hw/*.rc \ + "$ROOT"/vendor/etc/init/*.rc "$ROOT"/odm/etc/init/*.rc; do + [ -f "$rc" ] || continue + grep -q 'mount configfs' "$rc" || continue + new=/tmp/rc.$n.${rc##*/} + sed 's/^\([[:space:]]*\)\(mount configfs .*\)$/\1# qkx: tmpfs instead -- \2/' \ + "$rc" > "$new" + mount --bind "$new" "$rc" + log "configfs mount disabled in ${rc#$ROOT}" + n=$((n + 1)) +done +[ $n -gt 0 ] || log 'warning: no configfs mount found in the rc files' + +mountpoint -q "$ROOT/config" || mount -t tmpfs -o mode=0755,nodev,noexec,nosuid qkxcfg "$ROOT/config" +mkdir -p "$ROOT/config/usb_gadget" "$ROOT/config/sdcardfs" +log "sandbox configfs on ${ROOT}/config" + +# Suspects for the power/USB cutoff seen after launch: keep them from starting +# by hiding their rc files. Override with QKX_KEEP_RC=1 on the kernel cmdline. +if ! grep -q 'QKX_KEEP_RC=1' /proc/cmdline; then + : > /tmp/empty.rc + QKX_EXTRA_HIDE=$(sed -n "s/.*qkx_hide=\([^ ]*\).*/\1/p" /proc/cmdline | tr , " ") + for rc in vendor/etc/init/hvdcp_opti.rc odm/etc/init/charging_accessory_service.rc \ + odm/etc/init/android.hardware.usb-service.rc \ + system_ext/etc/init/init.trackingmisc.rc \ + system_ext/etc/init/init.vision.rc \ + $QKX_EXTRA_HIDE; do + [ -f "$ROOT/$rc" ] || continue + mount --bind /tmp/empty.rc "$ROOT/$rc" + log "disabled /$rc" + done +fi + +# Android-managed fallback logger. The executable is staged in /dev/.qkx by +# qkx-switch before second-stage init starts. +cat > /tmp/qkx-log.rc <<'EOF' +service qkx_klog /dev/.qkx/bin/qkx-logrun + class core + user root + group root + disabled + +on post-fs-data + start qkx_klog +EOF +if [ -f "$ROOT/system/etc/init/dmesgd.rc" ]; then + mount --bind /tmp/qkx-log.rc "$ROOT/system/etc/init/dmesgd.rc" + log 'installed Android-managed persistent logger' +fi + diff --git a/initramfs/os/qkx-splash b/initramfs/os/qkx-splash new file mode 100755 index 0000000..44eb2d3 Binary files /dev/null and b/initramfs/os/qkx-splash differ diff --git a/initramfs/os/qkx-switch b/initramfs/os/qkx-switch new file mode 100755 index 0000000..4c0705a --- /dev/null +++ b/initramfs/os/qkx-switch @@ -0,0 +1,114 @@ +#!/bin/busybox sh +# Run by PID 1 (init.sh) when qkx-launch-android asks for it: make the system +# image the real root and hand PID 1 to Android's init, as on a normal boot. +# +# The initramfs is emptied by switch_root, so the debug services are restarted +# from a BusyBox copy on the new root's /dev tmpfs (second-stage init leaves /dev +# mounted) and chrooted there, which keeps them working after the root moves. +ROOT=/android +Q=/dev/.qkx + +log() { echo "qkx-switch: $*"; } + +mkdir -p "$ROOT$Q/bin" +cp /bin/busybox "$ROOT$Q/busybox" +chroot "$ROOT" $Q/busybox --install -s $Q/bin +cp /bin/qkx-klogwatch "$ROOT$Q/bin/" +cat > "$ROOT$Q/bin/qkx-logrun" <<'EOF' +#!/dev/.qkx/busybox sh +mkdir -p /data/qkx +# /dev/kmsg readers begin at the oldest record. Drop the already captured boot +# backlog so a slow single-CPU run records the impending failure, not 2 MiB of +# historical messages. The independent retained-RAM console is unaffected. +/dev/.qkx/busybox dmesg -c >/dev/null +: > /data/qkx/kernel-current.raw +echo "qkx logger started $(date)" >> /data/qkx/kernel-current.raw +/dev/.qkx/busybox cat /dev/kmsg >> /data/qkx/kernel-current.raw & +while :; do + /dev/.qkx/busybox sync + /dev/.qkx/busybox sleep 2 +done +EOF +cat > "$ROOT$Q/bin/qkx-waitlog" <<'EOF' +#!/dev/.qkx/busybox sh +# Started after the staging mount is gone; wait for Android's mount_all. +while ! awk '$2 == "/data" { found=1 } END { exit !found }' /proc/mounts; do + /dev/.qkx/busybox sleep 1 +done +exec /dev/.qkx/bin/qkx-logrun +EOF +chmod 755 "$ROOT$Q/bin/qkx-logrun" "$ROOT$Q/bin/qkx-waitlog" +# Command helpers for the new root, e.g. returning to stock Android. +cat > "$ROOT$Q/profile" </dev/null +pkill -f qkx-klogwatch 2>/dev/null +pkill telnetd 2>/dev/null +sleep 1 +chroot "$ROOT" $Q/busybox env PATH=$Q/bin ENV=$Q/profile \ + $Q/busybox sh -c "while :; do telnetd -F -l $Q/bin/sh -p 23; sleep 1; done" \ + /dev/null 2>&1 & +chroot "$ROOT" $Q/busybox env PATH=$Q/bin \ + $Q/bin/qkx-klogwatch -f $Q/kernel.log /dev/null 2>&1 & +# Stream /dev/kmsg to the host over UDP (tools/klog-udp.py). Needs neither +# /data nor a live TCP session, so it covers the window before Android's disk +# logger starts. A restarted reader replays the backlog; the host dedups by +# sequence number. +chroot "$ROOT" $Q/busybox sh -c \ + "while :; do $Q/busybox cat /dev/kmsg | $Q/busybox nc -u 10.42.0.1 5140; $Q/busybox sleep 1; done" \ + /dev/null 2>&1 & +# netd replaces the routing policy with fwmark rules that send everything not +# on an Android-managed network to an unreachable table, which silently cuts +# the rescue ECM link about 70 ms after it starts. Keep a higher-priority rule +# for the rescue subnet; netd may flush it, so re-add it every second. +cat > "$ROOT$Q/bin/qkx-keepnet" <<'EOF' +#!/dev/.qkx/busybox sh +# Re-assert every piece of the rescue usb0 path each second: Android's netd +# may take the link down, drop the address, reorder policy rules or add +# firewall chains. Each step is idempotent. Log the first time each repair is +# needed so the persistent log shows what Android changed. +B=/dev/.qkx/busybox +IPT=/system/bin/iptables +note() { grep -q "$1" /dev/.qkx/keepnet.seen 2>/dev/null || + { echo "$1" >> /dev/.qkx/keepnet.seen; echo "qkx-keepnet: repaired $1" > /dev/kmsg; }; } +while :; do + $B ip link show usb0 | $B grep -q ',UP' || { $B ip link set usb0 up; note link; } + $B ip -4 addr show usb0 | $B grep -q 10.42.0.2 || + { $B ip addr add 10.42.0.2/24 dev usb0; note addr; } + $B ip route show table main | $B grep -q '10.42.0.0/24' || + { $B ip route add 10.42.0.0/24 dev usb0 table main; note route; } + for r in 'from 10.42.0.0/24' 'to 10.42.0.0/24'; do + $B ip rule show | $B grep -q "$r lookup main" || + { $B ip rule add $r lookup main pref 50; note "rule $r"; } + done + if [ -x $IPT ]; then + $IPT -w -C INPUT -i usb0 -j ACCEPT 2>/dev/null || + { $IPT -w -I INPUT 1 -i usb0 -j ACCEPT 2>/dev/null && note ipt-in; } + $IPT -w -C OUTPUT -o usb0 -j ACCEPT 2>/dev/null || + { $IPT -w -I OUTPUT 1 -o usb0 -j ACCEPT 2>/dev/null && note ipt-out; } + fi + $B sleep 1 +done +EOF +chmod 755 "$ROOT$Q/bin/qkx-keepnet" +chroot "$ROOT" $Q/bin/qkx-keepnet /dev/null 2>&1 & +# qkx-mount-os mounts data so the rescue shell can inspect it. Android's +# mount_all must mount it itself: leaving it mounted makes fs_mgr run e2fsck on +# a live filesystem and then continue after EBUSY. The retained kernel ring is +# used for post-mortem logging during system-as-root runs. +sync +if ! umount "$ROOT/data"; then + log 'refusing switch: could not unmount staged data image' + exit 1 +fi +# Android starts qkx-logrun from the generated post-fs-data rc. +# udhcpd keeps running from the old root; it only needs its open files. + +log 'switching root to the system image; Android init takes PID 1' +cd / +exec switch_root "$ROOT" /system/bin/init selinux_setup diff --git a/module/ion_secmap.c b/module/ion_secmap.c new file mode 100644 index 0000000..d3f224d --- /dev/null +++ b/module/ion_secmap.c @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Read-only list of the physical ranges stock ION has handed to secure VMs: + * live buffers carrying ION_FLAG_SECURE or a CP flag, and the pages parked in + * the system heap's secure pools. Those pages stay hyp-assigned across kexec, + * and touching one from the next kernel hangs the CPU. + */ +#include +#include +#include +#include +#include +#include +#include +#include "../../oculus-linux-kernel-android/drivers/staging/android/ion/ion.h" +#include "../../oculus-linux-kernel-android/drivers/staging/android/ion/ion_system_heap.h" +#include +#include "kgsl_device.h" + +/* Neither is exported; resolved at load time through a kprobe address. */ +static struct kgsl_driver *kgsl_drv; + +static unsigned long lookup(const char *name) +{ + static unsigned long (*kln)(const char *); + + if (!kln) { + struct kprobe kp = { .symbol_name = "kallsyms_lookup_name" }; + + if (register_kprobe(&kp)) + return 0; + kln = (void *)kp.addr; + unregister_kprobe(&kp); + } + return kln(name); +} + +#define SECURE_FLAGS (ION_FLAG_SECURE | GENMASK(30, 17)) + +static phys_addr_t run_start, run_end; +static unsigned long total; + +static void emit(phys_addr_t start, size_t len) +{ + total += len; + if (run_end == start) { + run_end += len; + return; + } + if (run_end) + pr_info("ionsec: %pa-%pa\n", &run_start, &run_end); + run_start = start; + run_end = start + len; +} + +static void dump_pool(struct ion_page_pool *pool) +{ + struct page *page; + + mutex_lock(&pool->mutex); + list_for_each_entry(page, &pool->high_items, lru) + emit(page_to_phys(page), PAGE_SIZE << pool->order); + list_for_each_entry(page, &pool->low_items, lru) + emit(page_to_phys(page), PAGE_SIZE << pool->order); + mutex_unlock(&pool->mutex); +} + +static void dump_buffer(struct ion_buffer *buf, const char *what) +{ + struct scatterlist *sg; + int i; + + if (!(buf->flags & SECURE_FLAGS) || !buf->sg_table) + return; + pr_info("ionsec: %s heap=%s flags=%#lx size=%zu\n", what, + buf->heap->name, buf->flags, buf->size); + for_each_sg(buf->sg_table->sgl, sg, buf->sg_table->nents, i) + emit(sg_phys(sg), sg->length); +} + +static unsigned long sg_total(struct sg_table *sgt) +{ + struct scatterlist *sg; + unsigned long n = 0; + int i; + + for_each_sg(sgt->sgl, sg, sgt->nents, i) { + emit(sg_phys(sg), sg->length); + n += sg->length; + } + return n; +} + +/* The GPU driver locks its secure buffers to VMID_CP_PIXEL on its own. */ +static void dump_kgsl(void) +{ + struct kgsl_process_private *private; + struct kgsl_global_memdesc *md; + struct kgsl_device *device; + struct kgsl_mem_entry *entry; + struct page **guard; + unsigned long found = 0; + int id; + + kgsl_drv = (struct kgsl_driver *)lookup("kgsl_driver"); + if (!kgsl_drv) { + pr_info("ionsec: kgsl_driver not found\n"); + return; + } + device = kgsl_drv->devp[0]; + guard = (struct page **)lookup("kgsl_secure_guard_page"); + if (guard && *guard) { + emit(page_to_phys(*guard), PAGE_SIZE); + found += PAGE_SIZE; + } + if (device) + list_for_each_entry(md, &device->globals, node) + if ((md->memdesc.flags & KGSL_MEMFLAGS_SECURE) && + md->memdesc.sgt) + found += sg_total(md->memdesc.sgt); + + spin_lock(&kgsl_drv->proclist_lock); + list_for_each_entry(private, &kgsl_drv->process_list, list) { + spin_lock(&private->mem_lock); + idr_for_each_entry(&private->mem_idr, entry, id) + if ((entry->memdesc.flags & KGSL_MEMFLAGS_SECURE) && + entry->memdesc.sgt) + found += sg_total(entry->memdesc.sgt); + spin_unlock(&private->mem_lock); + } + spin_unlock(&kgsl_drv->proclist_lock); + + pr_info("ionsec: kgsl secure found %lu KiB, driver counts %ld KiB\n", + found >> 10, atomic_long_read(&kgsl_drv->stats.secure) >> 10); +} + +static int __init ion_secmap_init(void) +{ + struct ion_device *idev; + struct ion_heap *heap; + struct rb_node *n; + struct file *f; + int vmid, i; + + f = filp_open("/dev/ion", O_RDONLY, 0); + if (IS_ERR(f)) + return PTR_ERR(f); + idev = container_of((struct miscdevice *)f->private_data, + struct ion_device, dev); + pr_info("ionsec: begin\n"); + + mutex_lock(&idev->buffer_lock); + for (n = rb_first(&idev->buffers); n; n = rb_next(n)) + dump_buffer(rb_entry(n, struct ion_buffer, node), "buffer"); + mutex_unlock(&idev->buffer_lock); + + down_read(&idev->lock); + plist_for_each_entry(heap, &idev->heaps, node) { + struct ion_system_heap *sys; + struct ion_buffer *buf; + + /* Freed buffers wait here, still assigned, for the free thread. */ + if (heap->flags & ION_HEAP_FLAG_DEFER_FREE) { + spin_lock(&heap->free_lock); + list_for_each_entry(buf, &heap->free_list, list) + dump_buffer(buf, "deferred"); + spin_unlock(&heap->free_lock); + } + if (heap->type != ION_HEAP_TYPE_SYSTEM) + continue; + sys = container_of(heap, struct ion_system_heap, heap); + for (vmid = 0; vmid < VMID_LAST; vmid++) + for (i = 0; i < MAX_ORDER; i++) + if (sys->secure_pools[vmid][i]) + dump_pool(sys->secure_pools[vmid][i]); + } + up_read(&idev->lock); + + dump_kgsl(); + emit(0, 0); + pr_info("ionsec: total %lu KiB\n", total >> 10); + filp_close(f, NULL); + return -EAGAIN; +} +module_init(ion_secmap_init); +MODULE_LICENSE("GPL"); diff --git a/module/ion_secmap.ko.off b/module/ion_secmap.ko.off new file mode 100644 index 0000000..bbf0492 Binary files /dev/null and b/module/ion_secmap.ko.off differ diff --git a/module/marker_read.c b/module/marker_read.c index 57d0f7c..631cd8a 100644 --- a/module/marker_read.c +++ b/module/marker_read.c @@ -10,6 +10,72 @@ #define QKX_LOG_SIZE 0x10000 #define QKX_LOG_MAGIC 0x514b584c #define QKX_LOG_PAGES (QKX_LOG_SIZE / PAGE_SIZE) +/* Must match include/linux/qkx_diag.h in the diagnostic kernel. */ +#define QKX_CTXAREA_SIZE 0x3000 +#define QKX_CTXAREA_MAGIC 0x514b5843 + +struct qkx_ctx { + u64 fn; + s32 irq; + char what[48]; + u64 since; + u64 arg0, arg1; +}; + +struct qkx_ctxarea { + u32 magic, nr_cpus, nr_kinds, rec_size; + u64 text, last_pet; + struct qkx_ctx rec[]; +}; + +/* Print per-CPU "inside" records; fn is printed relative to runtime _text. */ +static void qkx_dump_ctxarea(void *base) +{ + static const char * const kinds[] = { "hardirq", "sysfs-attr(unused)", "scm", "clk", "gdsc", "spmi", "cache", "cpu-pm", "irq-mask", "ufs-mmio" }; + struct qkx_ctxarea *a = base + QKX_LOG_SIZE - QKX_CTXAREA_SIZE; + u32 cpu, k, n = 0; + + if (a->magic != QKX_CTXAREA_MAGIC || a->rec_size != sizeof(struct qkx_ctx) || + a->nr_cpus > 64 || a->nr_kinds > 16 || + sizeof(*a) + a->nr_cpus * a->nr_kinds * sizeof(struct qkx_ctx) > QKX_CTXAREA_SIZE) { + pr_emerg("qkxctx: no context area (magic=%08x rec=%u)\n", a->magic, a->rec_size); + return; + } + pr_emerg("qkxctx: text=%llx last_pet=%llu.%09llu\n", a->text, + a->last_pet / 1000000000ULL, a->last_pet % 1000000000ULL); + for (cpu = 0; cpu < a->nr_cpus; cpu++) + for (k = 0; k < a->nr_kinds; k++) { + struct qkx_ctx *c = &a->rec[cpu * a->nr_kinds + k]; + + if (!c->fn) + continue; + c->what[sizeof(c->what) - 1] = 0; + pr_emerg("qkxctx: cpu%u %s since=%llu.%09llu irq/arg=%d what=%s fn=_text+%llx arg=%llx/%llx\n", + cpu, k < ARRAY_SIZE(kinds) ? kinds[k] : "?", + c->since / 1000000000ULL, c->since % 1000000000ULL, + c->irq, c->what, c->fn - a->text, c->arg0, c->arg1); + n++; + } + pr_emerg("qkxctx: %u active records\n", n); + for (cpu = 0; cpu < 8; cpu++) { + u64 *h = (void *)a + 0x2800 + cpu * 22 * sizeof(u64); + u64 cnt = h[0]; + + if (!cnt) + continue; + /* oldest to newest; bit0 = user mode */ + for (k = 0; k < 7 && k < cnt; k++) { + u64 slot = 1 + ((cnt - k) % 7) * 3; + u64 pc = h[slot], daif = h[slot + 1], gic = h[slot + 2]; + + pr_emerg("qkxpc: cpu%u tick=%llu %s %s%llx daif=%llx pmr=%x grp=%x\n", + cpu, cnt - k, (pc & 1) ? "user" : "kern", + (pc & 1) ? "" : "_text+", + (pc & 1) ? pc & ~1ULL : pc - a->text, daif, + (u32)gic, (u32)(gic >> 32)); + } + } +} struct qkx_log_header { u32 magic; @@ -30,7 +96,7 @@ static int __init qkx_marker_read_init(void) struct qkx_log_header *h; char *ordered; void *base; - u32 capacity = QKX_LOG_SIZE - sizeof(*h); + u32 capacity = QKX_LOG_SIZE - QKX_CTXAREA_SIZE - sizeof(*h); u32 pos, wraps, used, first, out = 0; unsigned int i; @@ -80,6 +146,7 @@ static int __init qkx_marker_read_init(void) memcpy(ordered + tail, base + sizeof(*h), used - tail); } ordered[used] = '\0'; + qkx_dump_ctxarea(base); vunmap(base); pr_emerg("qkx_marker_read: retained log bytes=%u wraps=%u pos=%u\n", diff --git a/module/qkx_giveback.c b/module/qkx_giveback.c new file mode 100644 index 0000000..f72c050 --- /dev/null +++ b/module/qkx_giveback.c @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Last step before kexec: hand every secure ION/kgsl page the stock kernel + * still has assigned to another VM back to HLOS, using the stock kernel's own + * unassign routines (the same calls it makes when freeing those buffers). + * The stock kernel's bookkeeping is left stale on purpose; it is about to be + * replaced. Pages the hypervisor refuses to return are logged so they can + * still be reserved. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../../oculus-linux-kernel-android/drivers/staging/android/ion/ion.h" +#include "../../oculus-linux-kernel-android/drivers/staging/android/ion/ion_system_heap.h" +#include "kgsl_device.h" + +#define SECURE_FLAGS (ION_FLAG_SECURE | GENMASK(30, 17)) + +static int (*unassign_flags)(struct sg_table *sgt, unsigned long flags, + bool set_page_private); +static int (*assign_table)(struct sg_table *table, u32 *source_vm_list, + int source_nelems, int *dest_vmids, + int *dest_perms, int dest_nelems); +static unsigned long ok_bytes, failed_bytes; + +static unsigned long lookup(const char *name) +{ + static unsigned long (*kln)(const char *); + + if (!kln) { + struct kprobe kp = { .symbol_name = "kallsyms_lookup_name" }; + + if (register_kprobe(&kp)) + return 0; + kln = (void *)kp.addr; + unregister_kprobe(&kp); + } + return kln(name); +} + +static unsigned long sgt_bytes(struct sg_table *sgt) +{ + struct scatterlist *sg; + unsigned long n = 0; + int i; + + for_each_sg(sgt->sgl, sg, sgt->nents, i) + n += sg->length; + return n; +} + +static void account(struct sg_table *sgt, int ret, const char *what) +{ + struct scatterlist *sg; + int i; + + if (!ret) { + ok_bytes += sgt_bytes(sgt); + return; + } + failed_bytes += sgt_bytes(sgt); + pr_info("giveback: %s failed %d\n", what, ret); + for_each_sg(sgt->sgl, sg, sgt->nents, i) + pr_info("giveback: kept 0x%llx-0x%llx\n", (u64)sg_phys(sg), + (u64)sg_phys(sg) + sg->length); +} + +/* Return one sg_table from @source to HLOS with full permissions. */ +static int to_hlos(struct sg_table *sgt, u32 source) +{ + int dest = VMID_HLOS, perm = PERM_READ | PERM_WRITE | PERM_EXEC; + int ret; + + do { + ret = assign_table(sgt, &source, 1, &dest, &perm, 1); + } while (ret == -EAGAIN); + return ret; +} + +static void page_to_hlos(struct page *page, size_t len, u32 source) +{ + struct scatterlist sgl; + struct sg_table sgt = { .sgl = &sgl, .nents = 1, .orig_nents = 1 }; + + sg_init_table(&sgl, 1); + sg_set_page(&sgl, page, len, 0); + account(&sgt, to_hlos(&sgt, source), "page"); +} + +static void ion_giveback(void) +{ + struct ion_device *idev; + struct ion_heap *heap; + struct rb_node *n; + struct file *f; + int vmid, i; + + f = filp_open("/dev/ion", O_RDONLY, 0); + if (IS_ERR(f)) { + pr_info("giveback: no /dev/ion\n"); + return; + } + idev = container_of((struct miscdevice *)f->private_data, + struct ion_device, dev); + + mutex_lock(&idev->buffer_lock); + for (n = rb_first(&idev->buffers); n; n = rb_next(n)) { + struct ion_buffer *buf = rb_entry(n, struct ion_buffer, node); + + if ((buf->flags & SECURE_FLAGS) && buf->sg_table) + account(buf->sg_table, + unassign_flags(buf->sg_table, buf->flags, false), + buf->heap->name); + } + mutex_unlock(&idev->buffer_lock); + + down_read(&idev->lock); + plist_for_each_entry(heap, &idev->heaps, node) { + struct ion_system_heap *sys; + struct ion_buffer *buf; + + if (heap->flags & ION_HEAP_FLAG_DEFER_FREE) { + spin_lock(&heap->free_lock); + list_for_each_entry(buf, &heap->free_list, list) + if ((buf->flags & SECURE_FLAGS) && buf->sg_table) + account(buf->sg_table, + unassign_flags(buf->sg_table, + buf->flags, false), + "deferred"); + spin_unlock(&heap->free_lock); + } + if (heap->type != ION_HEAP_TYPE_SYSTEM) + continue; + sys = container_of(heap, struct ion_system_heap, heap); + /* Pool pages sit assigned to the VM the pool is named after. */ + for (vmid = 0; vmid < VMID_LAST; vmid++) + for (i = 0; i < MAX_ORDER; i++) { + struct ion_page_pool *pool = + sys->secure_pools[vmid][i]; + struct page *page; + + if (!pool) + continue; + mutex_lock(&pool->mutex); + list_for_each_entry(page, &pool->high_items, lru) + page_to_hlos(page, + PAGE_SIZE << pool->order, vmid); + list_for_each_entry(page, &pool->low_items, lru) + page_to_hlos(page, + PAGE_SIZE << pool->order, vmid); + mutex_unlock(&pool->mutex); + } + } + up_read(&idev->lock); + filp_close(f, NULL); +} + +/* kgsl locks its secure memory to VMID_CP_PIXEL alone (kgsl_sharedmem.c). */ +static void kgsl_giveback(void) +{ + struct kgsl_driver *drv = (void *)lookup("kgsl_driver"); + struct page **guard = (void *)lookup("kgsl_secure_guard_page"); + struct kgsl_process_private *private; + struct kgsl_global_memdesc *md; + struct kgsl_mem_entry *entry; + int id; + + if (!drv) { + pr_info("giveback: no kgsl_driver\n"); + return; + } + if (guard && *guard) + page_to_hlos(*guard, PAGE_SIZE, VMID_CP_PIXEL); + if (drv->devp[0]) + list_for_each_entry(md, &drv->devp[0]->globals, node) + if ((md->memdesc.flags & KGSL_MEMFLAGS_SECURE) && + md->memdesc.sgt) + account(md->memdesc.sgt, + to_hlos(md->memdesc.sgt, VMID_CP_PIXEL), + "kgsl global"); + + /* hyp_assign sleeps, so collect under the spinlocks is not possible; + * userspace is stopped, so the lists no longer change. */ + list_for_each_entry(private, &drv->process_list, list) + idr_for_each_entry(&private->mem_idr, entry, id) + if ((entry->memdesc.flags & KGSL_MEMFLAGS_SECURE) && + entry->memdesc.sgt) + account(entry->memdesc.sgt, + to_hlos(entry->memdesc.sgt, + VMID_CP_PIXEL), + "kgsl entry"); +} + +static int __init giveback_init(void) +{ + unassign_flags = (void *)lookup("ion_hyp_unassign_sg_from_flags"); + assign_table = (void *)lookup("hyp_assign_table"); + if (!unassign_flags || !assign_table) + return -ENOENT; + + pr_info("giveback: begin\n"); + ion_giveback(); + kgsl_giveback(); + pr_info("giveback: returned %lu KiB, kept %lu KiB\n", + ok_bytes >> 10, failed_bytes >> 10); + return -EAGAIN; +} +module_init(giveback_init); +MODULE_LICENSE("GPL"); diff --git a/module/quest_kexec.ko.pre-adsp b/module/quest_kexec.ko.pre-adsp new file mode 100644 index 0000000..245e242 Binary files /dev/null and b/module/quest_kexec.ko.pre-adsp differ diff --git a/tools/device/Makefile b/tools/device/Makefile new file mode 100644 index 0000000..bcc6544 --- /dev/null +++ b/tools/device/Makefile @@ -0,0 +1,19 @@ +# Static ARM64 helpers: qkx_fsmap and qkx_rawcp run under Android, qkx_dm runs +# in the kexec target's initramfs. +CROSS_COMPILE ?= aarch64-linux-gnu- +CC := $(CROSS_COMPILE)gcc +STRIP := $(CROSS_COMPILE)strip +CFLAGS ?= -static -O2 -Wall -Wextra -Wno-sign-compare + +HELPERS := qkx_fsmap qkx_rawcp qkx_dm + +all: $(HELPERS) + +$(HELPERS): %: %.c + $(CC) $(CFLAGS) $< -o $@ + $(STRIP) $@ + +clean: + rm -f $(HELPERS) + +.PHONY: all clean diff --git a/tools/device/ion_secmap.c b/tools/device/ion_secmap.c new file mode 100644 index 0000000..c364897 --- /dev/null +++ b/tools/device/ion_secmap.c @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Read-only list of the physical ranges stock ION has handed to secure VMs: + * live buffers carrying ION_FLAG_SECURE or a CP flag, and the pages parked in + * the system heap's secure pools. Those pages stay hyp-assigned across kexec, + * and touching one from the next kernel hangs the CPU. + */ +#include +#include +#include +#include +#include +#include +#include +#include "../../oculus-linux-kernel-android/drivers/staging/android/ion/ion.h" +#include "../../oculus-linux-kernel-android/drivers/staging/android/ion/ion_system_heap.h" + +#define SECURE_FLAGS (ION_FLAG_SECURE | GENMASK(30, 17)) + +static phys_addr_t run_start, run_end; +static unsigned long total; + +static void emit(phys_addr_t start, size_t len) +{ + total += len; + if (run_end == start) { + run_end += len; + return; + } + if (run_end) + pr_info("ionsec: %pa-%pa\n", &run_start, &run_end); + run_start = start; + run_end = start + len; +} + +static void dump_pool(struct ion_page_pool *pool) +{ + struct page *page; + + mutex_lock(&pool->mutex); + list_for_each_entry(page, &pool->high_items, lru) + emit(page_to_phys(page), PAGE_SIZE << pool->order); + list_for_each_entry(page, &pool->low_items, lru) + emit(page_to_phys(page), PAGE_SIZE << pool->order); + mutex_unlock(&pool->mutex); +} + +static int __init ion_secmap_init(void) +{ + struct ion_device *idev; + struct ion_heap *heap; + struct rb_node *n; + struct file *f; + int vmid, i; + + f = filp_open("/dev/ion", O_RDONLY, 0); + if (IS_ERR(f)) + return PTR_ERR(f); + idev = container_of((struct miscdevice *)f->private_data, + struct ion_device, dev); + + mutex_lock(&idev->buffer_lock); + for (n = rb_first(&idev->buffers); n; n = rb_next(n)) { + struct ion_buffer *buf = rb_entry(n, struct ion_buffer, node); + struct scatterlist *sg; + + if (!(buf->flags & SECURE_FLAGS) || !buf->sg_table) + continue; + pr_info("ionsec: buffer heap=%s flags=%#lx size=%zu\n", + buf->heap->name, buf->flags, buf->size); + for_each_sg(buf->sg_table->sgl, sg, buf->sg_table->nents, i) + emit(sg_phys(sg), sg->length); + } + mutex_unlock(&idev->buffer_lock); + + down_read(&idev->lock); + plist_for_each_entry(heap, &idev->heaps, node) { + struct ion_system_heap *sys; + + if (heap->type != ION_HEAP_TYPE_SYSTEM) + continue; + sys = container_of(heap, struct ion_system_heap, heap); + for (vmid = 0; vmid < VMID_LAST; vmid++) + for (i = 0; i < MAX_ORDER; i++) + if (sys->secure_pools[vmid][i]) + dump_pool(sys->secure_pools[vmid][i]); + } + up_read(&idev->lock); + + emit(0, 0); + pr_info("ionsec: total %lu KiB\n", total >> 10); + filp_close(f, NULL); + return -EAGAIN; +} +module_init(ion_secmap_init); +MODULE_LICENSE("GPL"); diff --git a/tools/device/qkx-install.sh b/tools/device/qkx-install.sh new file mode 100755 index 0000000..2cd3f52 --- /dev/null +++ b/tools/device/qkx-install.sh @@ -0,0 +1,114 @@ +#!/system/bin/sh +# Device side of the custom-OS installer. Runs under Android as root. +# +# Images live as pinned f2fs files inside the work directory, but their contents +# are written straight to the userdata partition's blocks. Userdata is +# metadata-encrypted, so this is the only form the kexec target can read; from +# Android these files just look like garbage, which is expected. +# +# No partition is formatted and nothing outside these files is written. +set -e + +W=/data/local/tmp/qkx +BIN=$W/bin +IMG=$W/img +MAP=$W/maps +DEV=/dev/block/sda7 # userdata; verified 1:1 under dm-default-key + +usage() { + echo "usage: qkx-install.sh alloc " + echo " qkx-install.sh write " + echo " qkx-install.sh writez " + echo " qkx-install.sh verify " + echo " qkx-install.sh map " + echo " qkx-install.sh list" + echo " qkx-install.sh partitions" + exit 1 +} + +need_map() { + [ -s "$MAP/$1.map" ] || { echo "qkx-install: no map for $1"; exit 1; } +} + +cmd_alloc() { + name=$1; bytes=$2 + mkdir -p "$IMG" "$MAP" + # Reallocating would hand out different blocks, invalidating a map that a + # staged initramfs may already contain. + if [ -f "$IMG/$name.img" ]; then + echo "qkx-install: $name already allocated; remove $IMG/$name.img first" + exit 1 + fi + "$BIN/qkx_fsmap" alloc "$IMG/$name.img" "$bytes" + "$BIN/qkx_fsmap" map "$IMG/$name.img" > "$MAP/$name.map" + echo "qkx-install: $name allocated, $(wc -l < "$MAP/$name.map") extents" +} + +cmd_write() { + name=$1; srcfile=$2 + need_map "$name" + # Zero first: the blocks hold stale data, and a sparse copy would otherwise + # leave garbage wherever the image has holes. + "$BIN/qkx_rawcp" zero "$MAP/$name.map" "$DEV" + "$BIN/qkx_rawcp" copy "$MAP/$name.map" "$DEV" "$srcfile" + sync +} + +cmd_writez() { + name=$1; srcgz=$2 + need_map "$name" + "$BIN/qkx_rawcp" zero "$MAP/$name.map" "$DEV" + gzip -dc "$srcgz" | "$BIN/qkx_rawcp" copy "$MAP/$name.map" "$DEV" - + sync +} + +cmd_verify() { + name=$1; srcfile=$2 + need_map "$name" + # The map must still match the file, or we verified the wrong blocks. + "$BIN/qkx_fsmap" map "$IMG/$name.img" > "$MAP/$name.map.now" + if ! cmp "$MAP/$name.map" "$MAP/$name.map.now" >/dev/null 2>&1; then + echo "qkx-install: $name moved on disk; extents changed" + exit 1 + fi + rm -f "$MAP/$name.map.now" + case "$srcfile" in + *.gz) gzip -dc "$srcfile" | "$BIN/qkx_rawcp" verify "$MAP/$name.map" "$DEV" - ;; + *) "$BIN/qkx_rawcp" verify "$MAP/$name.map" "$DEV" "$srcfile" ;; + esac +} + +cmd_map() { + need_map "$1" + "$BIN/qkx_fsmap" map "$IMG/$1.img" +} + +cmd_partitions() { + # " " for every partition, so the kexec target can find + # them without a vendor init to create by-name symlinks. + for l in /dev/block/by-name/*; do + [ -e "$l" ] || continue + echo "${l##*/} $(readlink -f "$l")" + done +} + +cmd_list() { + for f in "$IMG"/*.img; do + [ -e "$f" ] || continue + n=${f##*/}; n=${n%.img} + echo "$n $(stat -c %s "$f") bytes, $(wc -l < "$MAP/$n.map") extents" + done +} + +[ $# -ge 1 ] || usage +op=$1; shift +case "$op" in +alloc) [ $# -eq 2 ] || usage; cmd_alloc "$@" ;; +write) [ $# -eq 2 ] || usage; cmd_write "$@" ;; +writez) [ $# -eq 2 ] || usage; cmd_writez "$@" ;; +verify) [ $# -eq 2 ] || usage; cmd_verify "$@" ;; +map) [ $# -eq 1 ] || usage; cmd_map "$@" ;; +list) cmd_list ;; +partitions) cmd_partitions ;; +*) usage ;; +esac diff --git a/tools/device/qkx-subsys-health b/tools/device/qkx-subsys-health new file mode 100755 index 0000000..f9cf0ac --- /dev/null +++ b/tools/device/qkx-subsys-health @@ -0,0 +1,39 @@ +#!/bin/sh +# Report whether the target kernel actually re-established the shared-memory +# link to the remote processors after kexec. +# +# SMEM is the table every AP<->DSP transport is built on (glink, smp2p, +# subsystem restart, socinfo). Stock prints a full socinfo line from it; if our +# kernel prints dummy values, SMEM lost its probe race and the DSPs -- which are +# still running from the stock kernel -- have no working link to the new AP. +# Android's audio/sensor/fastrpc HALs are the first things to depend on it. +echo "=== socinfo (dummy values mean SMEM was missed) ===" +dmesg | grep -iE 'socinfo_print|SMEM is not initialized|SMEM_HW_SW_BUILD_ID|by SBL' + +echo +echo "=== is the smem device bound to a driver? ===" +for d in /sys/devices/platform/soc/*smem* /sys/bus/platform/devices/*smem*; do + [ -e "$d" ] || continue + echo "$d -> driver: $(readlink -f "$d/driver" 2>/dev/null || echo NONE)" +done + +echo +echo "=== platform devices still waiting on deferred probe ===" +cat /sys/kernel/debug/devices_deferred 2>/dev/null | head -40 || + echo "(devices_deferred needs debugfs mounted)" + +echo +echo "=== rpmsg / glink endpoints (empty means no DSP link) ===" +ls /sys/bus/rpmsg/devices 2>/dev/null | head -30 || echo "(no rpmsg bus)" + +echo +echo "=== subsystem restart devices ===" +ls /dev/subsys_* 2>/dev/null || echo "(none)" +cat /sys/bus/msm_subsys/devices/*/name 2>/dev/null | head + +echo +echo "=== remote processor state ===" +for s in /sys/bus/msm_subsys/devices/subsys*/; do + [ -d "$s" ] || continue + echo "$(cat "$s/name" 2>/dev/null): restart_level=$(cat "$s/restart_level" 2>/dev/null) state=$(cat "$s/state" 2>/dev/null)" +done diff --git a/tools/device/qkx_dm b/tools/device/qkx_dm new file mode 100755 index 0000000..be6064d Binary files /dev/null and b/tools/device/qkx_dm differ diff --git a/tools/device/qkx_dm.c b/tools/device/qkx_dm.c new file mode 100644 index 0000000..186102a --- /dev/null +++ b/tools/device/qkx_dm.c @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * qkx_dm - build a dm-linear device from a pinned-file extent map. + * + * The kexec target cannot read Android's encrypted userdata filesystem, but the + * blocks of a pinned file are plain: we stitch its physical extents back into a + * contiguous block device, which then holds an image we can mount normally. + * + * qkx_dm create map file lines: " " + * qkx_dm remove + * + * create prints " " so the caller can mknod the node + * itself; devtmpfs naming for dm devices is not something to rely on here. + * + * Build: aarch64-linux-gnu-gcc -static -O2 qkx_dm.c -o qkx_dm + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DM_CONTROL "/dev/mapper/control" +#define BUF_SIZE (1 << 20) + +static char buf[BUF_SIZE]; + +static int die(const char *what) +{ + fprintf(stderr, "qkx_dm: %s: %s\n", what, strerror(errno)); + return 1; +} + +static struct dm_ioctl *init_hdr(const char *name) +{ + struct dm_ioctl *io = (struct dm_ioctl *)buf; + + memset(buf, 0, BUF_SIZE); + io->version[0] = 4; + io->version[1] = 0; + io->version[2] = 0; + io->data_size = BUF_SIZE; + io->data_start = sizeof(*io); + snprintf(io->name, sizeof(io->name), "%s", name); + return io; +} + +static int dm_open(void) +{ + int fd = open(DM_CONTROL, O_RDWR); + + if (fd < 0) { + /* device-mapper's control node is not created by devtmpfs. */ + if (errno == ENOENT) + fprintf(stderr, "qkx_dm: %s missing; create it with " + "mknod (major 10, see /proc/misc)\n", DM_CONTROL); + return -1; + } + return fd; +} + +static int do_remove(int fd, const char *name) +{ + struct dm_ioctl *io = init_hdr(name); + + if (ioctl(fd, DM_DEV_REMOVE, io)) + return die("DM_DEV_REMOVE"); + return 0; +} + +static int do_create(int fd, const char *name, const char *backing, const char *mapfile) +{ + unsigned long long off, phys, len, sectors = 0; + struct dm_ioctl *io; + struct stat st; + unsigned targets = 0; + char line[256]; + char *ptr; + FILE *f; + + if (stat(backing, &st)) + return die(backing); + + f = fopen(mapfile, "r"); + if (!f) + return die(mapfile); + + io = init_hdr(name); + if (ioctl(fd, DM_DEV_CREATE, io)) + return die("DM_DEV_CREATE"); + + io = init_hdr(name); + ptr = buf + sizeof(*io); + + while (fgets(line, sizeof(line), f)) { + struct dm_target_spec *spec; + char params[128]; + size_t plen; + + if (line[0] == '#' || line[0] == '\n') + continue; + if (sscanf(line, "%llu %llu %llu", &off, &phys, &len) != 3) { + fprintf(stderr, "qkx_dm: bad map line: %s", line); + return 1; + } + if ((off | phys | len) & 511) { + fprintf(stderr, "qkx_dm: extent not sector aligned: %s", line); + return 1; + } + if (off / 512 != sectors) { + fprintf(stderr, "qkx_dm: map is not contiguous at %llu\n", off); + return 1; + } + + spec = (struct dm_target_spec *)ptr; + spec->sector_start = sectors; + spec->length = len / 512; + spec->status = 0; + snprintf(spec->target_type, sizeof(spec->target_type), "linear"); + + snprintf(params, sizeof(params), "%u:%u %llu", + major(st.st_rdev), minor(st.st_rdev), phys / 512); + plen = strlen(params) + 1; + /* dm requires each target's parameters to be 8-byte aligned. */ + plen = (plen + 7) & ~7UL; + memcpy(ptr + sizeof(*spec), params, strlen(params) + 1); + spec->next = sizeof(*spec) + plen; + + ptr += spec->next; + sectors += len / 512; + targets++; + + if ((size_t)(ptr - buf) > BUF_SIZE - 512) { + fprintf(stderr, "qkx_dm: map too large\n"); + return 1; + } + } + fclose(f); + + if (!targets) { + fprintf(stderr, "qkx_dm: empty map\n"); + return 1; + } + + io->target_count = targets; + io->data_size = ptr - buf; + if (ioctl(fd, DM_TABLE_LOAD, io)) { + die("DM_TABLE_LOAD"); + goto fail; + } + + io = init_hdr(name); + if (ioctl(fd, DM_DEV_SUSPEND, io)) { + die("DM_DEV_SUSPEND"); + goto fail; + } + + printf("%u %u %llu\n", major(io->dev), minor(io->dev), sectors * 512); + return 0; + +fail: + do_remove(fd, name); + return 1; +} + +int main(int argc, char **argv) +{ + int fd, ret; + + fd = dm_open(); + if (fd < 0) + return 1; + + if (argc == 5 && !strcmp(argv[1], "create")) + ret = do_create(fd, argv[2], argv[3], argv[4]); + else if (argc == 3 && !strcmp(argv[1], "remove")) + ret = do_remove(fd, argv[2]); + else { + fprintf(stderr, "usage: qkx_dm create \n" + " qkx_dm remove \n"); + ret = 1; + } + close(fd); + return ret; +} diff --git a/tools/device/qkx_fsmap b/tools/device/qkx_fsmap new file mode 100755 index 0000000..4180963 Binary files /dev/null and b/tools/device/qkx_fsmap differ diff --git a/tools/device/qkx_fsmap.c b/tools/device/qkx_fsmap.c new file mode 100644 index 0000000..40d68e2 --- /dev/null +++ b/tools/device/qkx_fsmap.c @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * qkx_fsmap - allocate GC-immune (pinned) files on f2fs and dump their extents. + * + * Android's userdata is metadata-encrypted by dm-default-key and file-encrypted + * by FBE, so a kexec'd kernel cannot read the filesystem at all. Instead we let + * f2fs allocate and pin a file, then address its raw blocks on the underlying + * partition directly. Pinning keeps f2fs garbage collection from relocating the + * blocks, so the physical map stays valid across reboots. + * + * alloc create, pin and fully allocate a file + * map print " " in bytes + * mapdd same map in units, for dd's skip/seek/count + * + * mapdd exists because Android's shell evaluates $(( )) in 32 bits, so byte + * offsets into a 230 GiB partition silently overflow if divided in script. + * + * Build: aarch64-linux-gnu-gcc -static -O2 qkx_fsmap.c -o qkx_fsmap + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define F2FS_IOC_SET_PIN_FILE _IOW(0xf5, 13, uint32_t) +#define F2FS_IOC_GET_PIN_FILE _IOR(0xf5, 14, uint32_t) +#define F2FS_SUPER_MAGIC 0xf2f52010 + +#define MAX_EXTENTS 4096 + +static int die(const char *what) +{ + fprintf(stderr, "qkx_fsmap: %s: %s\n", what, strerror(errno)); + return 1; +} + +static int do_alloc(const char *path, unsigned long long size) +{ + struct statfs sfs; + uint32_t pin = 1; + int fd; + + fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0600); + if (fd < 0) + return die("open"); + + if (fstatfs(fd, &sfs)) + return die("fstatfs"); + if (sfs.f_type != F2FS_SUPER_MAGIC) { + fprintf(stderr, "qkx_fsmap: %s is not on f2fs (magic %#lx); " + "pinning is unavailable\n", path, (unsigned long)sfs.f_type); + return 1; + } + + /* Pin before allocating: f2fs only guarantees GC immunity for blocks + * that were allocated while the inode was already pinned. */ + if (ioctl(fd, F2FS_IOC_SET_PIN_FILE, &pin)) + return die("F2FS_IOC_SET_PIN_FILE"); + + if (fallocate(fd, 0, 0, (off_t)size)) + return die("fallocate"); + + if (fsync(fd)) + return die("fsync"); + + /* GET_PIN_FILE reports the GC failure count for the pinned inode, so a + * nonzero value means f2fs could not keep the blocks in place. */ + pin = 0; + if (!ioctl(fd, F2FS_IOC_GET_PIN_FILE, &pin) && pin) { + fprintf(stderr, "qkx_fsmap: %s reports %u pin/GC failures\n", + path, pin); + return 1; + } + close(fd); + return 0; +} + +static int do_map(const char *path, unsigned long long unit) +{ + char buf[sizeof(struct fiemap) + MAX_EXTENTS * sizeof(struct fiemap_extent)]; + struct fiemap *fm = (struct fiemap *)buf; + unsigned long long expect = 0; + struct stat st; + unsigned i; + int fd; + + fd = open(path, O_RDONLY); + if (fd < 0) + return die("open"); + if (fstat(fd, &st)) + return die("fstat"); + + memset(buf, 0, sizeof(buf)); + fm->fm_start = 0; + fm->fm_length = st.st_size; + fm->fm_flags = FIEMAP_FLAG_SYNC; + fm->fm_extent_count = MAX_EXTENTS; + if (ioctl(fd, FS_IOC_FIEMAP, fm)) + return die("FS_IOC_FIEMAP"); + + for (i = 0; i < fm->fm_mapped_extents; i++) { + struct fiemap_extent *e = &fm->fm_extents[i]; + + /* Anything unwritten, inline or delalloc has no stable physical + * location, so refuse rather than hand back a bogus map. + * + * ENCODED|DATA_ENCRYPTED is expected and fine: under FBE the file's + * contents are encrypted but its block placement is not affected, + * and we address those blocks directly instead of via the + * filesystem. NOT_ALIGNED is rejected because we need block-exact + * offsets. */ + if (e->fe_flags & (FIEMAP_EXTENT_UNKNOWN | FIEMAP_EXTENT_DELALLOC | + FIEMAP_EXTENT_DATA_INLINE | FIEMAP_EXTENT_DATA_TAIL | + FIEMAP_EXTENT_NOT_ALIGNED | + FIEMAP_EXTENT_UNWRITTEN)) { + fprintf(stderr, "qkx_fsmap: extent %u has unusable flags %#x\n", + i, e->fe_flags); + return 1; + } + if (e->fe_logical != expect) { + fprintf(stderr, "qkx_fsmap: hole before extent %u " + "(logical %llu, expected %llu)\n", i, + (unsigned long long)e->fe_logical, expect); + return 1; + } + expect = e->fe_logical + e->fe_length; + + if ((e->fe_logical | e->fe_physical | e->fe_length) % unit) { + fprintf(stderr, "qkx_fsmap: extent %u is not a multiple " + "of %llu\n", i, unit); + return 1; + } + printf("%llu %llu %llu\n", + (unsigned long long)e->fe_logical / unit, + (unsigned long long)e->fe_physical / unit, + (unsigned long long)e->fe_length / unit); + } + + if (expect < (unsigned long long)st.st_size) { + fprintf(stderr, "qkx_fsmap: mapped %llu of %llu bytes; " + "increase MAX_EXTENTS\n", expect, + (unsigned long long)st.st_size); + return 1; + } + close(fd); + return 0; +} + +int main(int argc, char **argv) +{ + if (argc == 4 && !strcmp(argv[1], "alloc")) + return do_alloc(argv[2], strtoull(argv[3], NULL, 0)); + if (argc == 3 && !strcmp(argv[1], "map")) + return do_map(argv[2], 1); + if (argc == 4 && !strcmp(argv[1], "mapdd")) { + unsigned long long unit = strtoull(argv[3], NULL, 0); + + if (!unit) { + fprintf(stderr, "qkx_fsmap: block size must be nonzero\n"); + return 1; + } + return do_map(argv[2], unit); + } + + fprintf(stderr, "usage: qkx_fsmap alloc \n" + " qkx_fsmap map \n" + " qkx_fsmap mapdd \n"); + return 1; +} diff --git a/tools/device/qkx_rawcp b/tools/device/qkx_rawcp new file mode 100755 index 0000000..2906e6d Binary files /dev/null and b/tools/device/qkx_rawcp differ diff --git a/tools/device/qkx_rawcp.c b/tools/device/qkx_rawcp.c new file mode 100644 index 0000000..841a89f --- /dev/null +++ b/tools/device/qkx_rawcp.c @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * qkx_rawcp - copy an image into a pinned file's raw blocks. + * + * Userdata is metadata-encrypted, so writing through the filesystem produces + * ciphertext that the kexec target cannot read. We instead write the image + * straight to the partition blocks that f2fs allocated to the pinned file, which + * the target reads back verbatim. + * + * qkx_rawcp zero write zeros over every extent + * qkx_rawcp copy |- copy image (sparse: skips zero blocks) + * qkx_rawcp verify |- compare image against the raw blocks + * + * The map is qkx_fsmap's byte-unit output: " ". + * Writing only ever touches those extents, never the surrounding filesystem. + * + * Build: aarch64-linux-gnu-gcc -static -O2 qkx_rawcp.c -o qkx_rawcp + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include + +#define CHUNK (1u << 20) +#define BLOCK 4096 + +enum mode { MODE_ZERO, MODE_COPY, MODE_VERIFY }; + +struct extent { + unsigned long long file; + unsigned long long phys; + unsigned long long len; +}; + +static unsigned char buf[CHUNK]; +static unsigned char cmp[CHUNK]; +static struct extent *ext; +static unsigned n_ext; + +static int die(const char *what) +{ + fprintf(stderr, "qkx_rawcp: %s: %s\n", what, strerror(errno)); + return 1; +} + +static int load_map(const char *path, unsigned long long *total) +{ + unsigned long long expect = 0; + char line[256]; + unsigned cap = 64; + FILE *f = fopen(path, "r"); + + if (!f) + return die(path); + ext = malloc(cap * sizeof(*ext)); + if (!ext) + return die("malloc"); + + while (fgets(line, sizeof(line), f)) { + struct extent e; + + if (line[0] == '#' || line[0] == '\n') + continue; + if (sscanf(line, "%llu %llu %llu", &e.file, &e.phys, &e.len) != 3) { + fprintf(stderr, "qkx_rawcp: bad map line: %s", line); + return 1; + } + if (e.file != expect) { + fprintf(stderr, "qkx_rawcp: map has a hole at %llu\n", e.file); + return 1; + } + if (e.len % BLOCK || e.phys % BLOCK) { + fprintf(stderr, "qkx_rawcp: extent is not %u-aligned\n", BLOCK); + return 1; + } + expect = e.file + e.len; + if (n_ext == cap) { + cap *= 2; + ext = realloc(ext, cap * sizeof(*ext)); + if (!ext) + return die("realloc"); + } + ext[n_ext++] = e; + } + fclose(f); + if (!n_ext) { + fprintf(stderr, "qkx_rawcp: empty map\n"); + return 1; + } + *total = expect; + return 0; +} + +static int is_zero(const unsigned char *p, size_t n) +{ + size_t i; + + for (i = 0; i < n; i++) + if (p[i]) + return 0; + return 1; +} + +/* Pipes return short reads; the image must be consumed in exact steps. */ +static size_t read_full(int fd, unsigned char *p, size_t n) +{ + size_t done = 0; + + while (done < n) { + ssize_t r = read(fd, p + done, n - done); + + if (r < 0) { + if (errno == EINTR) + continue; + return done; + } + if (!r) + break; + done += r; + } + return done; +} + +static int write_at(int fd, const unsigned char *p, size_t n, unsigned long long off) +{ + while (n) { + ssize_t w = pwrite(fd, p, n, (off_t)off); + + if (w < 0) { + if (errno == EINTR) + continue; + return die("pwrite"); + } + p += w; + n -= w; + off += w; + } + return 0; +} + +int main(int argc, char **argv) +{ + unsigned long long total, done = 0, written = 0, srcleft = ~0ULL; + int devfd, srcfd = -1, mode; + unsigned long long mismatch = 0; + const char *src = NULL; + unsigned i; + + if (argc >= 4 && !strcmp(argv[1], "zero")) + mode = MODE_ZERO; + else if (argc == 5 && !strcmp(argv[1], "copy")) + mode = MODE_COPY; + else if (argc == 5 && !strcmp(argv[1], "verify")) + mode = MODE_VERIFY; + else { + fprintf(stderr, "usage: qkx_rawcp zero \n" + " qkx_rawcp copy |-\n" + " qkx_rawcp verify |-\n"); + return 1; + } + + if (load_map(argv[2], &total)) + return 1; + + devfd = open(argv[3], mode == MODE_VERIFY ? O_RDONLY : O_RDWR); + if (devfd < 0) + return die(argv[3]); + + if (mode != MODE_ZERO) { + src = argv[4]; + srcfd = !strcmp(src, "-") ? STDIN_FILENO : open(src, O_RDONLY); + if (srcfd < 0) + return die(src); + } else { + memset(buf, 0, CHUNK); + } + + for (i = 0; i < n_ext; i++) { + unsigned long long off = 0; + + while (off < ext[i].len) { + size_t n = CHUNK; + size_t got; + + if (ext[i].len - off < n) + n = ext[i].len - off; + + if (mode == MODE_ZERO) { + if (write_at(devfd, buf, n, ext[i].phys + off)) + return 1; + written += n; + off += n; + done += n; + continue; + } + + got = srcleft ? read_full(srcfd, buf, n) : 0; + if (got < n) { + /* Short image: the rest of the file stays as the + * zeros written by the preceding zero pass. */ + srcleft = 0; + memset(buf + got, 0, n - got); + } + + if (mode == MODE_VERIFY) { + if (pread(devfd, cmp, n, (off_t)(ext[i].phys + off)) != (ssize_t)n) + return die("pread"); + if (memcmp(buf, cmp, n)) + mismatch += n; + } else if (!is_zero(buf, n)) { + if (write_at(devfd, buf, n, ext[i].phys + off)) + return 1; + written += n; + } + off += n; + done += n; + if (!(done % (256ULL << 20))) + fprintf(stderr, "qkx_rawcp: %llu MiB / %llu MiB\n", + done >> 20, total >> 20); + } + } + + if (mode != MODE_VERIFY && fsync(devfd)) + return die("fsync"); + close(devfd); + + if (mode == MODE_VERIFY) { + if (mismatch) { + fprintf(stderr, "qkx_rawcp: %llu bytes differ\n", mismatch); + return 1; + } + fprintf(stderr, "qkx_rawcp: verified %llu MiB\n", done >> 20); + return 0; + } + fprintf(stderr, "qkx_rawcp: covered %llu MiB, wrote %llu MiB\n", + done >> 20, written >> 20); + return 0; +} diff --git a/tools/dt-reorder.py b/tools/dt-reorder.py new file mode 100644 index 0000000..25aca7c --- /dev/null +++ b/tools/dt-reorder.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +"""Restore the original node order of a captured runtime device tree. + +capture.sh rebuilds the runtime tree (with the bootloader's overlays applied) +from a copy of /proc/device-tree using `dtc -I fs`. dtc reads that copy in the +host filesystem's directory order, which is effectively a hash, so every +node's children end up shuffled. Order is not cosmetic on this kernel: + + * /cpus order sets the logical CPU numbers (CPU1 became the prime core), + which breaks Android's cluster assumptions, thermal cooling-device names + and CPU affinities; + * drivers such as kgsl search forward from their own node for siblings. + +This keeps the runtime tree's content and reorders children and properties +to follow the boot FDT (/sys/firmware/fdt). Nodes or properties that exist +only in the runtime tree keep their relative order after the known ones. + +Usage: dt-reorder.py +""" +import struct +import sys + +FDT_MAGIC = 0xd00dfeed +BEGIN_NODE, END_NODE, PROP, NOP, END = 1, 2, 3, 4, 9 + + +class Node: + def __init__(self, name): + self.name = name + self.props = [] # [(name, bytes)] + self.children = [] # [Node] + + +def parse(blob): + (magic, totalsize, off_struct, off_strings, off_rsvmap, version, + last_comp, boot_cpuid, size_strings, size_struct) = struct.unpack_from('>10I', blob, 0) + if magic != FDT_MAGIC: + raise SystemExit('not an FDT') + strings = blob[off_strings:off_strings + size_strings] + + def string_at(off): + return strings[off:strings.index(b'\0', off)].decode() + + rsv = [] + p = off_rsvmap + while True: + addr, size = struct.unpack_from('>QQ', blob, p) + p += 16 + if addr == 0 and size == 0: + break + rsv.append((addr, size)) + + p = off_struct + stack = [] + root = None + while True: + tok, = struct.unpack_from('>I', blob, p) + p += 4 + if tok == BEGIN_NODE: + end = blob.index(b'\0', p) + node = Node(blob[p:end].decode()) + p = (end + 1 + 3) & ~3 + if stack: + stack[-1].children.append(node) + else: + root = node + stack.append(node) + elif tok == END_NODE: + stack.pop() + elif tok == PROP: + length, nameoff = struct.unpack_from('>II', blob, p) + p += 8 + stack[-1].props.append((string_at(nameoff), blob[p:p + length])) + p = (p + length + 3) & ~3 + elif tok == NOP: + continue + elif tok == END: + break + else: + raise SystemExit(f'bad token {tok} at {p - 4}') + return root, rsv, boot_cpuid, last_comp + + +def reorder(node, ref): + """Order node's props/children like ref's; recurse into matching children.""" + if ref is None: + for c in node.children: + reorder(c, None) + return + prop_rank = {n: i for i, (n, _) in enumerate(ref.props)} + node.props.sort(key=lambda pv: prop_rank.get(pv[0], len(prop_rank))) + child_rank = {c.name: i for i, c in enumerate(ref.children)} + ref_by_name = {c.name: c for c in ref.children} + # sort() is stable, so unknown children keep their relative order. + node.children.sort(key=lambda c: child_rank.get(c.name, len(child_rank))) + for c in node.children: + reorder(c, ref_by_name.get(c.name)) + + +def serialize(root, rsv, boot_cpuid, last_comp): + strings = bytearray() + string_off = {} + + def sref(name): + if name not in string_off: + string_off[name] = len(strings) + strings.extend(name.encode() + b'\0') + return string_off[name] + + st = bytearray() + + def pad(): + while len(st) % 4: + st.append(0) + + def emit(node): + st.extend(struct.pack('>I', BEGIN_NODE)) + st.extend(node.name.encode() + b'\0') + pad() + for name, val in node.props: + st.extend(struct.pack('>III', PROP, len(val), sref(name))) + st.extend(val) + pad() + for c in node.children: + emit(c) + st.extend(struct.pack('>I', END_NODE)) + + emit(root) + st.extend(struct.pack('>I', END)) + + rsvmap = b''.join(struct.pack('>QQ', a, s) for a, s in rsv) + struct.pack('>QQ', 0, 0) + off_rsvmap = 40 + off_rsvmap = (off_rsvmap + 7) & ~7 + off_struct = off_rsvmap + len(rsvmap) + off_strings = off_struct + len(st) + totalsize = off_strings + len(strings) + hdr = struct.pack('>10I', FDT_MAGIC, totalsize, off_struct, off_strings, off_rsvmap, + 17, last_comp, boot_cpuid, len(strings), len(st)) + out = bytearray(hdr) + out.extend(b'\0' * (off_rsvmap - len(out))) + out.extend(rsvmap) + out.extend(st) + out.extend(strings) + return bytes(out) + + +def count(node): + return 1 + sum(count(c) for c in node.children) + + +def main(): + runtime, rsv, boot_cpuid, last_comp = parse(open(sys.argv[1], 'rb').read()) + boot, _, _, _ = parse(open(sys.argv[2], 'rb').read()) + before = count(runtime) + reorder(runtime, boot) + assert count(runtime) == before + open(sys.argv[3], 'wb').write(serialize(runtime, rsv, boot_cpuid, last_comp)) + cpus = next((c for c in runtime.children if c.name == 'cpus'), None) + if cpus: + print('cpus:', ' '.join(c.name for c in cpus.children)) + print(f'reordered {before} nodes') + + +if __name__ == '__main__': + main() diff --git a/tools/host-monitor.py b/tools/host-monitor.py new file mode 100755 index 0000000..0cd8ec5 --- /dev/null +++ b/tools/host-monitor.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""Record, from the host, exactly when the device disappears. + +The UDP kernel log stops at the moment we lose either the device or the ECM +transport, and those look identical in the log. This samples three independent +signals with timestamps so they can be told apart afterwards: + + usb every connected gadget's VID:PID, straight from sysfs + net ICMP reachability of the rescue/target address + hdmesg the host's own USB stack messages (disconnect/reset/enumeration) + +A device-side reset shows the gadget vanishing from sysfs. Losing only the +network leaves the gadget present while ICMP stops. + +Usage: host-monitor.py [seconds] [target-ip] +""" +import os +import re +import subprocess +import sys +import time + +SYS = "/sys/bus/usb/devices" + + +def gadgets(): + """Current set of VID:PID strings, read directly from sysfs.""" + out = set() + try: + names = os.listdir(SYS) + except OSError: + return out + for name in names: + if ":" in name: # interfaces, not devices + continue + try: + with open(f"{SYS}/{name}/idVendor") as f: + vid = f.read().strip() + with open(f"{SYS}/{name}/idProduct") as f: + pid = f.read().strip() + except OSError: + continue + if vid in ("1d6b", "2833", "18d1"): # root hubs, Oculus, Google + out.add(f"{name}={vid}:{pid}") + return out + + +def host_usb_log(): + """Host kernel USB lines, best effort; needs no root on most systems.""" + try: + d = subprocess.run(["dmesg", "--notime", "-l", "info,warn,err"], + capture_output=True, text=True, timeout=5) + return [l for l in d.stdout.splitlines() + if re.search(r"usb|cdc_ether|ecm", l, re.I)][-3:] + except Exception: + return [] + + +def main(): + out_path = sys.argv[1] + limit = float(sys.argv[2]) if len(sys.argv) > 2 else 300.0 + target = sys.argv[3] if len(sys.argv) > 3 else "10.42.0.2" + + start = time.time() + prev_usb, prev_net, prev_log = None, None, None + with open(out_path, "w", buffering=1) as f: + f.write(f"# host-monitor start={time.strftime('%F %T')} target={target}\n") + while time.time() - start < limit: + t = time.time() - start + usb = gadgets() + net = subprocess.run(["ping", "-c1", "-W1", target], + capture_output=True).returncode == 0 + if usb != prev_usb: + f.write(f"{t:8.3f} usb {' '.join(sorted(usb)) or '(none)'}\n") + prev_usb = usb + if net != prev_net: + f.write(f"{t:8.3f} net {'UP' if net else 'DOWN'}\n") + prev_net = net + log = host_usb_log() + if log != prev_log: + for line in log: + f.write(f"{t:8.3f} hdmesg {line}\n") + prev_log = log + time.sleep(0.25) + f.write(f"{time.time() - start:8.3f} monitor-end\n") + + +if __name__ == "__main__": + main() diff --git a/tools/klog-udp.py b/tools/klog-udp.py new file mode 100755 index 0000000..3713a23 --- /dev/null +++ b/tools/klog-udp.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +"""Receive the target's /dev/kmsg UDP stream (see qkx-switch) into a file. + +Usage: klog-udp.py [port] + +Records are "prio,seq,usec,flags;text". A restarted reader on the target +replays the whole backlog, so records whose sequence number was already seen +are dropped. Every write is flushed: the target may die at any moment. +""" +import socket +import sys + +out = open(sys.argv[1], "a", buffering=1, errors="replace") +port = int(sys.argv[2]) if len(sys.argv) > 2 else 5140 +sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) +sock.bind(("0.0.0.0", port)) +seen = set() +partial = "" +while True: + data, _ = sock.recvfrom(65536) + partial += data.decode("utf-8", "replace") + *lines, partial = partial.split("\n") + for line in lines: + head = line.split(";", 1)[0].split(",") + if len(head) >= 2 and head[1].isdigit(): + seq = int(head[1]) + if seq in seen: + continue + seen.add(seq) + out.write(line + "\n") diff --git a/tools/os-install-modemfw.sh b/tools/os-install-modemfw.sh new file mode 100755 index 0000000..f4691eb --- /dev/null +++ b/tools/os-install-modemfw.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Pin a read-only copy of the modem_a partition (holds the ADSP/CDSP firmware +# images our pre-Android ADSP boot needs) the same way tools/os-install.sh pins +# system/vendor/odm/product: allocate a pinned file on userdata, write it with +# raw-block extent copies, verify, and record its extent map. +# +# The partition itself is only ever read (dd if=); nothing is +# written back to it. The target later mounts the pinned copy through +# dm-linear, exactly like the OS images, and never touches the raw partition. +# +# Usage: tools/os-install-modemfw.sh [out-dir] +set -euo pipefail + +OUT=${1:-out/os} +HERE=$(cd "$(dirname "$0")/.." && pwd) +W=/data/local/tmp/qkx +NAME=modemfw + +say() { printf '\n== %s\n' "$*"; } +sh_() { adb shell "su -c '$*'"; } +have() { adb exec-out "su -c 'test -f $1 && echo QKX_YES'" | grep -q QKX_YES; } + +[ -s "$OUT/partitions" ] || { echo "missing $OUT/partitions; run tools/os-install.sh first"; exit 1; } +PART_DEV=$(awk '$1 == "modem_a" { print $2 }' "$OUT/partitions") +[ -n "$PART_DEV" ] || { echo "modem_a not found in $OUT/partitions"; exit 1; } + +adb shell 'su -c id' | grep -q 'uid=0' || { echo 'rooted adb (su) required'; exit 1; } +mkdir -p "$OUT/maps" + +if have "$W/img/$NAME.img"; then + echo "== $NAME: already installed, skipping (rm $W/img/$NAME.img to redo)" +else + say "$NAME: reading size of $PART_DEV" + BASE=${PART_DEV##*/} + BYTES=$(adb exec-out "su -c 'cat /sys/class/block/$BASE/size'" | tr -d '\r') + BYTES=$((BYTES * 512)) + echo "$PART_DEV is $((BYTES / 1024 / 1024)) MiB" + + say "$NAME: dumping $PART_DEV (read-only) to device scratch space" + sh_ "dd if=$PART_DEV of=$W/modemfw-src.img bs=1M" + + say "$NAME: allocating pinned file" + BYTES=$(( (BYTES + 4095) / 4096 * 4096 )) + sh_ "sh $W/qkx-install.sh alloc $NAME $BYTES" + + say "$NAME: writing raw blocks from the dump" + sh_ "sh $W/qkx-install.sh write $NAME $W/modemfw-src.img" + + say "$NAME: verifying" + sh_ "sh $W/qkx-install.sh verify $NAME $W/modemfw-src.img" + sh_ "rm -f $W/modemfw-src.img" +fi + +say 'collecting map' +adb exec-out "su -c 'sh $W/qkx-install.sh map $NAME'" | tr -d '\r' > "$OUT/maps/$NAME.map" +[ -s "$OUT/maps/$NAME.map" ] || { echo "empty map for $NAME"; exit 1; } +printf '%-11s %s extents\n' "$NAME" "$(wc -l < "$OUT/maps/$NAME.map")" + +echo +echo "modemfw pinned and mapped; rerun tools/os-prep.sh to bundle the new map" diff --git a/tools/os-install.sh b/tools/os-install.sh new file mode 100755 index 0000000..00aa2ef --- /dev/null +++ b/tools/os-install.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# Install a custom Android OS into pinned files on the headset's userdata. +# +# Usage: tools/os-install.sh [out-dir] [data-size-GiB] +# +# The OS images and a fresh /data image are stored as pinned f2fs files under +# /data/local/tmp/qkx, with their contents written directly to the partition +# blocks f2fs allocated to them. That is what makes them readable from the kexec +# target, which cannot decrypt userdata. +# +# This writes to the userdata partition, but only inside blocks allocated to +# these files. It never formats a partition and never touches the stock OS. +set -euo pipefail + +OTA=${1:?usage: os-install.sh [out-dir] [data-size-GiB]} +OUT=${2:-out/os} +DATA_GIB=${3:-16} +HERE=$(cd "$(dirname "$0")/.." && pwd) +W=/data/local/tmp/qkx +# system.img is the root filesystem; the rest mount inside it. +IMAGES=(system system_ext vendor odm product) + +say() { printf '\n== %s\n' "$*"; } +sh_() { adb shell "su -c '$*'"; } +# su does not reliably propagate exit status, so test by echoing a marker. +have() { adb exec-out "su -c 'test -f $1 && echo QKX_YES'" | grep -q QKX_YES; } + +adb shell 'su -c id' | grep -q 'uid=0' || { echo 'rooted adb (su) required'; exit 1; } +for f in "${IMAGES[@]}"; do + [ -f "$OTA/$f.img" ] || { echo "missing $OTA/$f.img"; exit 1; } +done + +mkdir -p "$OUT/maps" + +say 'building device helpers' +make -C "$HERE/tools/device" >/dev/null + +say 'pushing helpers' +adb push "$HERE/tools/device/qkx_fsmap" "$HERE/tools/device/qkx_rawcp" \ + "$HERE/tools/device/qkx_dm" "$HERE/tools/device/qkx-install.sh" \ + /data/local/tmp/ >/dev/null +sh_ "mkdir -p $W/bin $W/img $W/maps && + cp /data/local/tmp/qkx_fsmap /data/local/tmp/qkx_rawcp /data/local/tmp/qkx_dm $W/bin/ && + cp /data/local/tmp/qkx-install.sh $W/ && + chmod 755 $W/bin/* $W/qkx-install.sh" + +# Record which block device backs each partition name. The target has no vendor +# init to create by-name symlinks, so its initramfs needs this mapping. +say 'recording partition map' +adb exec-out "su -c 'sh $W/qkx-install.sh partitions'" | tr -d '\r' > "$OUT/partitions" +grep -E '^(userdata|persist|metadata) ' "$OUT/partitions" || { + echo 'partition map looks wrong:'; head -3 "$OUT/partitions"; exit 1; } +adb exec-out 'su -c "getprop ro.boot.slot_suffix"' | tr -d '\r' > "$OUT/slot_suffix" +echo "slot: $(cat "$OUT/slot_suffix")" + +install_image() { + local name=$1 src=$2 bytes + bytes=$(stat -c %s "$src") + # f2fs allocates in 4 KiB blocks; round up so the whole image fits. + bytes=$(( (bytes + 4095) / 4096 * 4096 )) + + say "$name: allocating $((bytes / 1024 / 1024)) MiB" + sh_ "sh $W/qkx-install.sh alloc $name $bytes" + + say "$name: staging" + adb push "$src" "$W/stage.img" >/dev/null + + say "$name: writing raw blocks" + sh_ "sh $W/qkx-install.sh write $name $W/stage.img" + + say "$name: verifying" + sh_ "sh $W/qkx-install.sh verify $name $W/stage.img" + sh_ "rm -f $W/stage.img" +} + +for name in "${IMAGES[@]}"; do + if have "$W/img/$name.img"; then + echo "== $name: already installed, skipping (rm $W/img/$name.img to redo)" + continue + fi + install_image "$name" "$OTA/$name.img" +done + +if have "$W/img/data.img"; then + say "data: already installed, skipping (rm $W/img/data.img to redo)" +else + say "data: building empty ${DATA_GIB} GiB ext4" + DIMG=$OUT/data.img + rm -f "$DIMG" "$DIMG.gz" + truncate -s "${DATA_GIB}G" "$DIMG" + # Keep to features the 4.19 target kernel understands; newer e2fsprogs + # defaults (orphan_file, metadata_csum_seed) would make it unmountable. + mke2fs -q -t ext4 -b 4096 -I 256 -m 0 -L data -E nodiscard \ + -O ^orphan_file,^metadata_csum_seed,^casefold,^fast_commit,^large_dir \ + "$DIMG" + gzip -1 -c "$DIMG" > "$DIMG.gz" + ls -lh "$DIMG.gz" + + say "data: allocating ${DATA_GIB} GiB pinned" + sh_ "sh $W/qkx-install.sh alloc data $((DATA_GIB * 1024 * 1024 * 1024))" + + say 'data: staging compressed image' + adb push "$DIMG.gz" "$W/stage.img.gz" >/dev/null + + say 'data: writing raw blocks (zero pass then image)' + sh_ "sh $W/qkx-install.sh writez data $W/stage.img.gz" + + say 'data: verifying' + sh_ "sh $W/qkx-install.sh verify data $W/stage.img.gz" + sh_ "rm -f $W/stage.img.gz" +fi + +say 'collecting maps' +for name in "${IMAGES[@]}" data; do + # Read the map back from the file itself, so a stale copy cannot slip in. + adb exec-out "su -c 'sh $W/qkx-install.sh map $name'" | tr -d '\r' > "$OUT/maps/$name.map" + [ -s "$OUT/maps/$name.map" ] || { echo "empty map for $name"; exit 1; } + printf '%-11s %s extents\n' "$name" "$(wc -l < "$OUT/maps/$name.map")" +done + +say 'installed' +sh_ "sh $W/qkx-install.sh list" +echo +echo "maps and partition table saved in $OUT" +echo "next: tools/os-prep.sh $OUT out/payload" diff --git a/tools/os-prep.sh b/tools/os-prep.sh new file mode 100755 index 0000000..aeffe3f --- /dev/null +++ b/tools/os-prep.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Build a kexec payload that assembles the custom OS instead of a bare shell. +# Usage: os-prep.sh +# +# is what tools/os-install.sh produced (maps/ and partitions). +# The maps are baked into the initramfs, so re-run this after reinstalling an +# image: reallocating a pinned file hands out different blocks. +set -euo pipefail +HERE=$(cd "$(dirname "$0")/.." && pwd) +C=${1:?captured dir} IMG=${2:?target Image} OSDIR=${3:?os dir} OUT=${4:?out dir} +BB=${BUSYBOX:-$HERE/out/busybox} + +[ -f "$BB" ] || { echo "no busybox at $BB; run: make initramfs"; exit 1; } +[ -f "$OSDIR/partitions" ] || { echo "no $OSDIR/partitions; run tools/os-install.sh"; exit 1; } +make -C "$HERE/tools/device" >/dev/null + +mkdir -p "$(dirname "$OUT")" +RD=$OUT-initramfs.gz +python3 "$HERE/initramfs/build.py" --busybox "$BB" --output "$RD" \ + --os-dir "$OSDIR" --qkx-dm "$HERE/tools/device/qkx_dm" + +# A user build ignores androidboot.selinux; qkx_selinux_permissive (our kernel) +# keeps SELinux permissive so the debug shell survives init's policy load. +"$HERE/tools/prep.sh" "$C" "$IMG" "$RD" "$OUT" androidboot.selinux=permissive qkx_selinux_permissive=1 \ + hung_task_panic=1 hung_task_timeout_secs=30 softlockup_panic=1 audit=0 ${QKX_EXTRA_CMDLINE:-} +echo "payload with custom OS ready: $OUT" diff --git a/tools/qkx_follow.py b/tools/qkx_follow.py new file mode 100755 index 0000000..96a4cff --- /dev/null +++ b/tools/qkx_follow.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +"""Stream the target's kernel log to the host over telnet. + +Everything is written to a host file as it arrives, so the log survives a +target hang or reset. Usage: qkx_follow.py [grep-pattern] +""" +import os +import socket +import sys +import time + +out = open(sys.argv[1], "ab", buffering=0) +pattern = sys.argv[2] if len(sys.argv) > 2 else "" +cmd = "qkx-klogwatch" + (f" -g '{pattern}'" if pattern else "") + "\n" +s = socket.create_connection((os.environ.get("QKX_HOST", "10.42.0.2"), 23), timeout=10) +time.sleep(0.5) +s.sendall(b"stty -echo 2>/dev/null\n" + cmd.encode()) +s.settimeout(None) +while True: + try: + data = s.recv(65536) + except OSError: + break + if not data: + break + out.write(data) +out.write(b"\n[qkx_follow: connection closed]\n") diff --git a/tools/run.sh b/tools/run.sh index e1bb88a..8b00ae7 100755 --- a/tools/run.sh +++ b/tools/run.sh @@ -13,19 +13,102 @@ flush_rpmh=1 suspend_syncboss=1 disconnect_qmp=0 phase_delay_ms=300 $*" for f in "$MOD" "$HERE/module/marker_read.ko" "$P/Image" "$P/initramfs" "$P/boot.dtb"; do [ -f "$f" ] || { echo "missing $f"; exit 1; } done +# Stock hyp-assigns secure ION pages to other VMs; they stay locked after +# kexec and any access from the next kernel hangs the CPU. Collect them as +# late as possible and hand them over as one no-map reserved-memory node. +DTB=$P/boot.dtb +SECMAP=$HERE/module/ion_secmap.ko +if [ -f "$SECMAP" ]; then + adb push "$SECMAP" $T/ion_secmap.ko >/dev/null + # Preserve Android's live hardware state by default. Stopping services + # drops hardware/firmware votes and prevents this target from starting. + if [ "${QKX_STOP_ANDROID:-0}" = 1 ]; then + adb shell "su -c 'stop; for s in cameraserver virtual_camera bootanim \ + vendor.oculus.hardware.composer-service vendor.qti.hardware.display.composer \ + vendor.qti.hardware.display.allocator; do stop \$s; done; sleep 8; \ + echo 3 > /proc/sys/vm/drop_caches; sleep 2'" + fi + if [ "${GIVEBACK:-0}" = 1 ]; then + # Return the pages to HLOS with the stock unassign calls; only + # what the hypervisor refuses still needs reserving. + adb push "$HERE/module/qkx_giveback.ko" $T/qkx_giveback.ko >/dev/null + adb shell "su -c 'insmod $T/qkx_giveback.ko; dmesg'" | tac | + sed '/giveback: begin/q' > "$P/giveback.txt" + grep -o 'giveback: returned.*' "$P/giveback.txt" || + { echo "giveback did not run"; exit 1; } + grep -o 'giveback: kept 0x[0-9a-f]*-0x[0-9a-f]*' "$P/giveback.txt" | + sed 's/giveback: kept/ionsec:/' > "$P/ionsec.txt" + else + adb shell "su -c 'insmod $T/ion_secmap.ko; dmesg'" | tac | sed '/ionsec: begin/q' | + grep -o 'ionsec: 0x[0-9a-f]*-0x[0-9a-f]*' > "$P/ionsec.txt" + [ -s "$P/ionsec.txt" ] || { echo "ion_secmap returned no ranges"; exit 1; } + fi +fi +if [ -s "$P/ionsec.txt" ] && [ -f "$SECMAP" ]; then + DTB=$(mktemp --suffix=.dtb) + cp "$P/boot.dtb" "$DTB" + # Early memblock can't grow its region array, so every no-map range + # costs a split; merge into a few 2 MiB-aligned blocks. + REG=$(python3 - "$P/ionsec.txt" <<'EOF' +import re, sys +A = 2 << 20 +m = [] +for a, b in sorted(tuple(int(x, 16) for x in re.findall(r'0x[0-9a-f]+', l)) + for l in open(sys.argv[1])): + a &= ~(A - 1) + b = (b + A - 1) & ~(A - 1) + if m and a <= m[-1][1]: + m[-1][1] = max(m[-1][1], b) + else: + m.append([a, b]) +print(' '.join('0x%x 0x%x 0x%x 0x%x' % (a >> 32, a & 0xffffffff, + (b - a) >> 32, (b - a) & 0xffffffff) for a, b in m)) +EOF +) + fdtput -c "$DTB" /reserved-memory/qkx-ionsec && + fdtput -t x "$DTB" /reserved-memory/qkx-ionsec reg $REG && + fdtput -t s "$DTB" /reserved-memory/qkx-ionsec no-map "" || + { echo "failed to add ionsec reservation"; exit 1; } + echo "reserved $(wc -l < "$P/ionsec.txt") secure ranges as $(($(echo $REG | wc -w) / 4)) blocks" +fi + adb push "$MOD" $T/qkx.ko >/dev/null adb push "$HERE/module/marker_read.ko" $T/qkx_marker.ko >/dev/null adb push "$P/Image" $T/qkx-Image >/dev/null adb push "$P/initramfs" $T/qkx-initramfs >/dev/null -adb push "$P/boot.dtb" $T/qkx-boot.dtb >/dev/null +adb push "$DTB" $T/qkx-boot.dtb >/dev/null adb shell 'su -c "sync; sync"' -for pair in "$MOD:qkx.ko" "$P/Image:qkx-Image" "$P/initramfs:qkx-initramfs" "$P/boot.dtb:qkx-boot.dtb"; do +for pair in "$MOD:qkx.ko" "$P/Image:qkx-Image" "$P/initramfs:qkx-initramfs" "$DTB:qkx-boot.dtb"; do l=${pair%%:*}; r=${pair##*:} [ "$(md5sum < "$l" | cut -d' ' -f1)" = "$(adb shell "su -c 'md5sum $T/$r'" | awk '{print $1}')" ] || { echo "hash mismatch: $r"; exit 1; } done +# The PMIC latches why the SoC last went down. Read it here, before this run +# overwrites it, so a failed attempt can be attributed afterwards. +adb shell "su -c 'dmesg | grep -iE \"Power-on reason|Power-off reason\"'" 2>/dev/null | + sed 's/^\[[^]]*\] *//' | tr -d '\r' > "$P/prev-reset-reason.txt" +[ -s "$P/prev-reset-reason.txt" ] && + echo "previous shutdown: $(grep -m1 -i 'Power-off reason' "$P/prev-reset-reason.txt" | sed 's/.*Power-off reason: //')" + +# QSEE listeners are registered in TrustZone against stock-kernel buffers. +# Freezing their owners preserves those stale registrations across kexec, and +# the target then hangs in __qseecom_scm_call2_locked. Stop only TEE clients; +# do not use Android's broad `stop`, which drops required hardware votes. +if [ "${QKX_STOP_QSEE:-0}" = 1 ]; then + adb shell "su -c 'for s in vendor.oculus.hardware.attestation-service \ + vendor.oculus.devicecert-hal-1-0 gatekeeperd gatekeeper-1-0 \ + vendor.keymaster-4-1 vendor.spdaemon vendor.qseecomd; do \ + setprop ctl.stop \$s; done; sleep 2; \ + echo qsee_services_stopped; \ + ps -A | grep -E \"qseecomd|spdaemon|keymaster|gatekeeper|attestation|devicecert\" || true'" | tr -d '\r' +fi + # Invalidate the previous retained log so a reset can't reuse it. adb shell "su -c 'insmod $T/qkx_marker.ko clear=1; rmmod marker_read'" >/dev/null 2>&1 +# Assignments made after the ion_secmap snapshot would be missing from the +# reservation; hypwatch (if loaded) shows whether that window saw any. +adb shell "su -c 'cat /proc/qkx_hypwatch 2>/dev/null'" > "$P/hypwatch.txt" && + [ -s "$P/hypwatch.txt" ] && echo "hypwatch before jump: $(head -1 "$P/hypwatch.txt")" echo "verified; executing: $PARAMS" adb shell "su -c 'insmod $T/qkx.ko image=$T/qkx-Image initrd=$T/qkx-initramfs dtb=$T/qkx-boot.dtb $PARAMS'" sleep 2 diff --git a/tools/why-reset.sh b/tools/why-reset.sh new file mode 100755 index 0000000..cfca554 --- /dev/null +++ b/tools/why-reset.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Ask stock Android why the SoC went down last time. +# +# The PMIC latches a power-off reason across the reset, so after a failed run +# this distinguishes causes that are otherwise indistinguishable in our logs: +# +# PS_HOLD the SoC asked for the reset (kernel restart, watchdog bite, TZ) +# PMIC_WD the PMIC's own watchdog expired +# UVLO the supply collapsed (battery/charging) +# TFT/OTST3 thermal +# GP_FAULT/MBG_FAULT/OVLO PMIC fault +# +# Run it on stock, after the device has come back up. Read-only. +set -uo pipefail +T=${1:-} + +run() { adb shell "su -c '$1'" 2>/dev/null | tr -d '\r'; } + +echo "=== PMIC power-on / power-off reasons (latched across the reset) ===" +run 'dmesg | grep -iE "Power-on reason|Power-off reason"' | sed 's/^\[[^]]*\] *//' + +echo +echo "=== Android's view of the boot reason ===" +run 'getprop ro.boot.bootreason; getprop sys.boot.reason; getprop ro.boot.alarmboot' + +echo +echo "=== PMIC sysfs (same data, unparsed) ===" +run 'for f in /sys/devices/platform/soc/*/*/*/*power-on*/power_o*_reason; do + [ -r "$f" ] && echo "$f: $(cat $f)"; done' + +echo +echo "=== Did anything survive in pstore? (warm reset only) ===" +run 'ls -l /sys/fs/pstore/ 2>&1 | head' + +echo +echo "=== Battery / charger state now ===" +run 'dumpsys battery | grep -iE "level|powered|status|voltage|temperature"' + +[ -n "$T" ] && { echo; echo "(saved to $T)"; }