mirror of
https://github.com/YusufOzmen01/quest-kexec.git
synced 2026-10-26 19:37:39 +00:00
booting android
This commit is contained in:
35 files changed
+2601
-12
No files matched your search
+46
-4
@@ -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")
|
||||
|
||||
+31
-5
@@ -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
|
||||
Executable
+28
@@ -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 <path> 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
|
||||
Executable
+30
@@ -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 <absolute-path-inside-the-new-OS>}
|
||||
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"
|
||||
Executable
+48
@@ -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 "<prio>,<seq>,<usec>,<flags>;<text>"
|
||||
# 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; }
|
||||
Executable
+72
@@ -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 \
|
||||
</dev/null >/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/null >/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
|
||||
Executable
+129
@@ -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/<name> 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'
|
||||
Executable
+42
@@ -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 }'
|
||||
Executable
+66
@@ -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
|
||||
|
||||
Executable
BIN
Binary file not shown.
Executable
+114
@@ -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" <<EOF
|
||||
export PATH=$Q/bin:/system/bin:/vendor/bin
|
||||
export ENV=$Q/profile
|
||||
PS1='qkx-android# '
|
||||
EOF
|
||||
|
||||
# Replace the initramfs telnetd and log watcher with chrooted copies.
|
||||
kill $(cat /tmp/telnet.pid) 2>/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 >/dev/null 2>&1 &
|
||||
chroot "$ROOT" $Q/busybox env PATH=$Q/bin \
|
||||
$Q/bin/qkx-klogwatch -f $Q/kernel.log </dev/null >/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 >/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 >/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
|
||||
@@ -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 <linux/module.h>
|
||||
#include <linux/fs.h>
|
||||
#include <linux/miscdevice.h>
|
||||
#include <linux/rbtree.h>
|
||||
#include <linux/scatterlist.h>
|
||||
#include <linux/mm.h>
|
||||
#include <linux/msm_ion.h>
|
||||
#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 <linux/kprobes.h>
|
||||
#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");
|
||||
Binary file not shown.
+68
-1
@@ -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",
|
||||
|
||||
@@ -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 <linux/module.h>
|
||||
#include <linux/fs.h>
|
||||
#include <linux/miscdevice.h>
|
||||
#include <linux/rbtree.h>
|
||||
#include <linux/scatterlist.h>
|
||||
#include <linux/mm.h>
|
||||
#include <linux/msm_ion.h>
|
||||
#include <linux/kprobes.h>
|
||||
#include <soc/qcom/secure_buffer.h>
|
||||
#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");
|
||||
Binary file not shown.
@@ -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
|
||||
@@ -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 <linux/module.h>
|
||||
#include <linux/fs.h>
|
||||
#include <linux/miscdevice.h>
|
||||
#include <linux/rbtree.h>
|
||||
#include <linux/scatterlist.h>
|
||||
#include <linux/mm.h>
|
||||
#include <linux/msm_ion.h>
|
||||
#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");
|
||||
Executable
+114
@@ -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 <name> <bytes>"
|
||||
echo " qkx-install.sh write <name> <staged-file>"
|
||||
echo " qkx-install.sh writez <name> <staged-file.gz>"
|
||||
echo " qkx-install.sh verify <name> <staged-file>"
|
||||
echo " qkx-install.sh map <name>"
|
||||
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() {
|
||||
# "<name> <device node>" 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
|
||||
Executable
+39
@@ -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
|
||||
Executable
BIN
Binary file not shown.
@@ -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 <name> <backing-dev> <map-file> map file lines: "<off> <phys> <len>"
|
||||
* qkx_dm remove <name>
|
||||
*
|
||||
* create prints "<major> <minor> <bytes>" 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 <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/sysmacros.h>
|
||||
#include <unistd.h>
|
||||
#include <linux/dm-ioctl.h>
|
||||
|
||||
#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 <name> <backing-dev> <map-file>\n"
|
||||
" qkx_dm remove <name>\n");
|
||||
ret = 1;
|
||||
}
|
||||
close(fd);
|
||||
return ret;
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -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 <path> <bytes> create, pin and fully allocate a file
|
||||
* map <path> print "<file_offset> <phys_offset> <length>" in bytes
|
||||
* mapdd <path> <bs> same map in <bs> 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 <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/vfs.h>
|
||||
#include <unistd.h>
|
||||
#include <linux/fiemap.h>
|
||||
#include <linux/fs.h>
|
||||
|
||||
#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 <path> <bytes>\n"
|
||||
" qkx_fsmap map <path>\n"
|
||||
" qkx_fsmap mapdd <path> <blocksize>\n");
|
||||
return 1;
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -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 <map> <dev> write zeros over every extent
|
||||
* qkx_rawcp copy <map> <dev> <src>|- copy image (sparse: skips zero blocks)
|
||||
* qkx_rawcp verify <map> <dev> <src>|- compare image against the raw blocks
|
||||
*
|
||||
* The map is qkx_fsmap's byte-unit output: "<file_offset> <phys_offset> <length>".
|
||||
* 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 <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#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 <map> <dev>\n"
|
||||
" qkx_rawcp copy <map> <dev> <src>|-\n"
|
||||
" qkx_rawcp verify <map> <dev> <src>|-\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;
|
||||
}
|
||||
@@ -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 <runtime.dtb> <boot.fdt> <out.dtb>
|
||||
"""
|
||||
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()
|
||||
Executable
+89
@@ -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 <out.txt> [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()
|
||||
Executable
+31
@@ -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 <output-file> [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")
|
||||
Executable
+60
@@ -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=<partition>); 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"
|
||||
Executable
+125
@@ -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 <ota-image-dir> [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 <ota-image-dir> [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 <target-Image> out/payload"
|
||||
Executable
+26
@@ -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 <captured-dir> <target-Image> <os-dir> <out-dir>
|
||||
#
|
||||
# <os-dir> 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"
|
||||
Executable
+27
@@ -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 <output-file> [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")
|
||||
+85
-2
@@ -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
|
||||
|
||||
Executable
+39
@@ -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)"; }
|
||||
Reference in new issue
Block a user